@limetech/lime-elements
Version:
215 lines (214 loc) • 7.07 kB
JavaScript
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