UNPKG

@kelvininc/ui-components

Version:
429 lines (428 loc) 15.9 kB
import { arrow, autoUpdate, computePosition, hide, offset, shift } from "@floating-ui/dom"; import { h, Host } from "@stencil/core"; import { isNil } from "lodash-es"; import { DEFAULT_OFFSET, DEFAULT_SHIFT_CONFIG, getArrowElementPositionConfig, OFFSET_WITH_ARROW, PORTAL_Z_INDEX } from "./portal.config"; import { mergeComputePositionConfigs } from "../../utils/floating-ui.helper"; export class KvPortal { constructor() { /** @inheritdoc */ this.portalId = 'kv-portal'; /** @inheritdoc */ this.show = false; /** @inheritdoc */ this.autoUpdate = true; /** @inheritdoc */ this.animated = false; /** @inheritdoc */ this.withArrow = false; /** @inheritdoc */ this.delay = 0; /** @inheritdoc */ this.zIndex = PORTAL_Z_INDEX.show; this.visible = false; this.moved = false; this.getPortalArrowElement = () => { return this.element.shadowRoot.querySelector('#portal-arrow'); }; this.getMiddlewareConfig = () => { const arrowElement = this.getPortalArrowElement(); const offSet = this.withArrow ? OFFSET_WITH_ARROW : DEFAULT_OFFSET; const middleware = [offset(offSet), shift(DEFAULT_SHIFT_CONFIG)]; if (this.withArrow) middleware.push(arrow({ element: arrowElement, padding: 5 })); middleware.push(hide({ padding: 15 })); return middleware; }; this.getOptions = () => { const middleware = this.getMiddlewareConfig(); return mergeComputePositionConfigs({ middleware }, this.options); }; } showWatch(newValue) { if (newValue) { this.showPortalContent(); } else { this.hidePortalContent(); } } referenceWatch() { if (this.show) { this.showPortalContent(); } } getClosestThemedAncestor() { let current = this.element; while (current) { const themed = current.closest('[data-theme]'); if (themed) return themed; const root = current.getRootNode(); current = root instanceof ShadowRoot ? root.host : null; } return null; } createPortal() { this.portal = document.createElement('div'); this.portal.setAttribute('id', this.portalId); this.portal.style.zIndex = `${PORTAL_Z_INDEX.hidden}`; this.portal.style.position = 'absolute'; // Inherit data-theme from nearest themed ancestor so portal // content resolves the correct design token values. // Traverse shadow DOM boundaries since the portal may be // rendered inside another component's shadow tree (e.g. tooltip). const themedAncestor = this.getClosestThemedAncestor(); const theme = themedAncestor === null || themedAncestor === void 0 ? void 0 : themedAncestor.getAttribute('data-theme'); if (theme) { this.portal.setAttribute('data-theme', theme); } document.body.prepend(this.portal); } moveElementToPortal() { this.portal.appendChild(this.element); this.elementAppend.emit(this.element); } updatePosition() { computePosition(this.reference, this.portal, this.getOptions()).then(({ x, y, placement, middlewareData }) => { if (this.autoUpdate) { const { referenceHidden } = middlewareData.hide; if (this.show) { this.visible = !referenceHidden; this.portal.style.zIndex = referenceHidden ? `${PORTAL_Z_INDEX.hidden}` : `${this.zIndex}`; } } Object.assign(this.portal.style, { left: `${x}px`, top: `${y}px` }); if (this.withArrow) { const { x: arrowX, y: arrowY } = middlewareData.arrow; Object.assign(this.getPortalArrowElement().style, Object.assign({}, getArrowElementPositionConfig(arrowX, arrowY, placement))); } }); } calculatePosition() { if (!this.reference || !this.portal) { return; } if (this.autoUpdate) { this.closeAutoUpdate = autoUpdate(this.reference, this.portal, () => this.updatePosition()); } else { this.updatePosition(); } } showPortalContent() { if (!this.portal) return; this.portal.style.zIndex = `${this.zIndex}`; if (this.delay) { this.timeoutId = window.setTimeout(() => { this.visible = true; this.calculatePosition(); }, this.delay); } else { this.visible = true; this.calculatePosition(); } } hidePortalContent() { this.visible = false; this.portal.style.zIndex = `${PORTAL_Z_INDEX.hidden}`; if (this.timeoutId) { window.clearTimeout(this.timeoutId); this.timeoutId = undefined; } if (!isNil(this.closeAutoUpdate)) { this.closeAutoUpdate(); } } componentWillLoad() { this.createPortal(); } componentDidLoad() { this.moveElementToPortal(); if (this.show) { this.showPortalContent(); } } disconnectedCallback() { var _a; this.moved ? (_a = this.portal) === null || _a === void 0 ? void 0 : _a.remove() : (this.moved = true); } render() { return (h(Host, { key: 'edcd8472eb044ae6d4475cb1f8e63151b831157c' }, h("div", { key: '1ba1eb8760e0104a688235c34a5b3ee6e59fca0b', class: { 'portal-container': true, 'portal-container--animated': this.animated, 'portal-container--visible': this.visible } }, h("slot", { key: '94ed2ad8e45ac3e9287bb5d1dd40eeccfd7fde57' }), this.withArrow && h("div", { key: '04806f60effcb03f57fd7d5344000e9ba3922582', id: "portal-arrow", class: "portal-arrow" })))); } static get is() { return "kv-portal"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["portal.scss"] }; } static get styleUrls() { return { "$": ["portal.css"] }; } static get properties() { return { "portalId": { "type": "string", "attribute": "portal-id", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "(required) The portal id" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'kv-portal'" }, "reference": { "type": "unknown", "attribute": "reference", "mutable": false, "complexType": { "original": "HTMLElement", "resolved": "HTMLElement", "references": { "HTMLElement": { "location": "global", "id": "global::HTMLElement" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "(required) The reference element" }, "getter": false, "setter": false }, "options": { "type": "unknown", "attribute": "options", "mutable": false, "complexType": { "original": "ComputePositionConfig", "resolved": "{ strategy?: Strategy; placement?: Placement; middleware?: (false | { name: string; options?: any; fn: (state: { x: number; y: number; initialPlacement: Placement; strategy: Strategy; platform: Platform; placement: Placement; middlewareData: MiddlewareData; rects: ElementRects; elements: Elements; }) => Promisable<MiddlewareReturn>; })[]; platform?: Platform; }", "references": { "ComputePositionConfig": { "location": "import", "path": "@floating-ui/dom", "id": "../../node_modules/.pnpm/@floating-ui+dom@1.6.11/node_modules/@floating-ui/dom/dist/floating-ui.dom.d.ts::ComputePositionConfig" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "(optional) options used to compute the portal position" }, "getter": false, "setter": false }, "show": { "type": "boolean", "attribute": "show", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "(optional) toggles portal visibility" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "false" }, "autoUpdate": { "type": "boolean", "attribute": "auto-update", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "(optional) enable position auto update (default true)" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "true" }, "animated": { "type": "boolean", "attribute": "animated", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "(optional) if true portal content will gradually appear (default false)" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "false" }, "withArrow": { "type": "boolean", "attribute": "with-arrow", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "(optional) if true it will render an arrow pointing to the opening element (default false)" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "false" }, "delay": { "type": "number", "attribute": "delay", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "(optional) Delay to show portal in milliseconds. (default 0)" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "0" }, "zIndex": { "type": "number", "attribute": "z-index", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "(optional) the portal z-index (default: 9004)" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "PORTAL_Z_INDEX.show" } }; } static get states() { return { "visible": {} }; } static get events() { return [{ "method": "elementAppend", "name": "elementAppend", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "inheritdoc", "text": undefined }], "text": "Emitted when the element it's appended to the DOM" }, "complexType": { "original": "HTMLElement", "resolved": "HTMLElement", "references": { "HTMLElement": { "location": "global", "id": "global::HTMLElement" } } } }]; } static get elementRef() { return "element"; } static get watchers() { return [{ "propName": "show", "methodName": "showWatch" }, { "propName": "reference", "methodName": "referenceWatch" }]; } }