@nldd/design-system 0.8.41 → 0.8.43
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/README.md +19 -2
- package/dist/assets/styles/tokens.d.ts +25 -0
- package/dist/assets/styles/tokens.d.ts.map +1 -0
- package/dist/assets/styles/tokens.js +25 -0
- package/dist/assets/styles/tokens.js.map +1 -0
- package/dist/components/actions/button/button.d.ts +23 -5
- package/dist/components/actions/button/button.d.ts.map +1 -1
- package/dist/components/actions/button/button.js +45 -6
- package/dist/components/actions/button/button.js.map +1 -1
- package/dist/components/actions/button/button.styles.d.ts.map +1 -1
- package/dist/components/actions/button/button.styles.js +136 -50
- package/dist/components/actions/button/button.styles.js.map +1 -1
- package/dist/components/actions/button/button.template.d.ts.map +1 -1
- package/dist/components/actions/button/button.template.js +12 -1
- package/dist/components/actions/button/button.template.js.map +1 -1
- package/dist/components/actions/button-group/button-group.js +2 -2
- package/dist/components/actions/button-group/button-group.js.map +1 -1
- package/dist/components/actions/icon-button/icon-button.d.ts +21 -5
- package/dist/components/actions/icon-button/icon-button.d.ts.map +1 -1
- package/dist/components/actions/icon-button/icon-button.js +40 -6
- package/dist/components/actions/icon-button/icon-button.js.map +1 -1
- package/dist/components/actions/icon-button/icon-button.styles.d.ts.map +1 -1
- package/dist/components/actions/icon-button/icon-button.styles.js +153 -96
- package/dist/components/actions/icon-button/icon-button.styles.js.map +1 -1
- package/dist/components/actions/icon-button/icon-button.template.d.ts.map +1 -1
- package/dist/components/actions/icon-button/icon-button.template.js +11 -0
- package/dist/components/actions/icon-button/icon-button.template.js.map +1 -1
- package/dist/components/actions/link/link.d.ts +17 -6
- package/dist/components/actions/link/link.d.ts.map +1 -1
- package/dist/components/actions/link/link.js +15 -5
- package/dist/components/actions/link/link.js.map +1 -1
- package/dist/components/actions/link/link.styles.d.ts.map +1 -1
- package/dist/components/actions/link/link.styles.js +44 -6
- package/dist/components/actions/link/link.styles.js.map +1 -1
- package/dist/components/actions/link/link.template.d.ts.map +1 -1
- package/dist/components/actions/link/link.template.js +7 -9
- package/dist/components/actions/link/link.template.js.map +1 -1
- package/dist/components/actions/split-button/split-button.template.js +2 -2
- package/dist/components/actions/split-button/split-button.template.js.map +1 -1
- package/dist/components/actions/toolbar/toolbar.d.ts +4 -4
- package/dist/components/actions/toolbar/toolbar.d.ts.map +1 -1
- package/dist/components/actions/toolbar/toolbar.js +11 -11
- package/dist/components/actions/toolbar/toolbar.js.map +1 -1
- package/dist/components/actions/toolbar/toolbar.template.d.ts +2 -2
- package/dist/components/actions/toolbar/toolbar.template.d.ts.map +1 -1
- package/dist/components/actions/toolbar/toolbar.template.js +4 -4
- package/dist/components/actions/toolbar/toolbar.template.js.map +1 -1
- package/dist/components/components.js +1 -1
- package/dist/components/content/blockquote/blockquote.styles.js +1 -1
- package/dist/components/content/blockquote/blockquote.styles.js.map +1 -1
- package/dist/components/content/icon/icon-aliases.d.ts.map +1 -1
- package/dist/components/content/icon/icon-aliases.js +6 -1
- package/dist/components/content/icon/icon-aliases.js.map +1 -1
- package/dist/components/content/icon/icon-registry.d.ts.map +1 -1
- package/dist/components/content/icon/icon-registry.js +11 -7
- package/dist/components/content/icon/icon-registry.js.map +1 -1
- package/dist/components/content/tag/tag.d.ts +3 -3
- package/dist/components/content/tag/tag.d.ts.map +1 -1
- package/dist/components/content/tag/tag.js +3 -3
- package/dist/components/content/tag/tag.js.map +1 -1
- package/dist/components/content/tag/tag.styles.d.ts.map +1 -1
- package/dist/components/content/tag/tag.styles.js +107 -9
- package/dist/components/content/tag/tag.styles.js.map +1 -1
- package/dist/components/content/title/title.styles.js +18 -18
- package/dist/components/content/title/title.styles.js.map +1 -1
- package/dist/components/content/tooltip/tooltip.d.ts +13 -7
- package/dist/components/content/tooltip/tooltip.d.ts.map +1 -1
- package/dist/components/content/tooltip/tooltip.js +34 -22
- package/dist/components/content/tooltip/tooltip.js.map +1 -1
- package/dist/components/forms/form-field/form-field.styles.js +1 -1
- package/dist/components/{index-DMYkp2tq.js → index-By2jYHEw.js} +2934 -1234
- package/dist/components/inputs/combo-box/combo-box.d.ts +28 -4
- package/dist/components/inputs/combo-box/combo-box.d.ts.map +1 -1
- package/dist/components/inputs/combo-box/combo-box.js +86 -23
- package/dist/components/inputs/combo-box/combo-box.js.map +1 -1
- package/dist/components/inputs/combo-box/combo-box.styles.d.ts.map +1 -1
- package/dist/components/inputs/combo-box/combo-box.styles.js +2 -0
- package/dist/components/inputs/combo-box/combo-box.styles.js.map +1 -1
- package/dist/components/inputs/combo-box/combo-box.template.d.ts.map +1 -1
- package/dist/components/inputs/combo-box/combo-box.template.js +4 -2
- package/dist/components/inputs/combo-box/combo-box.template.js.map +1 -1
- package/dist/components/inputs/dropdown/dropdown.d.ts +11 -1
- package/dist/components/inputs/dropdown/dropdown.d.ts.map +1 -1
- package/dist/components/inputs/dropdown/dropdown.js +31 -2
- package/dist/components/inputs/dropdown/dropdown.js.map +1 -1
- package/dist/components/inputs/dropdown/dropdown.styles.d.ts.map +1 -1
- package/dist/components/inputs/dropdown/dropdown.styles.js +3 -1
- package/dist/components/inputs/dropdown/dropdown.styles.js.map +1 -1
- package/dist/components/inputs/dropdown/dropdown.template.js +1 -1
- package/dist/components/inputs/multi-line-text-field/multi-line-text-field.d.ts +1 -1
- package/dist/components/inputs/multi-line-text-field/multi-line-text-field.d.ts.map +1 -1
- package/dist/components/inputs/multi-line-text-field/multi-line-text-field.js +8 -2
- package/dist/components/inputs/multi-line-text-field/multi-line-text-field.js.map +1 -1
- package/dist/components/inputs/multi-line-text-field/multi-line-text-field.styles.d.ts.map +1 -1
- package/dist/components/inputs/multi-line-text-field/multi-line-text-field.styles.js +2 -0
- package/dist/components/inputs/multi-line-text-field/multi-line-text-field.styles.js.map +1 -1
- package/dist/components/inputs/number-field/number-field.d.ts +7 -4
- package/dist/components/inputs/number-field/number-field.d.ts.map +1 -1
- package/dist/components/inputs/number-field/number-field.js +15 -12
- package/dist/components/inputs/number-field/number-field.js.map +1 -1
- package/dist/components/inputs/number-field/number-field.styles.js +3 -3
- package/dist/components/inputs/password-field/password-field.d.ts +1 -1
- package/dist/components/inputs/password-field/password-field.d.ts.map +1 -1
- package/dist/components/inputs/password-field/password-field.js +8 -2
- package/dist/components/inputs/password-field/password-field.js.map +1 -1
- package/dist/components/inputs/password-field/password-field.styles.d.ts.map +1 -1
- package/dist/components/inputs/password-field/password-field.styles.js +2 -0
- package/dist/components/inputs/password-field/password-field.styles.js.map +1 -1
- package/dist/components/inputs/search-field/search-field.d.ts +6 -1
- package/dist/components/inputs/search-field/search-field.d.ts.map +1 -1
- package/dist/components/inputs/search-field/search-field.js +15 -2
- package/dist/components/inputs/search-field/search-field.js.map +1 -1
- package/dist/components/inputs/search-field/search-field.styles.d.ts.map +1 -1
- package/dist/components/inputs/search-field/search-field.styles.js +2 -0
- package/dist/components/inputs/search-field/search-field.styles.js.map +1 -1
- package/dist/components/inputs/segmented-control/segmented-control.d.ts +8 -2
- package/dist/components/inputs/segmented-control/segmented-control.d.ts.map +1 -1
- package/dist/components/inputs/segmented-control/segmented-control.js +25 -4
- package/dist/components/inputs/segmented-control/segmented-control.js.map +1 -1
- package/dist/components/inputs/segmented-control/segmented-control.styles.d.ts.map +1 -1
- package/dist/components/inputs/segmented-control/segmented-control.styles.js +65 -64
- package/dist/components/inputs/segmented-control/segmented-control.styles.js.map +1 -1
- package/dist/components/inputs/segmented-control/segmented-control.template.js +1 -1
- package/dist/components/inputs/switch/switch.d.ts +5 -0
- package/dist/components/inputs/switch/switch.d.ts.map +1 -1
- package/dist/components/inputs/switch/switch.js +7 -0
- package/dist/components/inputs/switch/switch.js.map +1 -1
- package/dist/components/inputs/switch-field/switch-field.d.ts +6 -0
- package/dist/components/inputs/switch-field/switch-field.d.ts.map +1 -1
- package/dist/components/inputs/switch-field/switch-field.js +8 -0
- package/dist/components/inputs/switch-field/switch-field.js.map +1 -1
- package/dist/components/inputs/text-field/text-field.d.ts +6 -2
- package/dist/components/inputs/text-field/text-field.d.ts.map +1 -1
- package/dist/components/inputs/text-field/text-field.js +15 -5
- package/dist/components/inputs/text-field/text-field.js.map +1 -1
- package/dist/components/inputs/text-field/text-field.styles.d.ts.map +1 -1
- package/dist/components/inputs/text-field/text-field.styles.js +2 -0
- package/dist/components/inputs/text-field/text-field.styles.js.map +1 -1
- package/dist/components/inputs/toggle-button/toggle-button.styles.d.ts.map +1 -1
- package/dist/components/inputs/toggle-button/toggle-button.styles.js +26 -6
- package/dist/components/inputs/toggle-button/toggle-button.styles.js.map +1 -1
- package/dist/components/inputs/token/token.d.ts +3 -3
- package/dist/components/inputs/token/token.d.ts.map +1 -1
- package/dist/components/inputs/token/token.js +6 -6
- package/dist/components/inputs/token/token.js.map +1 -1
- package/dist/components/inputs/token/token.styles.js +2 -2
- package/dist/components/inputs/token/token.template.js +1 -1
- package/dist/components/inputs/token/token.template.js.map +1 -1
- package/dist/components/layout/card/card.js +1 -1
- package/dist/components/layout/card/card.js.map +1 -1
- package/dist/components/layout/container/container.d.ts +4 -4
- package/dist/components/layout/container/container.js +4 -4
- package/dist/components/layout/container/container.styles.js +399 -399
- package/dist/components/layout/container/container.styles.js.map +1 -1
- package/dist/components/layout/page/page.js +1 -1
- package/dist/components/layout/page/page.js.map +1 -1
- package/dist/components/layout/page-sections/full-bleed-section/full-bleed-section.d.ts +6 -3
- package/dist/components/layout/page-sections/full-bleed-section/full-bleed-section.d.ts.map +1 -1
- package/dist/components/layout/page-sections/full-bleed-section/full-bleed-section.js +21 -5
- package/dist/components/layout/page-sections/full-bleed-section/full-bleed-section.js.map +1 -1
- package/dist/components/layout/page-sections/full-bleed-section/full-bleed-section.styles.js +9 -9
- package/dist/components/layout/page-sections/full-bleed-section/full-bleed-section.styles.js.map +1 -1
- package/dist/components/layout/page-sections/one-half-one-half-section/one-half-one-half-section.d.ts +6 -3
- package/dist/components/layout/page-sections/one-half-one-half-section/one-half-one-half-section.d.ts.map +1 -1
- package/dist/components/layout/page-sections/one-half-one-half-section/one-half-one-half-section.js +21 -5
- package/dist/components/layout/page-sections/one-half-one-half-section/one-half-one-half-section.js.map +1 -1
- package/dist/components/layout/page-sections/one-half-one-half-section/one-half-one-half-section.styles.js +12 -12
- package/dist/components/layout/page-sections/one-half-one-half-section/one-half-one-half-section.styles.js.map +1 -1
- package/dist/components/layout/page-sections/one-third-two-thirds-section/one-third-two-thirds-section.d.ts +6 -3
- package/dist/components/layout/page-sections/one-third-two-thirds-section/one-third-two-thirds-section.d.ts.map +1 -1
- package/dist/components/layout/page-sections/one-third-two-thirds-section/one-third-two-thirds-section.js +21 -5
- package/dist/components/layout/page-sections/one-third-two-thirds-section/one-third-two-thirds-section.js.map +1 -1
- package/dist/components/layout/page-sections/one-third-two-thirds-section/one-third-two-thirds-section.styles.js +12 -12
- package/dist/components/layout/page-sections/one-third-two-thirds-section/one-third-two-thirds-section.styles.js.map +1 -1
- package/dist/components/layout/page-sections/simple-section/simple-section.d.ts +6 -3
- package/dist/components/layout/page-sections/simple-section/simple-section.d.ts.map +1 -1
- package/dist/components/layout/page-sections/simple-section/simple-section.js +21 -5
- package/dist/components/layout/page-sections/simple-section/simple-section.js.map +1 -1
- package/dist/components/layout/page-sections/simple-section/simple-section.styles.js +9 -9
- package/dist/components/layout/page-sections/simple-section/simple-section.styles.js.map +1 -1
- package/dist/components/layout/page-sections/two-thirds-one-third-section/two-thirds-one-third-section.d.ts +6 -3
- package/dist/components/layout/page-sections/two-thirds-one-third-section/two-thirds-one-third-section.d.ts.map +1 -1
- package/dist/components/layout/page-sections/two-thirds-one-third-section/two-thirds-one-third-section.js +21 -5
- package/dist/components/layout/page-sections/two-thirds-one-third-section/two-thirds-one-third-section.js.map +1 -1
- package/dist/components/layout/page-sections/two-thirds-one-third-section/two-thirds-one-third-section.styles.js +12 -12
- package/dist/components/layout/page-sections/two-thirds-one-third-section/two-thirds-one-third-section.styles.js.map +1 -1
- package/dist/components/layout/popover/popover.js +1 -1
- package/dist/components/layout/popover/popover.js.map +1 -1
- package/dist/components/layout/sheet/sheet.js +3 -3
- package/dist/components/layout/sheet/sheet.js.map +1 -1
- package/dist/components/layout/sheet/sheet.styles.js +5 -5
- package/dist/components/layout/spacer/spacer.d.ts +28 -3
- package/dist/components/layout/spacer/spacer.d.ts.map +1 -1
- package/dist/components/layout/spacer/spacer.js +33 -2
- package/dist/components/layout/spacer/spacer.js.map +1 -1
- package/dist/components/layout/spacer/spacer.styles.d.ts.map +1 -1
- package/dist/components/layout/spacer/spacer.styles.js +157 -110
- package/dist/components/layout/spacer/spacer.styles.js.map +1 -1
- package/dist/components/layout/split-views/split-view-pane/split-view-pane.js +1 -1
- package/dist/components/layout/split-views/split-view-pane/split-view-pane.js.map +1 -1
- package/dist/components/layout/window/window.js +1 -1
- package/dist/components/layout/window/window.js.map +1 -1
- package/dist/components/lists-and-menus/cells/cell/cell.d.ts +4 -2
- package/dist/components/lists-and-menus/cells/cell/cell.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/cell/cell.js +17 -6
- package/dist/components/lists-and-menus/cells/cell/cell.js.map +1 -1
- package/dist/components/lists-and-menus/cells/cell/cell.styles.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/cell/cell.styles.js +22 -11
- package/dist/components/lists-and-menus/cells/cell/cell.styles.js.map +1 -1
- package/dist/components/lists-and-menus/cells/description-cell/description-cell.d.ts +2 -2
- package/dist/components/lists-and-menus/cells/description-cell/description-cell.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/description-cell/description-cell.js +12 -7
- package/dist/components/lists-and-menus/cells/description-cell/description-cell.js.map +1 -1
- package/dist/components/lists-and-menus/cells/description-cell/description-cell.styles.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/description-cell/description-cell.styles.js +4 -3
- package/dist/components/lists-and-menus/cells/description-cell/description-cell.styles.js.map +1 -1
- package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.d.ts +2 -2
- package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.js +2 -2
- package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.js.map +1 -1
- package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.styles.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.styles.js +50 -14
- package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.styles.js.map +1 -1
- package/dist/components/lists-and-menus/cells/spacer-cell/spacer-cell.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/spacer-cell/spacer-cell.js +1 -1
- package/dist/components/lists-and-menus/cells/spacer-cell/spacer-cell.js.map +1 -1
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.d.ts +31 -9
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.js +71 -14
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.js.map +1 -1
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.styles.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.styles.js +49 -8
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.styles.js.map +1 -1
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.template.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.template.js +33 -3
- package/dist/components/lists-and-menus/cells/text-cell/text-cell.template.js.map +1 -1
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.d.ts +31 -9
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.js +70 -13
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.js.map +1 -1
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.styles.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.styles.js +68 -8
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.styles.js.map +1 -1
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.template.d.ts.map +1 -1
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.template.js +36 -7
- package/dist/components/lists-and-menus/cells/title-cell/title-cell.template.js.map +1 -1
- package/dist/components/lists-and-menus/list/list.d.ts.map +1 -1
- package/dist/components/lists-and-menus/list/list.i18n.js +1 -1
- package/dist/components/lists-and-menus/list/list.i18n.js.map +1 -1
- package/dist/components/lists-and-menus/list/list.js +7 -0
- package/dist/components/lists-and-menus/list/list.js.map +1 -1
- package/dist/components/lists-and-menus/list-item/list-item.d.ts +5 -0
- package/dist/components/lists-and-menus/list-item/list-item.d.ts.map +1 -1
- package/dist/components/lists-and-menus/list-item/list-item.js +7 -0
- package/dist/components/lists-and-menus/list-item/list-item.js.map +1 -1
- package/dist/components/lists-and-menus/list-item/list-item.styles.js +1 -1
- package/dist/components/lists-and-menus/menu/menu.d.ts +254 -0
- package/dist/components/lists-and-menus/menu/menu.d.ts.map +1 -1
- package/dist/components/lists-and-menus/menu/menu.i18n.d.ts +1 -0
- package/dist/components/lists-and-menus/menu/menu.i18n.d.ts.map +1 -1
- package/dist/components/lists-and-menus/menu/menu.i18n.js +1 -0
- package/dist/components/lists-and-menus/menu/menu.i18n.js.map +1 -1
- package/dist/components/lists-and-menus/menu/menu.js +906 -15
- package/dist/components/lists-and-menus/menu/menu.js.map +1 -1
- package/dist/components/lists-and-menus/menu/menu.styles.d.ts +1 -0
- package/dist/components/lists-and-menus/menu/menu.styles.d.ts.map +1 -1
- package/dist/components/lists-and-menus/menu/menu.styles.js +141 -2
- package/dist/components/lists-and-menus/menu/menu.styles.js.map +1 -1
- package/dist/components/lists-and-menus/menu/menu.template.d.ts +2 -1
- package/dist/components/lists-and-menus/menu/menu.template.d.ts.map +1 -1
- package/dist/components/lists-and-menus/menu/menu.template.js +60 -0
- package/dist/components/lists-and-menus/menu/menu.template.js.map +1 -1
- package/dist/components/navigation/document-tab-bar/document-tab-bar.styles.js +1 -1
- package/dist/components/navigation/document-tab-bar/document-tab-bar.template.js +1 -1
- package/dist/components/navigation/document-tab-bar/document-tab-bar.template.js.map +1 -1
- package/dist/components/navigation/menu-bar/menu-bar.js +4 -4
- package/dist/components/navigation/menu-bar/menu-bar.js.map +1 -1
- package/dist/components/navigation/menu-bar-item/menu-bar-item.d.ts +1 -1
- package/dist/components/navigation/menu-bar-item/menu-bar-item.d.ts.map +1 -1
- package/dist/components/navigation/menu-bar-item/menu-bar-item.js +6 -6
- package/dist/components/navigation/menu-bar-item/menu-bar-item.js.map +1 -1
- package/dist/components/navigation/menu-bar-item/menu-bar-item.styles.js +2 -2
- package/dist/components/navigation/menu-bar-item/menu-bar-item.template.js +1 -1
- package/dist/components/navigation/menu-bar-item/menu-bar-item.template.js.map +1 -1
- package/dist/components/navigation/pagination/pagination.d.ts +3 -2
- package/dist/components/navigation/pagination/pagination.d.ts.map +1 -1
- package/dist/components/navigation/pagination/pagination.js +5 -4
- package/dist/components/navigation/pagination/pagination.js.map +1 -1
- package/dist/components/navigation/pagination/pagination.styles.d.ts.map +1 -1
- package/dist/components/navigation/pagination/pagination.styles.js +5 -3
- package/dist/components/navigation/pagination/pagination.styles.js.map +1 -1
- package/dist/components/navigation/pagination/pagination.template.js +1 -1
- package/dist/components/navigation/tab-bar/tab-bar.d.ts +3 -1
- package/dist/components/navigation/tab-bar/tab-bar.d.ts.map +1 -1
- package/dist/components/navigation/tab-bar/tab-bar.js +5 -3
- package/dist/components/navigation/tab-bar/tab-bar.js.map +1 -1
- package/dist/components/navigation/tab-bar/tab-bar.styles.d.ts.map +1 -1
- package/dist/components/navigation/tab-bar/tab-bar.styles.js +35 -15
- package/dist/components/navigation/tab-bar/tab-bar.styles.js.map +1 -1
- package/dist/components/navigation/top-navigation-bar/top-navigation-bar.js +2 -2
- package/dist/components/navigation/top-navigation-bar/top-navigation-bar.js.map +1 -1
- package/dist/components/navigation/top-navigation-bar/top-navigation-bar.styles.js +6 -6
- package/dist/components/navigation/top-navigation-bar/top-navigation-bar.styles.js.map +1 -1
- package/dist/components/navigation/top-title-bar/top-title-bar.styles.d.ts.map +1 -1
- package/dist/components/navigation/top-title-bar/top-title-bar.styles.js +2 -0
- package/dist/components/navigation/top-title-bar/top-title-bar.styles.js.map +1 -1
- package/dist/components/navigation/top-title-bar/top-title-bar.template.d.ts.map +1 -1
- package/dist/components/navigation/top-title-bar/top-title-bar.template.js +1 -0
- package/dist/components/navigation/top-title-bar/top-title-bar.template.js.map +1 -1
- package/dist/components/{prism-typescript-9LDqHHRS.js → prism-typescript-BlIir5CV.js} +1 -1
- package/dist/components/status-and-feedback/badge/badge.d.ts +3 -3
- package/dist/components/status-and-feedback/badge/badge.d.ts.map +1 -1
- package/dist/components/status-and-feedback/badge/badge.js +3 -3
- package/dist/components/status-and-feedback/badge/badge.js.map +1 -1
- package/dist/components/status-and-feedback/badge/badge.styles.d.ts.map +1 -1
- package/dist/components/status-and-feedback/badge/badge.styles.js +7 -6
- package/dist/components/status-and-feedback/badge/badge.styles.js.map +1 -1
- package/dist/components/status-and-feedback/inline-dialog/inline-dialog.d.ts +7 -4
- package/dist/components/status-and-feedback/inline-dialog/inline-dialog.d.ts.map +1 -1
- package/dist/components/status-and-feedback/inline-dialog/inline-dialog.js +10 -3
- package/dist/components/status-and-feedback/inline-dialog/inline-dialog.js.map +1 -1
- package/dist/components/status-and-feedback/inline-dialog/inline-dialog.styles.d.ts.map +1 -1
- package/dist/components/status-and-feedback/inline-dialog/inline-dialog.styles.js +12 -2
- package/dist/components/status-and-feedback/inline-dialog/inline-dialog.styles.js.map +1 -1
- package/dist/css/global.css +1 -0
- package/dist/css/reset.css +7 -0
- package/dist/css/rich-text.css +21 -21
- package/dist/css/settings.css +140 -98
- package/dist/fonts/JetBrainsMono-Italic[wght].woff2 +0 -0
- package/dist/fonts/JetBrainsMono[wght].woff2 +0 -0
- package/dist/fonts/RijksSansWeb-Italic.woff2 +0 -0
- package/dist/fonts/RijksSansWeb-Regular.woff2 +0 -0
- package/dist/utilities/visibility-mixin.d.ts +18 -12
- package/dist/utilities/visibility-mixin.d.ts.map +1 -1
- package/dist/utilities/visibility-mixin.js +21 -14
- package/dist/utilities/visibility-mixin.js.map +1 -1
- package/package.json +7 -3
- package/dist/fonts/IBMPlexMono-Bold.woff2 +0 -0
- package/dist/fonts/IBMPlexMono-Regular.woff2 +0 -0
- package/dist/fonts/RijksSansVF-Italic.ttf +0 -0
- package/dist/fonts/RijksSansVF-Regular.ttf +0 -0
|
@@ -7,8 +7,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { LitElement } from 'lit';
|
|
8
8
|
import { property, state } from 'lit/decorators.js';
|
|
9
9
|
import { computePosition, flip, shift, offset, size, autoUpdate } from '@floating-ui/dom';
|
|
10
|
-
import { menuStyles, menuItemStyles, menuDividerStyles } from './menu.styles.js';
|
|
11
|
-
import { menuTemplate, menuItemTemplate, menuDividerTemplate } from './menu.template.js';
|
|
10
|
+
import { menuStyles, menuItemStyles, menuDividerStyles, menuGroupStyles } from './menu.styles.js';
|
|
11
|
+
import { menuTemplate, menuItemTemplate, menuDividerTemplate, menuGroupTemplate } from './menu.template.js';
|
|
12
12
|
import { nlddMenuTranslations } from './menu.i18n.js';
|
|
13
13
|
import '../../lists-and-menus/cells/icon-cell/icon-cell.js';
|
|
14
14
|
import '../../lists-and-menus/cells/spacer-cell/spacer-cell.js';
|
|
@@ -17,6 +17,7 @@ import '../../content/icon/icon.js';
|
|
|
17
17
|
import '../../status-and-feedback/inline-dialog/inline-dialog.js';
|
|
18
18
|
import { isKeyboardMode } from '../../../utilities/input-modality.js';
|
|
19
19
|
import { POPOVER_REOPEN_GUARD_MS } from '../../../utilities/popover-guard.js';
|
|
20
|
+
import { breakpoints } from '../../../assets/styles/breakpoints.js';
|
|
20
21
|
// # nldd-menu-divider
|
|
21
22
|
export class NLDDMenuDivider extends LitElement {
|
|
22
23
|
render() {
|
|
@@ -27,6 +28,50 @@ NLDDMenuDivider.styles = menuDividerStyles;
|
|
|
27
28
|
if (!customElements.get('nldd-menu-divider')) {
|
|
28
29
|
customElements.define('nldd-menu-divider', NLDDMenuDivider);
|
|
29
30
|
}
|
|
31
|
+
// # nldd-menu-group
|
|
32
|
+
/**
|
|
33
|
+
* A labelled group of menu items inside an nldd-menu. Wraps its slotted
|
|
34
|
+
* items in `role="group"` with `aria-labelledby` pointing to the group's
|
|
35
|
+
* `text`, providing native ARIA group semantics that a flat title element
|
|
36
|
+
* can't deliver.
|
|
37
|
+
*
|
|
38
|
+
* The group renders an automatic divider above itself, except when it's
|
|
39
|
+
* the first child of the menu — so consumers don't need to manage
|
|
40
|
+
* separator placement around groups themselves. Spacing above the title
|
|
41
|
+
* is intentionally larger than below, to bind the title visually to the
|
|
42
|
+
* items it labels.
|
|
43
|
+
*
|
|
44
|
+
* Use the wrapper for grouping with a title; for ungrouped flat menus or
|
|
45
|
+
* a divider without a title, the existing `nldd-menu-item` +
|
|
46
|
+
* `nldd-menu-divider` flat structure keeps working unchanged.
|
|
47
|
+
*
|
|
48
|
+
* @attr {string} text - Group title text shown above the items.
|
|
49
|
+
*
|
|
50
|
+
* @slot - nldd-menu-item children (the items belonging to this group).
|
|
51
|
+
*/
|
|
52
|
+
export class NLDDMenuGroup extends LitElement {
|
|
53
|
+
constructor() {
|
|
54
|
+
super(...arguments);
|
|
55
|
+
this.text = '';
|
|
56
|
+
this._titleId = `nldd-menu-group-title-${NLDDMenuGroup._idCounter++}`;
|
|
57
|
+
}
|
|
58
|
+
render() {
|
|
59
|
+
return menuGroupTemplate(this);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
NLDDMenuGroup.styles = menuGroupStyles;
|
|
63
|
+
// SSR caveat: this counter is per module instance, not per render. If
|
|
64
|
+
// the component ever runs in a context where modules reload between
|
|
65
|
+
// server and client (SSR hydration, Vite HMR with fresh module state),
|
|
66
|
+
// IDs will mismatch and aria-labelledby refs will break. Browser-only
|
|
67
|
+
// today; revisit if hydration is added.
|
|
68
|
+
NLDDMenuGroup._idCounter = 0;
|
|
69
|
+
__decorate([
|
|
70
|
+
property({ type: String, reflect: true })
|
|
71
|
+
], NLDDMenuGroup.prototype, "text", void 0);
|
|
72
|
+
if (!customElements.get('nldd-menu-group')) {
|
|
73
|
+
customElements.define('nldd-menu-group', NLDDMenuGroup);
|
|
74
|
+
}
|
|
30
75
|
// # nldd-menu-item
|
|
31
76
|
/**
|
|
32
77
|
* A single item within an nldd-menu.
|
|
@@ -61,28 +106,96 @@ export class NLDDMenuItem extends LitElement {
|
|
|
61
106
|
this.queryMarkMode = 'predictive';
|
|
62
107
|
/** Set by nldd-menu. Not part of the public API. */
|
|
63
108
|
this.menuVariant = 'menu';
|
|
109
|
+
/** Tracks whether this item's submenu (if any) is currently open. Set by
|
|
110
|
+
* the parent nldd-menu via the `submenu-open`/`submenu-close` lifecycle. */
|
|
111
|
+
this._submenuOpen = false;
|
|
112
|
+
/** Cached reference to the direct child `<nldd-menu>` (this item's
|
|
113
|
+
* submenu). Resolved once at firstUpdated; mutating the children after
|
|
114
|
+
* mount is not supported in v1. */
|
|
115
|
+
this._cachedSubmenuEl = null;
|
|
116
|
+
this._submenuMutationObserver = null;
|
|
64
117
|
}
|
|
65
118
|
connectedCallback() {
|
|
66
119
|
super.connectedCallback();
|
|
67
120
|
if (!this.id) {
|
|
68
121
|
this.id = `nldd-menu-item-${NLDDMenuItem._idCounter++}`;
|
|
69
122
|
}
|
|
70
|
-
this.addEventListener('focusin', () => {
|
|
123
|
+
this.addEventListener('focusin', (e) => {
|
|
124
|
+
// Only react when focus actually lands on our own shadow content
|
|
125
|
+
// (the .menu__item button). focusin is composed and bubbles, so it
|
|
126
|
+
// also fires here when focus moves into a slotted descendant —
|
|
127
|
+
// notably when a child submenu's .menu element gets focused on
|
|
128
|
+
// open. Without this guard the parent's _handleMenuItemFocused
|
|
129
|
+
// re-highlights the opener after we just cleared it.
|
|
130
|
+
const realTarget = e.composedPath()[0];
|
|
131
|
+
if (!realTarget || !this.shadowRoot?.contains(realTarget))
|
|
132
|
+
return;
|
|
71
133
|
this.setAttribute('data-focused', '');
|
|
72
134
|
this.dispatchEvent(new CustomEvent('menu-item-focused', {
|
|
73
135
|
bubbles: true,
|
|
74
136
|
composed: true,
|
|
75
137
|
}));
|
|
76
138
|
});
|
|
77
|
-
this.addEventListener('focusout', () =>
|
|
139
|
+
this.addEventListener('focusout', (e) => {
|
|
140
|
+
// Mirror the focusin guard so a slotted descendant losing focus
|
|
141
|
+
// doesn't strip data-focused from the opener while it's still
|
|
142
|
+
// keyboard-focused itself.
|
|
143
|
+
const realTarget = e.composedPath()[0];
|
|
144
|
+
if (!realTarget || !this.shadowRoot?.contains(realTarget))
|
|
145
|
+
return;
|
|
146
|
+
this.removeAttribute('data-focused');
|
|
147
|
+
});
|
|
78
148
|
}
|
|
79
149
|
focus(options) {
|
|
80
150
|
const focusable = this.shadowRoot?.querySelector('button, a');
|
|
81
151
|
focusable?.focus(options);
|
|
82
152
|
}
|
|
153
|
+
get _submenuEl() {
|
|
154
|
+
return this._cachedSubmenuEl;
|
|
155
|
+
}
|
|
156
|
+
get _hasSubmenu() {
|
|
157
|
+
return this._cachedSubmenuEl !== null;
|
|
158
|
+
}
|
|
159
|
+
firstUpdated() {
|
|
160
|
+
// Resolve submenu once and cache. Also trigger a single re-render so
|
|
161
|
+
// the chevron + ARIA attrs reflect the now-known submenu state.
|
|
162
|
+
this._cachedSubmenuEl = this.querySelector(':scope > nldd-menu');
|
|
163
|
+
if (this._cachedSubmenuEl !== null) {
|
|
164
|
+
this.requestUpdate();
|
|
165
|
+
}
|
|
166
|
+
// Submenu attachment is one-shot in v1 — a nldd-menu added after this
|
|
167
|
+
// point would silently miss aria-controls, the chevron indicator, the
|
|
168
|
+
// hover-open lifecycle, and the close-state sync. Watch for late
|
|
169
|
+
// additions and warn so consumers don't chase the symptom.
|
|
170
|
+
this._submenuMutationObserver = new MutationObserver(() => {
|
|
171
|
+
const current = this.querySelector(':scope > nldd-menu');
|
|
172
|
+
if (current && current !== this._cachedSubmenuEl) {
|
|
173
|
+
console.warn('[nldd-menu-item] A nldd-menu child was added after mount. '
|
|
174
|
+
+ 'Submenu attachment is resolved once at firstUpdated in v1, '
|
|
175
|
+
+ 'so this menu will not be treated as a submenu. Define the '
|
|
176
|
+
+ 'nldd-menu child before the item is connected to the DOM.');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
this._submenuMutationObserver.observe(this, { childList: true });
|
|
180
|
+
}
|
|
181
|
+
disconnectedCallback() {
|
|
182
|
+
super.disconnectedCallback();
|
|
183
|
+
this._submenuMutationObserver?.disconnect();
|
|
184
|
+
this._submenuMutationObserver = null;
|
|
185
|
+
}
|
|
83
186
|
_handleClick() {
|
|
84
187
|
if (this.disabled)
|
|
85
188
|
return;
|
|
189
|
+
// Submenu items don't fire `select` — they open their submenu instead.
|
|
190
|
+
// Item is either an action OR a submenu opener, not both.
|
|
191
|
+
if (this._hasSubmenu) {
|
|
192
|
+
this.dispatchEvent(new CustomEvent('submenu-open', {
|
|
193
|
+
detail: { submenu: this._submenuEl, item: this },
|
|
194
|
+
bubbles: true,
|
|
195
|
+
composed: false,
|
|
196
|
+
}));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
86
199
|
this.dispatchEvent(new CustomEvent('select', {
|
|
87
200
|
bubbles: true,
|
|
88
201
|
composed: true,
|
|
@@ -132,6 +245,9 @@ __decorate([
|
|
|
132
245
|
__decorate([
|
|
133
246
|
state()
|
|
134
247
|
], NLDDMenuItem.prototype, "menuVariant", void 0);
|
|
248
|
+
__decorate([
|
|
249
|
+
state()
|
|
250
|
+
], NLDDMenuItem.prototype, "_submenuOpen", void 0);
|
|
135
251
|
if (!customElements.get('nldd-menu-item')) {
|
|
136
252
|
customElements.define('nldd-menu-item', NLDDMenuItem);
|
|
137
253
|
}
|
|
@@ -195,12 +311,34 @@ export class NLDDMenu extends LitElement {
|
|
|
195
311
|
* Unset keys fall back to the Dutch default.
|
|
196
312
|
*/
|
|
197
313
|
this.translations = {};
|
|
314
|
+
/**
|
|
315
|
+
* @internal — development only.
|
|
316
|
+
*
|
|
317
|
+
* Renders a translucent SVG overlay showing the safe triangle's current
|
|
318
|
+
* shape. Reflected as `debug-safe-triangle` so it can be toggled at
|
|
319
|
+
* runtime in DevTools, but it must not be enabled in shipped code: the
|
|
320
|
+
* overlay attaches a popover to `document.body` and is intended purely
|
|
321
|
+
* as a development aid.
|
|
322
|
+
*/
|
|
323
|
+
this.debugSafeTriangle = false;
|
|
198
324
|
/**
|
|
199
325
|
* Custom filter function. Defaults to case-insensitive substring match
|
|
200
326
|
* on text, value, and aliases attributes.
|
|
201
327
|
*/
|
|
202
328
|
this.filterFn = defaultFilterFn;
|
|
203
329
|
this._isEmpty = false;
|
|
330
|
+
/** Currently open child submenu (a direct descendant nldd-menu opened
|
|
331
|
+
* by one of this menu's items). null when no submenu is open. */
|
|
332
|
+
this._activeSubmenu = null;
|
|
333
|
+
this._activeSubmenuOpener = null;
|
|
334
|
+
/** When this menu is itself a submenu, points to the parent menu that
|
|
335
|
+
* opened it. Set by the parent's _handleSubmenuOpen. null on the root.
|
|
336
|
+
* @internal */
|
|
337
|
+
this._parentMenu = null;
|
|
338
|
+
/** The menu-item that triggered this submenu — used to label the back
|
|
339
|
+
* button in drill-in mode.
|
|
340
|
+
* @internal */
|
|
341
|
+
this._parentItem = null;
|
|
204
342
|
this._isOpen = false;
|
|
205
343
|
this._closedAt = 0;
|
|
206
344
|
this._cleanupAutoUpdate = null;
|
|
@@ -221,60 +359,289 @@ export class NLDDMenu extends LitElement {
|
|
|
221
359
|
this.showPopover();
|
|
222
360
|
}
|
|
223
361
|
};
|
|
362
|
+
// — Safe triangle (cascade only) ——————————————————————————————————————
|
|
363
|
+
//
|
|
364
|
+
// While a cascade submenu is open, a global mousemove listener tracks
|
|
365
|
+
// the cursor against a "safe triangle" (Mayank/ishadeed pattern). The
|
|
366
|
+
// triangle's apex is captured once on the first mousemove after submenu
|
|
367
|
+
// open; the base is the submenu's two near corners (top + bottom of the
|
|
368
|
+
// edge facing the parent — left edge if submenu is right of cursor,
|
|
369
|
+
// right edge if floating-ui flipped it to the other side). The wedge
|
|
370
|
+
// stays fixed until the cursor enters the submenu, so the user has a
|
|
371
|
+
// predictable, stable area to traverse without losing the submenu.
|
|
372
|
+
// While the cursor is inside the triangle, peer items don't activate
|
|
373
|
+
// and the linger-close timer is held off.
|
|
374
|
+
this._hoverOpenTimer = null;
|
|
375
|
+
this._hoverCloseTimer = null;
|
|
376
|
+
this._safeTriangleListener = null;
|
|
377
|
+
this._safeTriangleStallTimer = null;
|
|
378
|
+
this._lastCursorPos = null;
|
|
379
|
+
this._safeTriangleApex = null;
|
|
380
|
+
this._safeTriangleSubmenu = null;
|
|
381
|
+
this._movingTowardSubmenu = false;
|
|
224
382
|
this._handleMenuItemMouseenter = (event) => {
|
|
225
383
|
const item = event.target.closest('nldd-menu-item');
|
|
226
|
-
if (!item
|
|
384
|
+
if (!item)
|
|
227
385
|
return;
|
|
228
|
-
this.
|
|
386
|
+
this._activateItem(item);
|
|
229
387
|
};
|
|
230
388
|
this._handleMouseleave = () => {
|
|
231
389
|
if (this.variant !== 'listbox')
|
|
232
390
|
this._clearHighlight();
|
|
391
|
+
this._cancelHoverOpen();
|
|
392
|
+
// The hover-guard takes over close behaviour while the cursor is
|
|
393
|
+
// outside both the parent menu and the active submenu.
|
|
233
394
|
};
|
|
395
|
+
// — Debug overlay ————————————————————————————————————————————————————
|
|
396
|
+
this._safeTriangleOverlay = null;
|
|
234
397
|
this._handleMenuItemFocused = (event) => {
|
|
235
398
|
const item = event.target.closest('nldd-menu-item');
|
|
236
399
|
if (!item || item.disabled || item.hasAttribute('hidden'))
|
|
237
400
|
return;
|
|
401
|
+
// Same caveat as _handleMenuItemMouseenter: this listener catches
|
|
402
|
+
// focused events that bubble up from descendant submenus too. Skip
|
|
403
|
+
// those so we don't highlight items that aren't ours.
|
|
404
|
+
if (item.closest('nldd-menu') !== this)
|
|
405
|
+
return;
|
|
238
406
|
this._setHighlight(item);
|
|
239
407
|
};
|
|
408
|
+
/**
|
|
409
|
+
* Open a submenu in response to one of this menu's items dispatching
|
|
410
|
+
* `submenu-open`. Branches on drill-in vs cascade mode for anchor +
|
|
411
|
+
* placement, but the lifecycle (popover.show, listen for close, clear
|
|
412
|
+
* state on hide) is the same for both modes.
|
|
413
|
+
*/
|
|
414
|
+
this._handleSubmenuOpen = (event) => {
|
|
415
|
+
// Only handle events from items that are direct children of this menu.
|
|
416
|
+
// Items inside a sub-submenu fire their own submenu-open which bubbles
|
|
417
|
+
// here too — we let that one bubble past, our descendant menu handles it.
|
|
418
|
+
const { item, submenu } = event.detail;
|
|
419
|
+
if (item.closest('nldd-menu') !== this)
|
|
420
|
+
return;
|
|
421
|
+
event.stopPropagation();
|
|
422
|
+
// Same submenu already open — bail before re-running the open path. Without
|
|
423
|
+
// this, hover-opening then clicking the same opener (or rapid double-click)
|
|
424
|
+
// would stack a second `toggle` listener; `showPopover()` is a no-op on an
|
|
425
|
+
// already-open popover so both listeners would survive and double-fire on
|
|
426
|
+
// the next close.
|
|
427
|
+
if (this._activeSubmenu === submenu)
|
|
428
|
+
return;
|
|
429
|
+
// Close any other submenu that's already open in this menu before
|
|
430
|
+
// opening a new one — only one peer submenu visible at a time.
|
|
431
|
+
if (this._activeSubmenu && this._activeSubmenu !== submenu) {
|
|
432
|
+
this._activeSubmenu.hidePopover?.();
|
|
433
|
+
}
|
|
434
|
+
submenu._parentMenu = this;
|
|
435
|
+
submenu._parentItem = item;
|
|
436
|
+
if (this._drillInMode) {
|
|
437
|
+
// Drill-in: anchor to the root's anchor so all submenus open at the
|
|
438
|
+
// same screen position — visually stacks. Inherit root placement
|
|
439
|
+
// for consistent direction. Back button rendered in template.
|
|
440
|
+
const root = this._rootMenu;
|
|
441
|
+
submenu.anchorElement = root._getAnchorEl();
|
|
442
|
+
submenu.placement = root.placement;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
// Cascade: anchor to the parent item, open beside it.
|
|
446
|
+
submenu.anchorElement = item;
|
|
447
|
+
submenu.placement = 'right-start';
|
|
448
|
+
}
|
|
449
|
+
this._activeSubmenu = submenu;
|
|
450
|
+
this._activeSubmenuOpener = item;
|
|
451
|
+
item._submenuOpen = true;
|
|
452
|
+
// Clear [highlighted] on the opener — the "logically current" item is
|
|
453
|
+
// now in the submenu, not on the opener. The opener still shows visibly
|
|
454
|
+
// active via .menu__item[aria-expanded="true"] (lighter neutral); CSS
|
|
455
|
+
// :hover upgrades it to accent if the cursor returns to it.
|
|
456
|
+
item.removeAttribute('highlighted');
|
|
457
|
+
// Listen once for the submenu's close so we can clear state, ARIA and
|
|
458
|
+
// any leftover safe-triangle plumbing from the cascade open path.
|
|
459
|
+
const onToggle = (e) => {
|
|
460
|
+
const tg = e;
|
|
461
|
+
if (tg.newState !== 'closed')
|
|
462
|
+
return;
|
|
463
|
+
submenu.removeEventListener('toggle', onToggle);
|
|
464
|
+
if (this._activeSubmenu === submenu) {
|
|
465
|
+
this._activeSubmenu = null;
|
|
466
|
+
this._activeSubmenuOpener = null;
|
|
467
|
+
}
|
|
468
|
+
submenu._parentMenu = null;
|
|
469
|
+
submenu._parentItem = null;
|
|
470
|
+
item._submenuOpen = false;
|
|
471
|
+
// Drop any safe-triangle "in transit" highlight on the opener —
|
|
472
|
+
// without this, an opener whose submenu was closed via stall-
|
|
473
|
+
// dismissal or programmatic hidePopover keeps the bold accent
|
|
474
|
+
// even though the close should also drop the visual signal.
|
|
475
|
+
// Skip when the item is currently focused — keyboard close
|
|
476
|
+
// (ArrowLeft) sync-focuses the opener before this async toggle
|
|
477
|
+
// fires, and that focus already set [highlighted] via the
|
|
478
|
+
// menu-item-focused chain; stripping it here would leave the
|
|
479
|
+
// opener visibly unhighlighted despite being focused.
|
|
480
|
+
if (!item.hasAttribute('data-focused')) {
|
|
481
|
+
item.removeAttribute('highlighted');
|
|
482
|
+
}
|
|
483
|
+
this._cancelHoverClose();
|
|
484
|
+
this._stopSafeTriangle();
|
|
485
|
+
// Counterpart to `submenu-open` — consumers tracking submenu state
|
|
486
|
+
// via declarative event listeners (rather than reaching into our
|
|
487
|
+
// internals) get a clean close signal here.
|
|
488
|
+
this.dispatchEvent(new CustomEvent('submenu-close', {
|
|
489
|
+
detail: { submenu, item },
|
|
490
|
+
bubbles: true,
|
|
491
|
+
composed: false,
|
|
492
|
+
}));
|
|
493
|
+
};
|
|
494
|
+
submenu.addEventListener('toggle', onToggle);
|
|
495
|
+
submenu.showPopover?.();
|
|
496
|
+
// Start the hover triangle tracker for cascade-mode submenus only.
|
|
497
|
+
// Drill-in mode replaces the parent view, so there's no "moving toward"
|
|
498
|
+
// path between two visible menus to protect.
|
|
499
|
+
if (!this._drillInMode) {
|
|
500
|
+
// Wait one frame so the submenu's getBoundingClientRect reflects its
|
|
501
|
+
// final position (computePosition runs in the toggle handler).
|
|
502
|
+
requestAnimationFrame(() => {
|
|
503
|
+
if (this._activeSubmenu === submenu) {
|
|
504
|
+
this._startSafeTriangle(submenu);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
/** Close this submenu when the back button is clicked, returning to the
|
|
510
|
+
* parent view (which is still open as a popover behind this one in
|
|
511
|
+
* drill-in mode).
|
|
512
|
+
* @internal */
|
|
513
|
+
this._handleBack = () => {
|
|
514
|
+
this.hidePopover?.();
|
|
515
|
+
};
|
|
516
|
+
/** Cursor moved onto the drill-in back button — clear any item highlight
|
|
517
|
+
* so we don't show two "active" elements at once (back button + first
|
|
518
|
+
* item that was auto-highlighted on open).
|
|
519
|
+
* @internal */
|
|
520
|
+
this._handleBackMouseenter = () => {
|
|
521
|
+
this._clearHighlight();
|
|
522
|
+
};
|
|
523
|
+
/** Last cached drill-in mode value, so we only act when it actually
|
|
524
|
+
* changes across a resize event. */
|
|
525
|
+
this._lastDrillInMode = null;
|
|
526
|
+
/** Close any open submenu when the cascade ↔ drill-in threshold is
|
|
527
|
+
* crossed during a resize. Switching the rendering of an already-open
|
|
528
|
+
* submenu mid-flight (anchor + placement + back button) is more
|
|
529
|
+
* disorienting than a clean reset to the root view. */
|
|
530
|
+
this._handleViewportResize = () => {
|
|
531
|
+
const current = this._drillInMode;
|
|
532
|
+
if (this._lastDrillInMode === null) {
|
|
533
|
+
this._lastDrillInMode = current;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (this._lastDrillInMode === current)
|
|
537
|
+
return;
|
|
538
|
+
this._lastDrillInMode = current;
|
|
539
|
+
if (this._activeSubmenu) {
|
|
540
|
+
this._activeSubmenu.hidePopover?.();
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
/**
|
|
544
|
+
* Close this menu when a `select` event bubbles up — selecting an item
|
|
545
|
+
* anywhere in the menu (or any descendant submenu) closes the entire
|
|
546
|
+
* popover chain so the action feels final. The select event is dispatched
|
|
547
|
+
* with `composed: true` so it crosses every ancestor menu in the chain.
|
|
548
|
+
*/
|
|
549
|
+
this._handleSelectChainClose = () => {
|
|
550
|
+
this.hidePopover?.();
|
|
551
|
+
};
|
|
240
552
|
// — Private handlers ——————————————————————————————————————————————————————
|
|
241
553
|
this._handleKeydown = (event) => {
|
|
242
554
|
const items = this._getVisibleItems();
|
|
243
555
|
if (items.length === 0)
|
|
244
556
|
return;
|
|
557
|
+
// keydown bubbles up the flat tree, so this handler fires on every
|
|
558
|
+
// ancestor menu in the chain. Bail when focus belongs to a descendant
|
|
559
|
+
// submenu — that submenu's own handler already processed the key, and
|
|
560
|
+
// processing it here too would advance navigation an extra step per
|
|
561
|
+
// ancestor (or close an extra level on Escape).
|
|
562
|
+
const focusedDescendant = this.querySelector('nldd-menu-item[data-focused]');
|
|
563
|
+
if (focusedDescendant && focusedDescendant.closest('nldd-menu') !== this)
|
|
564
|
+
return;
|
|
245
565
|
const index = this._getFocusedIndex(items);
|
|
246
566
|
switch (event.key) {
|
|
247
567
|
case 'ArrowDown': {
|
|
248
568
|
event.preventDefault();
|
|
569
|
+
event.stopPropagation();
|
|
249
570
|
const next = index === -1 ? 0 : index < items.length - 1 ? index + 1 : 0;
|
|
250
571
|
items[next].focus();
|
|
251
572
|
break;
|
|
252
573
|
}
|
|
253
574
|
case 'ArrowUp': {
|
|
254
575
|
event.preventDefault();
|
|
576
|
+
event.stopPropagation();
|
|
255
577
|
const prev = index === -1 ? items.length - 1 : index > 0 ? index - 1 : items.length - 1;
|
|
256
578
|
items[prev].focus();
|
|
257
579
|
break;
|
|
258
580
|
}
|
|
259
581
|
case 'Home': {
|
|
260
582
|
event.preventDefault();
|
|
583
|
+
event.stopPropagation();
|
|
261
584
|
items[0].focus();
|
|
262
585
|
break;
|
|
263
586
|
}
|
|
264
587
|
case 'End': {
|
|
265
588
|
event.preventDefault();
|
|
589
|
+
event.stopPropagation();
|
|
266
590
|
items[items.length - 1].focus();
|
|
267
591
|
break;
|
|
268
592
|
}
|
|
593
|
+
case 'ArrowRight': {
|
|
594
|
+
// Open the focused item's submenu if it has one. Mode-agnostic:
|
|
595
|
+
// the parent menu's _handleSubmenuOpen handler routes to cascade
|
|
596
|
+
// or drill-in based on viewport.
|
|
597
|
+
const focused = items[index];
|
|
598
|
+
if (focused?._hasSubmenu) {
|
|
599
|
+
event.preventDefault();
|
|
600
|
+
event.stopPropagation();
|
|
601
|
+
focused._handleClick();
|
|
602
|
+
// Focus the first item in the submenu once it's open.
|
|
603
|
+
requestAnimationFrame(() => {
|
|
604
|
+
this._activeSubmenu?._getVisibleItems()[0]?.focus();
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case 'ArrowLeft': {
|
|
610
|
+
// In a submenu (cascade or drill-in): close it and return focus
|
|
611
|
+
// to the parent item. On the root menu, ArrowLeft is a no-op.
|
|
612
|
+
if (this._isSubmenu) {
|
|
613
|
+
event.preventDefault();
|
|
614
|
+
event.stopPropagation();
|
|
615
|
+
const parentItem = this._parentItem;
|
|
616
|
+
this.hidePopover();
|
|
617
|
+
parentItem?.focus();
|
|
618
|
+
}
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
269
621
|
case 'Escape': {
|
|
270
622
|
event.preventDefault();
|
|
623
|
+
event.stopPropagation();
|
|
271
624
|
this.hidePopover();
|
|
272
|
-
|
|
273
|
-
|
|
625
|
+
// On a submenu close, return focus to the parent item rather
|
|
626
|
+
// than the root anchor — keeps the user oriented in the chain.
|
|
627
|
+
const focusTarget = this._isSubmenu
|
|
628
|
+
? this._parentItem
|
|
629
|
+
: this._getAnchorEl();
|
|
630
|
+
focusTarget?.focus();
|
|
274
631
|
break;
|
|
275
632
|
}
|
|
633
|
+
default: {
|
|
634
|
+
// Typeahead (ARIA APG menu pattern): typing a single printable
|
|
635
|
+
// character jumps to the next item whose visible text starts
|
|
636
|
+
// with it. Repeated presses cycle through matches.
|
|
637
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
638
|
+
this._handleTypeahead(event, items, index);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
276
641
|
}
|
|
277
642
|
};
|
|
643
|
+
this._typeaheadBuffer = '';
|
|
644
|
+
this._typeaheadTimer = null;
|
|
278
645
|
this._handleToggle = async (event) => {
|
|
279
646
|
const toggleEvent = event;
|
|
280
647
|
this._isOpen = toggleEvent.newState === 'open';
|
|
@@ -319,6 +686,15 @@ export class NLDDMenu extends LitElement {
|
|
|
319
686
|
get _resolvedEmptyText() {
|
|
320
687
|
return this.emptyText || this._t('components.menu.empty-text');
|
|
321
688
|
}
|
|
689
|
+
/** Accessible name for the drill-in back button. Prefixes the parent
|
|
690
|
+
* item's label with the localised "back" word so AT announces "Back:
|
|
691
|
+
* Bestand" instead of an ambiguous "Bestand, button". Only used when
|
|
692
|
+
* this menu is itself a submenu in drill-in mode. */
|
|
693
|
+
get _resolvedBackLabel() {
|
|
694
|
+
const back = this._t('components.menu.back');
|
|
695
|
+
const parent = this._parentItem?.text ?? '';
|
|
696
|
+
return parent ? `${back}: ${parent}` : back;
|
|
697
|
+
}
|
|
322
698
|
// — Lifecycle ——————————————————————————————————————————————————————————————
|
|
323
699
|
updated(changedProperties) {
|
|
324
700
|
if (changedProperties.has('width')) {
|
|
@@ -351,9 +727,385 @@ export class NLDDMenu extends LitElement {
|
|
|
351
727
|
return document.getElementById(this.anchor);
|
|
352
728
|
return null;
|
|
353
729
|
}
|
|
354
|
-
|
|
730
|
+
/**
|
|
731
|
+
* Item-activation logic shared between the native `mouseenter` capture
|
|
732
|
+
* handler and the safe-triangle recovery paths (sideways exit, direction
|
|
733
|
+
* reversal, stall dismissal). Those paths know which item is under the
|
|
734
|
+
* cursor — they don't need to synthesize a MouseEvent just to re-enter
|
|
735
|
+
* the handler. Factoring out the body keeps the entry contracts honest:
|
|
736
|
+
* the recovery sites pass an already-resolved item rather than relying
|
|
737
|
+
* on `event.target` semantics they'd have to fake.
|
|
738
|
+
*/
|
|
739
|
+
_activateItem(item) {
|
|
740
|
+
if (item.disabled || item.hasAttribute('hidden'))
|
|
741
|
+
return;
|
|
742
|
+
// The mouseenter listener is registered with capture:true, so it fires
|
|
743
|
+
// for events targeted at items in descendant submenus too. Bail out
|
|
744
|
+
// for those — the descendant menu has its own listener that will
|
|
745
|
+
// handle them. (Synthetic callers can hit this branch when the item
|
|
746
|
+
// under the cursor belongs to a nested menu.)
|
|
747
|
+
if (item.closest('nldd-menu') !== this)
|
|
748
|
+
return;
|
|
749
|
+
// Hover-triangle guard: while the user is on a path toward the active
|
|
750
|
+
// submenu, peer items don't get highlighted and don't schedule
|
|
751
|
+
// hover-opens. Without this, the highlight flickers across every item
|
|
752
|
+
// the cursor brushes past on its diagonal route to the submenu.
|
|
753
|
+
if (this._movingTowardSubmenu && item !== this._activeSubmenuOpener)
|
|
754
|
+
return;
|
|
755
|
+
this._setHighlight(item);
|
|
756
|
+
// Cursor is on a real item now (not in transit) — cancel any pending
|
|
757
|
+
// linger-close from a previous sweep through.
|
|
758
|
+
this._cancelHoverClose();
|
|
759
|
+
if (this._drillInMode)
|
|
760
|
+
return; // Touch / narrow viewport: no hover-open.
|
|
761
|
+
// Settled on a peer item (not the active submenu's opener) — close the
|
|
762
|
+
// active submenu immediately. If this peer itself has a submenu, the
|
|
763
|
+
// hover-open below will then schedule its own. We don't wait for the
|
|
764
|
+
// linger-close: the user has already committed to a different item.
|
|
765
|
+
if (this._activeSubmenu && item !== this._activeSubmenuOpener) {
|
|
766
|
+
this._activeSubmenu.hidePopover?.();
|
|
767
|
+
}
|
|
768
|
+
// Always cancel any pending hover-open from a previous item — moving
|
|
769
|
+
// the cursor onto a peer (with or without submenu) means the previous
|
|
770
|
+
// item's submenu should not open after its delay. Then schedule a new
|
|
771
|
+
// one only when the new item itself has a submenu.
|
|
772
|
+
this._cancelHoverOpen();
|
|
773
|
+
if (item._hasSubmenu && this._activeSubmenuOpener !== item) {
|
|
774
|
+
this._hoverOpenTimer = window.setTimeout(() => {
|
|
775
|
+
this._hoverOpenTimer = null;
|
|
776
|
+
item._handleClick();
|
|
777
|
+
}, 150);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/** Resolve the menu-item under a viewport point and activate it via
|
|
781
|
+
* `_activateItem`. Used by safe-triangle recovery paths whose natural
|
|
782
|
+
* mouseenter has already fired (and bailed) — they need to re-trigger
|
|
783
|
+
* activation without synthesizing a MouseEvent. */
|
|
784
|
+
_activateItemAt(p) {
|
|
785
|
+
const el = document.elementFromPoint(p.x, p.y);
|
|
786
|
+
const item = el?.closest('nldd-menu-item');
|
|
787
|
+
if (item)
|
|
788
|
+
this._activateItem(item);
|
|
789
|
+
}
|
|
790
|
+
/** Start the global mouse tracker that powers the safe triangle. Called
|
|
791
|
+
* by _handleSubmenuOpen when a submenu opens in cascade mode. The
|
|
792
|
+
* listener is a thin orchestrator over the named phase methods below. */
|
|
793
|
+
_startSafeTriangle(submenu) {
|
|
794
|
+
this._stopSafeTriangle();
|
|
795
|
+
this._lastCursorPos = null;
|
|
796
|
+
this._safeTriangleApex = null;
|
|
797
|
+
this._safeTriangleSubmenu = submenu;
|
|
798
|
+
this._movingTowardSubmenu = false;
|
|
799
|
+
this._safeTriangleListener = (e) => {
|
|
800
|
+
const p = { x: e.clientX, y: e.clientY };
|
|
801
|
+
const r = submenu.getBoundingClientRect();
|
|
802
|
+
// 1. In submenu (or any nested submenu) — arrived, clean up.
|
|
803
|
+
if (NLDDMenu._isPointInMenuTree(p, submenu)) {
|
|
804
|
+
this._safeTriangleArrived(p);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const openerRect = this._activeSubmenuOpener?.getBoundingClientRect();
|
|
808
|
+
const cursorOnOpener = NLDDMenu._rectContainsPoint(openerRect, p);
|
|
809
|
+
const lastOnOpener = NLDDMenu._rectContainsPoint(openerRect, this._lastCursorPos);
|
|
810
|
+
// 2. Returned to opener — drop apex so the next exit can re-pin
|
|
811
|
+
// a fresh wedge. Overlay stays drawn until next render.
|
|
812
|
+
if (this._safeTriangleApex !== null && cursorOnOpener) {
|
|
813
|
+
this._safeTriangleApex = null;
|
|
814
|
+
}
|
|
815
|
+
const wasMoving = this._movingTowardSubmenu;
|
|
816
|
+
if (this._safeTriangleApex === null) {
|
|
817
|
+
// 3a. Still on opener — wait for an exit direction.
|
|
818
|
+
if (cursorOnOpener) {
|
|
819
|
+
this._safeTriangleWaitOnOpener(p, r, openerRect);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
// 3b. Crossed top/bottom edge of opener — pin apex.
|
|
823
|
+
if (openerRect && lastOnOpener && (p.y > openerRect.bottom || p.y < openerRect.top)) {
|
|
824
|
+
this._safeTrianglePinApex(p, openerRect);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
// 3c. Exited sideways — no protection, fall through to
|
|
828
|
+
// linger-close so the submenu closes if cursor leaves
|
|
829
|
+
// the menu entirely.
|
|
830
|
+
this._safeTriangleSidewaysExit(p, wasMoving);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
// 4. Apex pinned — test whether cursor is inside the wedge.
|
|
835
|
+
this._safeTrianglePolygonTest(p, r);
|
|
836
|
+
}
|
|
837
|
+
this._lastCursorPos = p;
|
|
838
|
+
// 5. Common per-frame work: highlight, recovery, timers.
|
|
839
|
+
this._safeTriangleSyncOpenerHighlight();
|
|
840
|
+
this._safeTriangleDirectionReversalRecovery(p, wasMoving);
|
|
841
|
+
this._safeTriangleScheduleStall();
|
|
842
|
+
if (this._movingTowardSubmenu) {
|
|
843
|
+
this._cancelHoverClose();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
this._safeTriangleMaybeScheduleLingerClose(p);
|
|
847
|
+
};
|
|
848
|
+
window.addEventListener('mousemove', this._safeTriangleListener);
|
|
849
|
+
}
|
|
850
|
+
// — Safe-triangle phase methods ————————————————————————————————————————
|
|
851
|
+
/** Cursor entered the submenu (or any nested submenu) — clean up triangle
|
|
852
|
+
* state but leave the window mousemove listener alive. The listener still
|
|
853
|
+
* needs to fire while the cursor is inside the submenu so we can detect a
|
|
854
|
+
* return trip to the opener and re-pin a fresh apex for another exit. */
|
|
855
|
+
_safeTriangleArrived(p) {
|
|
856
|
+
this._movingTowardSubmenu = false;
|
|
857
|
+
this._cancelHoverClose();
|
|
858
|
+
this._lastCursorPos = p;
|
|
859
|
+
this._safeTriangleApex = null;
|
|
860
|
+
this._activeSubmenuOpener?.removeAttribute('highlighted');
|
|
861
|
+
this._removeSafeTriangleOverlay();
|
|
862
|
+
}
|
|
863
|
+
/** Cursor still on the opener — wait for an exit direction. Sets the
|
|
864
|
+
* "in transit" flag (so brief peer pass-throughs don't activate) and
|
|
865
|
+
* renders a live debug preview that follows the cursor. */
|
|
866
|
+
_safeTriangleWaitOnOpener(p, r, openerRect) {
|
|
867
|
+
this._movingTowardSubmenu = true;
|
|
868
|
+
this._lastCursorPos = p;
|
|
869
|
+
// Set opener highlight up front so the bold accent is already on
|
|
870
|
+
// while the cursor is still on the opener — avoids a visible "pop"
|
|
871
|
+
// when the cursor first crosses the bottom edge.
|
|
872
|
+
if (this._activeSubmenuOpener
|
|
873
|
+
&& !this._activeSubmenuOpener.hasAttribute('highlighted')) {
|
|
874
|
+
this._setHighlight(this._activeSubmenuOpener);
|
|
875
|
+
}
|
|
876
|
+
if (this.debugSafeTriangle && openerRect) {
|
|
877
|
+
const nearX = p.x < r.left ? r.left : r.right;
|
|
878
|
+
this._renderSafeTriangleOverlay([
|
|
879
|
+
{ x: p.x - NLDDMenu._SAFE_TRIANGLE_APEX_X_OFFSET, y: p.y },
|
|
880
|
+
{ x: nearX, y: r.top },
|
|
881
|
+
{ x: nearX, y: r.bottom },
|
|
882
|
+
]);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/** Cursor crossed the opener's top or bottom edge — pin apex there.
|
|
886
|
+
* x is clamped to the opener's range so the wedge starts at the visual
|
|
887
|
+
* edge regardless of how far past the edge this mousemove sample is, then
|
|
888
|
+
* shifted by _SAFE_TRIANGLE_APEX_X_OFFSET to widen the wedge at the
|
|
889
|
+
* cursor side (the 4px headroom that makes brief diagonal wobble forgive). */
|
|
890
|
+
_safeTrianglePinApex(p, openerRect) {
|
|
891
|
+
const apexX = Math.max(openerRect.left, Math.min(openerRect.right, p.x))
|
|
892
|
+
- NLDDMenu._SAFE_TRIANGLE_APEX_X_OFFSET;
|
|
893
|
+
const apexY = p.y > openerRect.bottom ? openerRect.bottom : openerRect.top;
|
|
894
|
+
this._safeTriangleApex = { x: apexX, y: apexY };
|
|
895
|
+
this._movingTowardSubmenu = true;
|
|
896
|
+
}
|
|
897
|
+
/** Cursor exited the opener sideways (left/right) or with no opener
|
|
898
|
+
* history — no triangle protection. Drop the flag and the overlay,
|
|
899
|
+
* and synthesize mouseenter on the element under the cursor so peers
|
|
900
|
+
* activate immediately (the natural mouseenter fired with flag=true
|
|
901
|
+
* during the on-opener phase and got bailed). */
|
|
902
|
+
_safeTriangleSidewaysExit(p, wasMoving) {
|
|
903
|
+
this._movingTowardSubmenu = false;
|
|
904
|
+
this._lastCursorPos = p;
|
|
905
|
+
this._removeSafeTriangleOverlay();
|
|
906
|
+
if (wasMoving)
|
|
907
|
+
this._activateItemAt(p);
|
|
908
|
+
}
|
|
909
|
+
/** Apex pinned — test whether the current cursor position is inside the
|
|
910
|
+
* wedge. Picks the submenu edge facing the cursor (left for right-start,
|
|
911
|
+
* right when floating-ui flipped it). */
|
|
912
|
+
_safeTrianglePolygonTest(p, r) {
|
|
913
|
+
const nearX = p.x < r.left ? r.left : r.right;
|
|
914
|
+
const apex = this._safeTriangleApex;
|
|
915
|
+
// Single-point apex (no jitter band): the apex is already offset left
|
|
916
|
+
// of the actual crossing point by _SAFE_TRIANGLE_APEX_X_OFFSET, so the
|
|
917
|
+
// cursor sits comfortably inside the wedge near the opener edge — no
|
|
918
|
+
// degenerate cases where a 1px jitter pops the cursor out at the apex.
|
|
919
|
+
const wedge = [
|
|
920
|
+
apex,
|
|
921
|
+
{ x: nearX, y: r.top },
|
|
922
|
+
{ x: nearX, y: r.bottom },
|
|
923
|
+
];
|
|
924
|
+
this._movingTowardSubmenu = NLDDMenu._pointInPolygon(p, wedge);
|
|
925
|
+
if (this.debugSafeTriangle) {
|
|
926
|
+
this._renderSafeTriangleOverlay(wedge);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/** Sync opener highlight with the in-transit flag. Uses _setHighlight so
|
|
930
|
+
* any peer that briefly got highlighted (e.g. via the synthetic
|
|
931
|
+
* mouseenter on a wedge-edge wobble) is cleared. Skip when the opener
|
|
932
|
+
* already has the attribute to avoid DOM churn each frame. */
|
|
933
|
+
_safeTriangleSyncOpenerHighlight() {
|
|
934
|
+
if (this._activeSubmenuOpener && this._movingTowardSubmenu
|
|
935
|
+
&& !this._activeSubmenuOpener.hasAttribute('highlighted')) {
|
|
936
|
+
this._setHighlight(this._activeSubmenuOpener);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/** When the in-transit flag flips true → false, the cursor may already
|
|
940
|
+
* be sitting on a peer item whose natural mouseenter fired (and bailed)
|
|
941
|
+
* earlier. mouseenter won't re-fire until the cursor leaves and re-
|
|
942
|
+
* enters, so synthesize one here to activate the peer immediately. */
|
|
943
|
+
_safeTriangleDirectionReversalRecovery(p, wasMoving) {
|
|
944
|
+
if (!wasMoving || this._movingTowardSubmenu)
|
|
945
|
+
return;
|
|
946
|
+
this._activateItemAt(p);
|
|
947
|
+
}
|
|
948
|
+
/** Re-arm the stall-dismissal timer. If the cursor sits motionless
|
|
949
|
+
* inside the safe triangle long enough, drop the protection so the
|
|
950
|
+
* element under the cursor can become interactive without nudging. */
|
|
951
|
+
_safeTriangleScheduleStall() {
|
|
952
|
+
if (this._safeTriangleStallTimer !== null) {
|
|
953
|
+
clearTimeout(this._safeTriangleStallTimer);
|
|
954
|
+
this._safeTriangleStallTimer = null;
|
|
955
|
+
}
|
|
956
|
+
if (!this._movingTowardSubmenu)
|
|
957
|
+
return;
|
|
958
|
+
this._safeTriangleStallTimer = window.setTimeout(() => {
|
|
959
|
+
this._safeTriangleStallTimer = null;
|
|
960
|
+
this._movingTowardSubmenu = false;
|
|
961
|
+
this._safeTriangleApex = null;
|
|
962
|
+
if (this._lastCursorPos)
|
|
963
|
+
this._activateItemAt(this._lastCursorPos);
|
|
964
|
+
}, NLDDMenu._SAFE_TRIANGLE_STALL_DISMISS_MS);
|
|
965
|
+
}
|
|
966
|
+
/** Schedule submenu close when cursor sits outside the parent menu rect.
|
|
967
|
+
* No-op if cursor is inside the menu (peer-hover logic handles it) or
|
|
968
|
+
* if a close is already scheduled. */
|
|
969
|
+
_safeTriangleMaybeScheduleLingerClose(p) {
|
|
970
|
+
const submenu = this._safeTriangleSubmenu;
|
|
971
|
+
if (!submenu)
|
|
972
|
+
return;
|
|
973
|
+
const rootRect = this.getBoundingClientRect();
|
|
974
|
+
const inRoot = NLDDMenu._rectContainsPoint(rootRect, p);
|
|
975
|
+
if (!inRoot && this._hoverCloseTimer === null) {
|
|
976
|
+
this._hoverCloseTimer = window.setTimeout(() => {
|
|
977
|
+
this._hoverCloseTimer = null;
|
|
978
|
+
if (this._activeSubmenu === submenu) {
|
|
979
|
+
submenu.hidePopover?.();
|
|
980
|
+
}
|
|
981
|
+
}, NLDDMenu._SUBMENU_LINGER_CLOSE_MS);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/** True when the rect contains the point (inclusive). Returns false for
|
|
985
|
+
* a missing rect or point — convenient for the optional-chained call
|
|
986
|
+
* sites in the safe-triangle logic. */
|
|
987
|
+
static _rectContainsPoint(rect, point) {
|
|
988
|
+
return !!rect && !!point
|
|
989
|
+
&& point.x >= rect.left && point.x <= rect.right
|
|
990
|
+
&& point.y >= rect.top && point.y <= rect.bottom;
|
|
991
|
+
}
|
|
992
|
+
_stopSafeTriangle() {
|
|
993
|
+
if (this._safeTriangleListener !== null) {
|
|
994
|
+
window.removeEventListener('mousemove', this._safeTriangleListener);
|
|
995
|
+
this._safeTriangleListener = null;
|
|
996
|
+
}
|
|
997
|
+
if (this._safeTriangleStallTimer !== null) {
|
|
998
|
+
clearTimeout(this._safeTriangleStallTimer);
|
|
999
|
+
this._safeTriangleStallTimer = null;
|
|
1000
|
+
}
|
|
1001
|
+
this._lastCursorPos = null;
|
|
1002
|
+
this._safeTriangleApex = null;
|
|
1003
|
+
this._safeTriangleSubmenu = null;
|
|
1004
|
+
this._movingTowardSubmenu = false;
|
|
1005
|
+
this._removeSafeTriangleOverlay();
|
|
1006
|
+
}
|
|
1007
|
+
_renderSafeTriangleOverlay(vertices) {
|
|
1008
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
1009
|
+
if (this._safeTriangleOverlay === null) {
|
|
1010
|
+
const wrapper = document.createElement('div');
|
|
1011
|
+
wrapper.setAttribute('popover', 'manual');
|
|
1012
|
+
wrapper.setAttribute('aria-hidden', 'true');
|
|
1013
|
+
wrapper.style.cssText = 'position:fixed;inset:0;width:100vw;height:100vh;pointer-events:none;border:0;padding:0;margin:0;background:transparent;overflow:visible;';
|
|
1014
|
+
const svg = document.createElementNS(NS, 'svg');
|
|
1015
|
+
svg.setAttribute('width', '100%');
|
|
1016
|
+
svg.setAttribute('height', '100%');
|
|
1017
|
+
const polygon = document.createElementNS(NS, 'polygon');
|
|
1018
|
+
polygon.setAttribute('fill', 'rgba(255, 0, 128, 0.15)');
|
|
1019
|
+
polygon.setAttribute('stroke', 'rgba(255, 0, 128, 0.85)');
|
|
1020
|
+
polygon.setAttribute('stroke-width', '1');
|
|
1021
|
+
svg.appendChild(polygon);
|
|
1022
|
+
wrapper.appendChild(svg);
|
|
1023
|
+
document.body.appendChild(wrapper);
|
|
1024
|
+
wrapper.showPopover();
|
|
1025
|
+
this._safeTriangleOverlay = wrapper;
|
|
1026
|
+
}
|
|
1027
|
+
const polygon = this._safeTriangleOverlay.querySelector('polygon');
|
|
1028
|
+
polygon.setAttribute('points', vertices.map(v => `${v.x},${v.y}`).join(' '));
|
|
1029
|
+
}
|
|
1030
|
+
_removeSafeTriangleOverlay() {
|
|
1031
|
+
this._safeTriangleOverlay?.hidePopover?.();
|
|
1032
|
+
this._safeTriangleOverlay?.remove();
|
|
1033
|
+
this._safeTriangleOverlay = null;
|
|
1034
|
+
}
|
|
1035
|
+
_cancelHoverOpen() {
|
|
1036
|
+
if (this._hoverOpenTimer !== null) {
|
|
1037
|
+
clearTimeout(this._hoverOpenTimer);
|
|
1038
|
+
this._hoverOpenTimer = null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
_cancelHoverClose() {
|
|
1042
|
+
if (this._hoverCloseTimer !== null) {
|
|
1043
|
+
clearTimeout(this._hoverCloseTimer);
|
|
1044
|
+
this._hoverCloseTimer = null;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
/** Recursively checks whether a point sits inside the given menu OR any of
|
|
1048
|
+
* its descendant submenus. Used by the hover guard so a cursor deep inside
|
|
1049
|
+
* a nested submenu still reads as "in the safe area" for ancestor menus —
|
|
1050
|
+
* without this, an ancestor's linger-close fires and cascades the whole
|
|
1051
|
+
* chain shut. */
|
|
1052
|
+
static _isPointInMenuTree(p, menu) {
|
|
1053
|
+
const r = menu.getBoundingClientRect();
|
|
1054
|
+
if (p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom)
|
|
1055
|
+
return true;
|
|
1056
|
+
if (menu._activeSubmenu)
|
|
1057
|
+
return NLDDMenu._isPointInMenuTree(p, menu._activeSubmenu);
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
/** Convex point-in-polygon test via consistent edge-cross-product sign.
|
|
1061
|
+
* Vertices must be ordered (clockwise or counter-clockwise). */
|
|
1062
|
+
static _pointInPolygon(p, vertices) {
|
|
1063
|
+
let hasNeg = false;
|
|
1064
|
+
let hasPos = false;
|
|
1065
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
1066
|
+
const a = vertices[i];
|
|
1067
|
+
const b = vertices[(i + 1) % vertices.length];
|
|
1068
|
+
const cross = (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y);
|
|
1069
|
+
if (cross < 0)
|
|
1070
|
+
hasNeg = true;
|
|
1071
|
+
if (cross > 0)
|
|
1072
|
+
hasPos = true;
|
|
1073
|
+
if (hasNeg && hasPos)
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
static _getDrillInModeQuery() {
|
|
1079
|
+
if (NLDDMenu._drillInModeQuery === null) {
|
|
1080
|
+
// Use the shared breakpoint constant so the JS-side threshold can't
|
|
1081
|
+
// drift from the CSS-side one (spacer.styles.ts and friends pull
|
|
1082
|
+
// from the same source).
|
|
1083
|
+
NLDDMenu._drillInModeQuery = matchMedia(`(pointer: coarse), (max-width: ${breakpoints.smMax})`);
|
|
1084
|
+
}
|
|
1085
|
+
return NLDDMenu._drillInModeQuery;
|
|
1086
|
+
}
|
|
1087
|
+
get _drillInMode() {
|
|
1088
|
+
return NLDDMenu._getDrillInModeQuery().matches;
|
|
1089
|
+
}
|
|
1090
|
+
/** Walks the parent-menu chain up to the root (the menu that wasn't
|
|
1091
|
+
* opened by another menu — has no _parentMenu). */
|
|
1092
|
+
get _rootMenu() {
|
|
1093
|
+
let m = this;
|
|
1094
|
+
while (m._parentMenu)
|
|
1095
|
+
m = m._parentMenu;
|
|
1096
|
+
return m;
|
|
1097
|
+
}
|
|
1098
|
+
/** True when this menu is itself a submenu (was opened by another menu's
|
|
1099
|
+
* item). The root menu returns false. */
|
|
1100
|
+
get _isSubmenu() {
|
|
1101
|
+
return this._parentMenu !== null;
|
|
1102
|
+
}
|
|
355
1103
|
connectedCallback() {
|
|
356
1104
|
super.connectedCallback();
|
|
1105
|
+
if (!this.id) {
|
|
1106
|
+
// Auto-id so submenu openers can reference us via aria-controls.
|
|
1107
|
+
this.id = `nldd-menu-${NLDDMenu._menuIdCounter++}`;
|
|
1108
|
+
}
|
|
357
1109
|
if (!this.hasAttribute('popover')) {
|
|
358
1110
|
this.setAttribute('popover', '');
|
|
359
1111
|
}
|
|
@@ -362,33 +1114,68 @@ export class NLDDMenu extends LitElement {
|
|
|
362
1114
|
this.addEventListener('mouseenter', this._handleMenuItemMouseenter, true);
|
|
363
1115
|
this.addEventListener('mouseleave', this._handleMouseleave);
|
|
364
1116
|
this.addEventListener('menu-item-focused', this._handleMenuItemFocused);
|
|
1117
|
+
this.addEventListener('submenu-open', this._handleSubmenuOpen);
|
|
1118
|
+
this.addEventListener('select', this._handleSelectChainClose);
|
|
365
1119
|
document.addEventListener('click', this._handleDocumentClick);
|
|
1120
|
+
// Close any open submenu when the viewport crosses the cascade ↔ drill-in
|
|
1121
|
+
// threshold mid-session — re-rendering between modes mid-flight would
|
|
1122
|
+
// require recomputing anchors and is more disorienting than a clean reset.
|
|
1123
|
+
// Subscribe to the MediaQueryList's `change` event rather than every
|
|
1124
|
+
// `window.resize` frame: it fires only when the threshold is actually
|
|
1125
|
+
// crossed and matches the comment above the cached query.
|
|
1126
|
+
NLDDMenu._getDrillInModeQuery().addEventListener('change', this._handleViewportResize);
|
|
366
1127
|
}
|
|
367
1128
|
firstUpdated() {
|
|
368
1129
|
this._updateEmptyState();
|
|
369
1130
|
}
|
|
370
1131
|
disconnectedCallback() {
|
|
371
1132
|
super.disconnectedCallback();
|
|
1133
|
+
// Close any open child submenu before tearing down — the submenu is a
|
|
1134
|
+
// document-anchored popover and would otherwise outlive its parent.
|
|
1135
|
+
// The toggle listener wired in _handleSubmenuOpen still fires from
|
|
1136
|
+
// the hidePopover, but the cleanup below has not run yet so its
|
|
1137
|
+
// state-clear path is safe.
|
|
1138
|
+
if (this._activeSubmenu) {
|
|
1139
|
+
this._activeSubmenu.hidePopover?.();
|
|
1140
|
+
}
|
|
372
1141
|
this.removeEventListener('toggle', this._handleToggle);
|
|
373
1142
|
this.removeEventListener('keydown', this._handleKeydown);
|
|
374
1143
|
this.removeEventListener('mouseenter', this._handleMenuItemMouseenter, true);
|
|
375
1144
|
this.removeEventListener('mouseleave', this._handleMouseleave);
|
|
376
1145
|
this.removeEventListener('menu-item-focused', this._handleMenuItemFocused);
|
|
1146
|
+
this.removeEventListener('submenu-open', this._handleSubmenuOpen);
|
|
1147
|
+
this.removeEventListener('select', this._handleSelectChainClose);
|
|
377
1148
|
document.removeEventListener('click', this._handleDocumentClick);
|
|
1149
|
+
NLDDMenu._getDrillInModeQuery().removeEventListener('change', this._handleViewportResize);
|
|
1150
|
+
this._cancelHoverOpen();
|
|
1151
|
+
this._cancelHoverClose();
|
|
1152
|
+
this._stopSafeTriangle();
|
|
1153
|
+
if (this._typeaheadTimer !== null) {
|
|
1154
|
+
clearTimeout(this._typeaheadTimer);
|
|
1155
|
+
this._typeaheadTimer = null;
|
|
1156
|
+
}
|
|
378
1157
|
this._cleanupAutoUpdate?.();
|
|
379
1158
|
this._cleanupAutoUpdate = null;
|
|
380
1159
|
}
|
|
381
1160
|
// — Internal helpers ——————————————————————————————————————————————————————
|
|
382
1161
|
_getVisibleItems() {
|
|
383
|
-
|
|
1162
|
+
// Light-DOM querySelectorAll returns ALL menu-item descendants — items
|
|
1163
|
+
// inside nested submenus included. Filter to items that belong directly
|
|
1164
|
+
// to this menu (closest enclosing nldd-menu === this), so navigation
|
|
1165
|
+
// and highlight management stay scoped to one level at a time.
|
|
1166
|
+
return Array.from(this.querySelectorAll('nldd-menu-item:not([hidden]):not([disabled])')).filter(item => item.closest('nldd-menu') === this);
|
|
384
1167
|
}
|
|
385
1168
|
_getFocusedIndex(items) {
|
|
386
1169
|
return items.findIndex(item => item.hasAttribute('data-focused'));
|
|
387
1170
|
}
|
|
388
1171
|
_clearHighlight() {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1172
|
+
// Same scope rule as _getVisibleItems — only clear highlights on items
|
|
1173
|
+
// that belong directly to this menu. Light-DOM querySelectorAll would
|
|
1174
|
+
// otherwise also clear items inside nested submenus, silently
|
|
1175
|
+
// corrupting their highlight state.
|
|
1176
|
+
Array.from(this.querySelectorAll('nldd-menu-item'))
|
|
1177
|
+
.filter(item => item.closest('nldd-menu') === this)
|
|
1178
|
+
.forEach(item => item.removeAttribute('highlighted'));
|
|
392
1179
|
}
|
|
393
1180
|
_setHighlight(target) {
|
|
394
1181
|
this._clearHighlight();
|
|
@@ -412,7 +1199,10 @@ export class NLDDMenu extends LitElement {
|
|
|
412
1199
|
const isFirst = index === 0;
|
|
413
1200
|
const isLast = index === visible.length - 1;
|
|
414
1201
|
const prevIsDivider = index > 0 && visible[index - 1].tagName.toLowerCase() === 'nldd-menu-divider';
|
|
415
|
-
|
|
1202
|
+
// nldd-menu-group renders its own auto-divider above; suppress an
|
|
1203
|
+
// explicit divider that would render right next to it.
|
|
1204
|
+
const nextIsGroup = index < visible.length - 1 && visible[index + 1].tagName.toLowerCase() === 'nldd-menu-group';
|
|
1205
|
+
if (isFirst || isLast || prevIsDivider || nextIsGroup) {
|
|
416
1206
|
el.setAttribute('hidden', '');
|
|
417
1207
|
}
|
|
418
1208
|
});
|
|
@@ -434,12 +1224,25 @@ export class NLDDMenu extends LitElement {
|
|
|
434
1224
|
item.toggleAttribute('hidden', !matches);
|
|
435
1225
|
item.query = (matches && query) ? query : '';
|
|
436
1226
|
});
|
|
1227
|
+
this._updateGroupVisibility();
|
|
437
1228
|
this._setHighlight(null);
|
|
438
1229
|
this._updateEmptyState();
|
|
439
1230
|
this._updateDividerVisibility();
|
|
440
1231
|
if (this._isOpen)
|
|
441
1232
|
this.reposition();
|
|
442
1233
|
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Hide each nldd-menu-group whose items are all filtered out — a labelled
|
|
1236
|
+
* heading above an empty section reads as broken. Runs after filter() has
|
|
1237
|
+
* updated individual item visibility.
|
|
1238
|
+
*/
|
|
1239
|
+
_updateGroupVisibility() {
|
|
1240
|
+
const groups = this.querySelectorAll('nldd-menu-group');
|
|
1241
|
+
groups.forEach(group => {
|
|
1242
|
+
const visibleItems = group.querySelectorAll('nldd-menu-item:not([hidden])');
|
|
1243
|
+
group.toggleAttribute('hidden', visibleItems.length === 0);
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
443
1246
|
/**
|
|
444
1247
|
* Move both focus and highlight to the next, previous, or first visible item.
|
|
445
1248
|
*/
|
|
@@ -497,10 +1300,17 @@ export class NLDDMenu extends LitElement {
|
|
|
497
1300
|
if (!anchorEl || !this._isOpen)
|
|
498
1301
|
return;
|
|
499
1302
|
const viewportMargin = parseInt(getComputedStyle(this).getPropertyValue('--_viewport-margin'));
|
|
1303
|
+
// Cascade-mode submenus: shift up by the menu's own padding so the
|
|
1304
|
+
// first submenu item lines up vertically with the parent opener item.
|
|
1305
|
+
// Without this, the submenu's top edge aligns with the opener and the
|
|
1306
|
+
// inner padding pushes the first item down, leaving a visible step.
|
|
1307
|
+
const submenuPadding = (this._isSubmenu && !this._drillInMode)
|
|
1308
|
+
? parseInt(getComputedStyle(this).getPropertyValue('--_menu-padding')) || 0
|
|
1309
|
+
: 0;
|
|
500
1310
|
const { x, y } = await computePosition(anchorEl, this, {
|
|
501
1311
|
placement: this.placement,
|
|
502
1312
|
middleware: [
|
|
503
|
-
offset(0),
|
|
1313
|
+
offset({ mainAxis: 0, alignmentAxis: -submenuPadding }),
|
|
504
1314
|
flip({ padding: viewportMargin }),
|
|
505
1315
|
shift({ padding: viewportMargin }),
|
|
506
1316
|
size({
|
|
@@ -516,11 +1326,86 @@ export class NLDDMenu extends LitElement {
|
|
|
516
1326
|
top: `${y}px`,
|
|
517
1327
|
});
|
|
518
1328
|
}
|
|
1329
|
+
/**
|
|
1330
|
+
* ARIA APG typeahead: characters typed within 500ms accumulate into a
|
|
1331
|
+
* buffer; the menu jumps to the first item whose text starts with the
|
|
1332
|
+
* buffer (case-insensitive).
|
|
1333
|
+
*
|
|
1334
|
+
* Single-char buffer (first press or after reset) cycles through matches
|
|
1335
|
+
* — repeated presses of the same letter step through every item starting
|
|
1336
|
+
* with it. Multi-char buffer matches from the currently-focused item, so
|
|
1337
|
+
* extending the prefix while the current item still matches keeps focus
|
|
1338
|
+
* stable instead of jumping forward.
|
|
1339
|
+
*/
|
|
1340
|
+
_handleTypeahead(event, items, currentIndex) {
|
|
1341
|
+
// Extend (or seed) the buffer and (re)arm the reset timer.
|
|
1342
|
+
if (this._typeaheadTimer !== null) {
|
|
1343
|
+
clearTimeout(this._typeaheadTimer);
|
|
1344
|
+
}
|
|
1345
|
+
this._typeaheadBuffer += event.key.toLowerCase();
|
|
1346
|
+
this._typeaheadTimer = window.setTimeout(() => {
|
|
1347
|
+
this._typeaheadBuffer = '';
|
|
1348
|
+
this._typeaheadTimer = null;
|
|
1349
|
+
}, NLDDMenu._TYPEAHEAD_RESET_MS);
|
|
1350
|
+
// Single char → cycle past the current item; multi char → start AT the
|
|
1351
|
+
// current item so a still-matching prefix doesn't move focus.
|
|
1352
|
+
const start = this._typeaheadBuffer.length === 1 && currentIndex >= 0
|
|
1353
|
+
? currentIndex + 1
|
|
1354
|
+
: Math.max(0, currentIndex);
|
|
1355
|
+
for (let i = 0; i < items.length; i++) {
|
|
1356
|
+
const idx = (start + i) % items.length;
|
|
1357
|
+
if (items[idx].text.toLowerCase().startsWith(this._typeaheadBuffer)) {
|
|
1358
|
+
event.preventDefault();
|
|
1359
|
+
event.stopPropagation();
|
|
1360
|
+
items[idx].focus();
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
519
1365
|
render() {
|
|
520
1366
|
return menuTemplate.call(this, this._isEmpty, this.variant);
|
|
521
1367
|
}
|
|
522
1368
|
}
|
|
523
1369
|
NLDDMenu.styles = menuStyles;
|
|
1370
|
+
/** Wait this long after cursor stops being on a path toward the submenu
|
|
1371
|
+
* before closing. Smooths over brief cursor stalls and accidental nudges. */
|
|
1372
|
+
NLDDMenu._SUBMENU_LINGER_CLOSE_MS = 300;
|
|
1373
|
+
/** When the cursor sits motionless inside the safe triangle for this
|
|
1374
|
+
* long, dismiss the triangle and let whatever item is under the cursor
|
|
1375
|
+
* become interactive. Without this, a paused cursor would stay
|
|
1376
|
+
* "protected" indefinitely and the user couldn't peer-hover. */
|
|
1377
|
+
NLDDMenu._SAFE_TRIANGLE_STALL_DISMISS_MS = 750;
|
|
1378
|
+
/** Pull the wedge apex this many pixels away from the submenu edge (i.e.
|
|
1379
|
+
* to the left when the submenu is on the right). Widens the wedge near
|
|
1380
|
+
* the cursor so brief diagonal wobble doesn't pop the cursor out at the
|
|
1381
|
+
* apex, and avoids the degenerate single-line wedge when the cursor
|
|
1382
|
+
* crossed the opener edge exactly at the same x as the near submenu
|
|
1383
|
+
* corner. The shift is always negative — the standard placement is
|
|
1384
|
+
* submenu-right, so "left" is the intuitive forgive direction. */
|
|
1385
|
+
NLDDMenu._SAFE_TRIANGLE_APEX_X_OFFSET = 4;
|
|
1386
|
+
/**
|
|
1387
|
+
* Drill-in mode is the touch-friendly rendering: a submenu replaces its
|
|
1388
|
+
* parent's view by anchoring to the root anchor (so visually it stacks
|
|
1389
|
+
* over the parent) and gets a back-button header. Otherwise (cascade)
|
|
1390
|
+
* the submenu opens beside its parent item.
|
|
1391
|
+
*
|
|
1392
|
+
* Detection is based on pointer type and viewport width — touch devices
|
|
1393
|
+
* and narrow viewports drill in, everything else cascades. No consumer
|
|
1394
|
+
* override; the choice is environment-driven.
|
|
1395
|
+
*
|
|
1396
|
+
* Cached MediaQueryList shared across instances — `matchMedia()` returns
|
|
1397
|
+
* a live object and re-evaluating it for every mouseenter/submenu-open
|
|
1398
|
+
* is wasteful. Instances also subscribe to `change` so resize events
|
|
1399
|
+
* naturally route through `_handleViewportResize`.
|
|
1400
|
+
*/
|
|
1401
|
+
NLDDMenu._drillInModeQuery = null;
|
|
1402
|
+
// — Lifecycle callbacks ————————————————————————————————————————————————————
|
|
1403
|
+
NLDDMenu._menuIdCounter = 0;
|
|
1404
|
+
/** Reset the typeahead buffer after this much idle time. 500ms is the
|
|
1405
|
+
* common ARIA APG / OS convention — long enough to type a 2-3 char prefix
|
|
1406
|
+
* comfortably, short enough that an unrelated later keystroke starts a
|
|
1407
|
+
* fresh search. */
|
|
1408
|
+
NLDDMenu._TYPEAHEAD_RESET_MS = 500;
|
|
524
1409
|
__decorate([
|
|
525
1410
|
property({ type: String, reflect: true })
|
|
526
1411
|
], NLDDMenu.prototype, "anchor", void 0);
|
|
@@ -548,12 +1433,18 @@ __decorate([
|
|
|
548
1433
|
__decorate([
|
|
549
1434
|
property({ type: Object })
|
|
550
1435
|
], NLDDMenu.prototype, "translations", void 0);
|
|
1436
|
+
__decorate([
|
|
1437
|
+
property({ type: Boolean, reflect: true, attribute: 'debug-safe-triangle' })
|
|
1438
|
+
], NLDDMenu.prototype, "debugSafeTriangle", void 0);
|
|
551
1439
|
__decorate([
|
|
552
1440
|
property({ attribute: false })
|
|
553
1441
|
], NLDDMenu.prototype, "filterFn", void 0);
|
|
554
1442
|
__decorate([
|
|
555
1443
|
state()
|
|
556
1444
|
], NLDDMenu.prototype, "_isEmpty", void 0);
|
|
1445
|
+
__decorate([
|
|
1446
|
+
state()
|
|
1447
|
+
], NLDDMenu.prototype, "_parentItem", void 0);
|
|
557
1448
|
if (!customElements.get('nldd-menu')) {
|
|
558
1449
|
customElements.define('nldd-menu', NLDDMenu);
|
|
559
1450
|
}
|