UNPKG

@limetech/lime-elements

Version:
215 lines (214 loc) • 7.07 kB
import { h, } from '@stencil/core'; import { createRandomString } from '../../util/random-string'; import { zipObject } from 'lodash-es'; import { portalContains } from '../portal/contains'; import { ESCAPE } from '../../util/keycodes'; /** * A popover is an impermanent layer that is displayed on top of other content * when user taps an element that triggers the popover. This element can be * practically anything, a button, piece of text, and icon, etc. * * Popovers are nonmodal, thus they don't have a semi-transparent backdrop layer * that darkens the content below them. Also for the same reason, they can be * dismissed by tapping or clicking another part of the screen, but not by a * clicking a button or link on the popover itself. * * ## Usage * Use a popover to show **options** or **information** related to the trigger * onscreen. A typical use case for popovers is a tooltip, in which you show * help text or contextual information to users. * * Popovers are most appropriate on larger screens but can be used on smaller * screens too, as long as their content is responsive and takes into account * the context they are displayed on. * * When a popover is displayed, interactions with other controls are blocked, * until user dismisses the popover. * * * ## Layout * Popovers has only one slot in which you can import a custom web-component. * * :::note * You must make sure that web-components that you import into the slot has * a `width: 100%;` style so that it can horizontally stretch across the popover. * * However, `width` of the popover can be controlled by specifying a CSS variable * of `--popover-surface-width`. If you don't specify any width, the popover * will grow as wide as its content. * ::: * * :::important * Do not make a popover too big. They should never take over the entire screen. * If your content is that big, you should probably be using a Modal instead. * ::: * * @slot - Content to put inside the surface * @exampleComponent limel-example-popover-basic * @exampleComponent limel-example-popover-trigger-interaction * @exampleComponent limel-example-popover-styling */ export class Popover { constructor() { this.handleGlobalKeyPress = (event) => { if (event.key !== ESCAPE) { return; } event.stopPropagation(); event.preventDefault(); this.close.emit(); }; this.setTriggerAttributes = (element) => { const attributes = { 'aria-haspopup': true, 'aria-expanded': this.open, 'aria-controls': this.portalId, role: 'button', }; for (const [key, value] of Object.entries(attributes)) { if (value) { element.setAttribute(key, String(value)); } else { element.removeAttribute(key); } } }; this.open = false; this.openDirection = undefined; this.portalId = createRandomString(); this.globalClickListener = this.globalClickListener.bind(this); } watchOpen() { this.setupGlobalHandlers(); } componentWillLoad() { this.setupGlobalHandlers(); } componentDidRender() { const slotElement = this.host.shadowRoot.querySelector('slot'); // eslint-disable-next-line unicorn/no-array-for-each slotElement.assignedElements().forEach(this.setTriggerAttributes); } setupGlobalHandlers() { if (this.open) { document.addEventListener('click', this.globalClickListener, { capture: true, }); document.addEventListener('keyup', this.handleGlobalKeyPress); } else { document.removeEventListener('click', this.globalClickListener); document.removeEventListener('keyup', this.handleGlobalKeyPress); } } render() { const cssProperties = this.getCssProperties(); const popoverZIndex = getComputedStyle(this.host).getPropertyValue('--popover-z-index'); return (h("div", { class: "trigger-anchor" }, h("slot", { name: "trigger" }), h("limel-portal", { visible: this.open, containerId: this.portalId, containerStyle: { 'z-index': popoverZIndex }, openDirection: this.openDirection }, h("limel-popover-surface", { contentCollection: this.host.children, style: cssProperties })))); } globalClickListener(event) { const element = event.target; const clickedInside = portalContains(this.host, element); if (this.open && !clickedInside) { event.stopPropagation(); event.preventDefault(); this.close.emit(); } } getCssProperties() { const propertyNames = [ '--popover-surface-width', '--popover-body-background-color', '--popover-border-radius', '--popover-box-shadow', ]; const style = getComputedStyle(this.host); const values = propertyNames.map((property) => { return style.getPropertyValue(property); }); return zipObject(propertyNames, values); } static get is() { return "limel-popover"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["popover.scss"] }; } static get styleUrls() { return { "$": ["popover.css"] }; } static get properties() { return { "open": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "True if the content within the popover should be visible" }, "attribute": "open", "reflect": false, "defaultValue": "false" }, "openDirection": { "type": "string", "mutable": false, "complexType": { "original": "OpenDirection", "resolved": "\"bottom\" | \"bottom-end\" | \"bottom-start\" | \"left\" | \"left-end\" | \"left-start\" | \"right\" | \"right-end\" | \"right-start\" | \"top\" | \"top-end\" | \"top-start\"", "references": { "OpenDirection": { "location": "import", "path": "../menu/menu.types" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Decides the popover's location in relation to its trigger" }, "attribute": "open-direction", "reflect": true } }; } static get events() { return [{ "method": "close", "name": "close", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emits an event when the component is closing" }, "complexType": { "original": "void", "resolved": "void", "references": {} } }]; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "open", "methodName": "watchOpen" }]; } } //# sourceMappingURL=popover.js.map