UNPKG

@scania/tegel

Version:
592 lines (591 loc) 23 kB
import { Host, h, } from "@stencil/core"; import { createPopper } from "@popperjs/core"; import generateUniqueId from "../../utils/generateUniqueId"; import { generateClassList } from "../../utils/classList"; export class TdsPopoverCore { constructor() { /** Decides if the component should be visible from the start. */ this.defaultShow = false; /** Whether the popover should animate when being opened/closed or not */ this.animation = 'none'; /** Controls whether the Popover is shown or not. If this is set hiding and showing * will be decided by this prop and will need to be controlled from the outside. This * also means that clicking outside of the popover won't close it. Takes precedence over `defaultShow` prop. */ this.show = null; /** Decides the placement of the Popover Menu */ this.placement = 'auto'; /** Sets the offset skidding */ this.offsetSkidding = 0; /** Sets the offset distance */ this.offsetDistance = 8; /** Array of modifier objects to pass to popper.js. See https://popper.js.org/docs/v2/modifiers/ */ this.modifiers = []; /** What triggers the popover to show */ this.trigger = 'click'; /** Decides if the popover should hide automatically. * Alternatevly it can be hidden externally based on emitted events. */ this.autoHide = true; this.renderedShowValue = false; this.popperInstance = null; this.isShown = false; this.disableLogic = false; this.hasShownAtLeastOnce = false; this.openedByKeyboard = false; this.uuid = generateUniqueId(); this.setIsShown = (isShown) => { if (typeof isShown === 'function') { this.isShown = isShown(this.isShown); } else { this.isShown = isShown; } if (this.isShown) { this.hasShownAtLeastOnce = true; this.internalTdsShow.emit(); } else { this.internalTdsClose.emit(); this.openedByKeyboard = false; } }; this.onClickTarget = (event) => { event.stopPropagation(); // Check if event was triggered by keyboard (Enter or Space) this.openedByKeyboard = event.type === 'keydown' || event.key === 'Enter' || event.key === ' '; this.setIsShown((isShown) => !isShown); }; this.handleShow = (event) => { event.stopPropagation(); // Check if event was triggered by keyboard (tab focus) this.openedByKeyboard = event.type === 'focusin'; this.setIsShown(true); }; this.handleHide = (event) => { event.stopPropagation(); this.setIsShown(false); }; } /** Property for closing popover programmatically */ async close() { this.setIsShown(false); } onAnyClick(event) { if (this.trigger === 'click' && this.isShown && this.show === null) { // Source: https://lamplightdev.com/blog/2021/04/10/how-to-detect-clicks-outside-of-a-web-component/ const isClickOutside = !event.composedPath().includes(this.host); if (isClickOutside) { this.setIsShown(false); } } } onTdsShow(event) { if (this.show === null) { const target = event.target; if (target.id !== `tds-popover-core-${this.uuid}`) { this.setIsShown(false); } } } handleKeydown(event) { if (event.key === 'Escape' && this.isShown) { this.setIsShown(false); } } /* To observe any change of show prop after an initial load */ onShowChange(newValue) { this.setIsShown(newValue); } onReferenceElChanged(newValue, oldValue) { if (newValue !== oldValue) { this.initialize({ referenceEl: newValue, trigger: this.trigger }); } } onTriggerChanged(newValue) { this.initialize({ referenceEl: this.referenceEl, trigger: newValue, }); } onIsShownChange(newValue) { if (newValue && this.openedByKeyboard) { // Wait for the next render cycle to ensure the popover is visible setTimeout(() => { this.focusFirstElement(); }, 0); } } focusFirstElement() { // Try to find a focusable element inside the popover const focusableElements = this.host.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'); if (focusableElements.length > 0) { focusableElements[0].focus(); } else { // If no focusable elements found, make the popover itself focusable this.host.setAttribute('tabindex', '0'); this.host.focus(); } } initialize({ referenceEl, trigger, }) { var _a, _b, _c, _d, _e, _f; this.cleanUp(); if (typeof referenceEl !== 'undefined') { this.target = referenceEl; } else { this.target = this.selector ? document.querySelector(this.selector) : null; } this.popperInstance = this.target ? createPopper(this.target, this.host, { strategy: 'fixed', placement: this.placement, modifiers: [ { name: 'offset', options: { offset: [this.offsetSkidding, this.offsetDistance], }, }, ...this.modifiers, ], }) : null; if (!this.popperInstance) { console.error(`Could not initialize: reference element not found.`); } if (trigger === 'click' && this.show === null) { (_a = this.target) === null || _a === void 0 ? void 0 : _a.addEventListener('click', this.onClickTarget); // Also handle keyboard activation via Enter and Space (_b = this.target) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.openedByKeyboard = true; this.onClickTarget(e); } }); } if (trigger === 'hover' || trigger === 'hover-popover') { // For tabbing over element (_c = this.target) === null || _c === void 0 ? void 0 : _c.addEventListener('focusin', this.handleShow); (_d = this.target) === null || _d === void 0 ? void 0 : _d.addEventListener('focusout', this.handleHide); // For hovering over element with selector (_e = this.target) === null || _e === void 0 ? void 0 : _e.addEventListener('mouseenter', this.handleShow); (_f = this.target) === null || _f === void 0 ? void 0 : _f.addEventListener('mouseleave', this.handleHide); // For hovering over Popover itself if (trigger === 'hover-popover') { this.host.addEventListener('mouseenter', this.handleShow); this.host.addEventListener('mouseleave', this.handleHide); } } } cleanUp() { var _a, _b, _c, _d, _e, _f, _g, _h, _j; (_a = this.target) === null || _a === void 0 ? void 0 : _a.removeEventListener('click', this.onClickTarget); (_b = this.target) === null || _b === void 0 ? void 0 : _b.removeEventListener('keydown', this.onClickTarget); (_c = this.target) === null || _c === void 0 ? void 0 : _c.removeEventListener('focusin', this.handleShow); (_d = this.target) === null || _d === void 0 ? void 0 : _d.removeEventListener('focusout', this.handleHide); (_e = this.target) === null || _e === void 0 ? void 0 : _e.removeEventListener('mouseenter', this.handleShow); (_f = this.target) === null || _f === void 0 ? void 0 : _f.removeEventListener('mouseleave', this.handleHide); (_g = this.host) === null || _g === void 0 ? void 0 : _g.removeEventListener('mouseenter', this.handleShow); (_h = this.host) === null || _h === void 0 ? void 0 : _h.removeEventListener('mouseleave', this.handleHide); (_j = this.popperInstance) === null || _j === void 0 ? void 0 : _j.destroy(); } connectedCallback() { if (this.selector === undefined && this.referenceEl === undefined) { this.disableLogic = true; console.warn('TDS-POPOVER-CORE: Popover internal logic disabled. Please provide a `selector` or `referenceEl` prop'); return; } this.initialize({ referenceEl: this.referenceEl, trigger: this.trigger, }); } /* To enable initial loading of a component if user controls show prop */ componentWillLoad() { // Ensure initial visibility is handled properly if (this.show === true || this.defaultShow === true) { this.setIsShown(true); } else { this.setIsShown(false); } } componentDidRender() { var _a; if (this.isShown && !this.renderedShowValue && !this.disableLogic) { // Here we update the popper position since its position is wrong // before it is rendered. (_a = this.popperInstance) === null || _a === void 0 ? void 0 : _a.update(); } this.renderedShowValue = this.isShown; } disconnectedCallback() { this.cleanUp(); } render() { const classes = { 'is-shown': (this.isShown && this.animation === 'none') || this.animation === undefined, 'is-hidden': (!this.isShown && this.animation === 'none') || this.animation === undefined, 'initially-hidden': !this.hasShownAtLeastOnce, 'tds-animation-enter-fade': this.isShown && this.animation === 'fade', 'tds-animation-exit-fade': !this.isShown && this.animation === 'fade', }; const classList = generateClassList(classes); return (h(Host, { key: '24f0a1d31e1dd556cf3552f3c7a6ede8ce35dfc2', role: this.host.getAttribute('role'), class: classList, id: `tds-popover-core-${this.uuid}` }, h("slot", { key: '3e8df41cea7f8ed636974571bd1d5e8cbd802732' }))); } static get is() { return "tds-popover-core"; } static get encapsulation() { return "scoped"; } static get originalStyleUrls() { return { "$": ["tds-popover-core.scss"] }; } static get styleUrls() { return { "$": ["tds-popover-core.css"] }; } static get properties() { return { "selector": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The CSS-selector for an element that will trigger the pop-over" }, "getter": false, "setter": false, "reflect": false, "attribute": "selector" }, "referenceEl": { "type": "unknown", "mutable": false, "complexType": { "original": "HTMLElement | null", "resolved": "HTMLElement | null | undefined", "references": { "HTMLElement": { "location": "global", "id": "global::HTMLElement" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "Element that will trigger the pop-over (takes priority over selector)" }, "getter": false, "setter": false }, "defaultShow": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Decides if the component should be visible from the start." }, "getter": false, "setter": false, "reflect": false, "attribute": "default-show", "defaultValue": "false" }, "animation": { "type": "string", "mutable": false, "complexType": { "original": "'none' | 'fade' | string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Whether the popover should animate when being opened/closed or not" }, "getter": false, "setter": false, "reflect": false, "attribute": "animation", "defaultValue": "'none'" }, "show": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean | null", "resolved": "boolean | null", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Controls whether the Popover is shown or not. If this is set hiding and showing\nwill be decided by this prop and will need to be controlled from the outside. This\nalso means that clicking outside of the popover won't close it. Takes precedence over `defaultShow` prop." }, "getter": false, "setter": false, "reflect": false, "attribute": "show", "defaultValue": "null" }, "placement": { "type": "string", "mutable": false, "complexType": { "original": "Placement", "resolved": "\"auto\" | \"auto-end\" | \"auto-start\" | \"bottom\" | \"bottom-end\" | \"bottom-start\" | \"left\" | \"left-end\" | \"left-start\" | \"right\" | \"right-end\" | \"right-start\" | \"top\" | \"top-end\" | \"top-start\"", "references": { "Placement": { "location": "import", "path": "@popperjs/core", "id": "node_modules::Placement", "referenceLocation": "Placement" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Decides the placement of the Popover Menu" }, "getter": false, "setter": false, "reflect": false, "attribute": "placement", "defaultValue": "'auto'" }, "offsetSkidding": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the offset skidding" }, "getter": false, "setter": false, "reflect": false, "attribute": "offset-skidding", "defaultValue": "0" }, "offsetDistance": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the offset distance" }, "getter": false, "setter": false, "reflect": false, "attribute": "offset-distance", "defaultValue": "8" }, "modifiers": { "type": "unknown", "mutable": false, "complexType": { "original": "object[]", "resolved": "object[]", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Array of modifier objects to pass to popper.js. See https://popper.js.org/docs/v2/modifiers/" }, "getter": false, "setter": false, "defaultValue": "[]" }, "trigger": { "type": "string", "mutable": false, "complexType": { "original": "'click' | 'hover' | 'hover-popover'", "resolved": "\"click\" | \"hover\" | \"hover-popover\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "What triggers the popover to show" }, "getter": false, "setter": false, "reflect": false, "attribute": "trigger", "defaultValue": "'click'" }, "autoHide": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Decides if the popover should hide automatically.\nAlternatevly it can be hidden externally based on emitted events." }, "getter": false, "setter": false, "reflect": false, "attribute": "auto-hide", "defaultValue": "true" } }; } static get states() { return { "renderedShowValue": {}, "popperInstance": {}, "target": {}, "isShown": {}, "disableLogic": {}, "hasShownAtLeastOnce": {}, "openedByKeyboard": {} }; } static get events() { return [{ "method": "internalTdsShow", "name": "internalTdsShow", "bubbles": true, "cancelable": false, "composed": false, "docs": { "tags": [{ "name": "internal", "text": "Show event." }], "text": "" }, "complexType": { "original": "object", "resolved": "object", "references": {} } }, { "method": "internalTdsClose", "name": "internalTdsClose", "bubbles": false, "cancelable": false, "composed": false, "docs": { "tags": [{ "name": "internal", "text": "Close event." }], "text": "" }, "complexType": { "original": "object", "resolved": "object", "references": {} } }]; } static get methods() { return { "close": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "Property for closing popover programmatically", "tags": [] } } }; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "show", "methodName": "onShowChange" }, { "propName": "referenceEl", "methodName": "onReferenceElChanged" }, { "propName": "trigger", "methodName": "onTriggerChanged" }, { "propName": "isShown", "methodName": "onIsShownChange" }]; } static get listeners() { return [{ "name": "click", "method": "onAnyClick", "target": "window", "capture": false, "passive": false }, { "name": "internalTdsShow", "method": "onTdsShow", "target": "window", "capture": false, "passive": false }, { "name": "keydown", "method": "handleKeydown", "target": "window", "capture": false, "passive": false }]; } }