UNPKG

@scania/tegel

Version:
562 lines (561 loc) 21.3 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() { this.uuid = generateUniqueId(); this.setIsShown = function 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; } }.bind(this); this.onClickTarget = function 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); }.bind(this); this.handleShow = function handleShow(event) { event.stopPropagation(); // Check if event was triggered by keyboard (tab focus) this.openedByKeyboard = event.type === 'focusin'; this.setIsShown(true); }.bind(this); this.handleHide = function handleHide(event) { event.stopPropagation(); this.setIsShown(false); }.bind(this); this.selector = undefined; this.referenceEl = undefined; this.defaultShow = false; this.animation = 'none'; this.show = null; this.placement = 'auto'; this.offsetSkidding = 0; this.offsetDistance = 8; this.modifiers = []; this.trigger = 'click'; this.autoHide = true; this.renderedShowValue = false; this.popperInstance = undefined; this.target = undefined; this.isShown = false; this.disableLogic = false; this.hasShownAtLeastOnce = false; this.openedByKeyboard = 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, }) { 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) { this.target.addEventListener('click', this.onClickTarget); // Also handle keyboard activation via Enter and Space this.target.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 this.target.addEventListener('focusin', this.handleShow); this.target.addEventListener('focusout', this.handleHide); // For hovering over element with selector this.target.addEventListener('mouseenter', this.handleShow); this.target.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() { if (this.isShown && !this.renderedShowValue && !this.disableLogic) { // Here we update the popper position since its position is wrong // before it is rendered. this.popperInstance.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: 'e00527f6f668e309529ff607c744eb64bba30c6a', role: this.host.getAttribute('role'), class: classList, id: `tds-popover-core-${this.uuid}` }, h("slot", { key: 'e562ffae5ecbca928a541de6b50f88efa1eded8c' }))); } 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", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The CSS-selector for an element that will trigger the pop-over" }, "attribute": "selector", "reflect": false }, "referenceEl": { "type": "unknown", "mutable": false, "complexType": { "original": "HTMLElement | null", "resolved": "HTMLElement", "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)" } }, "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." }, "attribute": "default-show", "reflect": false, "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" }, "attribute": "animation", "reflect": false, "defaultValue": "'none'" }, "show": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "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." }, "attribute": "show", "reflect": false, "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" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Decides the placement of the Popover Menu" }, "attribute": "placement", "reflect": false, "defaultValue": "'auto'" }, "offsetSkidding": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the offset skidding" }, "attribute": "offset-skidding", "reflect": false, "defaultValue": "0" }, "offsetDistance": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the offset distance" }, "attribute": "offset-distance", "reflect": false, "defaultValue": "8" }, "modifiers": { "type": "unknown", "mutable": false, "complexType": { "original": "Object[]", "resolved": "Object[]", "references": { "Object": { "location": "global", "id": "global::Object" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Array of modifier objects to pass to popper.js. See https://popper.js.org/docs/v2/modifiers/" }, "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" }, "attribute": "trigger", "reflect": false, "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." }, "attribute": "auto-hide", "reflect": false, "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": "{}", "resolved": "{}", "references": {} } }, { "method": "internalTdsClose", "name": "internalTdsClose", "bubbles": false, "cancelable": false, "composed": false, "docs": { "tags": [{ "name": "internal", "text": "Close event." }], "text": "" }, "complexType": { "original": "{}", "resolved": "{}", "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 }]; } }