@erplora/outfitkit
Advanced tools
| import './ok-app-launcher.js'; |
@@ -38,5 +38,10 @@ import { LitElement } from 'lit'; | ||
| private portalRoot; | ||
| private lockedNodes; | ||
| private previouslyFocused; | ||
| disconnectedCallback(): void; | ||
| private ensurePortal; | ||
| protected updated(): void; | ||
| private static supportsInert; | ||
| private lockPage; | ||
| private releasePage; | ||
| private bind; | ||
@@ -43,0 +48,0 @@ private unbind; |
@@ -19,3 +19,3 @@ import { LitElement, css, render, nothing, html } from "lit"; | ||
| }; | ||
| class OkAppLauncher extends LitElement { | ||
| const _OkAppLauncher = class _OkAppLauncher2 extends LitElement { | ||
| constructor() { | ||
@@ -31,2 +31,4 @@ super(...arguments); | ||
| this.portalRoot = null; | ||
| this.lockedNodes = []; | ||
| this.previouslyFocused = null; | ||
| } | ||
@@ -249,2 +251,3 @@ static { | ||
| this.unbind(); | ||
| this.releasePage(); | ||
| const host = this.portalRoot?.host; | ||
@@ -271,4 +274,50 @@ this.portalRoot = null; | ||
| if (!this.open && !this.portalRoot) return; | ||
| render(this.open ? this.overlayTemplate() : nothing, this.ensurePortal()); | ||
| const root = this.ensurePortal(); | ||
| render(this.open ? this.overlayTemplate() : nothing, root); | ||
| if (this.open) this.lockPage(root.host); | ||
| else this.releasePage(); | ||
| } | ||
| // `inert` is the mechanism: one attribute takes a subtree out of the accessibility tree, out of | ||
| // the tab order and out of hit-testing. Supported by every target engine (Chrome 102+, Safari | ||
| // 15.5+, Firefox 112+, and the WebViews of Tauri/Android based on them). Probed at lock time — | ||
| // not at module load — so a host that polyfills it later is honoured. | ||
| static supportsInert() { | ||
| return typeof HTMLElement !== "undefined" && "inert" in HTMLElement.prototype; | ||
| } | ||
| // Locks the page while the sheet is open: every top-level node of `<body>` except our own portal | ||
| // becomes `inert` + `aria-hidden`. Where `inert` is not supported, inline `pointer-events:none` | ||
| // keeps at least the click-blocking half of the contract explicit. | ||
| lockPage(portalHost) { | ||
| if (this.lockedNodes.length) return; | ||
| const fallback = !_OkAppLauncher2.supportsInert(); | ||
| const active = document.activeElement; | ||
| this.previouslyFocused = active instanceof HTMLElement && active !== document.body ? active : null; | ||
| for (const node of Array.from(document.body.children)) { | ||
| if (!(node instanceof HTMLElement) || node === portalHost) continue; | ||
| this.lockedNodes.push({ | ||
| el: node, | ||
| hadInert: node.hasAttribute("inert"), | ||
| prevAriaHidden: node.getAttribute("aria-hidden"), | ||
| prevPointerEvents: fallback ? node.style.pointerEvents : null | ||
| }); | ||
| node.setAttribute("inert", ""); | ||
| node.setAttribute("aria-hidden", "true"); | ||
| if (fallback) node.style.pointerEvents = "none"; | ||
| } | ||
| } | ||
| // Releases the lock restoring the EXACT previous state of every node (a node that was already | ||
| // inert stays inert) and hands focus back to where it was before opening. | ||
| releasePage() { | ||
| if (!this.lockedNodes.length) return; | ||
| for (const { el, hadInert, prevAriaHidden, prevPointerEvents } of this.lockedNodes) { | ||
| if (!hadInert) el.removeAttribute("inert"); | ||
| if (prevAriaHidden === null) el.removeAttribute("aria-hidden"); | ||
| else el.setAttribute("aria-hidden", prevAriaHidden); | ||
| if (prevPointerEvents !== null) el.style.pointerEvents = prevPointerEvents; | ||
| } | ||
| this.lockedNodes = []; | ||
| const focusTarget = this.previouslyFocused; | ||
| this.previouslyFocused = null; | ||
| if (focusTarget?.isConnected) focusTarget.focus({ preventScroll: true }); | ||
| } | ||
| bind() { | ||
@@ -385,15 +434,16 @@ document.addEventListener("keydown", this.onKeydown); | ||
| } | ||
| } | ||
| }; | ||
| __decorateClass([ | ||
| property({ attribute: false }) | ||
| ], OkAppLauncher.prototype, "apps"); | ||
| ], _OkAppLauncher.prototype, "apps"); | ||
| __decorateClass([ | ||
| property({ attribute: false }) | ||
| ], OkAppLauncher.prototype, "labels"); | ||
| ], _OkAppLauncher.prototype, "labels"); | ||
| __decorateClass([ | ||
| state() | ||
| ], OkAppLauncher.prototype, "open"); | ||
| ], _OkAppLauncher.prototype, "open"); | ||
| __decorateClass([ | ||
| state() | ||
| ], OkAppLauncher.prototype, "shown"); | ||
| ], _OkAppLauncher.prototype, "shown"); | ||
| let OkAppLauncher = _OkAppLauncher; | ||
| define("ok-app-launcher", OkAppLauncher); | ||
@@ -400,0 +450,0 @@ export { |
+1
-1
| { | ||
| "name": "@erplora/outfitkit", | ||
| "version": "0.1.32", | ||
| "version": "0.1.33", | ||
| "description": "OutfitKit — librería de Web Components (Lit) que CONSTRUYE lo que Ionic no tiene (tree, data-table rica, inline-feedback, kpi/stat, stepper/wizard, calendar, kanban…) sobre primitivos de Ionic. Ionic es la base; OutfitKit cubre los huecos. npm + CDN, imports individuales, CSP-safe. Tema vía tokens --ok-* (fallback a --ion-*).", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Sorry, the diff of this file is too big to display
2106408
0.24%293
0.34%60886
0.15%