{"version":3,"file":"navigation-menu.cjs","names":[],"sources":["../src/overlay/navigation-menu/navigation-menu.ts"],"sourcesContent":["import { createStableId, define, getHost, html, onCleanup, onMounted, prop, useEmit, useSlots } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\n\nimport type { OverlayOpenChangeDetail } from '../../core';\n\nimport { createDropdownPositioner, createOutsidePointerDismissal, lifecycleSignal } from '../../core';\nimport { disablableBundle } from '../../shared';\nimport { reducedMotionMixin } from '../../styles';\nimport hostStyles from './navigation-menu.css?inline';\nimport itemStyles from './navigation-menu-item.css?inline';\nimport panelStyles from './navigation-menu-panel.css?inline';\n\nexport type OreNavigationMenuEvents = {\n  'open-change': OverlayOpenChangeDetail & { value: string | null };\n  select: NavigationMenuSelectDetail;\n};\n\nexport type NavigationMenuSelectDetail = {\n  href: string;\n  value: string;\n};\n\nexport type OreNavigationMenuItemProps = {\n  disabled?: boolean;\n  value: string;\n};\n\nexport type OreNavigationMenuProps = {\n  /** Close after activating a panel link. */\n  'close-on-select'?: boolean;\n  columns?: number;\n  'default-open'?: string;\n  disabled?: boolean;\n  /** Accessible label for the trigger region. */\n  label?: string;\n  open?: string;\n  placement?: 'bottom-end' | 'bottom-start';\n};\n\nconst parseOptionalString = (value: string | null): string | undefined => value ?? undefined;\n\n/**\n * A trigger for an associated navigation menu panel.\n *\n * @element ore-navigation-menu-item\n *\n * @attr {boolean} disabled - Disables panel activation\n * @attr {string} value - Unique value matched by a panel's `for` attribute\n *\n * @slot - Trigger label and optional icon\n */\nexport const NAVIGATION_MENU_ITEM_TAG = 'ore-navigation-menu-item' as const;\ndefine<OreNavigationMenuItemProps>(NAVIGATION_MENU_ITEM_TAG, {\n  props: { disabled: prop.bool(false), value: prop.string('') },\n  setup(props) {\n    return html`\n      <button class=\"trigger\" type=\"button\" ?disabled=${props.disabled}><slot></slot></button>\n    `;\n  },\n  styles: [itemStyles],\n});\n\n/**\n * An anchored navigation menu panel associated with an item.\n *\n * @element ore-navigation-menu-panel\n *\n * @attr {string} for - Value of the associated navigation menu item\n *\n * @slot - Panel content\n * @slot footer - Optional panel footer\n *\n * @part content - Grid container for panel content\n * @part footer - Container for optional footer content\n */\nexport const NAVIGATION_MENU_PANEL_TAG = 'ore-navigation-menu-panel' as const;\ndefine<{ for?: string }>(NAVIGATION_MENU_PANEL_TAG, {\n  props: { for: prop.string() },\n  setup() {\n    const slots = useSlots();\n\n    return html`\n      <div class=\"content\" part=\"content\"><slot></slot></div>\n      <div class=\"footer\" part=\"footer\" ?hidden=${() => !slots.has('footer').value}><slot name=\"footer\"></slot></div>\n    `;\n  },\n  styles: [panelStyles],\n});\n\n/**\n * A horizontal set of triggers with anchored, non-modal navigation panels.\n *\n * @element ore-navigation-menu\n *\n * @attr {string} open - Controlled active trigger value\n * @attr {string} default-open - Initial uncontrolled active trigger value\n * @attr {boolean} close-on-select - Closes after activating a panel link\n * @attr {number} columns - Number of panel content columns\n * @attr {boolean} disabled - Disables all triggers\n * @attr {string} label - Accessible navigation region label\n * @attr {'bottom-start'|'bottom-end'} placement - Panel placement relative to its trigger\n *\n * @slot - Navigation menu items and panels\n *\n * @event open-change - Requested active panel change\n * @event select - A panel link was activated\n *\n * @cssprop --navigation-menu-panel-width - Maximum panel width\n * @cssprop --navigation-menu-panel-columns - Fallback number of panel content columns\n * @cssprop --navigation-menu-backdrop-blur - Frosted panel backdrop blur\n */\nexport const NAVIGATION_MENU_TAG = 'ore-navigation-menu' as const;\ndefine<OreNavigationMenuProps>(NAVIGATION_MENU_TAG, {\n  props: {\n    ...disablableBundle,\n    'close-on-select': prop.bool(true),\n    columns: prop.number(),\n    'default-open': prop.string(),\n    label: prop.string('Navigation menu'),\n    open: { default: undefined as string | undefined, parse: parseOptionalString },\n    placement: prop.oneOf(['bottom-end', 'bottom-start'] as const, 'bottom-start'),\n  },\n  setup(props) {\n    const host = getHost();\n    const emit = useEmit<OreNavigationMenuEvents>();\n    const abortSignal = lifecycleSignal(onCleanup);\n    const panelId = createStableId('navigation-menu');\n    const isDisabled = computed(() => props.disabled.value);\n    const activeValue = signal<string | null>(null);\n    let activeTrigger: HTMLElement | null = null;\n    let activePanel: HTMLElement | null = null;\n    let stopPositioning: (() => void) | null = null;\n\n    const items = () => Array.from(host.querySelectorAll<HTMLElement>(`${NAVIGATION_MENU_ITEM_TAG}:not([disabled])`));\n    const valueOf = (item: HTMLElement) => item.getAttribute('value') ?? '';\n    const panels = () => Array.from(host.querySelectorAll<HTMLElement>(NAVIGATION_MENU_PANEL_TAG));\n    const panelFor = (value: string) => panels().find((panel) => panel.getAttribute('for') === value) ?? null;\n    const isControlled = () => props.open.value !== undefined;\n    const panelIdFor = (panel: HTMLElement) => (panel.id ||= createStableId('navigation-menu-panel'));\n\n    const positioner = createDropdownPositioner({\n      getFloating: () => activePanel,\n      getPlacement: () => props.placement.value ?? 'bottom-start',\n      getReference: () => activeTrigger?.shadowRoot?.querySelector<HTMLElement>('.trigger') ?? activeTrigger,\n      matchWidth: false,\n      offsetPx: 8,\n      useClippingAncestor: false,\n    });\n\n    const panelColumns = (): number | undefined => {\n      const columns = props.columns.value;\n\n      return columns === undefined ? undefined : Math.max(1, Math.trunc(columns));\n    };\n\n    const showPanel = (panel: HTMLElement): void => {\n      panel.hidden = false;\n      panel.dataset.open = '';\n\n      if ('showPopover' in panel) {\n        try {\n          if (!panel.matches(':popover-open')) panel.showPopover();\n        } catch {\n          // Sandboxed documents can reject the Popover API; data-open remains the fixed-position fallback.\n        }\n      }\n    };\n\n    const hidePanel = (panel: HTMLElement): void => {\n      delete panel.dataset.open;\n\n      if ('hidePopover' in panel) {\n        try {\n          if (panel.matches(':popover-open')) panel.hidePopover();\n        } catch {\n          // The fallback visibility state is still cleared below.\n        }\n      }\n\n      panel.hidden = true;\n    };\n\n    const applyActive = (value: string | null): void => {\n      const nextPanel = value === null ? null : panelFor(value);\n\n      stopPositioning?.();\n      stopPositioning = null;\n      activeValue.value = value;\n      activeTrigger = value ? (items().find((item) => valueOf(item) === value) ?? null) : null;\n      activePanel = nextPanel;\n\n      for (const item of items()) {\n        const expanded = valueOf(item) === value;\n        const trigger = item.shadowRoot?.querySelector<HTMLElement>('.trigger');\n\n        item.setAttribute('aria-expanded', String(expanded));\n        trigger?.setAttribute('aria-expanded', String(expanded));\n      }\n      for (const panel of panels()) {\n        if (panel !== nextPanel) hidePanel(panel);\n      }\n\n      if (nextPanel) {\n        const columns = panelColumns();\n\n        if (columns !== undefined) nextPanel.style.setProperty('--navigation-menu-panel-columns', String(columns));\n\n        nextPanel.setAttribute('aria-labelledby', activeTrigger?.id ?? '');\n        showPanel(nextPanel);\n        positioner.update();\n        stopPositioning = positioner.startAutoUpdate?.() ?? null;\n      }\n    };\n\n    const requestActive = (value: string | null, reason: OverlayOpenChangeDetail['reason']): void => {\n      if (isDisabled.value || activeValue.value === value) return;\n\n      if (value !== null && !panelFor(value)) return;\n\n      if (!isControlled()) applyActive(value);\n\n      emit('open-change', { open: value !== null, reason, value });\n    };\n\n    onCleanup(() => {\n      stopPositioning?.();\n\n      for (const panel of panels()) hidePanel(panel);\n    });\n\n    createOutsidePointerDismissal({\n      getTargets: () => [host, activePanel],\n      isActive: () => activeValue.value !== null,\n      onDismiss: () => requestActive(null, 'outsideClick'),\n      signal: abortSignal,\n    });\n\n    onMounted(() => {\n      const values = new Set<string>();\n\n      for (const panel of panels()) {\n        panel.setAttribute('popover', 'manual');\n        panel.setAttribute('role', 'region');\n        panelIdFor(panel);\n        hidePanel(panel);\n\n        panel.addEventListener(\n          'click',\n          (event) => {\n            const link = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[href]') : null;\n\n            if (!link || !panel.contains(link)) return;\n\n            const value = activeValue.value;\n\n            if (!value) return;\n\n            emit('select', { href: link.href, value });\n\n            if (props['close-on-select'].value && !link.closest('[data-navigation-menu-keep-open]')) {\n              requestActive(null, 'select');\n            }\n          },\n          { signal: abortSignal },\n        );\n      }\n\n      document.addEventListener(\n        'keydown',\n        (event) => {\n          if (event.key !== 'Escape' || activeValue.value === null) return;\n\n          event.preventDefault();\n          requestActive(null, 'escape');\n          activeTrigger?.shadowRoot?.querySelector<HTMLElement>('.trigger')?.focus();\n        },\n        { signal: abortSignal },\n      );\n\n      for (const item of items()) {\n        const value = valueOf(item);\n        const panel = panelFor(value);\n\n        if (!value || values.has(value) || !panel) continue;\n\n        values.add(value);\n        item.id ||= createStableId('navigation-menu-trigger');\n\n        const trigger = item.shadowRoot?.querySelector<HTMLButtonElement>('.trigger');\n\n        if (!trigger) continue;\n\n        trigger.setAttribute('aria-controls', panelIdFor(panel));\n        trigger.setAttribute('aria-haspopup', 'true');\n        trigger.setAttribute('aria-expanded', 'false');\n        item.setAttribute('aria-expanded', 'false');\n        trigger.addEventListener('click', () => requestActive(activeValue.value === value ? null : value, 'click'), {\n          signal: abortSignal,\n        });\n        trigger.addEventListener(\n          'keydown',\n          (event) => {\n            const all = items();\n            const index = all.indexOf(item);\n\n            if (\n              event.key === 'ArrowLeft' ||\n              event.key === 'ArrowRight' ||\n              event.key === 'Home' ||\n              event.key === 'End'\n            ) {\n              event.preventDefault();\n\n              const next =\n                event.key === 'Home'\n                  ? all[0]\n                  : event.key === 'End'\n                    ? all.at(-1)\n                    : all[(index + (event.key === 'ArrowRight' ? 1 : all.length - 1)) % all.length];\n\n              next?.shadowRoot?.querySelector<HTMLElement>('.trigger')?.focus();\n            } else if (event.key === 'ArrowDown') {\n              event.preventDefault();\n              requestActive(value, 'keyboard');\n              requestAnimationFrame(() =>\n                activePanel?.querySelector<HTMLElement>('a, button, input, [tabindex]:not([tabindex=\"-1\"])')?.focus(),\n              );\n            }\n          },\n          { signal: abortSignal },\n        );\n      }\n\n      if (props.open.value !== undefined) applyActive(props.open.value);\n      else if (props['default-open'].value) applyActive(props['default-open'].value);\n    });\n\n    watch(props.open, (value) => {\n      if (value !== undefined) applyActive(value);\n    });\n\n    watch(props.disabled, (disabled) => {\n      if (disabled) applyActive(null);\n    });\n\n    return html`\n      <nav class=\"triggers\" aria-label=\"${props.label}\" id=\"${panelId}\"><slot></slot></nav>\n      <slot name=\"panel\"></slot>\n      <slot name=\"footer\"></slot>\n    `;\n  },\n  styles: [reducedMotionMixin, hostStyles],\n});\n"],"mappings":"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"}