@helixui/library 3.3.1-next.115 → 3.3.1-next.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +445 -276
- package/dist/components/hx-accordion/hx-accordion-item.d.ts +35 -0
- package/dist/components/hx-accordion/hx-accordion-item.d.ts.map +1 -1
- package/dist/components/hx-checkbox/hx-checkbox.d.ts +153 -1
- package/dist/components/hx-checkbox/hx-checkbox.d.ts.map +1 -1
- package/dist/components/hx-checkbox/hx-checkbox.styles.d.ts.map +1 -1
- package/dist/components/hx-checkbox/index.js +1 -1
- package/dist/components/hx-checkbox-group/hx-checkbox-group.d.ts +151 -2
- package/dist/components/hx-checkbox-group/hx-checkbox-group.d.ts.map +1 -1
- package/dist/components/hx-checkbox-group/index.js +1 -1
- package/dist/components/hx-color-picker/hx-color-picker.d.ts +163 -1
- package/dist/components/hx-color-picker/hx-color-picker.d.ts.map +1 -1
- package/dist/components/hx-color-picker/hx-color-picker.styles.d.ts.map +1 -1
- package/dist/components/hx-color-picker/index.js +1 -1
- package/dist/components/hx-combobox/hx-combobox.d.ts +311 -2
- package/dist/components/hx-combobox/hx-combobox.d.ts.map +1 -1
- package/dist/components/hx-combobox/index.js +1 -1
- package/dist/components/hx-date-picker/hx-date-picker.d.ts +182 -56
- package/dist/components/hx-date-picker/hx-date-picker.d.ts.map +1 -1
- package/dist/components/hx-date-picker/hx-date-picker.styles.d.ts.map +1 -1
- package/dist/components/hx-date-picker/index.js +1 -1
- package/dist/components/hx-dialog/hx-dialog.d.ts +240 -0
- package/dist/components/hx-dialog/hx-dialog.d.ts.map +1 -1
- package/dist/components/hx-dialog/index.js +1 -1
- package/dist/components/hx-dropdown/hx-dropdown.d.ts +80 -0
- package/dist/components/hx-dropdown/hx-dropdown.d.ts.map +1 -1
- package/dist/components/hx-dropdown/index.js +1 -1
- package/dist/components/hx-field/hx-field.d.ts +109 -0
- package/dist/components/hx-field/hx-field.d.ts.map +1 -1
- package/dist/components/hx-field/index.js +1 -1
- package/dist/components/hx-popover/hx-popover.d.ts +91 -0
- package/dist/components/hx-popover/hx-popover.d.ts.map +1 -1
- package/dist/components/hx-popover/index.js +1 -1
- package/dist/components/hx-radio-group/hx-radio-group.d.ts +152 -1
- package/dist/components/hx-radio-group/hx-radio-group.d.ts.map +1 -1
- package/dist/components/hx-radio-group/hx-radio.d.ts +14 -0
- package/dist/components/hx-radio-group/hx-radio.d.ts.map +1 -1
- package/dist/components/hx-radio-group/index.js +1 -1
- package/dist/components/hx-select/hx-select.d.ts +303 -2
- package/dist/components/hx-select/hx-select.d.ts.map +1 -1
- package/dist/components/hx-select/hx-select.styles.d.ts.map +1 -1
- package/dist/components/hx-select/index.js +1 -1
- package/dist/components/hx-side-nav/hx-nav-item.styles.d.ts.map +1 -1
- package/dist/components/hx-side-nav/index.js +1 -1
- package/dist/components/hx-switch/hx-switch.d.ts +78 -1
- package/dist/components/hx-switch/hx-switch.d.ts.map +1 -1
- package/dist/components/hx-switch/hx-switch.styles.d.ts.map +1 -1
- package/dist/components/hx-switch/index.js +1 -1
- package/dist/components/hx-toggle-button/hx-toggle-button.d.ts +110 -0
- package/dist/components/hx-toggle-button/hx-toggle-button.d.ts.map +1 -1
- package/dist/components/hx-toggle-button/hx-toggle-button.styles.d.ts.map +1 -1
- package/dist/components/hx-toggle-button/index.js +1 -1
- package/dist/components/hx-tooltip/hx-tooltip.d.ts +52 -0
- package/dist/components/hx-tooltip/hx-tooltip.d.ts.map +1 -1
- package/dist/components/hx-tooltip/index.js +1 -1
- package/dist/css/helix-all.css +98 -1
- package/dist/css/helix-forms.css +98 -1
- package/dist/css/hx-checkbox.css +18 -0
- package/dist/css/hx-color-picker.css +25 -0
- package/dist/css/hx-date-picker.css +2 -1
- package/dist/css/hx-select.css +19 -0
- package/dist/css/hx-switch.css +17 -0
- package/dist/css/hx-toggle-button.css +17 -0
- package/dist/css/index.css +1 -1
- package/dist/css/manifest.json +2 -1
- package/dist/index.js +15 -15
- package/dist/shared/aria-flatten-DY6v2vah.js +22 -0
- package/dist/shared/aria-flatten-DY6v2vah.js.map +1 -0
- package/dist/shared/aria-idref-Q0yiSR3p.js +104 -0
- package/dist/shared/aria-idref-Q0yiSR3p.js.map +1 -0
- package/dist/shared/hx-accordion-ZVzgDzTG.js.map +1 -1
- package/dist/shared/hx-checkbox-BdgoUeWi.js +696 -0
- package/dist/shared/hx-checkbox-BdgoUeWi.js.map +1 -0
- package/dist/shared/hx-checkbox-group-LWezHrvS.js +496 -0
- package/dist/shared/hx-checkbox-group-LWezHrvS.js.map +1 -0
- package/dist/shared/hx-color-picker-DVhZl88b.js +1221 -0
- package/dist/shared/hx-color-picker-DVhZl88b.js.map +1 -0
- package/dist/shared/hx-combobox-DvlezcDV.js +1359 -0
- package/dist/shared/hx-combobox-DvlezcDV.js.map +1 -0
- package/dist/shared/{hx-date-picker-2iRG1p74.js → hx-date-picker-N-0aG5XL.js} +542 -206
- package/dist/shared/hx-date-picker-N-0aG5XL.js.map +1 -0
- package/dist/shared/hx-dialog-DzB7VytW.js +717 -0
- package/dist/shared/hx-dialog-DzB7VytW.js.map +1 -0
- package/dist/shared/{hx-dropdown-LyaRc8Rf.js → hx-dropdown-DJWlF94E.js} +130 -77
- package/dist/shared/hx-dropdown-DJWlF94E.js.map +1 -0
- package/dist/shared/{hx-field-B3Qo8OLS.js → hx-field-zw0U1KVi.js} +99 -38
- package/dist/shared/hx-field-zw0U1KVi.js.map +1 -0
- package/dist/shared/{hx-nav-item-xqRPOCWX.js → hx-nav-item-CODtUlew.js} +13 -9
- package/dist/shared/{hx-nav-item-xqRPOCWX.js.map → hx-nav-item-CODtUlew.js.map} +1 -1
- package/dist/shared/{hx-popover-B-FP3-wW.js → hx-popover-CHxWY_cd.js} +123 -66
- package/dist/shared/hx-popover-CHxWY_cd.js.map +1 -0
- package/dist/shared/hx-radio-CeGzARNk.js +822 -0
- package/dist/shared/hx-radio-CeGzARNk.js.map +1 -0
- package/dist/shared/hx-select-DrcS-YRJ.js +1089 -0
- package/dist/shared/hx-select-DrcS-YRJ.js.map +1 -0
- package/dist/shared/hx-switch-BX_8uNUs.js +540 -0
- package/dist/shared/hx-switch-BX_8uNUs.js.map +1 -0
- package/dist/shared/{hx-toggle-button-iLiYrMbD.js → hx-toggle-button-Dcz9IlUm.js} +226 -65
- package/dist/shared/hx-toggle-button-Dcz9IlUm.js.map +1 -0
- package/dist/shared/{hx-tooltip-nYOv9OLu.js → hx-tooltip-DVqtKPCD.js} +68 -46
- package/dist/shared/hx-tooltip-DVqtKPCD.js.map +1 -0
- package/dist/utils/aria-flatten.d.ts +56 -0
- package/dist/utils/aria-flatten.d.ts.map +1 -0
- package/dist/utils/aria-idref.d.ts +127 -0
- package/dist/utils/aria-idref.d.ts.map +1 -0
- package/figma-inventory.json +64 -1
- package/package.json +2 -2
- package/dist/shared/hx-checkbox-D7xma9YH.js +0 -524
- package/dist/shared/hx-checkbox-D7xma9YH.js.map +0 -1
- package/dist/shared/hx-checkbox-group-C9n315Ju.js +0 -323
- package/dist/shared/hx-checkbox-group-C9n315Ju.js.map +0 -1
- package/dist/shared/hx-color-picker-uRc865FJ.js +0 -882
- package/dist/shared/hx-color-picker-uRc865FJ.js.map +0 -1
- package/dist/shared/hx-combobox-DDzqNKEW.js +0 -924
- package/dist/shared/hx-combobox-DDzqNKEW.js.map +0 -1
- package/dist/shared/hx-date-picker-2iRG1p74.js.map +0 -1
- package/dist/shared/hx-dialog-DRN_1-Y-.js +0 -514
- package/dist/shared/hx-dialog-DRN_1-Y-.js.map +0 -1
- package/dist/shared/hx-dropdown-LyaRc8Rf.js.map +0 -1
- package/dist/shared/hx-field-B3Qo8OLS.js.map +0 -1
- package/dist/shared/hx-popover-B-FP3-wW.js.map +0 -1
- package/dist/shared/hx-radio-CJvNU2yP.js +0 -621
- package/dist/shared/hx-radio-CJvNU2yP.js.map +0 -1
- package/dist/shared/hx-select-C8fEHQhC.js +0 -807
- package/dist/shared/hx-select-C8fEHQhC.js.map +0 -1
- package/dist/shared/hx-switch-BrZFaRue.js +0 -420
- package/dist/shared/hx-switch-BrZFaRue.js.map +0 -1
- package/dist/shared/hx-toggle-button-iLiYrMbD.js.map +0 -1
- package/dist/shared/hx-tooltip-nYOv9OLu.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hx-dialog-DzB7VytW.js","sources":["../../src/components/hx-dialog/hx-dialog.styles.ts","../../src/components/hx-dialog/hx-dialog.ts"],"sourcesContent":["import { css } from 'lit';\n\n/**\n * hx-dialog styles.\n *\n * Component-tier tokens with two-level var() fallback:\n * var(--hx-dialog-{prop}, var(--hx-color-{semantic}, #hex))\n * Inner hex fallbacks track the \"precision cool\" palette (3.2.0):\n * neutral-0 = #FFFFFF, neutral-100 = #EBEEE9, neutral-200 = #D6DBD5,\n * neutral-500 = #66787B, neutral-800 = #202B39, neutral-900 = #0D1825,\n * primary-500 = #429797.\n */\nexport const helixDialogStyles = css`\n :host {\n display: contents;\n }\n\n /* ─── Native dialog reset ─── */\n\n dialog {\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n max-width: 100%;\n max-height: 100%;\n overflow: visible;\n /* D5 — ensure native dialog element renders above the non-modal backdrop sibling */\n position: relative;\n z-index: calc(var(--hx-z-index-modal, 1400) + 1);\n }\n\n /* ─── Dialog container ─── */\n\n .dialog {\n display: flex;\n flex-direction: column;\n position: relative;\n background-color: var(--hx-dialog-bg, var(--hx-color-surface-default, #ffffff));\n color: var(--hx-dialog-color, var(--hx-color-text-primary, #0d1825));\n border-radius: var(--hx-dialog-border-radius, var(--hx-border-radius-lg, 0.5rem));\n box-shadow: var(--hx-dialog-shadow, var(--hx-shadow-xl, 0 20px 25px -5px rgb(0 0 0 / 0.1)));\n width: var(--hx-dialog-width, var(--hx-container-narrow, 32rem));\n max-width: calc(100vw - var(--hx-space-8, 2rem));\n max-height: calc(100vh - var(--hx-space-8, 2rem));\n overflow: hidden;\n outline: none;\n\n /* Open/close animation */\n opacity: 0;\n transform: translateY(var(--hx-space-4, 1rem)) scale(0.97);\n transition:\n opacity var(--hx-duration-normal, 200ms) var(--hx-easing-out, ease-out),\n transform var(--hx-duration-normal, 200ms) var(--hx-easing-out, ease-out);\n }\n\n dialog[open] .dialog {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .dialog {\n transition: none;\n }\n\n .dialog__close-btn {\n transition: none;\n }\n }\n\n /* ─── Native backdrop (modal mode) ─── */\n\n dialog::backdrop {\n background-color: var(\n --hx-dialog-backdrop-color,\n var(--hx-color-surface-overlay, rgba(0, 0, 0, 0.75))\n );\n opacity: 0;\n transition: opacity var(--hx-duration-normal, 200ms) var(--hx-easing-out, ease-out);\n }\n\n dialog[open]::backdrop {\n opacity: var(--hx-dialog-backdrop-opacity, 0.5);\n }\n\n @media (prefers-reduced-motion: reduce) {\n dialog::backdrop {\n transition: none;\n }\n }\n\n /* ─── Non-modal backdrop overlay ─── */\n\n .dialog-backdrop {\n position: fixed;\n inset: 0;\n background-color: var(\n --hx-dialog-backdrop-color,\n var(--hx-color-surface-overlay, rgba(0, 0, 0, 0.75))\n );\n opacity: var(--hx-dialog-backdrop-opacity, 0.5);\n /* D5 — backdrop z-index must be lower than the dialog element's z-index */\n z-index: var(--hx-z-index-modal, 1400);\n }\n\n /* ─── Header ─── */\n\n .dialog__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--hx-dialog-header-padding, var(--hx-space-5, 1.25rem) var(--hx-space-6, 1.5rem));\n border-bottom: var(--hx-border-width-thin, 1px) solid\n var(--hx-dialog-header-border-color, var(--hx-color-border-default, #d6dbd5));\n gap: var(--hx-space-4, 1rem);\n flex-shrink: 0;\n }\n\n .dialog__heading {\n margin: 0;\n font-family: var(--hx-dialog-font-family, var(--hx-font-family-sans, sans-serif));\n font-size: var(--hx-font-size-lg, 1.125rem);\n font-weight: var(--hx-font-weight-semibold, 600);\n line-height: var(--hx-line-height-tight, 1.25);\n color: var(--hx-dialog-heading-color, var(--hx-color-text-primary, #0d1825));\n flex: 1 1 auto;\n }\n\n /* ─── Built-in close button (D17) ─── */\n\n .dialog__close-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n /* WCAG 2.5.5 (healthcare mandate): minimum 44x44px touch target */\n min-width: var(--hx-touch-target-min, 2.75rem);\n min-height: var(--hx-touch-target-min, 2.75rem);\n width: var(--hx-touch-target-min, 2.75rem);\n height: var(--hx-touch-target-min, 2.75rem);\n padding: 0;\n margin-inline-start: auto;\n background: transparent;\n border: none;\n border-radius: var(--hx-border-radius-sm, 0.25rem);\n cursor: pointer;\n color: var(--hx-dialog-close-btn-color, var(--hx-color-text-muted, #4a5362));\n font-size: var(--hx-font-size-xl, 1.25rem);\n line-height: 1; /* intentional literal: icon button needs line-height 1; no token maps to exactly 1 */\n transition:\n color var(--hx-duration-fast, 100ms) ease,\n background-color var(--hx-duration-fast, 100ms) ease;\n }\n\n .dialog__close-btn::before {\n content: '×';\n }\n\n .dialog__close-btn:hover {\n color: var(--hx-dialog-close-btn-hover-color, var(--hx-color-text-primary, #0d1825));\n background-color: var(--hx-dialog-close-btn-hover-bg, var(--hx-color-surface-sunken, #ebeee9));\n }\n\n .dialog__close-btn:focus-visible {\n outline: var(--hx-focus-ring-width, 2px) solid\n var(--hx-dialog-close-btn-focus-ring-color, var(--hx-focus-ring-color, #0f7078));\n outline-offset: var(--hx-focus-ring-offset, 2px);\n }\n\n /* ─── Body ─── */\n\n .dialog__body {\n flex: 1 1 auto;\n padding: var(--hx-dialog-body-padding, var(--hx-space-6, 1.5rem));\n overflow-y: auto;\n overscroll-behavior: contain;\n }\n\n /* ─── Footer ─── */\n\n .dialog__footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--hx-space-3, 0.75rem);\n padding: var(--hx-dialog-footer-padding, var(--hx-space-4, 1rem) var(--hx-space-6, 1.5rem));\n border-top: var(--hx-border-width-thin, 1px) solid\n var(--hx-dialog-footer-border-color, var(--hx-color-border-default, #d6dbd5));\n flex-shrink: 0;\n }\n\n /* ─── Visually-hidden description (D8) ─── */\n\n .dialog__description {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n\n /* ─── Forced Colors (Windows High Contrast) ─── */\n /* Belt-and-suspenders: rich per-class HC overrides PLUS the forcedColorsSurface mixin. */\n\n @media (forced-colors: active) {\n .dialog {\n border: 1px solid CanvasText;\n }\n\n .dialog__header {\n border-bottom-color: CanvasText;\n }\n\n .dialog__footer {\n border-top-color: CanvasText;\n }\n\n .dialog__close-btn {\n color: ButtonText;\n border: 1px solid ButtonText;\n }\n }\n`;\n","import { html, nothing, type PropertyValues } from 'lit';\nimport '../../utilities/document-token-adoption.js';\nimport { customElement, property, query, state } from 'lit/decorators.js';\nimport { lockBodyScroll, unlockBodyScroll } from '../../utils/body-scroll-lock.js';\nimport { HelixElement, createIdCounter } from '../../base/index.js';\nimport { helixDialogStyles } from './hx-dialog.styles.js';\nimport { forcedColorsSurface } from '../../styles/forced-colors.js';\nimport { devWarn } from '../../utils/dev-warn.js';\nimport { flattenAccName } from '../../utils/aria-flatten.js';\nimport {\n installAriaIdrefMirror,\n resolveIdrefTokens,\n supportsIdrefElementReferences,\n type AriaIdrefMirrorHandle,\n} from '../../utils/aria-idref.js';\n\nconst _nextDialogId = createIdCounter('hx-dialog');\n\n// Module-level constant avoids rebuilding the selector string on every _getFocusableElements call.\n// Pattern matches hx-drawer's FOCUSABLE_SELECTORS constant at module scope.\nconst FOCUSABLE_SELECTORS = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n 'details > summary',\n].join(',');\n\n/**\n * A modal and non-modal dialog component built on the native HTML `<dialog>` element.\n * Provides focus trapping, backdrop interaction, keyboard navigation, and full\n * ARIA labelling for enterprise healthcare accessibility requirements.\n *\n * ## Architecture Note: Host-Canonical ARIA (group-4a round-1, Path A — native-dialog adaptation)\n *\n * Unlike `hx-drawer` (which uses an inner `<div role=\"dialog\">` and can fully\n * host-canonicalize the role), `hx-dialog` is built on the native\n * `<dialog>` HTMLDialogElement. The native element has an **implicit\n * `role=\"dialog\"`** baked in by the browser that **cannot be stripped**, so\n * full host-canonical role takeover would create nested-dialog announcements.\n *\n * **Path A (adopted):** the host owns label / description projection via\n * `ElementInternals` (`internals.ariaLabelledByElements`,\n * `internals.ariaDescribedByElements`, `internals.ariaLabel`) but **does NOT**\n * set `internals.role`. The native inner `<dialog>` continues to be the\n * announced surface. Consumer light-DOM IDREFs project across the shadow\n * boundary via `internals.aria*Elements` on the host.\n *\n * **Hybrid fallback (always-on belt-and-suspenders):** because some assistive\n * technologies may walk the native `<dialog>` first and ignore host\n * `internals.aria*Elements`, the resolved label / description text is **also**\n * serialized into `aria-label` / `aria-describedby` on the inner native\n * `<dialog>` element. Consumers therefore get name/description on every AT,\n * with the IDL-ref path providing live DOM-text-update tracking when the AT\n * honours it. This forfeits live-text tracking on the inner-dialog fallback\n * (the serialized text is recomputed on every sync, which is good enough since\n * mutation observers re-fire `_syncHostAriaSemantics` on consumer text edits).\n *\n * Why we do NOT set `internals.role = 'alertdialog'` either: setting role on\n * the host while the native `<dialog>` keeps `role=\"dialog\"` would announce\n * BOTH a host alertdialog AND an inner dialog. Instead, the alertdialog\n * variant continues to write `role=\"alertdialog\"` directly on the inner\n * `<dialog>` element (the platform allows overriding the implicit `dialog`\n * role with the more specific `alertdialog`).\n *\n * Naming precedence (W3C AccName 1.2 §4.3.1):\n *\n * 1. Consumer `aria-labelledby` on the host — IDREFs resolved across the\n * shadow boundary via `resolveIdrefTokens` (closest scope first, then\n * ancestor shadow hosts, then owner document).\n * 2. Consumer `aria-label` on the host.\n * 3. `<slot name=\"header\">` text content (multi-node aggregation per\n * AccName 1.2 §4.3.10 — decorative `aria-hidden` / `[hidden]` subtrees\n * contribute zero to the name).\n * 4. `heading` property — explicit author-provided heading text.\n * 5. Hard-coded literal `\"Dialog\"` (last-resort accessible name).\n *\n * Description channel: the host's `internals.ariaDescribedByElements` carries\n * the resolved IDREF chain on the modern path. The inner native `<dialog>` ALSO\n * receives a serialized `aria-describedby` chain — when a consumer description\n * resolves, a synthesized in-shadow `<span id=\"${id}-consumer-desc\">` is\n * appended to the existing `description` span (if any) and the inner\n * `<dialog>`'s `aria-describedby` references both same-root ids. `aria-description`\n * is intentionally NEVER written — W3C AccName ignores it whenever\n * `aria-describedby` is also present.\n *\n * Slot mutation observers track:\n * 1. The header slot's text content (in-place i18n re-renders).\n * 2. Consumer-resolved external IDREF targets (so a consumer mutating\n * `<label id=\"x\">Patient</label>` in place re-flows the name).\n * 3. Host attribute mutations (delegated to `installAriaIdrefMirror`,\n * which also catches late-inserted IDREF targets and id renames in\n * every relevant root).\n * 4. Authentic consumer `aria-describedby` retraction (oldValue !== null &&\n * newValue === null) via a dedicated `attributeOldValue: true` observer.\n *\n * **First-paint slot state seeding intentionally omitted:** seeding\n * `_hasHeaderSlot` / `_headerSlotText` from `firstUpdated()` would schedule an\n * extra Lit re-render that subtly reorders the open-dialog promise chain\n * (`updateComplete.then(...) → showModal() → updateComplete.then(...) →\n * focus first focusable`). On Chromium, that reordering interleaves the\n * native dialog's modal activation with the focus-restore step and causes\n * focus-trap test failures. The slotchange handler runs one microtask later\n * and `_syncHostAriaSemantics()` from `updated()` picks up the resolved state\n * on the next paint — close enough that AT never observes the unnamed window.\n * Mirrors the same intentional decision documented in hx-drawer round-1.\n *\n * Focus trap, ESC dismiss with `hx-cancel` BEFORE `hx-close`, focus-restore\n * via `_triggerElement`, and native `showModal()` semantics are unchanged\n * from the pre-host-canonical implementation.\n *\n * @summary Accessible dialog overlay for confirmations, forms, and detailed content.\n *\n * @tag hx-dialog\n *\n * @slot - Default slot for the dialog body content.\n * @slot header - Slot for custom header content. When provided, replaces the built-in heading.\n * @slot footer - Slot for action buttons or footer content.\n *\n * @fires {CustomEvent<void>} hx-open - Fired when the dialog opens.\n * @fires {CustomEvent<void>} hx-close - Fired when the dialog closes for any reason.\n * @fires {CustomEvent<void>} hx-cancel - Fired when the dialog is dismissed via Escape key or cancel action.\n *\n * **Event naming rationale:** hx-dialog intentionally uses `hx-open`/`hx-close`/`hx-cancel`\n * instead of the `hx-show`/`hx-hide`/`hx-after-show`/`hx-after-hide` pattern used by overlay\n * components (hx-drawer, hx-popover, hx-tooltip). This aligns with the native `<dialog>`\n * element's `close` and `cancel` events and communicates that the dialog is a stateful container\n * (open/closed) rather than a transient visibility toggle (show/hide).\n *\n * @csspart dialog - The inner container div that holds the dialog content.\n * @csspart backdrop - The non-modal backdrop overlay element.\n * @csspart header - The header region containing the heading and header slot.\n * @csspart close-button - The built-in close button in the dialog header.\n * @csspart body - The scrollable body region containing the default slot.\n * @csspart footer - The footer region containing the footer slot.\n *\n * @cssprop [--hx-dialog-bg=var(--hx-color-neutral-0)] - Dialog background color.\n * @cssprop [--hx-dialog-color=var(--hx-color-neutral-900)] - Dialog text color.\n * @cssprop [--hx-dialog-border-radius=var(--hx-border-radius-lg)] - Dialog corner radius.\n * @cssprop [--hx-dialog-shadow=var(--hx-shadow-xl)] - Dialog box shadow.\n * @cssprop [--hx-dialog-width=32rem] - Dialog width.\n * @cssprop [--hx-dialog-backdrop-color=var(--hx-color-neutral-900)] - Backdrop overlay color.\n * @cssprop [--hx-dialog-backdrop-opacity=0.5] - Backdrop overlay opacity (set to 0 to hide; note\n * that opacity:0 makes the backdrop invisible but still present in the layout — use pointer-events\n * carefully if you need a fully non-blocking backdrop).\n * @cssprop [--hx-dialog-header-padding] - Padding inside the dialog header.\n * @cssprop [--hx-dialog-header-border-color=var(--hx-color-neutral-200)] - Header bottom border color.\n * @cssprop [--hx-dialog-heading-color=var(--hx-color-neutral-900)] - Heading text color.\n * @cssprop [--hx-dialog-body-padding] - Padding inside the dialog body.\n * @cssprop [--hx-dialog-footer-padding] - Padding inside the dialog footer.\n * @cssprop [--hx-dialog-footer-border-color=var(--hx-color-neutral-200)] - Footer top border color.\n *\n * @remarks\n * **Browser support for `::backdrop`:** The `dialog::backdrop` pseudo-element inside Shadow DOM\n * is well-supported in Chrome/Chromium and Firefox 122+. For Firefox < 122, modal backdrop\n * animation will silently fall back to no animation. A non-modal backdrop fallback is rendered\n * for non-modal dialogs.\n *\n * **Drupal integration:** This component is Twig-renderable via attributes (`heading`, `open`,\n * `modal`, `close-on-backdrop`). For trigger-button wiring in Drupal behaviors:\n * ```js\n * Drupal.behaviors.hxDialog = {\n * attach(context) {\n * context.querySelectorAll('[data-hx-dialog-trigger]').forEach((btn) => {\n * btn.addEventListener('click', () => {\n * const id = btn.getAttribute('data-hx-dialog-trigger');\n * document.getElementById(id)?.showModal();\n * });\n * });\n * },\n * };\n * ```\n * Focus restoration to the trigger element is handled automatically by the component.\n * @cssprop [--hx-z-index-modal] - Z-index layer.\n * @cssprop [--hx-color-neutral-0] - Color.\n * @cssprop [--hx-color-neutral-900] - Color.\n * @cssprop [--hx-border-radius-lg] - CSS custom property.\n * @cssprop [--hx-shadow-xl] - Box shadow.\n * @cssprop [--hx-container-narrow] - CSS custom property.\n * @cssprop [--hx-space-8] - Spacing token.\n * @cssprop [--hx-space-4] - Spacing token.\n * @cssprop [--hx-duration-normal] - Animation duration.\n * @cssprop [--hx-easing-out] - CSS custom property.\n * @cssprop [--hx-space-5] - Spacing token.\n * @cssprop [--hx-space-6] - Spacing token.\n * @cssprop [--hx-border-width-thin] - Width.\n * @cssprop [--hx-color-neutral-200] - Color.\n * @cssprop [--hx-dialog-font-family=var(--hx-font-family-sans)] - CSS custom property.\n * @cssprop [--hx-font-family-sans] - Font family.\n * @cssprop [--hx-font-size-lg] - Font size.\n * @cssprop [--hx-font-weight-semibold] - Font weight.\n * @cssprop [--hx-line-height-tight] - Line height.\n * @cssprop [--hx-touch-target-min] - Minimum touch target size.\n * @cssprop [--hx-border-radius-sm] - CSS custom property.\n * @cssprop [--hx-color-neutral-500] - Color.\n * @cssprop [--hx-font-size-xl] - Font size.\n * @cssprop [--hx-duration-fast] - Animation duration.\n * @cssprop [--hx-color-neutral-100] - Color.\n * @cssprop [--hx-focus-ring-width] - Width.\n * @cssprop [--hx-dialog-close-btn-focus-ring-color] - Color.\n * @cssprop [--hx-focus-ring-color] - Color.\n * @cssprop [--hx-color-primary-500] - Color.\n * @cssprop [--hx-focus-ring-offset] - CSS custom property.\n * @cssprop [--hx-space-3] - Spacing token.\n */\n@customElement('hx-dialog')\nexport class HelixDialog extends HelixElement {\n static override styles = [helixDialogStyles, forcedColorsSurface];\n\n // D10 — observe aria-label attribute without shadowing ARIAMixin.ariaLabel\n static override get observedAttributes(): string[] {\n return [...super.observedAttributes, 'aria-label'];\n }\n\n /**\n * Test seam: when set to `true` or `false`, overrides the platform\n * `supportsIdrefElementReferences` probe before `connectedCallback`\n * seeds `_supportsIdrefRefs`. Mirrors hx-drawer round-1 — tests must\n * select the path BEFORE the host connects so synthetic environments\n * match a legacy engine. Production code MUST NOT touch this field.\n * It is `static` so the cleanup is global and obvious.\n * @internal\n */\n static __testSupportsIdrefRefsOverride: boolean | null = null;\n\n // ─── Queries ───\n\n /** @internal */\n @query('dialog')\n private _dialogEl: HTMLDialogElement | null | undefined;\n\n // ─── Internal state ───\n\n /** Tracks whether a header slot has been assigned content. * @internal\n */\n @state()\n private _hasHeaderSlot = false;\n\n /** Tracks whether a footer slot has been assigned content. * @internal\n */\n @state()\n private _hasFooterSlot = false;\n\n /** Cached focusable elements — populated on open, cleared on close. */\n /** @internal */\n private _cachedFocusableElements: HTMLElement[] = [];\n\n /**\n * Guards against re-entrant open/close calls within a single async open cycle.\n *\n * STATE MANAGEMENT CONTRACT\n * ─────────────────────────\n * `this.open` (the Lit property) is the single source of truth for dialog open state.\n * All native `<dialog>` state changes (`showModal()`, `show()`, `close()`) flow exclusively\n * from `updated()` → `_openDialog()` / `_closeDialog()`. External callers MUST only set\n * `this.open`; they must never call native dialog methods directly.\n *\n * `_isTransitioning` is set to `true` at the start of `_openDialog()` to prevent a second\n * open call from running concurrently while the first is awaiting `updateComplete`. It is\n * cleared synchronously after the async tail completes. A 200 ms fallback timeout ensures\n * the flag is always released even if `updateComplete` never resolves (e.g. detached DOM).\n *\n * `_closeDialog()` does NOT use `_isTransitioning` as a guard — it always runs immediately\n * to honour a `this.open = false` that arrives during the open async tail. The open async\n * tail checks `this.open` before touching focus so it can abort cleanly.\n */\n /** @internal */\n private _isTransitioning = false;\n\n /** Fallback timer that releases `_isTransitioning` if the open async tail never fires. */\n /** @internal */\n private _transitionFallbackTimer: ReturnType<typeof setTimeout> | null = null;\n\n /** The element that had focus when the dialog opened — restored on close (D1). */\n /** @internal */\n private _triggerElement: HTMLElement | null = null;\n\n /** Pending returnValue to pass to native dialog.close() (D11). */\n /** @internal */\n private _pendingReturnValue: string | undefined = undefined;\n\n // ─── Unique IDs for aria-labelledby / aria-describedby ───\n\n /** @internal */\n private readonly _dialogId = _nextDialogId();\n /** @internal */\n private readonly _headingId = `${this._dialogId}-heading`;\n /** @internal */\n private readonly _descriptionId = `${this._dialogId}-description`;\n /**\n * Id of the synthesized in-shadow span that mirrors consumer-resolved\n * description text. Belt-and-suspenders: the host's\n * `internals.ariaDescribedByElements` carries the live element references on\n * the modern path; this in-shadow span is the fallback referenced via\n * `aria-describedby` on the inner native `<dialog>` so AT that walks the\n * native dialog first (and ignores host IDL refs) still finds an\n * announceable description.\n * @internal\n */\n private readonly _consumerDescId = `${this._dialogId}-consumer-desc`;\n /**\n * Id of the synthesized in-shadow span that mirrors the resolved accessible\n * NAME when consumer IDREFs / slotted header text need to be projected onto\n * the inner native `<dialog>`'s `aria-labelledby`. The native dialog cannot\n * cross the shadow boundary to resolve light-DOM ids, so we surface a\n * same-shadow-root span carrying the flattened text. The host\n * `internals.ariaLabelledByElements` continues to carry live IDL refs on the\n * modern path; this span is the hybrid-fallback target. `aria-label` carries\n * the same string as a second redundancy when no labelled-by chain exists.\n * @internal\n */\n private readonly _consumerLabelId = `${this._dialogId}-consumer-label`;\n\n // ─── Host-canonical ARIA state ───\n\n /**\n * Whether the runtime exposes IDL element references on ElementInternals.\n * Drives the modern-vs-fallback ARIA projection in `_syncHostAriaSemantics`.\n * @internal\n */\n @state() private _supportsIdrefRefs = true;\n\n /**\n * Direct references to ALL labellable elements projected into\n * `<slot name=\"header\">`. Aggregates every assigned element so composed\n * headers (e.g. `<svg slot=\"header\" aria-hidden=\"true\">…</svg><span slot=\"header\">Patient</span>`)\n * project the FULL visible label via `internals.ariaLabelledByElements`\n * while `flattenAccName` strips the decorative subtree per AccName 1.2.\n * @internal\n */\n private _slottedHeaderEls: Element[] = [];\n\n /**\n * Flattened text content of the slotted header nodes, used for the no-IDL-ref\n * fallback `internals.ariaLabel` and the inner-dialog hybrid `aria-label`.\n * @internal\n */\n @state() private _headerSlotText = '';\n\n /**\n * Most recently observed consumer-supplied `aria-labelledby` token list on\n * the host. Refreshed every sync via `getAttribute()` — the host attribute\n * IS the live source of truth, so `removeAttribute` is observable on the\n * next sync (it returns `null`).\n * @internal\n */\n private _consumerLabelledBy: string | null = null;\n /** @internal — see `_consumerLabelledBy`. */\n private _consumerDescribedBy: string | null = null;\n\n /**\n * Handle for the shared IDREF mirror. See `installAriaIdrefMirror()`.\n * @internal\n */\n private _ariaMirror: AriaIdrefMirrorHandle | null = null;\n\n /**\n * Watches in-place text mutations on the assigned slotted header nodes\n * (e.g. consumer i18n re-renders that mutate `textContent` instead of\n * replacing the node). `slotchange` does NOT fire on descendant text\n * mutations, so this observer is the only signal that keeps the host's\n * accessible name synchronized with the visible header text.\n * @internal\n */\n private _headerSlotTextObserver: MutationObserver | null = null;\n\n /**\n * Watches in-place text / visibility mutations on consumer light-DOM\n * elements resolved from host `aria-labelledby` / `aria-describedby`.\n * Reinstalled on every sync against the deduped union of resolved\n * elements; disconnects automatically when the consumer retracts both\n * IDREF chains.\n * @internal\n */\n private _externalRefsObserver: MutationObserver | null = null;\n\n /**\n * Dedicated host observer scoped to `aria-describedby` with\n * `attributeOldValue: true`. Catches authentic consumer retraction\n * (oldValue !== null && newValue === null) so the cached baseline\n * follows the live attribute.\n * @internal\n */\n private _hostDescribedByObserver: MutationObserver | null = null;\n\n // ─── Public Properties ───\n\n /**\n * Controls whether the dialog is open.\n * @attr open\n */\n @property({ type: Boolean, reflect: true })\n open = false;\n\n /**\n * When true, dialog renders as a modal with backdrop and focus trap using the native\n * `showModal()` API. When false (default), dialog renders as a non-modal overlay using\n * the native `show()` API. Defaults to false, consistent with HTML boolean attribute\n * semantics (absent = false, present = true).\n * @attr modal\n */\n @property({ type: Boolean, reflect: true })\n modal = false;\n\n /**\n * When true, clicking the backdrop closes the dialog.\n * @attr close-on-backdrop\n */\n @property({\n attribute: 'close-on-backdrop',\n reflect: true,\n converter: {\n fromAttribute: (value: string | null) => value !== 'false',\n toAttribute: (value: boolean) => String(value),\n },\n })\n closeOnBackdrop = true;\n\n /**\n * Text content for the dialog heading. Used as the accessible label via aria-labelledby.\n * @attr heading\n */\n @property({ type: String, reflect: true })\n heading = '';\n\n /**\n * ARIA role variant. Use `'alertdialog'` for urgent dialogs requiring immediate attention\n * (e.g., drug interaction warnings, critical lab alerts). Defaults to `'dialog'`.\n * @attr variant\n */\n @property({ type: String, reflect: true })\n variant: 'dialog' | 'alertdialog' = 'dialog';\n\n /**\n * Optional description text linked to the dialog via `aria-describedby`.\n * When provided, screen readers will announce this text when the dialog receives focus.\n * Recommended for dialogs that surface critical clinical information.\n * @attr description\n */\n @property({ type: String })\n description = '';\n\n /** Accessible label for the close button. Override for localized text. */\n @property({ type: String, attribute: 'label-close' })\n labelClose = 'Close dialog';\n\n /**\n * Returns the dialog's return value — the string passed to `close(returnValue)`.\n * Mirrors `HTMLDialogElement.returnValue`.\n */\n get returnValue(): string {\n return this._dialogEl?.returnValue ?? '';\n }\n\n // ─── Lifecycle ───\n\n // D10 — re-render when aria-label attribute changes (without declaring a shadowing property)\n override attributeChangedCallback(\n name: string,\n oldVal: string | null,\n newVal: string | null,\n ): void {\n super.attributeChangedCallback(name, oldVal, newVal);\n if (name === 'aria-label' && oldVal !== newVal) {\n this.requestUpdate('aria-label', oldVal);\n }\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n\n // Honour the static test override so synthetic environments choose the\n // path BEFORE connect runs.\n const ctor = this.constructor as typeof HelixDialog;\n this._supportsIdrefRefs =\n ctor.__testSupportsIdrefRefsOverride !== null\n ? ctor.__testSupportsIdrefRefsOverride\n : supportsIdrefElementReferences(this._internals);\n\n // ARCHITECTURE NOTE — Path A for native `<dialog>`:\n // We deliberately do NOT set `internals.role` here. The native inner\n // `<dialog>` already has an implicit `role=\"dialog\"` baked in by the\n // browser and that role cannot be stripped. Setting `internals.role` on\n // the host would create nested-dialog announcements (host=dialog +\n // inner=dialog) on AT that honour both surfaces. The native dialog stays\n // the announced surface; the host only carries the LABEL/DESCRIPTION\n // projection chain via `internals.aria*Elements`. Likewise we do not set\n // `internals.ariaModal` — the native dialog's `showModal()` already\n // declares modality at the platform level.\n\n // Install the dedicated `aria-describedby` retraction observer BEFORE the\n // seeded `_syncHostAriaSemantics()` call below — mirrors hx-drawer round-1\n // (and hx-combobox round-10 finding 1) — so authentic consumer clears\n // propagate immediately instead of waiting for the next render.\n this._hostDescribedByObserver = new MutationObserver((records) => {\n let consumerCleared = false;\n for (const record of records) {\n if (record.attributeName !== 'aria-describedby') continue;\n const oldValue = record.oldValue;\n const newValue = this.getAttribute('aria-describedby');\n if (oldValue !== null && newValue === null) {\n this._consumerDescribedBy = null;\n consumerCleared = true;\n }\n }\n if (consumerCleared) {\n this._syncHostAriaSemantics();\n }\n });\n this._hostDescribedByObserver.observe(this, {\n attributes: true,\n attributeFilter: ['aria-describedby'],\n attributeOldValue: true,\n });\n\n // Seed root-independent semantics from connect so the host's accessible\n // name projects before first paint. The mirror's initial sync also fires\n // synchronously inside `installAriaIdrefMirror`.\n this._syncHostAriaSemantics();\n this._ariaMirror = installAriaIdrefMirror(this, () => {\n this._syncHostAriaSemantics();\n });\n }\n\n override firstUpdated(): void {\n // Warn when no accessible heading is available.\n // _hasHeaderSlot is maintained by the slotchange handler; check it here\n // on first paint so a missing heading triggers the dev warning immediately.\n if (!this.heading.trim() && !this._hasHeaderSlot) {\n devWarn(\n 'hx-dialog',\n 'No heading or header slot provided. Dialog will use a fallback aria-label. Provide a `heading` attribute or populate the `header` slot for a descriptive accessible name.',\n );\n }\n // Intentionally NOT seeding `_hasHeaderSlot` / `_headerSlotText` from\n // firstUpdated. See the architecture note on the class JSDoc — proactive\n // seeding here schedules an extra Lit re-render that subtly reorders the\n // open-dialog promise chain (`updateComplete.then(...) → showModal() →\n // updateComplete.then(...) → focus first focusable`). On Chromium that\n // reordering interleaves modal activation with the focus-restore step\n // and breaks focus-trap test assertions. The slotchange handler runs one\n // microtask later and `_syncHostAriaSemantics()` from `updated()` picks\n // up the resolved state on the very next paint — close enough that AT\n // never observes the unnamed window.\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this._clearTransitionFallback();\n this._isTransitioning = false;\n this._removeGlobalListeners();\n // Restore body scroll if disconnected while open\n if (this.modal && this.open) {\n unlockBodyScroll();\n }\n this._ariaMirror?.disconnect();\n this._ariaMirror = null;\n this._headerSlotTextObserver?.disconnect();\n this._headerSlotTextObserver = null;\n this._externalRefsObserver?.disconnect();\n this._externalRefsObserver = null;\n this._hostDescribedByObserver?.disconnect();\n this._hostDescribedByObserver = null;\n }\n\n override updated(changedProperties: PropertyValues<this>): void {\n super.updated(changedProperties);\n\n if (changedProperties.has('open')) {\n if (this.open) {\n this._openDialog();\n } else {\n this._closeDialog();\n }\n }\n\n // Re-sync host ARIA on every update — `heading` / `description` /\n // `_hasHeaderSlot` / `_headerSlotText` / consumer attributes can all\n // change between renders and the projection is the SSOT for AT.\n this._syncHostAriaSemantics();\n }\n\n // ─── Public Methods ───\n\n /** Opens the dialog in the mode determined by the `modal` property. */\n show(): void {\n this.open = true;\n }\n\n /** Opens the dialog as a modal regardless of the `modal` property setting. */\n showModal(): void {\n this.modal = true;\n this.open = true;\n }\n\n /**\n * Closes the dialog.\n * @param returnValue - Optional return value string stored as `dialog.returnValue`.\n */\n close(returnValue?: string): void {\n if (returnValue !== undefined) {\n this._pendingReturnValue = returnValue;\n }\n this.open = false;\n }\n\n // ─── Private: Open / Close ───\n\n /** Clears the fallback timer that releases `_isTransitioning`. @internal */\n private _clearTransitionFallback(): void {\n if (this._transitionFallbackTimer !== null) {\n clearTimeout(this._transitionFallbackTimer);\n this._transitionFallbackTimer = null;\n }\n }\n\n /** @internal */\n private _openDialog(): void {\n const dialog = this._dialogEl;\n if (!dialog) return;\n\n // Guard: already open in the native dialog — nothing to do.\n if (dialog.open) return;\n\n // Guard: re-entrant call during our own async open tail — skip.\n if (this._isTransitioning) return;\n\n this._isTransitioning = true;\n\n // 200 ms fallback — releases the transitioning flag if updateComplete never\n // resolves (e.g. component detached mid-cycle or in a test environment that\n // does not flush promises). Prevents the dialog from getting permanently stuck.\n this._clearTransitionFallback();\n this._transitionFallbackTimer = setTimeout(() => {\n this._transitionFallbackTimer = null;\n this._isTransitioning = false;\n }, 200);\n\n // D1 — store the element that triggered the dialog open for focus restoration on close\n const active = document.activeElement;\n this._triggerElement = active instanceof HTMLElement ? active : null;\n\n if (this.modal) {\n // showModal() throws if the dialog is already in the DOM as open — guard above\n // ensures dialog.open is false before reaching here.\n dialog.showModal();\n // D4 — lock body scroll when modal dialog is open. Uses a shared reference-counted\n // lock so that simultaneous hx-dialog / hx-drawer instances don't clobber each other\n // when one closes before the other (see utils/body-scroll-lock.ts).\n lockBodyScroll();\n } else {\n dialog.show();\n }\n\n this._addGlobalListeners();\n\n // Cache focusable elements after the dialog is open in the DOM.\n void this.updateComplete.then(() => {\n // Cancel if `this.open` was set to false during this async tail — `_closeDialog`\n // already ran synchronously and we must not clobber its state.\n this._clearTransitionFallback();\n this._isTransitioning = false;\n if (!this.open) return;\n\n this._cachedFocusableElements = this._getFocusableElements();\n // D3 — explicitly move initial focus to the first focusable element inside the dialog\n // (browser's built-in focus delegation cannot reach slotted light DOM through Shadow DOM)\n this._cachedFocusableElements[0]?.focus();\n });\n\n this.dispatchEvent(\n new CustomEvent<void>('hx-open', {\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n /** @internal */\n private _closeDialog(): void {\n const dialog = this._dialogEl;\n if (!dialog) return;\n\n // Guard: already closed in the native dialog — nothing to do, but still\n // release any stuck transitioning state so the next open can proceed.\n if (!dialog.open) {\n // Release transitioning lock in case we are in the open async tail.\n this._clearTransitionFallback();\n this._isTransitioning = false;\n return;\n }\n\n // Close always wins over a concurrent open async tail. We clear the\n // transitioning flag and cancel the fallback so the open tail's own\n // early-return check (`if (!this.open) return`) fires correctly.\n this._clearTransitionFallback();\n this._isTransitioning = false;\n\n // D11 — forward returnValue to native dialog.close() if provided\n if (this._pendingReturnValue !== undefined) {\n dialog.close(this._pendingReturnValue);\n this._pendingReturnValue = undefined;\n } else {\n dialog.close();\n }\n\n // D4 — release body scroll lock only when this dialog was opened as modal.\n // Non-modal dialogs never call lockBodyScroll(), so the unlock must be symmetric.\n if (this.modal) {\n unlockBodyScroll();\n }\n\n this._removeGlobalListeners();\n this._cachedFocusableElements = [];\n\n // D1 — restore focus to the element that opened the dialog (WCAG 2.4.3)\n this._triggerElement?.focus();\n this._triggerElement = null;\n\n this.dispatchEvent(\n new CustomEvent<void>('hx-close', {\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n // ─── Event Listeners ───\n\n /** @internal */\n private _addGlobalListeners(): void {\n this._dialogEl?.addEventListener('keydown', this._handleKeyDown);\n this._dialogEl?.addEventListener('click', this._handleDialogClick);\n this._dialogEl?.addEventListener('cancel', this._handleNativeCancel);\n }\n\n /** @internal */\n private _removeGlobalListeners(): void {\n this._dialogEl?.removeEventListener('keydown', this._handleKeyDown);\n this._dialogEl?.removeEventListener('click', this._handleDialogClick);\n this._dialogEl?.removeEventListener('cancel', this._handleNativeCancel);\n }\n\n // ─── Keyboard Handler ───\n\n /** @internal */\n private _handleKeyDown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') {\n // Native dialog fires a 'cancel' event before close when Escape is pressed.\n // We prevent default here and handle it ourselves so we fire hx-cancel\n // before setting open = false (which triggers hx-close).\n e.preventDefault();\n this._cancel();\n return;\n }\n\n if (e.key === 'Tab' && this.modal) {\n this._trapFocus(e);\n }\n };\n\n // ─── Focus Trap ───\n\n /** @internal */\n private _getFocusableElements(): HTMLElement[] {\n // Collect focusable elements from slotted light DOM content only.\n // Shadow DOM elements (e.g., the built-in close button) remain accessible via\n // the native <dialog> tab order — including them here would cause focus to land\n // on shadow DOM elements whose document.activeElement resolves to the host,\n // breaking the test assertions and D7 initial focus behavior.\n const slots = this.shadowRoot?.querySelectorAll<HTMLSlotElement>('slot') ?? [];\n const lightFocusable: HTMLElement[] = [];\n\n slots.forEach((slot) => {\n slot.assignedElements({ flatten: true }).forEach((el) => {\n if (el instanceof HTMLElement) {\n if (el.matches(FOCUSABLE_SELECTORS)) {\n lightFocusable.push(el);\n }\n el.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS).forEach((child) => {\n lightFocusable.push(child);\n });\n }\n });\n });\n\n const filtered = lightFocusable.filter(\n (el) => !el.hasAttribute('disabled') && el.getAttribute('tabindex') !== '-1',\n );\n\n // WCAG 2.4.3: if no light DOM focusable elements exist, fall back to the shadow\n // close button so the dialog always has at least one reachable focus target.\n if (filtered.length === 0) {\n const closeBtn = this.shadowRoot?.querySelector<HTMLElement>('.dialog__close-btn');\n if (closeBtn) filtered.push(closeBtn);\n }\n\n return filtered;\n }\n\n /** @internal */\n private _trapFocus(e: KeyboardEvent): void {\n const focusable =\n this._cachedFocusableElements.length > 0\n ? this._cachedFocusableElements\n : this._getFocusableElements();\n if (focusable.length === 0) {\n e.preventDefault();\n return;\n }\n\n const [first, ...rest] = focusable;\n const last = rest.length > 0 ? rest[rest.length - 1] : first;\n\n if (!first || !last) return;\n\n const active = document.activeElement;\n // Also check shadow root active element\n const shadowActive = this.shadowRoot?.activeElement;\n const currentActiveEl = shadowActive ?? active;\n const currentActive = currentActiveEl instanceof HTMLElement ? currentActiveEl : null;\n\n // The shadow close button may be the first focusable element when no light DOM\n // content exists (WCAG 2.1.2). Check both the element reference and shadow root\n // active element so Shift+Tab wraps correctly across the shadow boundary.\n const closeBtn = this.shadowRoot?.querySelector<HTMLElement>('.dialog__close-btn');\n\n if (e.shiftKey) {\n // Shift+Tab: if focus is on first, wrap to last\n const isOnFirst =\n currentActive === first ||\n (closeBtn !== null && shadowActive === closeBtn && first === closeBtn);\n if (isOnFirst) {\n e.preventDefault();\n last.focus();\n }\n } else {\n // Tab: if focus is on last, wrap to first\n if (currentActive === last) {\n e.preventDefault();\n first.focus();\n }\n }\n }\n\n // ─── Backdrop Click ───\n\n /** @internal */\n private _handleDialogClick = (e: MouseEvent): void => {\n if (!this.closeOnBackdrop) return;\n\n // The native dialog element fills only the content area in showModal().\n // Clicks on the backdrop reach the <dialog> element itself.\n // We detect this by checking whether the click target is the dialog element.\n const target = e.target as HTMLElement;\n if (target === this._dialogEl) {\n this._cancel();\n }\n };\n\n // ─── Non-modal backdrop click ───\n\n /** @internal */\n private _handleBackdropClick = (): void => {\n if (!this.closeOnBackdrop) return;\n this._cancel();\n };\n\n // ─── Native cancel (Escape via browser, before our handler runs) ───\n\n /** @internal */\n private _handleNativeCancel = (e: Event): void => {\n // We always prevent the native cancel so we can manage close state ourselves.\n e.preventDefault();\n };\n\n // ─── Cancel logic ───\n\n /** @internal */\n private _cancel(): void {\n this.dispatchEvent(\n new CustomEvent<void>('hx-cancel', {\n bubbles: true,\n composed: true,\n }),\n );\n\n this.open = false;\n // hx-close is dispatched by _closeDialog() which is called via the open property setter\n }\n\n // ─── Slot change handlers ───\n\n /** @internal */\n private _handleHeaderSlotChange(e: Event): void {\n if (!(e.target instanceof HTMLSlotElement)) return;\n const state = this._readHeaderSlotState(e.target);\n this._hasHeaderSlot = state.hasUsefulName || state.hasAnyAssigned;\n this._slottedHeaderEls = state.elements;\n this._headerSlotText = state.text;\n this._installHeaderSlotTextObserver(state.elements);\n this._syncHostAriaSemantics();\n }\n\n /** @internal */\n private _handleFooterSlotChange(e: Event): void {\n const slot = e.target as HTMLSlotElement;\n this._hasFooterSlot = slot.assignedNodes({ flatten: true }).length > 0;\n }\n\n // ─── Host-canonical ARIA helpers ───\n\n /**\n * Reads the header slot's assigned nodes and computes the discriminated\n * naming state. Aggregates ALL assigned elements (not just the first) so\n * composed headers project the FULL visible label via\n * `internals.ariaLabelledByElements`. Per AccName 1.2 §4.3.10,\n * `aria-hidden=\"true\"` / `[hidden]` elements contribute zero to the\n * accessible name but stay in `elements` so AT walking IDL refs sees the\n * full visible group. `hasUsefulName` is gated on the flattened text\n * length: a slot containing only decorative wrappers does NOT name the\n * dialog, and the host falls through to the next naming source.\n *\n * `hasAnyAssigned` is the legacy semantic kept for the existing dev-warning\n * + `_renderHeader()` empty-slot flag (the heading / built-in close button\n * area is rendered regardless of useful-name state when the consumer has\n * projected SOMETHING into the header slot).\n * @internal\n */\n private _readHeaderSlotState(slot: HTMLSlotElement): {\n hasUsefulName: boolean;\n hasAnyAssigned: boolean;\n elements: Element[];\n text: string;\n } {\n const nodes = slot.assignedNodes({ flatten: true });\n const elements: Element[] = [];\n const fragments: string[] = [];\n let hasAnyAssigned = false;\n for (const node of nodes) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n hasAnyAssigned = true;\n const el = node as Element;\n elements.push(el);\n if (el.getAttribute('aria-hidden') === 'true') continue;\n const elText = flattenAccName(el);\n if (elText) fragments.push(elText);\n } else if (node.nodeType === Node.TEXT_NODE) {\n const txt = (node.textContent ?? '').trim();\n if (txt) {\n fragments.push(txt);\n hasAnyAssigned = true;\n }\n }\n }\n const trimmedText = fragments.join(' ').replace(/\\s+/g, ' ').trim();\n return {\n hasUsefulName: trimmedText.length > 0,\n hasAnyAssigned,\n elements,\n text: trimmedText,\n };\n }\n\n /**\n * (Re-)installs the mutation observer over the current set of slotted header\n * elements. On any descendant text/visibility mutation we re-flatten and\n * re-sync so the host's accessible name tracks the visible header.\n * @internal\n */\n private _installHeaderSlotTextObserver(elements: Element[]): void {\n this._headerSlotTextObserver?.disconnect();\n if (elements.length === 0) {\n this._headerSlotTextObserver = null;\n return;\n }\n const observer = new MutationObserver(() => {\n const fragments: string[] = [];\n for (const el of elements) {\n if (el.getAttribute('aria-hidden') === 'true') continue;\n const t = flattenAccName(el);\n if (t) fragments.push(t);\n }\n const trimmed = fragments.join(' ').replace(/\\s+/g, ' ').trim();\n this._headerSlotText = trimmed;\n this._syncHostAriaSemantics();\n });\n for (const el of elements) {\n observer.observe(el, {\n characterData: true,\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['aria-hidden', 'hidden'],\n });\n }\n this._headerSlotTextObserver = observer;\n }\n\n /**\n * (Re-)installs a `MutationObserver` against the deduped union of\n * consumer-resolved label/description elements. Watches `characterData`,\n * `childList`, `subtree`, and `aria-hidden` / `hidden` attributes so any\n * in-place mutation on the referenced light-DOM nodes triggers a fresh\n * sync — keeping the modern-path IDL refs and the fallback-path text\n * flatten aligned with the live consumer text.\n * @internal\n */\n private _installExternalRefsObserver(elements: Element[]): void {\n if (this._externalRefsObserver) {\n this._externalRefsObserver.disconnect();\n this._externalRefsObserver = null;\n }\n if (elements.length === 0) return;\n const unique = new Set<Element>(elements);\n const observer = new MutationObserver(() => {\n this._syncHostAriaSemantics();\n });\n for (const el of unique) {\n observer.observe(el, {\n characterData: true,\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: ['aria-hidden', 'hidden'],\n });\n }\n this._externalRefsObserver = observer;\n }\n\n /**\n * Resolves consumer-supplied label/description IDREFs on the host and\n * projects the canonical dialog ARIA onto **both** surfaces:\n *\n * 1. The **host** via `ElementInternals` (modern path) — IDL element\n * references that AT honouring the host-internals contract pick up\n * across the shadow boundary.\n * 2. The inner native `<dialog>` via attribute writes — hybrid fallback\n * so AT that walks the native dialog first (and ignores host\n * `internals.aria*Elements`) still finds an announceable name and\n * description.\n *\n * Path A native-dialog adaptation: the host does NOT carry `internals.role`\n * or `internals.ariaModal` — the native `<dialog>` already declares those at\n * the platform level and rewriting them on the host would create\n * nested-dialog announcements.\n *\n * The inner `<dialog>` keeps `role=\"alertdialog\"` ONLY when `variant ===\n * 'alertdialog'` (the platform allows overriding the implicit `dialog` role\n * with the more specific `alertdialog`); otherwise the implicit `dialog`\n * role wins.\n *\n * Naming precedence (W3C AccName 1.2 §4.3.1):\n * 1. Consumer `aria-labelledby` (resolved IDREFs, text-flattened)\n * 2. Consumer `aria-label`\n * 3. Slotted `<slot name=\"header\">` text\n * 4. `heading` property\n * 5. Hard-coded `\"Dialog\"`\n * @internal\n */\n private _syncHostAriaSemantics(): void {\n const internals = this._internals;\n\n // Refresh the consumer baseline. The host attribute IS the live source\n // of truth — `null` authentically represents consumer retraction.\n const liveLabelledBy = this.getAttribute('aria-labelledby');\n this._consumerLabelledBy = liveLabelledBy;\n const liveDescribedBy = this.getAttribute('aria-describedby');\n this._consumerDescribedBy = liveDescribedBy;\n\n const consumerLabelEls = resolveIdrefTokens(this, this._consumerLabelledBy);\n const hasEffectiveLabelledBy = consumerLabelEls.length > 0;\n const consumerDescEls = resolveIdrefTokens(this, this._consumerDescribedBy);\n\n // Observe in-place mutations on the resolved external IDREF targets.\n // Without this a consumer mutating `<h2 id=\"x\">Patient</h2>` → \"Member\"\n // in place leaves the host's flattened `aria-label` stuck on \"Patient\".\n this._installExternalRefsObserver([...consumerLabelEls, ...consumerDescEls]);\n\n // Per AccName 1.2 §4.3.10, top-level aria-hidden / hidden elements\n // contribute zero to the name. Filter them from the IDL-refs path so the\n // modern path matches the fallback path's text-flatten behavior.\n const isVisibleForAccName = (el: Element): boolean =>\n el.getAttribute('aria-hidden') !== 'true' && !el.hasAttribute('hidden');\n\n const liveAriaLabel = this.getAttribute('aria-label');\n const hostAriaLabel = liveAriaLabel !== null ? liveAriaLabel.trim() : '';\n\n // Build the augmented label-elements list used by the modern path.\n // Slotted-header elements feed in only when no consumer aria-labelledby\n // resolved (AccName 1.2 precedence: external > slot > property).\n const labelElsForInternals: Element[] = [];\n labelElsForInternals.push(...consumerLabelEls.filter(isVisibleForAccName));\n if (!hasEffectiveLabelledBy && !hostAriaLabel && this._slottedHeaderEls.length > 0) {\n // Aggregate every slotted header element so AT composes icon + text.\n labelElsForInternals.push(...this._slottedHeaderEls.filter(isVisibleForAccName));\n }\n\n const descElsForInternals: Element[] = [...consumerDescEls.filter(isVisibleForAccName)];\n\n // ─── Compute the resolved accessible name (text-flatten path) ───\n const flattenText = (els: Element[]): string =>\n els\n .filter(isVisibleForAccName)\n .map((el) => flattenAccName(el))\n .filter((t) => t.length > 0)\n .join(' ');\n\n let resolvedName = '';\n if (hasEffectiveLabelledBy) {\n resolvedName = flattenText(consumerLabelEls);\n }\n if (!resolvedName && hostAriaLabel) {\n resolvedName = hostAriaLabel;\n }\n if (!resolvedName && this._headerSlotText) {\n resolvedName = this._headerSlotText;\n }\n if (!resolvedName && this.heading.trim()) {\n resolvedName = this.heading.trim();\n }\n if (!resolvedName) {\n // Last-resort literal — preserves the pre-host-canonical default so an\n // unlabeled dialog still has SOME announced name. Consumer responsibility\n // to provide a meaningful one in real usage.\n resolvedName = 'Dialog';\n }\n\n // ─── Modern-path: ElementInternals IDL element references ───\n type InternalsWithIdrefRefs = ElementInternals & {\n ariaLabelledByElements: Element[] | null;\n ariaDescribedByElements: Element[] | null;\n };\n if (this._supportsIdrefRefs) {\n const refsInternals = internals as InternalsWithIdrefRefs;\n refsInternals.ariaLabelledByElements =\n labelElsForInternals.length > 0 ? labelElsForInternals : null;\n refsInternals.ariaDescribedByElements =\n descElsForInternals.length > 0 ? descElsForInternals : null;\n // Forward `aria-label` to `internals.ariaLabel` ONLY when no labelledby\n // resolved — per AccName 1.2 a non-empty aria-label outranks\n // aria-labelledby, and we never want to silently erase the IDL-ref\n // resolution. When labelledby is present, `null` removes the override\n // so element references win.\n if (hasEffectiveLabelledBy) {\n internals.ariaLabel = null;\n } else {\n internals.ariaLabel = resolvedName;\n }\n } else {\n // Fallback path: write the flattened name directly to internals.\n // Older engines without IDL refs use this as the canonical name.\n internals.ariaLabel = resolvedName;\n }\n\n // ─── Synthesized in-shadow consumer-description span ───\n // Mirror consumer-resolved description text into a same-root span so the\n // inner native `<dialog>`'s `aria-describedby` resolves cross-shadow\n // without pointing at light-DOM ids (which do not resolve from inside a\n // shadow root). `aria-description` is intentionally NEVER written.\n const consumerDescSpan = this.shadowRoot?.getElementById(this._consumerDescId) ?? null;\n const consumerDescText = flattenText(consumerDescEls);\n if (consumerDescSpan && consumerDescSpan.textContent !== consumerDescText) {\n consumerDescSpan.textContent = consumerDescText;\n }\n\n // ─── Synthesized in-shadow consumer-label span (hybrid fallback) ───\n // The native `<dialog>` cannot resolve light-DOM ids written to its\n // `aria-labelledby` from inside the shadow root, so we surface the\n // flattened resolved name on a same-shadow-root span and reference it.\n // The host's `internals.ariaLabelledByElements` continues to carry live\n // IDL refs on the modern path; this span is the hybrid-fallback target\n // when AT walks the native dialog first.\n const consumerLabelSpan = this.shadowRoot?.getElementById(this._consumerLabelId) ?? null;\n if (consumerLabelSpan && consumerLabelSpan.textContent !== resolvedName) {\n consumerLabelSpan.textContent = resolvedName;\n }\n\n // ─── Inner native <dialog> attribute reconciliation ───\n // Hybrid fallback: write `aria-label` / `aria-labelledby` /\n // `aria-describedby` directly on the inner native `<dialog>`. The native\n // dialog cannot be stripped of its implicit `role=\"dialog\"`, so it stays\n // the announced surface; this projection guarantees AT that walks the\n // native dialog first still finds an announceable name and description.\n //\n // Naming-projection cascade for the inner <dialog>:\n //\n // 1. Consumer `aria-labelledby` resolved cross-shadow → write\n // `aria-labelledby=\"${_consumerLabelId}\"` (the synthesized span\n // carries the flattened text from the cross-shadow IDREF chain).\n // 2. Slotted header text only (no consumer aria-* on host) →\n // `aria-labelledby=\"${_consumerLabelId}\"` (same span carries the\n // flattened slot text — cross-shadow IDREF resolution from a native\n // dialog inside a shadow root is unreliable, the same-root span is\n // the stable target).\n // 3. Consumer `aria-label` literal on host → mirror to inner dialog's\n // `aria-label` (no IDREF indirection needed).\n // 4. `heading` property → `aria-labelledby=\"${_headingId}\"` (same-root\n // <h2> id is the natural target; preserves the pre-host-canonical\n // contract).\n // 5. Fallback \"Dialog\" literal → `aria-label=\"Dialog\"`.\n //\n // Steps 1, 2, 4 take the `aria-labelledby` path. Steps 3, 5 take the\n // `aria-label` path. Per AccName precedence we never set both at once.\n const dialogEl = this._dialogEl ?? null;\n if (dialogEl) {\n const hasHeadingProp = this.heading.trim().length > 0;\n let wantLabelledBy: string | null = null;\n let wantLabel: string | null = null;\n if (hasEffectiveLabelledBy) {\n // Cross-shadow consumer IDREF chain → surface flattened name via the\n // synthesized consumer-label span.\n wantLabelledBy = this._consumerLabelId;\n } else if (this._headerSlotText) {\n // Slot-projected header content → surface flattened name via the\n // synthesized consumer-label span.\n wantLabelledBy = this._consumerLabelId;\n } else if (hostAriaLabel) {\n // Consumer aria-label is a literal string — mirror it directly,\n // preserving the pre-host-canonical contract on the inner dialog.\n wantLabel = hostAriaLabel;\n } else if (hasHeadingProp) {\n // Heading property renders as a same-root <h2 id={_headingId}>.\n wantLabelledBy = this._headingId;\n } else {\n // Last-resort literal \"Dialog\".\n wantLabel = resolvedName;\n }\n\n if (wantLabelledBy) {\n if (dialogEl.getAttribute('aria-labelledby') !== wantLabelledBy) {\n dialogEl.setAttribute('aria-labelledby', wantLabelledBy);\n }\n } else if (dialogEl.hasAttribute('aria-labelledby')) {\n dialogEl.removeAttribute('aria-labelledby');\n }\n if (wantLabel) {\n if (dialogEl.getAttribute('aria-label') !== wantLabel) {\n dialogEl.setAttribute('aria-label', wantLabel);\n }\n } else if (dialogEl.hasAttribute('aria-label')) {\n dialogEl.removeAttribute('aria-label');\n }\n\n // ─── aria-describedby on inner <dialog> ───\n // Chain the existing `description` span (when the property is set) and\n // the synthesized consumer-description span (when consumer IDREFs\n // resolved). Same-shadow-root ids resolve cleanly; cross-shadow consumer\n // ids are ignored at the AT level so we never write them directly.\n const descTokens: string[] = [];\n if (this.description) descTokens.push(this._descriptionId);\n if (consumerDescText && consumerDescSpan) descTokens.push(this._consumerDescId);\n const wantDescribedBy = descTokens.length > 0 ? descTokens.join(' ') : null;\n if (wantDescribedBy) {\n if (dialogEl.getAttribute('aria-describedby') !== wantDescribedBy) {\n dialogEl.setAttribute('aria-describedby', wantDescribedBy);\n }\n } else if (dialogEl.hasAttribute('aria-describedby')) {\n dialogEl.removeAttribute('aria-describedby');\n }\n\n // ─── aria-modal on inner <dialog> ───\n // Native `showModal()` already declares modality at the platform level,\n // making `aria-modal=\"true\"` strictly redundant. We keep the explicit\n // attribute on the inner dialog for backward compatibility with\n // consumer code / tests that check for it, AND because some legacy AT\n // implementations rely on the explicit attribute rather than the\n // platform modal flag.\n if (this.modal) {\n if (dialogEl.getAttribute('aria-modal') !== 'true') {\n dialogEl.setAttribute('aria-modal', 'true');\n }\n } else if (dialogEl.hasAttribute('aria-modal')) {\n dialogEl.removeAttribute('aria-modal');\n }\n\n // Strip `aria-description` defensively — never written on either path.\n if (dialogEl.hasAttribute('aria-description')) {\n dialogEl.removeAttribute('aria-description');\n }\n }\n }\n\n // ─── Render Helpers ───\n\n /** @internal */\n private _renderHeader() {\n const hasHeading = this.heading.trim().length > 0;\n\n // Always render header to include the built-in close button (D17)\n return html`\n <div part=\"header\" class=\"dialog__header\">\n ${hasHeading\n ? html`<h2 id=${this._headingId} class=\"dialog__heading\">${this.heading}</h2>`\n : nothing}\n <slot name=\"header\" @slotchange=${this._handleHeaderSlotChange}></slot>\n <button\n part=\"close-button\"\n class=\"dialog__close-btn\"\n type=\"button\"\n aria-label=${this.labelClose}\n @click=${() => this.close()}\n ></button>\n </div>\n `;\n }\n\n /** @internal */\n private _renderFooter() {\n return html`\n <div part=\"footer\" class=\"dialog__footer\" ?hidden=${!this._hasFooterSlot}>\n <slot name=\"footer\" @slotchange=${this._handleFooterSlotChange}></slot>\n </div>\n `;\n }\n\n /** @internal */\n private _renderNonModalBackdrop() {\n if (this.modal || !this.open) return nothing;\n return html`\n <div\n part=\"backdrop\"\n class=\"dialog-backdrop\"\n @click=${this._handleBackdropClick}\n aria-hidden=\"true\"\n ></div>\n `;\n }\n\n // D8 — render visually-hidden description for aria-describedby\n /** @internal */\n private _renderDescription() {\n if (!this.description) return nothing;\n return html`<span id=${this._descriptionId} class=\"dialog__description\"\n >${this.description}</span\n >`;\n }\n\n // ─── Render ───\n\n override render() {\n // Path A native-dialog adaptation:\n // - The inner native `<dialog>` no longer carries `aria-labelledby` /\n // `aria-label` / `aria-describedby` / `aria-modal` from inline render\n // bindings. Those are projected imperatively in\n // `_syncHostAriaSemantics()` so the host-canonical IDL-ref path and\n // the hybrid inner-dialog fallback stay in lockstep.\n // - `role` is still bound inline because it depends on the\n // `variant` property and the platform allows overriding the implicit\n // `dialog` role with `alertdialog`.\n // - `aria-modal` is intentionally OMITTED — `showModal()` already\n // declares modality at the platform level. Setting it explicitly is\n // redundant and creates double-announcement on some AT.\n return html`\n ${this._renderNonModalBackdrop()}\n <dialog role=${this.variant !== 'dialog' ? this.variant : nothing}>\n <div part=\"dialog\" class=\"dialog\">\n ${this._renderHeader()} ${this._renderDescription()}\n <div part=\"body\" class=\"dialog__body\">\n <slot></slot>\n </div>\n ${this._renderFooter()}\n </div>\n <!--\n Synthesized in-shadow span carrying the resolved accessible NAME for\n the hybrid inner-dialog fallback (consumer aria-labelledby IDREF\n chain flattened, or consumer aria-label, or slotted header text).\n The host's \\`internals.ariaLabelledByElements\\` carries the live IDL\n refs on the modern path; this span is the same-shadow-root target\n referenced by the inner native \\`<dialog>\\`'s \\`aria-labelledby\\`\n when the name source lives outside the shadow root. Updated\n imperatively in \\`_syncHostAriaSemantics()\\`.\n -->\n <span id=${this._consumerLabelId} class=\"dialog__description\" aria-hidden=\"false\"></span>\n <!--\n Synthesized in-shadow span carrying consumer-resolved description\n text. Updated imperatively on every sync. The inner native\n \\`<dialog>\\`'s \\`aria-describedby\\` references this span so\n cross-shadow consumer descriptions resolve through the standard\n described-by channel without writing light-DOM ids that cannot\n resolve from inside a shadow root. \\`aria-description\\` is\n intentionally NEVER written — AccName ignores it whenever\n \\`aria-describedby\\` is present.\n -->\n <span id=${this._consumerDescId} class=\"dialog__description\" aria-hidden=\"false\"></span>\n </dialog>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'hx-dialog': HelixDialog;\n }\n interface HTMLElementEventMap {\n 'hx-open': CustomEvent<void>;\n 'hx-close': CustomEvent<void>;\n 'hx-cancel': CustomEvent<void>;\n }\n}\n"],"names":["helixDialogStyles","css","_nextDialogId","createIdCounter","FOCUSABLE_SELECTORS","HelixDialog","HelixElement","_a","name","oldVal","newVal","ctor","supportsIdrefElementReferences","records","consumerCleared","record","oldValue","newValue","installAriaIdrefMirror","unlockBodyScroll","_b","_c","_d","changedProperties","returnValue","dialog","active","lockBodyScroll","slots","lightFocusable","slot","el","child","filtered","closeBtn","focusable","first","rest","last","shadowActive","currentActiveEl","currentActive","state","nodes","elements","fragments","hasAnyAssigned","node","elText","flattenAccName","txt","trimmedText","observer","t","trimmed","unique","internals","liveLabelledBy","liveDescribedBy","consumerLabelEls","resolveIdrefTokens","hasEffectiveLabelledBy","consumerDescEls","isVisibleForAccName","liveAriaLabel","hostAriaLabel","labelElsForInternals","descElsForInternals","flattenText","els","resolvedName","refsInternals","consumerDescSpan","consumerDescText","consumerLabelSpan","dialogEl","hasHeadingProp","wantLabelledBy","wantLabel","descTokens","wantDescribedBy","hasHeading","html","nothing","forcedColorsSurface","__decorateClass","query","property","value","customElement"],"mappings":"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"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { css as
|
|
2
|
-
import { property as
|
|
1
|
+
import { css as f, nothing as m, html as b } from "lit";
|
|
2
|
+
import { property as c, state as h, query as p, customElement as g } from "lit/decorators.js";
|
|
3
3
|
import { d as v } from "./dev-warn-YlwPHjtX.js";
|
|
4
4
|
import { f as _ } from "./forced-colors-CTEDFRGa.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
5
|
+
import { f as y } from "./aria-flatten-DY6v2vah.js";
|
|
6
|
+
import { i as x, r as w } from "./aria-idref-Q0yiSR3p.js";
|
|
7
|
+
import { H as A } from "./helix-element-BNEYeiys.js";
|
|
8
|
+
import { c as L } from "./id-counter-DuX8vsui.js";
|
|
9
|
+
const k = f`
|
|
8
10
|
:host {
|
|
9
11
|
display: inline-block;
|
|
10
12
|
position: relative;
|
|
@@ -60,15 +62,15 @@ const x = u`
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
`;
|
|
63
|
-
var
|
|
64
|
-
for (var
|
|
65
|
-
(
|
|
66
|
-
return
|
|
65
|
+
var C = Object.defineProperty, E = Object.getOwnPropertyDescriptor, l = (e, t, i, s) => {
|
|
66
|
+
for (var r = s > 1 ? void 0 : s ? E(t, i) : t, n = e.length - 1, a; n >= 0; n--)
|
|
67
|
+
(a = e[n]) && (r = (s ? a(t, i, r) : a(r)) || r);
|
|
68
|
+
return s && r && C(t, i, r), r;
|
|
67
69
|
};
|
|
68
|
-
const
|
|
69
|
-
let
|
|
70
|
+
const O = L("hx-dropdown");
|
|
71
|
+
let o = class extends A {
|
|
70
72
|
constructor() {
|
|
71
|
-
super(...arguments), this.open = !1, this.placement = "bottom-start", this.label = "Menu", this.disabled = !1, this.distance = 4, this._panelVisible = !1, this._documentListenerAttached = !1, this._panelId = `${
|
|
73
|
+
super(...arguments), this.open = !1, this.placement = "bottom-start", this.label = "Menu", this.disabled = !1, this.distance = 4, this._panelVisible = !1, this._resolvedLabel = "", this._consumerLabelledBy = null, this._ariaMirror = null, this._externalRefsObserver = null, this._documentListenerAttached = !1, this._panelId = `${O()}-panel`, this._handleKeydown = (e) => {
|
|
72
74
|
e.key === "Escape" && this.open ? (e.stopPropagation(), this._hide(!0)) : e.key === "Tab" && this.open ? this._hide(!1) : this.open && (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Home" || e.key === "End") && (e.preventDefault(), this._handleMenuNavigation(e.key));
|
|
73
75
|
}, this._handleOutsideClick = (e) => {
|
|
74
76
|
e.composedPath().includes(this) || this._hide();
|
|
@@ -76,10 +78,13 @@ let r = class extends y {
|
|
|
76
78
|
}
|
|
77
79
|
// ─── Lifecycle ───
|
|
78
80
|
connectedCallback() {
|
|
79
|
-
super.connectedCallback(), this.addEventListener("keydown", this._handleKeydown)
|
|
81
|
+
super.connectedCallback(), this.addEventListener("keydown", this._handleKeydown), this._syncResolvedLabel(), this._ariaMirror = x(this, () => {
|
|
82
|
+
this._syncResolvedLabel();
|
|
83
|
+
});
|
|
80
84
|
}
|
|
81
85
|
disconnectedCallback() {
|
|
82
|
-
|
|
86
|
+
var e, t;
|
|
87
|
+
super.disconnectedCallback(), this.removeEventListener("keydown", this._handleKeydown), this._documentListenerAttached && (document.removeEventListener("click", this._handleOutsideClick, { capture: !0 }), this._documentListenerAttached = !1), (e = this._ariaMirror) == null || e.disconnect(), this._ariaMirror = null, (t = this._externalRefsObserver) == null || t.disconnect(), this._externalRefsObserver = null;
|
|
83
88
|
}
|
|
84
89
|
// ─── Open/Close ───
|
|
85
90
|
/** @internal */
|
|
@@ -96,8 +101,8 @@ let r = class extends y {
|
|
|
96
101
|
_hide(e = !0) {
|
|
97
102
|
var t;
|
|
98
103
|
if (this.open && (this.open = !1, this._panelVisible = !1, this._documentListenerAttached && (document.removeEventListener("click", this._handleOutsideClick, { capture: !0 }), this._documentListenerAttached = !1), this.dispatchEvent(new CustomEvent("hx-hide", { bubbles: !0, composed: !0 })), e)) {
|
|
99
|
-
const
|
|
100
|
-
|
|
104
|
+
const i = (t = this.shadowRoot) == null ? void 0 : t.querySelector('slot[name="trigger"]'), s = i == null ? void 0 : i.assignedElements()[0];
|
|
105
|
+
s == null || s.focus();
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
// ─── Positioning ───
|
|
@@ -105,14 +110,14 @@ let r = class extends y {
|
|
|
105
110
|
async _updatePosition() {
|
|
106
111
|
const e = this._triggerWrapper, t = this._panel;
|
|
107
112
|
if (!e || !t) return;
|
|
108
|
-
const
|
|
109
|
-
placement:
|
|
113
|
+
const i = this.placement.replace(/^start$/, "left").replace(/^end$/, "right"), { computePosition: s, flip: r, shift: n, offset: a } = await import("@floating-ui/dom"), { x: d, y: u } = await s(e, t, {
|
|
114
|
+
placement: i,
|
|
110
115
|
strategy: "fixed",
|
|
111
|
-
middleware: [
|
|
116
|
+
middleware: [a(this.distance), r(), n({ padding: 8 })]
|
|
112
117
|
});
|
|
113
118
|
Object.assign(t.style, {
|
|
114
|
-
left: `${
|
|
115
|
-
top: `${
|
|
119
|
+
left: `${d}px`,
|
|
120
|
+
top: `${u}px`
|
|
116
121
|
});
|
|
117
122
|
}
|
|
118
123
|
// ─── Event Handlers ───
|
|
@@ -127,54 +132,54 @@ let r = class extends y {
|
|
|
127
132
|
// P2-01: Move focus among menuitem elements using arrow keys.
|
|
128
133
|
/** @internal */
|
|
129
134
|
_handleMenuNavigation(e) {
|
|
130
|
-
var
|
|
135
|
+
var r;
|
|
131
136
|
const t = this._getFocusableMenuItems();
|
|
132
137
|
if (t.length === 0) return;
|
|
133
|
-
const
|
|
134
|
-
let
|
|
135
|
-
e === "ArrowDown" ?
|
|
138
|
+
const i = t.indexOf(document.activeElement);
|
|
139
|
+
let s;
|
|
140
|
+
e === "ArrowDown" ? s = i < t.length - 1 ? i + 1 : 0 : e === "ArrowUp" ? s = i > 0 ? i - 1 : t.length - 1 : e === "Home" ? s = 0 : s = t.length - 1, (r = t[s]) == null || r.focus();
|
|
136
141
|
}
|
|
137
142
|
// P0-01 / P2-01: Get focusable menu items from slotted content.
|
|
138
143
|
/** @internal */
|
|
139
144
|
_getFocusableMenuItems() {
|
|
140
145
|
const e = this._panel;
|
|
141
146
|
if (!e) return [];
|
|
142
|
-
const t = e.querySelector("slot"),
|
|
143
|
-
for (const
|
|
144
|
-
|
|
145
|
-
return
|
|
147
|
+
const t = e.querySelector("slot"), i = (t == null ? void 0 : t.assignedElements({ flatten: !0 })) ?? [], s = [];
|
|
148
|
+
for (const r of i)
|
|
149
|
+
r instanceof HTMLElement && (r.matches('[role="menuitem"]') ? s.push(r) : r.querySelectorAll('[role="menuitem"]').forEach((n) => s.push(n)));
|
|
150
|
+
return s;
|
|
146
151
|
}
|
|
147
152
|
// P0-01: Find the first focusable element in slotted panel content.
|
|
148
153
|
/** @internal */
|
|
149
154
|
_getFirstFocusableItem() {
|
|
150
155
|
const e = this._panel;
|
|
151
156
|
if (!e) return null;
|
|
152
|
-
const t = e.querySelector("slot"),
|
|
153
|
-
for (const
|
|
154
|
-
if (!(
|
|
155
|
-
if (
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
157
|
+
const t = e.querySelector("slot"), i = (t == null ? void 0 : t.assignedElements({ flatten: !0 })) ?? [], s = '[role="menuitem"], button, [tabindex]:not([tabindex="-1"]), a[href], input, select, textarea';
|
|
158
|
+
for (const r of i) {
|
|
159
|
+
if (!(r instanceof HTMLElement)) continue;
|
|
160
|
+
if (r.matches(s)) return r;
|
|
161
|
+
const n = r.querySelector(s);
|
|
162
|
+
if (n) return n;
|
|
158
163
|
}
|
|
159
164
|
return null;
|
|
160
165
|
}
|
|
161
166
|
/** @internal */
|
|
162
167
|
_handlePanelClick(e) {
|
|
163
|
-
var
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
166
|
-
const
|
|
168
|
+
var n;
|
|
169
|
+
const i = e.target.closest('[role="menuitem"], [data-value]');
|
|
170
|
+
if (!i) return;
|
|
171
|
+
const s = i.dataset.value ?? i.getAttribute("value") ?? null, r = ((n = i.textContent) == null ? void 0 : n.trim()) ?? "";
|
|
167
172
|
this.dispatchEvent(
|
|
168
173
|
new CustomEvent("hx-select", {
|
|
169
174
|
bubbles: !0,
|
|
170
175
|
composed: !0,
|
|
171
|
-
detail: { value:
|
|
176
|
+
detail: { value: s, label: r }
|
|
172
177
|
})
|
|
173
178
|
), this._hide();
|
|
174
179
|
}
|
|
175
180
|
// ─── Render ───
|
|
176
181
|
render() {
|
|
177
|
-
return
|
|
182
|
+
return b`
|
|
178
183
|
<div
|
|
179
184
|
part="trigger"
|
|
180
185
|
class="trigger-wrapper"
|
|
@@ -188,7 +193,7 @@ let r = class extends y {
|
|
|
188
193
|
id=${this._panelId}
|
|
189
194
|
role="menu"
|
|
190
195
|
aria-hidden=${this._panelVisible ? m : "true"}
|
|
191
|
-
aria-label=${this.
|
|
196
|
+
aria-label=${this._resolvedLabel}
|
|
192
197
|
class=${this._panelVisible ? "panel panel--visible" : "panel"}
|
|
193
198
|
@click=${this._handlePanelClick}
|
|
194
199
|
>
|
|
@@ -199,10 +204,10 @@ let r = class extends y {
|
|
|
199
204
|
// ─── Panel slot validation ───
|
|
200
205
|
/** @internal */
|
|
201
206
|
_onPanelSlotChange(e) {
|
|
202
|
-
const
|
|
203
|
-
|
|
207
|
+
const s = e.target.assignedElements({ flatten: !0 }).filter((r) => r.tagName.toLowerCase() !== "hx-dropdown-item");
|
|
208
|
+
s.length > 0 && v(
|
|
204
209
|
"hx-dropdown",
|
|
205
|
-
`Default slot should contain only hx-dropdown-item elements. Found unexpected: ${
|
|
210
|
+
`Default slot should contain only hx-dropdown-item elements. Found unexpected: ${s.map((r) => `<${r.tagName.toLowerCase()}>`).join(", ")}. Non-hx-dropdown-item children will be included in keyboard navigation incorrectly.`
|
|
206
211
|
);
|
|
207
212
|
}
|
|
208
213
|
// ─── ARIA setup for trigger slot ───
|
|
@@ -215,49 +220,97 @@ let r = class extends y {
|
|
|
215
220
|
}
|
|
216
221
|
/** @internal */
|
|
217
222
|
_setupTriggerAria() {
|
|
218
|
-
var
|
|
219
|
-
const e = (
|
|
223
|
+
var i;
|
|
224
|
+
const e = (i = this.shadowRoot) == null ? void 0 : i.querySelector('slot[name="trigger"]');
|
|
220
225
|
if (!e) return;
|
|
221
226
|
const t = e.assignedElements()[0];
|
|
222
227
|
t ? (t.setAttribute("aria-haspopup", "menu"), t.setAttribute("aria-expanded", String(this.open)), this.removeAttribute("aria-expanded")) : this.setAttribute("aria-expanded", String(this.open));
|
|
223
228
|
}
|
|
229
|
+
willUpdate(e) {
|
|
230
|
+
super.willUpdate(e), e.has("label") && this._syncResolvedLabel();
|
|
231
|
+
}
|
|
224
232
|
updated(e) {
|
|
225
233
|
var t;
|
|
226
234
|
if (super.updated(e), e.has("open")) {
|
|
227
|
-
const
|
|
228
|
-
|
|
235
|
+
const i = (t = this.shadowRoot) == null ? void 0 : t.querySelector('slot[name="trigger"]'), s = i == null ? void 0 : i.assignedElements()[0];
|
|
236
|
+
s ? s.setAttribute("aria-expanded", String(this.open)) : this.setAttribute("aria-expanded", String(this.open));
|
|
229
237
|
}
|
|
230
238
|
}
|
|
239
|
+
// ─── Host-attribute label mirror ───
|
|
240
|
+
/**
|
|
241
|
+
* (Re-)installs a `MutationObserver` over the deduped union of
|
|
242
|
+
* consumer-resolved label elements, watching for in-place text /
|
|
243
|
+
* visibility mutations so the panel's `aria-label` tracks live consumer
|
|
244
|
+
* text. See `hx-popover._installExternalRefsObserver` for the matching
|
|
245
|
+
* shape used across the host-attribute-mirror family.
|
|
246
|
+
* @internal
|
|
247
|
+
*/
|
|
248
|
+
_installExternalRefsObserver(e) {
|
|
249
|
+
if (this._externalRefsObserver && (this._externalRefsObserver.disconnect(), this._externalRefsObserver = null), e.length === 0) return;
|
|
250
|
+
const t = new Set(e), i = new MutationObserver(() => {
|
|
251
|
+
this._syncResolvedLabel();
|
|
252
|
+
});
|
|
253
|
+
for (const s of t)
|
|
254
|
+
i.observe(s, {
|
|
255
|
+
characterData: !0,
|
|
256
|
+
subtree: !0,
|
|
257
|
+
childList: !0,
|
|
258
|
+
attributes: !0,
|
|
259
|
+
attributeFilter: ["aria-hidden", "hidden"]
|
|
260
|
+
});
|
|
261
|
+
this._externalRefsObserver = i;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Resolves the menu panel's accessible name from host attributes and the
|
|
265
|
+
* `label` property. AccName 1.2 §4.3.1 precedence:
|
|
266
|
+
* 1. Host `aria-labelledby` (resolved IDREFs, flattened)
|
|
267
|
+
* 2. Host `aria-label`
|
|
268
|
+
* 3. `label` property
|
|
269
|
+
* 4. Literal `"Menu"` (last-resort)
|
|
270
|
+
* @internal
|
|
271
|
+
*/
|
|
272
|
+
_syncResolvedLabel() {
|
|
273
|
+
const e = this.getAttribute("aria-labelledby");
|
|
274
|
+
this._consumerLabelledBy = e;
|
|
275
|
+
const t = w(this, e);
|
|
276
|
+
this._installExternalRefsObserver(t);
|
|
277
|
+
const i = (d) => d.getAttribute("aria-hidden") !== "true" && !d.hasAttribute("hidden"), s = t.filter(i).map((d) => y(d)).filter((d) => d.length > 0).join(" ").replace(/\s+/g, " ").trim(), r = this.getAttribute("aria-label"), n = r !== null ? r.trim() : "";
|
|
278
|
+
let a = "";
|
|
279
|
+
s ? a = s : n ? a = n : this.label ? a = this.label : a = "Menu", this._resolvedLabel = a;
|
|
280
|
+
}
|
|
231
281
|
};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
],
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
],
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
],
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
],
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
],
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
],
|
|
251
|
-
|
|
282
|
+
o.styles = [k, _];
|
|
283
|
+
l([
|
|
284
|
+
c({ type: Boolean, reflect: !0 })
|
|
285
|
+
], o.prototype, "open", 2);
|
|
286
|
+
l([
|
|
287
|
+
c({ type: String, reflect: !0 })
|
|
288
|
+
], o.prototype, "placement", 2);
|
|
289
|
+
l([
|
|
290
|
+
c()
|
|
291
|
+
], o.prototype, "label", 2);
|
|
292
|
+
l([
|
|
293
|
+
c({ type: Boolean, reflect: !0 })
|
|
294
|
+
], o.prototype, "disabled", 2);
|
|
295
|
+
l([
|
|
296
|
+
c({ type: Number })
|
|
297
|
+
], o.prototype, "distance", 2);
|
|
298
|
+
l([
|
|
299
|
+
h()
|
|
300
|
+
], o.prototype, "_panelVisible", 2);
|
|
301
|
+
l([
|
|
302
|
+
h()
|
|
303
|
+
], o.prototype, "_resolvedLabel", 2);
|
|
304
|
+
l([
|
|
252
305
|
p('[part="panel"]')
|
|
253
|
-
],
|
|
254
|
-
|
|
306
|
+
], o.prototype, "_panel", 2);
|
|
307
|
+
l([
|
|
255
308
|
p('[part="trigger"]')
|
|
256
|
-
],
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
],
|
|
309
|
+
], o.prototype, "_triggerWrapper", 2);
|
|
310
|
+
o = l([
|
|
311
|
+
g("hx-dropdown")
|
|
312
|
+
], o);
|
|
260
313
|
export {
|
|
261
|
-
|
|
314
|
+
o as H
|
|
262
315
|
};
|
|
263
|
-
//# sourceMappingURL=hx-dropdown-
|
|
316
|
+
//# sourceMappingURL=hx-dropdown-DJWlF94E.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hx-dropdown-DJWlF94E.js","sources":["../../src/components/hx-dropdown/hx-dropdown.styles.ts","../../src/components/hx-dropdown/hx-dropdown.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const helixDropdownStyles = css`\n :host {\n display: inline-block;\n position: relative;\n }\n\n :host([disabled]) {\n pointer-events: none;\n opacity: var(--hx-opacity-disabled, 0.5);\n }\n\n .trigger-wrapper {\n display: inline-block;\n }\n\n [part='panel'] {\n position: fixed;\n z-index: var(--hx-dropdown-panel-z-index, 1000);\n min-width: var(--hx-dropdown-panel-min-width, 160px);\n background: var(--hx-dropdown-panel-bg, var(--hx-color-surface-default, #ffffff));\n border: 1px solid var(--hx-dropdown-panel-border-color, var(--hx-color-border-default, #d6dbd5));\n border-radius: var(--hx-dropdown-panel-border-radius, var(--hx-border-radius-md, 0.375rem));\n box-shadow: var(\n --hx-dropdown-panel-shadow,\n 0 4px 16px var(--hx-overlay-black-12, rgba(0, 0, 0, 0.12))\n );\n visibility: hidden;\n opacity: 0;\n pointer-events: none;\n transition:\n opacity var(--hx-transition-fast, 150ms ease),\n visibility var(--hx-transition-fast, 150ms ease);\n outline: none;\n }\n\n [part='panel'].panel--visible {\n visibility: visible;\n opacity: 1;\n pointer-events: auto;\n }\n\n @media (prefers-reduced-motion: reduce) {\n [part='panel'] {\n transition: none;\n }\n }\n\n /* ─── High Contrast Mode (forced-colors) ─── */\n\n @media (forced-colors: active) {\n [part='panel'] {\n background-color: Canvas;\n border: 2px solid CanvasText;\n }\n }\n`;\n","import { html, nothing, type PropertyValues } from 'lit';\nimport '../../utilities/document-token-adoption.js';\nimport { customElement, property, state, query } from 'lit/decorators.js';\nimport { devWarn } from '../../utils/dev-warn.js';\nimport { HelixElement } from '../../base/index.js';\nimport type { Placement as FloatingPlacement } from '@floating-ui/dom';\nimport { createIdCounter } from '../../base/index.js';\nimport { forcedColorsInteractive } from '../../styles/forced-colors.js';\nimport { helixDropdownStyles } from './hx-dropdown.styles.js';\nimport { flattenAccName } from '../../utils/aria-flatten.js';\nimport {\n installAriaIdrefMirror,\n resolveIdrefTokens,\n type AriaIdrefMirrorHandle,\n} from '../../utils/aria-idref.js';\n\n// P2-03: Export so TypeScript consumers can import this type for prop typing.\nexport type DropdownPlacement =\n | 'top'\n | 'top-start'\n | 'top-end'\n | 'bottom'\n | 'bottom-start'\n | 'bottom-end'\n | 'start'\n | 'end';\n\nconst _nextDropdownId = createIdCounter('hx-dropdown');\n\n/**\n * A dropdown component — a button that opens a floating panel on click.\n *\n * ## Architecture Note: Host-Attribute Label Mirror (group-4 round-1)\n *\n * The announced surface is the inner `[part=\"panel\"]` element, which carries\n * `role=\"menu\"`. The host wraps a slotted trigger and the floating panel and\n * does NOT claim a role itself (apart from the round-35-style host\n * `aria-expanded` fallback used only when the trigger slot is empty).\n *\n * Because the panel lives in shadow DOM and `ElementInternals` IDL refs on\n * the host project semantics OUTWARD (host → AT) rather than INWARD\n * (host → shadow descendant), we use the **host-attribute mirror** pattern:\n * resolve consumer `aria-labelledby` IDREFs against the host's composed-tree\n * roots, text-flatten via `flattenAccName`, and write the result to the\n * panel's `aria-label`. Host `aria-label` outranks the `label` property in\n * the same precedence used by every host-canonical hx-* control.\n *\n * Naming precedence (W3C AccName 1.2 §4.3.1):\n * 1. Host `aria-labelledby` (resolved IDREFs, text-flattened)\n * 2. Host `aria-label`\n * 3. `label` property\n * 4. Hard-coded literal `\"Menu\"` (last-resort accessible name)\n *\n * **Group 5 boundary (intentional):** This round is **additive only** — the\n * host-label pipeline is the entire change. The panel's `role=\"menu\"` and\n * the menuitem-roving keyboard pattern are already implemented per APG and\n * are NOT touched here. Group 5 (composite navigation: menu, menubar,\n * menuitem, tabs, tree) will own any broader refactor of the menu role and\n * roving-tabindex semantics. Codex reviewers: please scope findings to the\n * host-label pipeline; do not flag missing roving-focus / typeahead /\n * `aria-orientation` work here.\n *\n * `aria-controls` is intentionally omitted on the trigger: the panel lives\n * in shadow DOM and IDREF values cannot be resolved across shadow\n * boundaries by assistive technology (axe-core flags this as a critical\n * violation if attempted). See `_setupTriggerAria` for the inline note.\n *\n * @summary Button that opens a floating menu panel on click.\n *\n * @tag hx-dropdown\n *\n * @slot trigger - The element that opens the dropdown (e.g. hx-button).\n * @slot - Default slot for dropdown panel content (e.g. menu items).\n *\n * @fires {CustomEvent<void>} hx-show - Dispatched when the dropdown is opened.\n * @fires {CustomEvent<void>} hx-hide - Dispatched when the dropdown is closed.\n * @fires {CustomEvent<{value: string | null; label: string}>} hx-select - Dispatched when a menu item is selected.\n *\n * @csspart trigger - The trigger wrapper element.\n * @csspart panel - The floating panel element.\n *\n * @cssprop [--hx-dropdown-panel-bg=var(--hx-color-neutral-0)] - Panel background color.\n * @cssprop [--hx-dropdown-panel-border-color=var(--hx-color-neutral-200)] - Panel border color.\n * @cssprop [--hx-dropdown-panel-border-radius=var(--hx-border-radius-md)] - Panel border radius.\n * @cssprop [--hx-dropdown-panel-shadow=0 4px 16px rgba(0,0,0,0.12)] - Panel box shadow.\n * @cssprop [--hx-dropdown-panel-z-index=1000] - Panel z-index.\n * @cssprop [--hx-dropdown-panel-min-width=160px] - Panel minimum width.\n *\n * @example\n * ```html\n * <hx-dropdown>\n * <button slot=\"trigger\">Open Menu</button>\n * <ul>\n * <li data-value=\"edit\">Edit</li>\n * <li data-value=\"delete\">Delete</li>\n * </ul>\n * </hx-dropdown>\n * ```\n * @cssprop [--hx-opacity-disabled] - Opacity.\n * @cssprop [--hx-color-neutral-0] - Color.\n * @cssprop [--hx-color-neutral-200] - Color.\n * @cssprop [--hx-border-radius-md] - CSS custom property.\n * @cssprop [--hx-overlay-black-12] - Overlay color.\n * @cssprop [--hx-transition-fast] - Transition timing.\n */\n@customElement('hx-dropdown')\nexport class HelixDropdown extends HelixElement {\n static override styles = [helixDropdownStyles, forcedColorsInteractive];\n\n // ─── Public Properties ───\n\n /**\n * Whether the dropdown panel is open.\n * @attr open\n */\n @property({ type: Boolean, reflect: true })\n open = false;\n\n /**\n * Preferred placement of the panel relative to the trigger.\n * @attr placement\n */\n @property({ type: String, reflect: true })\n placement:\n | 'top'\n | 'top-start'\n | 'top-end'\n | 'bottom'\n | 'bottom-start'\n | 'bottom-end'\n | 'start'\n | 'end' = 'bottom-start';\n\n /**\n * Accessible label for the dropdown menu panel. Override for i18n.\n * @attr label\n */\n @property() label = 'Menu';\n\n /**\n * Whether the dropdown is disabled. Prevents opening.\n * @attr disabled\n */\n @property({ type: Boolean, reflect: true })\n disabled = false;\n\n /**\n * Gap in pixels between the trigger and the panel.\n * @attr distance\n */\n @property({ type: Number })\n distance = 4;\n\n // ─── Internal State ───\n\n /**\n * Whether the dropdown panel is currently visible.\n * @internal\n */\n @state() private _panelVisible = false;\n\n /**\n * Resolved accessible name for the menu panel — the value written to the\n * inner `[part=\"panel\"]` `aria-label`. Recomputed on every sync per\n * AccName 1.2 §4.3.1 precedence: host `aria-labelledby` (flattened) >\n * host `aria-label` > `label` property > literal `\"Menu\"`.\n * @internal\n */\n @state() private _resolvedLabel = '';\n\n /**\n * Most recently observed consumer-supplied `aria-labelledby` token list on\n * the host. Refreshed every sync via `getAttribute()`.\n * @internal\n */\n private _consumerLabelledBy: string | null = null;\n\n /**\n * Handle for the shared host attribute / root id observer.\n * @internal\n */\n private _ariaMirror: AriaIdrefMirrorHandle | null = null;\n\n /**\n * Watches in-place text / visibility mutations on consumer light-DOM\n * elements resolved from the host's `aria-labelledby`.\n * @internal\n */\n private _externalRefsObserver: MutationObserver | null = null;\n\n /**\n * Guards against accumulating multiple document click listeners when open state\n * changes faster than the microtask queue can process removeEventListener calls.\n * @internal\n */\n private _documentListenerAttached = false;\n\n // P1-02: Unique panel ID for aria-controls.\n /**\n * Unique ID assigned to the floating panel element, referenced by `aria-controls` on the trigger.\n * @internal\n */\n private _panelId = `${_nextDropdownId()}-panel`;\n\n /**\n * Reference to the floating panel element inside the shadow DOM.\n * @internal\n */\n @query('[part=\"panel\"]') private _panel: HTMLElement | undefined;\n /**\n * Reference to the trigger wrapper element inside the shadow DOM.\n * @internal\n */\n @query('[part=\"trigger\"]') private _triggerWrapper: HTMLElement | undefined;\n\n // ─── Lifecycle ───\n\n override connectedCallback(): void {\n super.connectedCallback();\n this.addEventListener('keydown', this._handleKeydown);\n // Seed the host-attribute label mirror BEFORE first paint so the panel's\n // `aria-label` carries the resolved name on its very first render.\n this._syncResolvedLabel();\n this._ariaMirror = installAriaIdrefMirror(this, () => {\n this._syncResolvedLabel();\n });\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeEventListener('keydown', this._handleKeydown);\n if (this._documentListenerAttached) {\n document.removeEventListener('click', this._handleOutsideClick, { capture: true });\n this._documentListenerAttached = false;\n }\n this._ariaMirror?.disconnect();\n this._ariaMirror = null;\n this._externalRefsObserver?.disconnect();\n this._externalRefsObserver = null;\n }\n\n // ─── Open/Close ───\n\n /** @internal */\n private async _show(): Promise<void> {\n if (this.open || this.disabled) return;\n this.open = true;\n this._panelVisible = true;\n // Add outside-click listener synchronously before any await so it is registered\n // by the time the test fires an outside click after a single await el.updateComplete.\n if (!this._documentListenerAttached) {\n document.addEventListener('click', this._handleOutsideClick, { capture: true });\n this._documentListenerAttached = true;\n }\n await this.updateComplete;\n // P0-01: Fix focus management — use slot.assignedElements() to traverse slotted (light DOM) content.\n // Focus is set after updateComplete (panel is rendered) but before _updatePosition so\n // it executes in the same microtask as the test's await-continuation.\n const panel = this._panel;\n if (panel) {\n const firstFocusable = this._getFirstFocusableItem();\n firstFocusable?.focus();\n }\n await this._updatePosition();\n this.dispatchEvent(new CustomEvent<void>('hx-show', { bubbles: true, composed: true }));\n }\n\n // P2-02: returnFocus=true only on Escape; Tab should let focus advance naturally.\n /** @internal */\n private _hide(returnFocus = true): void {\n if (!this.open) return;\n this.open = false;\n this._panelVisible = false;\n if (this._documentListenerAttached) {\n document.removeEventListener('click', this._handleOutsideClick, { capture: true });\n this._documentListenerAttached = false;\n }\n this.dispatchEvent(new CustomEvent<void>('hx-hide', { bubbles: true, composed: true }));\n if (returnFocus) {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]');\n const trigger = slot?.assignedElements()[0] as HTMLElement | undefined;\n trigger?.focus();\n }\n }\n\n // ─── Positioning ───\n\n /** @internal */\n private async _updatePosition(): Promise<void> {\n const reference = this._triggerWrapper;\n const panel = this._panel;\n if (!reference || !panel) return;\n\n // Map 'start' and 'end' to floating-ui's 'left'/'right'\n const floatingPlacement = this.placement\n .replace(/^start$/, 'left')\n .replace(/^end$/, 'right') as FloatingPlacement;\n\n const { computePosition, flip, shift, offset } = await import('@floating-ui/dom');\n const { x, y } = await computePosition(reference, panel, {\n placement: floatingPlacement,\n strategy: 'fixed',\n middleware: [offset(this.distance), flip(), shift({ padding: 8 })],\n });\n\n Object.assign(panel.style, {\n left: `${x}px`,\n top: `${y}px`,\n });\n }\n\n // ─── Event Handlers ───\n\n /** @internal */\n private _handleTriggerClick(e: MouseEvent): void {\n e.stopPropagation();\n if (this.open) {\n this._hide();\n } else {\n void this._show();\n }\n }\n\n /** @internal */\n private _handleTriggerKeydown(e: KeyboardEvent): void {\n if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {\n e.preventDefault();\n void this._show();\n }\n }\n\n /** @internal */\n private _handleKeydown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape' && this.open) {\n e.stopPropagation();\n this._hide(true); // return focus to trigger on Escape\n } else if (e.key === 'Tab' && this.open) {\n // P2-02: Do not return focus to trigger on Tab — let focus advance naturally to next page element.\n this._hide(false);\n } else if (\n this.open &&\n (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End')\n ) {\n // P2-01: Arrow key roving within panel per APG Menu Button pattern.\n e.preventDefault();\n this._handleMenuNavigation(e.key);\n }\n };\n\n // P2-01: Move focus among menuitem elements using arrow keys.\n /** @internal */\n private _handleMenuNavigation(key: string): void {\n const items = this._getFocusableMenuItems();\n if (items.length === 0) return;\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n let nextIndex: number;\n if (key === 'ArrowDown') {\n nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;\n } else if (key === 'ArrowUp') {\n nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;\n } else if (key === 'Home') {\n nextIndex = 0;\n } else {\n nextIndex = items.length - 1;\n }\n items[nextIndex]?.focus();\n }\n\n // P0-01 / P2-01: Get focusable menu items from slotted content.\n /** @internal */\n private _getFocusableMenuItems(): HTMLElement[] {\n const panel = this._panel;\n if (!panel) return [];\n const slot = panel.querySelector<HTMLSlotElement>('slot');\n const assignedNodes = slot?.assignedElements({ flatten: true }) ?? [];\n const items: HTMLElement[] = [];\n for (const node of assignedNodes) {\n if (!(node instanceof HTMLElement)) continue;\n if (node.matches('[role=\"menuitem\"]')) {\n items.push(node);\n } else {\n node.querySelectorAll<HTMLElement>('[role=\"menuitem\"]').forEach((item) => items.push(item));\n }\n }\n return items;\n }\n\n // P0-01: Find the first focusable element in slotted panel content.\n /** @internal */\n private _getFirstFocusableItem(): HTMLElement | null {\n const panel = this._panel;\n if (!panel) return null;\n const slot = panel.querySelector<HTMLSlotElement>('slot');\n const assignedNodes = slot?.assignedElements({ flatten: true }) ?? [];\n const focusableSelector =\n '[role=\"menuitem\"], button, [tabindex]:not([tabindex=\"-1\"]), a[href], input, select, textarea';\n for (const node of assignedNodes) {\n if (!(node instanceof HTMLElement)) continue;\n if (node.matches(focusableSelector)) return node;\n const found = node.querySelector<HTMLElement>(focusableSelector);\n if (found) return found;\n }\n return null;\n }\n\n /** @internal */\n private _handleOutsideClick = (e: MouseEvent): void => {\n const path = e.composedPath();\n if (!path.includes(this)) {\n this._hide();\n }\n };\n\n /** @internal */\n private _handlePanelClick(e: MouseEvent): void {\n const target = e.target as HTMLElement;\n // P2-06: Narrow selector — bare 'li' and 'button' cause spurious hx-select events.\n const item = target.closest<HTMLElement>('[role=\"menuitem\"], [data-value]');\n if (!item) return;\n\n const value = item.dataset['value'] ?? item.getAttribute('value') ?? null;\n const label = item.textContent?.trim() ?? '';\n\n this.dispatchEvent(\n new CustomEvent<{ value: string | null; label: string }>('hx-select', {\n bubbles: true,\n composed: true,\n detail: { value, label },\n }),\n );\n\n this._hide();\n }\n\n // ─── Render ───\n\n override render() {\n return html`\n <div\n part=\"trigger\"\n class=\"trigger-wrapper\"\n @click=${this._handleTriggerClick}\n @keydown=${this._handleTriggerKeydown}\n >\n <slot name=\"trigger\" @slotchange=${this._onTriggerSlotChange}></slot>\n </div>\n <div\n part=\"panel\"\n id=${this._panelId}\n role=\"menu\"\n aria-hidden=${this._panelVisible ? nothing : 'true'}\n aria-label=${this._resolvedLabel}\n class=${this._panelVisible ? 'panel panel--visible' : 'panel'}\n @click=${this._handlePanelClick}\n >\n <slot @slotchange=${this._onPanelSlotChange}></slot>\n </div>\n `;\n }\n\n // ─── Panel slot validation ───\n\n /** @internal */\n private _onPanelSlotChange(e: Event): void {\n const slot = e.target as HTMLSlotElement;\n const assigned = slot.assignedElements({ flatten: true });\n const nonItems = assigned.filter((el) => el.tagName.toLowerCase() !== 'hx-dropdown-item');\n if (nonItems.length > 0) {\n devWarn(\n 'hx-dropdown',\n `Default slot should contain only hx-dropdown-item elements. Found unexpected: ${nonItems.map((el) => `<${el.tagName.toLowerCase()}>`).join(', ')}. Non-hx-dropdown-item children will be included in keyboard navigation incorrectly.`,\n );\n }\n }\n\n // ─── ARIA setup for trigger slot ───\n\n /** @internal */\n private _onTriggerSlotChange(): void {\n this._setupTriggerAria();\n }\n\n override firstUpdated(): void {\n this._setupTriggerAria();\n }\n\n /** @internal */\n private _setupTriggerAria(): void {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]');\n if (!slot) return;\n const trigger = slot.assignedElements()[0] as HTMLElement | undefined;\n if (trigger) {\n // P1-01: Use aria-haspopup=\"menu\" per ARIA 1.1+ / APG Menu Button pattern.\n trigger.setAttribute('aria-haspopup', 'menu');\n trigger.setAttribute('aria-expanded', String(this.open));\n // aria-controls is intentionally omitted: the panel lives in Shadow DOM and\n // IDREF values cannot be resolved across shadow boundaries by assistive technology.\n // P2-06: Remove host fallback when a trigger element is present.\n this.removeAttribute('aria-expanded');\n } else {\n // P2-06: Fallback — set aria-expanded on host when trigger slot is empty or unassigned.\n this.setAttribute('aria-expanded', String(this.open));\n }\n }\n\n override willUpdate(changedProperties: PropertyValues<this>): void {\n super.willUpdate(changedProperties);\n // `label` property changes must flow into the resolved name BEFORE\n // render so the new fallback is in place on the same paint. See\n // `hx-popover.willUpdate()` for the same rationale.\n if (changedProperties.has('label')) {\n this._syncResolvedLabel();\n }\n }\n\n override updated(changedProperties: PropertyValues<this>): void {\n super.updated(changedProperties);\n if (changedProperties.has('open')) {\n // Keep aria-expanded in sync\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]');\n const trigger = slot?.assignedElements()[0] as HTMLElement | undefined;\n if (trigger) {\n trigger.setAttribute('aria-expanded', String(this.open));\n } else {\n // P2-06: Fallback — keep host aria-expanded in sync when trigger slot is empty.\n this.setAttribute('aria-expanded', String(this.open));\n }\n }\n }\n\n // ─── Host-attribute label mirror ───\n\n /**\n * (Re-)installs a `MutationObserver` over the deduped union of\n * consumer-resolved label elements, watching for in-place text /\n * visibility mutations so the panel's `aria-label` tracks live consumer\n * text. See `hx-popover._installExternalRefsObserver` for the matching\n * shape used across the host-attribute-mirror family.\n * @internal\n */\n private _installExternalRefsObserver(elements: Element[]): void {\n if (this._externalRefsObserver) {\n this._externalRefsObserver.disconnect();\n this._externalRefsObserver = null;\n }\n if (elements.length === 0) return;\n const unique = new Set<Element>(elements);\n const observer = new MutationObserver(() => {\n this._syncResolvedLabel();\n });\n for (const el of unique) {\n observer.observe(el, {\n characterData: true,\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: ['aria-hidden', 'hidden'],\n });\n }\n this._externalRefsObserver = observer;\n }\n\n /**\n * Resolves the menu panel's accessible name from host attributes and the\n * `label` property. AccName 1.2 §4.3.1 precedence:\n * 1. Host `aria-labelledby` (resolved IDREFs, flattened)\n * 2. Host `aria-label`\n * 3. `label` property\n * 4. Literal `\"Menu\"` (last-resort)\n * @internal\n */\n private _syncResolvedLabel(): void {\n const liveLabelledBy = this.getAttribute('aria-labelledby');\n this._consumerLabelledBy = liveLabelledBy;\n const consumerLabelEls = resolveIdrefTokens(this, liveLabelledBy);\n\n this._installExternalRefsObserver(consumerLabelEls);\n\n const isVisibleForAccName = (el: Element): boolean =>\n el.getAttribute('aria-hidden') !== 'true' && !el.hasAttribute('hidden');\n\n const flattenedFromIdrefs = consumerLabelEls\n .filter(isVisibleForAccName)\n .map((el) => flattenAccName(el))\n .filter((t) => t.length > 0)\n .join(' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\n const liveAriaLabel = this.getAttribute('aria-label');\n const hostAriaLabel = liveAriaLabel !== null ? liveAriaLabel.trim() : '';\n\n let resolved = '';\n if (flattenedFromIdrefs) {\n resolved = flattenedFromIdrefs;\n } else if (hostAriaLabel) {\n resolved = hostAriaLabel;\n } else if (this.label) {\n resolved = this.label;\n } else {\n resolved = 'Menu';\n }\n\n this._resolvedLabel = resolved;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'hx-dropdown': HelixDropdown;\n }\n interface HTMLElementEventMap {\n 'hx-show': CustomEvent<void>;\n 'hx-hide': CustomEvent<void>;\n 'hx-select': CustomEvent<{ value: string | null; label: string }>;\n }\n}\n"],"names":["helixDropdownStyles","css","_nextDropdownId","createIdCounter","HelixDropdown","HelixElement","installAriaIdrefMirror","_a","_b","firstFocusable","returnFocus","slot","trigger","reference","panel","floatingPlacement","computePosition","flip","shift","offset","x","y","key","items","currentIndex","nextIndex","assignedNodes","node","item","focusableSelector","found","value","label","html","nothing","nonItems","el","devWarn","changedProperties","elements","unique","observer","liveLabelledBy","consumerLabelEls","resolveIdrefTokens","isVisibleForAccName","flattenedFromIdrefs","flattenAccName","t","liveAriaLabel","hostAriaLabel","resolved","forcedColorsInteractive","__decorateClass","property","state","query","customElement"],"mappings":"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"}
|