@kelvininc/ui-components
Version:
Kelvin UI Components
429 lines (428 loc) • 15.9 kB
JavaScript
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"
}];
}
}