mtrl 0.3.8 → 0.3.9
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/dist/LICENSE +21 -0
- package/dist/README.md +324 -0
- package/dist/components/badge/api.d.ts +48 -0
- package/{src/components/badge/badge.ts → dist/components/badge/badge.d.ts} +14 -57
- package/dist/components/badge/config.d.ts +79 -0
- package/dist/components/badge/constants.d.ts +35 -0
- package/dist/components/badge/features.d.ts +36 -0
- package/dist/components/badge/index.d.ts +8 -0
- package/dist/components/badge/types.d.ts +256 -0
- package/dist/components/bottom-app-bar/bottom-app-bar.d.ts +12 -0
- package/dist/components/bottom-app-bar/config.d.ts +16 -0
- package/dist/components/bottom-app-bar/constants.d.ts +22 -0
- package/{src/components/bottom-app-bar/index.ts → dist/components/bottom-app-bar/index.d.ts} +0 -9
- package/dist/components/bottom-app-bar/types.d.ts +96 -0
- package/dist/components/button/api.d.ts +47 -0
- package/dist/components/button/button.d.ts +25 -0
- package/dist/components/button/config.d.ts +59 -0
- package/dist/components/button/constants.d.ts +43 -0
- package/dist/components/button/index.d.ts +6 -0
- package/dist/components/button/types.d.ts +265 -0
- package/dist/components/card/api.d.ts +11 -0
- package/{src/components/card/card.ts → dist/components/card/card.d.ts} +10 -58
- package/dist/components/card/config.d.ts +105 -0
- package/dist/components/card/constants.d.ts +85 -0
- package/dist/components/card/content.d.ts +92 -0
- package/dist/components/card/features.d.ts +131 -0
- package/{src/components/card/index.ts → dist/components/card/index.d.ts} +13 -74
- package/dist/components/card/types.d.ts +471 -0
- package/dist/components/carousel/api.d.ts +33 -0
- package/dist/components/carousel/carousel.d.ts +75 -0
- package/dist/components/carousel/config.d.ts +45 -0
- package/dist/components/carousel/constants.d.ts +165 -0
- package/dist/components/carousel/features/drag.d.ts +8 -0
- package/{src/components/carousel/features/index.ts → dist/components/carousel/features/index.d.ts} +1 -4
- package/dist/components/carousel/features/slides.d.ts +8 -0
- package/{src/components/carousel/index.ts → dist/components/carousel/index.d.ts} +10 -29
- package/dist/components/carousel/types.d.ts +276 -0
- package/dist/components/checkbox/api.d.ts +7 -0
- package/dist/components/checkbox/checkbox.d.ts +65 -0
- package/dist/components/checkbox/config.d.ts +44 -0
- package/dist/components/checkbox/constants.d.ts +63 -0
- package/{src/components/checkbox/index.ts → dist/components/checkbox/index.d.ts} +8 -25
- package/dist/components/checkbox/types.d.ts +242 -0
- package/dist/components/chips/api.d.ts +49 -0
- package/dist/components/chips/chip/api.d.ts +23 -0
- package/dist/components/chips/chip/chip.d.ts +8 -0
- package/dist/components/chips/chip/config.d.ts +57 -0
- package/dist/components/chips/chip/constants.d.ts +38 -0
- package/dist/components/chips/chip/index.d.ts +2 -0
- package/dist/components/chips/chip/types.d.ts +11 -0
- package/dist/components/chips/chips.d.ts +17 -0
- package/dist/components/chips/config.d.ts +67 -0
- package/dist/components/chips/constants.d.ts +46 -0
- package/dist/components/chips/features/chip-items.d.ts +8 -0
- package/dist/components/chips/features/container.d.ts +8 -0
- package/dist/components/chips/features/controller.d.ts +9 -0
- package/{src/components/chips/features/index.ts → dist/components/chips/features/index.d.ts} +1 -2
- package/dist/components/chips/features/label.d.ts +8 -0
- package/dist/components/chips/index.d.ts +4 -0
- package/dist/components/chips/schema.d.ts +34 -0
- package/dist/components/chips/types.d.ts +398 -0
- package/dist/components/datepicker/api.d.ts +9 -0
- package/dist/components/datepicker/config.d.ts +79 -0
- package/dist/components/datepicker/constants.d.ts +97 -0
- package/dist/components/datepicker/datepicker.d.ts +8 -0
- package/dist/components/datepicker/index.d.ts +3 -0
- package/dist/components/datepicker/render.d.ts +43 -0
- package/dist/components/datepicker/types.d.ts +321 -0
- package/dist/components/datepicker/utils.d.ts +84 -0
- package/dist/components/dialog/api.d.ts +59 -0
- package/dist/components/dialog/config.d.ts +91 -0
- package/dist/components/dialog/constants.d.ts +116 -0
- package/{src/components/dialog/dialog.ts → dist/components/dialog/dialog.d.ts} +16 -69
- package/dist/components/dialog/features.d.ts +37 -0
- package/dist/components/dialog/index.d.ts +14 -0
- package/dist/components/dialog/types.d.ts +483 -0
- package/dist/components/divider/config.d.ts +143 -0
- package/dist/components/divider/constants.d.ts +45 -0
- package/{src/components/divider/divider.ts → dist/components/divider/divider.d.ts} +9 -37
- package/dist/components/divider/features.d.ts +50 -0
- package/{src/components/divider/index.ts → dist/components/divider/index.d.ts} +6 -12
- package/dist/components/divider/types.d.ts +124 -0
- package/dist/components/extended-fab/api.d.ts +83 -0
- package/dist/components/extended-fab/config.d.ts +75 -0
- package/dist/components/extended-fab/constants.d.ts +82 -0
- package/dist/components/extended-fab/extended-fab.d.ts +61 -0
- package/{src/components/extended-fab/index.ts → dist/components/extended-fab/index.d.ts} +6 -15
- package/dist/components/extended-fab/types.d.ts +703 -0
- package/dist/components/fab/api.d.ts +64 -0
- package/dist/components/fab/config.d.ts +71 -0
- package/dist/components/fab/constants.d.ts +75 -0
- package/{src/components/fab/fab.ts → dist/components/fab/fab.d.ts} +10 -46
- package/{src/components/fab/index.ts → dist/components/fab/index.d.ts} +6 -15
- package/dist/components/fab/types.d.ts +577 -0
- package/dist/components/index.d.ts +76 -0
- package/dist/components/list/api.d.ts +106 -0
- package/dist/components/list/config.d.ts +61 -0
- package/dist/components/list/constants.d.ts +76 -0
- package/dist/components/list/features/index.d.ts +2 -0
- package/dist/components/list/features/listmanager.d.ts +9 -0
- package/dist/components/list/features/selection.d.ts +9 -0
- package/dist/components/list/index.d.ts +12 -0
- package/dist/components/list/list.d.ts +11 -0
- package/dist/components/list/types.d.ts +294 -0
- package/dist/components/menu/api.d.ts +58 -0
- package/dist/components/menu/config.d.ts +78 -0
- package/dist/components/menu/constants.d.ts +106 -0
- package/dist/components/menu/features/controller.d.ts +10 -0
- package/dist/components/menu/features/index.d.ts +6 -0
- package/dist/components/menu/features/keyboard.d.ts +26 -0
- package/dist/components/menu/features/opener.d.ts +10 -0
- package/dist/components/menu/features/position.d.ts +18 -0
- package/dist/components/menu/features/submenu.d.ts +10 -0
- package/{src/components/menu/index.ts → dist/components/menu/index.d.ts} +11 -27
- package/dist/components/menu/menu.d.ts +29 -0
- package/dist/components/menu/types.d.ts +338 -0
- package/dist/components/navigation/api.d.ts +8 -0
- package/dist/components/navigation/config.d.ts +34 -0
- package/dist/components/navigation/constants.d.ts +137 -0
- package/dist/components/navigation/features/controller.d.ts +30 -0
- package/dist/components/navigation/features/items.d.ts +32 -0
- package/dist/components/navigation/index.d.ts +3 -0
- package/dist/components/navigation/nav-item.d.ts +30 -0
- package/dist/components/navigation/navigation.d.ts +8 -0
- package/dist/components/navigation/system/core.d.ts +72 -0
- package/dist/components/navigation/system/events.d.ts +35 -0
- package/dist/components/navigation/system/index.d.ts +10 -0
- package/dist/components/navigation/system/mobile.d.ts +52 -0
- package/dist/components/navigation/system/state.d.ts +22 -0
- package/dist/components/navigation/system/types.d.ts +305 -0
- package/dist/components/navigation/types.d.ts +216 -0
- package/dist/components/progress/api.d.ts +46 -0
- package/dist/components/progress/config.d.ts +63 -0
- package/dist/components/progress/constants.d.ts +58 -0
- package/dist/components/progress/index.d.ts +3 -0
- package/dist/components/progress/progress.d.ts +24 -0
- package/dist/components/progress/types.d.ts +199 -0
- package/dist/components/radios/api.d.ts +37 -0
- package/dist/components/radios/config.d.ts +42 -0
- package/dist/components/radios/constants.d.ts +114 -0
- package/dist/components/radios/index.d.ts +3 -0
- package/dist/components/radios/radio.d.ts +8 -0
- package/dist/components/radios/radios.d.ts +8 -0
- package/dist/components/radios/types.d.ts +189 -0
- package/dist/components/search/api.d.ts +55 -0
- package/dist/components/search/config.d.ts +73 -0
- package/dist/components/search/constants.d.ts +128 -0
- package/{src/components/search/features/index.ts → dist/components/search/features/index.d.ts} +1 -2
- package/dist/components/search/features/search.d.ts +7 -0
- package/dist/components/search/features/states.d.ts +8 -0
- package/dist/components/search/features/structure.d.ts +7 -0
- package/dist/components/search/index.d.ts +3 -0
- package/dist/components/search/search.d.ts +8 -0
- package/dist/components/search/types.d.ts +132 -0
- package/dist/components/segmented-button/config.d.ts +65 -0
- package/dist/components/segmented-button/constants.d.ts +85 -0
- package/dist/components/segmented-button/index.d.ts +4 -0
- package/dist/components/segmented-button/segment.d.ts +15 -0
- package/dist/components/segmented-button/segmented-button.d.ts +49 -0
- package/dist/components/segmented-button/types.d.ts +257 -0
- package/dist/components/select/api.d.ts +8 -0
- package/dist/components/select/config.d.ts +18 -0
- package/dist/components/select/constants.d.ts +113 -0
- package/dist/components/select/features.d.ts +13 -0
- package/{src/components/select/index.ts → dist/components/select/index.d.ts} +7 -20
- package/dist/components/select/select.d.ts +36 -0
- package/dist/components/select/types.d.ts +302 -0
- package/dist/components/sheet/api.d.ts +37 -0
- package/dist/components/sheet/config.d.ts +42 -0
- package/dist/components/sheet/constants.d.ts +136 -0
- package/dist/components/sheet/features/content.d.ts +6 -0
- package/dist/components/sheet/features/gestures.d.ts +6 -0
- package/{src/components/sheet/features/index.ts → dist/components/sheet/features/index.d.ts} +1 -2
- package/dist/components/sheet/features/position.d.ts +7 -0
- package/dist/components/sheet/features/state.d.ts +6 -0
- package/dist/components/sheet/features/title.d.ts +6 -0
- package/dist/components/sheet/index.d.ts +3 -0
- package/dist/components/sheet/sheet.d.ts +8 -0
- package/dist/components/sheet/types.d.ts +250 -0
- package/dist/components/slider/api.d.ts +57 -0
- package/dist/components/slider/config.d.ts +75 -0
- package/dist/components/slider/constants.d.ts +138 -0
- package/dist/components/slider/features/controller.d.ts +9 -0
- package/dist/components/slider/features/handlers.d.ts +25 -0
- package/dist/components/slider/features/index.d.ts +3 -0
- package/dist/components/slider/features/range.d.ts +8 -0
- package/dist/components/slider/features/states.d.ts +9 -0
- package/dist/components/slider/index.d.ts +3 -0
- package/dist/components/slider/schema.d.ts +108 -0
- package/dist/components/slider/slider.d.ts +18 -0
- package/dist/components/slider/types.d.ts +170 -0
- package/dist/components/snackbar/api.d.ts +7 -0
- package/dist/components/snackbar/config.d.ts +55 -0
- package/dist/components/snackbar/constants.d.ts +88 -0
- package/dist/components/snackbar/features.d.ts +13 -0
- package/dist/components/snackbar/index.d.ts +3 -0
- package/dist/components/snackbar/position.d.ts +15 -0
- package/dist/components/snackbar/queue.d.ts +7 -0
- package/dist/components/snackbar/snackbar.d.ts +8 -0
- package/dist/components/snackbar/types.d.ts +172 -0
- package/dist/components/switch/api.d.ts +7 -0
- package/dist/components/switch/config.d.ts +34 -0
- package/dist/components/switch/constants.d.ts +88 -0
- package/dist/components/switch/features.d.ts +59 -0
- package/dist/components/switch/index.d.ts +4 -0
- package/dist/components/switch/switch.d.ts +8 -0
- package/dist/components/switch/types.d.ts +131 -0
- package/dist/components/tabs/api.d.ts +52 -0
- package/dist/components/tabs/config.d.ts +39 -0
- package/dist/components/tabs/constants.d.ts +142 -0
- package/dist/components/tabs/features.d.ts +133 -0
- package/dist/components/tabs/index.d.ts +11 -0
- package/dist/components/tabs/indicator.d.ts +49 -0
- package/dist/components/tabs/responsive.d.ts +38 -0
- package/dist/components/tabs/scroll-indicators.d.ts +18 -0
- package/dist/components/tabs/state.d.ts +53 -0
- package/dist/components/tabs/tab-api.d.ts +43 -0
- package/dist/components/tabs/tab.d.ts +7 -0
- package/dist/components/tabs/tabs.d.ts +27 -0
- package/dist/components/tabs/types.d.ts +390 -0
- package/dist/components/tabs/utils.d.ts +17 -0
- package/dist/components/textfield/api.d.ts +8 -0
- package/dist/components/textfield/config.d.ts +34 -0
- package/dist/components/textfield/constants.d.ts +148 -0
- package/{src/components/textfield/features/index.ts → dist/components/textfield/features/index.d.ts} +1 -6
- package/dist/components/textfield/features/leading-icon.d.ts +55 -0
- package/dist/components/textfield/features/placement.d.ts +28 -0
- package/dist/components/textfield/features/prefix-text.d.ts +54 -0
- package/dist/components/textfield/features/suffix-text.d.ts +54 -0
- package/dist/components/textfield/features/supporting-text.d.ts +59 -0
- package/dist/components/textfield/features/trailing-icon.d.ts +55 -0
- package/dist/components/textfield/index.d.ts +3 -0
- package/dist/components/textfield/textfield.d.ts +36 -0
- package/dist/components/textfield/types.d.ts +217 -0
- package/dist/components/timepicker/api.d.ts +24 -0
- package/dist/components/timepicker/clockdial.d.ts +34 -0
- package/dist/components/timepicker/config.d.ts +75 -0
- package/dist/components/timepicker/constants.d.ts +266 -0
- package/dist/components/timepicker/index.d.ts +4 -0
- package/dist/components/timepicker/render.d.ts +9 -0
- package/dist/components/timepicker/timepicker.d.ts +8 -0
- package/dist/components/timepicker/types.d.ts +284 -0
- package/dist/components/timepicker/utils.d.ts +74 -0
- package/dist/components/tooltip/api.d.ts +18 -0
- package/dist/components/tooltip/config.d.ts +38 -0
- package/dist/components/tooltip/constants.d.ts +108 -0
- package/dist/components/tooltip/index.d.ts +3 -0
- package/dist/components/tooltip/tooltip.d.ts +8 -0
- package/dist/components/tooltip/types.d.ts +188 -0
- package/dist/components/top-app-bar/config.d.ts +16 -0
- package/dist/components/top-app-bar/constants.d.ts +74 -0
- package/{src/components/top-app-bar/index.ts → dist/components/top-app-bar/index.d.ts} +2 -4
- package/dist/components/top-app-bar/top-app-bar.d.ts +68 -0
- package/dist/components/top-app-bar/types.d.ts +118 -0
- package/dist/constants.d.ts +30 -0
- package/dist/core/collection/adapters/base.d.ts +47 -0
- package/dist/core/collection/adapters/route.d.ts +149 -0
- package/dist/core/collection/collection.d.ts +131 -0
- package/dist/core/collection/index.d.ts +10 -0
- package/dist/core/collection/list-manager/config.d.ts +29 -0
- package/dist/core/collection/list-manager/dom-elements.d.ts +30 -0
- package/dist/core/collection/list-manager/index.d.ts +61 -0
- package/dist/core/collection/list-manager/item-measurement.d.ts +91 -0
- package/dist/core/collection/list-manager/renderer.d.ts +31 -0
- package/dist/core/collection/list-manager/scroll-tracker.d.ts +20 -0
- package/dist/core/collection/list-manager/state.d.ts +60 -0
- package/dist/core/collection/list-manager/types.d.ts +361 -0
- package/dist/core/collection/list-manager/utils/recycling.d.ts +34 -0
- package/dist/core/collection/list-manager/utils/visibility.d.ts +45 -0
- package/dist/core/compose/base.d.ts +31 -0
- package/dist/core/compose/component.d.ts +61 -0
- package/dist/core/compose/features/badge.d.ts +43 -0
- package/dist/core/compose/features/checkable.d.ts +59 -0
- package/dist/core/compose/features/constants.d.ts +45 -0
- package/dist/core/compose/features/debounce.d.ts +84 -0
- package/dist/core/compose/features/disabled.d.ts +47 -0
- package/dist/core/compose/features/events.d.ts +37 -0
- package/dist/core/compose/features/gestures/longpress.d.ts +85 -0
- package/dist/core/compose/features/gestures/pan.d.ts +108 -0
- package/dist/core/compose/features/gestures/pinch.d.ts +111 -0
- package/dist/core/compose/features/gestures/rotate.d.ts +111 -0
- package/dist/core/compose/features/gestures/swipe.d.ts +149 -0
- package/dist/core/compose/features/gestures/tap.d.ts +79 -0
- package/dist/core/compose/features/gestures.d.ts +86 -0
- package/dist/core/compose/features/icon.d.ts +71 -0
- package/{src/core/compose/features/index.ts → dist/core/compose/features/index.d.ts} +7 -8
- package/dist/core/compose/features/input.d.ts +71 -0
- package/dist/core/compose/features/lifecycle.d.ts +61 -0
- package/dist/core/compose/features/position.d.ts +51 -0
- package/dist/core/compose/features/ripple.d.ts +61 -0
- package/dist/core/compose/features/size.d.ts +17 -0
- package/dist/core/compose/features/style.d.ts +16 -0
- package/dist/core/compose/features/text.d.ts +63 -0
- package/dist/core/compose/features/textinput.d.ts +93 -0
- package/dist/core/compose/features/textlabel.d.ts +57 -0
- package/dist/core/compose/features/throttle.d.ts +75 -0
- package/dist/core/compose/features/track.d.ts +42 -0
- package/dist/core/compose/features/variant.d.ts +17 -0
- package/dist/core/compose/features/withEvents.d.ts +45 -0
- package/dist/core/compose/index.d.ts +17 -0
- package/{src/core/compose/pipe.ts → dist/core/compose/pipe.d.ts} +4 -20
- package/dist/core/compose/utils/type-guards.d.ts +27 -0
- package/dist/core/composition/features/dom.d.ts +19 -0
- package/dist/core/composition/features/icon.d.ts +45 -0
- package/{src/core/composition/features/index.ts → dist/core/composition/features/index.d.ts} +1 -6
- package/dist/core/composition/features/label.d.ts +49 -0
- package/{src/core/composition/features/layout.ts → dist/core/composition/features/layout.d.ts} +8 -24
- package/{src/core/composition/index.ts → dist/core/composition/index.d.ts} +4 -14
- package/{src/core/config/component-config.ts → dist/core/config/component.d.ts} +34 -82
- package/dist/core/config.d.ts +130 -0
- package/{src/core/dom/attributes.ts → dist/core/dom/attributes.d.ts} +2 -13
- package/dist/core/dom/classes.d.ts +42 -0
- package/dist/core/dom/create.d.ts +124 -0
- package/dist/core/dom/events.d.ts +69 -0
- package/{src/core/dom/index.ts → dist/core/dom/index.d.ts} +1 -5
- package/dist/core/dom/utils.d.ts +42 -0
- package/dist/core/gestures/index.d.ts +12 -0
- package/dist/core/gestures/longpress.d.ts +23 -0
- package/dist/core/gestures/manager.d.ts +14 -0
- package/dist/core/gestures/pan.d.ts +12 -0
- package/dist/core/gestures/pinch.d.ts +14 -0
- package/dist/core/gestures/rotate.d.ts +14 -0
- package/dist/core/gestures/swipe.d.ts +20 -0
- package/dist/core/gestures/tap.d.ts +12 -0
- package/dist/core/gestures/types.d.ts +320 -0
- package/dist/core/gestures/utils.d.ts +57 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/layout/array.d.ts +15 -0
- package/dist/core/layout/config.d.ts +32 -0
- package/dist/core/layout/create.d.ts +14 -0
- package/dist/core/layout/index.d.ts +13 -0
- package/dist/core/layout/jsx.d.ts +13 -0
- package/dist/core/layout/object.d.ts +14 -0
- package/dist/core/layout/processor.d.ts +28 -0
- package/dist/core/layout/result.d.ts +12 -0
- package/dist/core/layout/template.d.ts +12 -0
- package/dist/core/layout/types.d.ts +137 -0
- package/dist/core/layout/utils.d.ts +38 -0
- package/dist/core/state/disabled.d.ts +32 -0
- package/dist/core/state/emitter.d.ts +40 -0
- package/dist/core/state/events.d.ts +36 -0
- package/{src/core/state/index.ts → dist/core/state/index.d.ts} +1 -7
- package/dist/core/state/lifecycle.d.ts +57 -0
- package/dist/core/state/store.d.ts +82 -0
- package/dist/core/utils/background.d.ts +40 -0
- package/dist/core/utils/index.d.ts +23 -0
- package/dist/core/utils/mobile.d.ts +54 -0
- package/dist/core/utils/object.d.ts +13 -0
- package/dist/core/utils/performance.d.ts +79 -0
- package/dist/core/utils/validate.d.ts +73 -0
- package/dist/index.cjs +64 -0
- package/dist/index.cjs.map +285 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +53 -14854
- package/dist/index.js.map +285 -0
- package/dist/package.json +39 -0
- package/dist/styles.css +7 -0
- package/package.json +23 -4
- package/.env +0 -15
- package/.typedocignore +0 -11
- package/CONTRIBUTING.md +0 -218
- package/DOCS.md +0 -153
- package/TESTING.md +0 -214
- package/git-user-stats.js +0 -545
- package/index.ts +0 -10
- package/src/components/badge/api.ts +0 -313
- package/src/components/badge/config.ts +0 -153
- package/src/components/badge/features.ts +0 -194
- package/src/components/badge/index.ts +0 -90
- package/src/components/badge/types.ts +0 -279
- package/src/components/bottom-app-bar/bottom-app-bar.ts +0 -154
- package/src/components/bottom-app-bar/config.ts +0 -29
- package/src/components/bottom-app-bar/types.ts +0 -114
- package/src/components/button/api.ts +0 -172
- package/src/components/button/button.ts +0 -112
- package/src/components/button/config.ts +0 -96
- package/src/components/button/index.ts +0 -37
- package/src/components/button/types.ts +0 -290
- package/src/components/card/api.ts +0 -222
- package/src/components/card/config.ts +0 -304
- package/src/components/card/content.ts +0 -343
- package/src/components/card/features.ts +0 -407
- package/src/components/card/types.ts +0 -497
- package/src/components/carousel/api.ts +0 -147
- package/src/components/carousel/carousel.ts +0 -242
- package/src/components/carousel/config.ts +0 -91
- package/src/components/carousel/constants.ts +0 -181
- package/src/components/carousel/features/drag.ts +0 -388
- package/src/components/carousel/features/slides.ts +0 -682
- package/src/components/carousel/types.ts +0 -327
- package/src/components/checkbox/api.ts +0 -82
- package/src/components/checkbox/checkbox.ts +0 -142
- package/src/components/checkbox/config.ts +0 -89
- package/src/components/checkbox/types.ts +0 -342
- package/src/components/chips/api.ts +0 -194
- package/src/components/chips/chip/api.ts +0 -233
- package/src/components/chips/chip/chip.ts +0 -131
- package/src/components/chips/chip/config.ts +0 -91
- package/src/components/chips/chip/index.ts +0 -3
- package/src/components/chips/chips.md +0 -481
- package/src/components/chips/chips.ts +0 -75
- package/src/components/chips/config.ts +0 -109
- package/src/components/chips/constants.ts +0 -61
- package/src/components/chips/features/chip-items.ts +0 -33
- package/src/components/chips/features/container.ts +0 -77
- package/src/components/chips/features/controller.ts +0 -448
- package/src/components/chips/features/label.ts +0 -108
- package/src/components/chips/index.ts +0 -11
- package/src/components/chips/schema.ts +0 -61
- package/src/components/chips/types.ts +0 -469
- package/src/components/datepicker/api.ts +0 -265
- package/src/components/datepicker/config.ts +0 -141
- package/src/components/datepicker/datepicker.ts +0 -341
- package/src/components/datepicker/index.ts +0 -12
- package/src/components/datepicker/render.ts +0 -450
- package/src/components/datepicker/types.ts +0 -397
- package/src/components/datepicker/utils.ts +0 -289
- package/src/components/dialog/api.ts +0 -317
- package/src/components/dialog/config.ts +0 -116
- package/src/components/dialog/features.ts +0 -907
- package/src/components/dialog/index.ts +0 -141
- package/src/components/dialog/types.ts +0 -553
- package/src/components/divider/config.ts +0 -165
- package/src/components/divider/features.ts +0 -233
- package/src/components/divider/types.ts +0 -132
- package/src/components/extended-fab/api.ts +0 -193
- package/src/components/extended-fab/config.ts +0 -140
- package/src/components/extended-fab/extended-fab.ts +0 -153
- package/src/components/extended-fab/types.ts +0 -749
- package/src/components/fab/api.ts +0 -137
- package/src/components/fab/config.ts +0 -121
- package/src/components/fab/types.ts +0 -615
- package/src/components/list/api.ts +0 -82
- package/src/components/list/config.ts +0 -63
- package/src/components/list/features.ts +0 -229
- package/src/components/list/index.ts +0 -67
- package/src/components/list/list-item.ts +0 -163
- package/src/components/list/list.ts +0 -108
- package/src/components/list/types.ts +0 -396
- package/src/components/list/utils.ts +0 -98
- package/src/components/menu/api.ts +0 -230
- package/src/components/menu/config.ts +0 -127
- package/src/components/menu/features/anchor.ts +0 -394
- package/src/components/menu/features/controller.ts +0 -1423
- package/src/components/menu/features/index.ts +0 -13
- package/src/components/menu/features/position.ts +0 -353
- package/src/components/menu/menu.ts +0 -121
- package/src/components/menu/types.ts +0 -392
- package/src/components/navigation/api.ts +0 -142
- package/src/components/navigation/config.ts +0 -73
- package/src/components/navigation/features/controller.ts +0 -273
- package/src/components/navigation/features/items.ts +0 -353
- package/src/components/navigation/index.ts +0 -11
- package/src/components/navigation/nav-item.ts +0 -196
- package/src/components/navigation/navigation.ts +0 -115
- package/src/components/navigation/system/core.ts +0 -302
- package/src/components/navigation/system/events.ts +0 -240
- package/src/components/navigation/system/index.ts +0 -184
- package/src/components/navigation/system/mobile.ts +0 -278
- package/src/components/navigation/system/state.ts +0 -77
- package/src/components/navigation/system/types.ts +0 -364
- package/src/components/navigation/types.ts +0 -292
- package/src/components/progress/api.ts +0 -178
- package/src/components/progress/config.ts +0 -122
- package/src/components/progress/index.ts +0 -4
- package/src/components/progress/progress.ts +0 -159
- package/src/components/progress/types.ts +0 -255
- package/src/components/radios/api.ts +0 -125
- package/src/components/radios/config.ts +0 -59
- package/src/components/radios/constants.ts +0 -19
- package/src/components/radios/index.ts +0 -3
- package/src/components/radios/radio.ts +0 -292
- package/src/components/radios/radios.ts +0 -43
- package/src/components/radios/types.ts +0 -219
- package/src/components/search/api.ts +0 -203
- package/src/components/search/config.ts +0 -86
- package/src/components/search/features/search.ts +0 -717
- package/src/components/search/features/states.ts +0 -169
- package/src/components/search/features/structure.ts +0 -197
- package/src/components/search/index.ts +0 -7
- package/src/components/search/search.ts +0 -52
- package/src/components/search/types.ts +0 -175
- package/src/components/segmented-button/config.ts +0 -119
- package/src/components/segmented-button/index.ts +0 -4
- package/src/components/segmented-button/segment.ts +0 -108
- package/src/components/segmented-button/segmented-button.ts +0 -361
- package/src/components/segmented-button/types.ts +0 -306
- package/src/components/select/api.ts +0 -78
- package/src/components/select/config.ts +0 -76
- package/src/components/select/features.ts +0 -331
- package/src/components/select/select.ts +0 -73
- package/src/components/select/types.ts +0 -355
- package/src/components/sheet/api.ts +0 -96
- package/src/components/sheet/config.ts +0 -65
- package/src/components/sheet/features/content.ts +0 -51
- package/src/components/sheet/features/gestures.ts +0 -177
- package/src/components/sheet/features/position.ts +0 -41
- package/src/components/sheet/features/state.ts +0 -116
- package/src/components/sheet/features/title.ts +0 -86
- package/src/components/sheet/index.ts +0 -12
- package/src/components/sheet/sheet.ts +0 -56
- package/src/components/sheet/types.ts +0 -294
- package/src/components/slider/accessibility.md +0 -59
- package/src/components/slider/api.ts +0 -192
- package/src/components/slider/config.ts +0 -118
- package/src/components/slider/features/controller.ts +0 -737
- package/src/components/slider/features/handlers.ts +0 -497
- package/src/components/slider/features/index.ts +0 -5
- package/src/components/slider/features/range.ts +0 -104
- package/src/components/slider/features/states.ts +0 -195
- package/src/components/slider/index.ts +0 -17
- package/src/components/slider/schema.ts +0 -141
- package/src/components/slider/slider.ts +0 -76
- package/src/components/slider/types.ts +0 -223
- package/src/components/snackbar/api.ts +0 -162
- package/src/components/snackbar/config.ts +0 -61
- package/src/components/snackbar/features.ts +0 -76
- package/src/components/snackbar/index.ts +0 -9
- package/src/components/snackbar/position.ts +0 -79
- package/src/components/snackbar/queue.ts +0 -76
- package/src/components/snackbar/snackbar.ts +0 -60
- package/src/components/snackbar/types.ts +0 -159
- package/src/components/switch/api.ts +0 -93
- package/src/components/switch/config.ts +0 -56
- package/src/components/switch/features.ts +0 -198
- package/src/components/switch/index.ts +0 -8
- package/src/components/switch/switch.ts +0 -52
- package/src/components/switch/types.ts +0 -168
- package/src/components/tabs/api.ts +0 -221
- package/src/components/tabs/config.ts +0 -73
- package/src/components/tabs/features.ts +0 -403
- package/src/components/tabs/index.ts +0 -46
- package/src/components/tabs/indicator.ts +0 -285
- package/src/components/tabs/responsive.ts +0 -144
- package/src/components/tabs/scroll-indicators.ts +0 -149
- package/src/components/tabs/state.ts +0 -186
- package/src/components/tabs/tab-api.ts +0 -266
- package/src/components/tabs/tab.ts +0 -267
- package/src/components/tabs/tabs.ts +0 -71
- package/src/components/tabs/types.ts +0 -461
- package/src/components/tabs/utils.ts +0 -107
- package/src/components/textfield/api.ts +0 -197
- package/src/components/textfield/config.ts +0 -52
- package/src/components/textfield/features/leading-icon.ts +0 -127
- package/src/components/textfield/features/placement.ts +0 -149
- package/src/components/textfield/features/prefix-text.ts +0 -107
- package/src/components/textfield/features/suffix-text.ts +0 -100
- package/src/components/textfield/features/supporting-text.ts +0 -113
- package/src/components/textfield/features/trailing-icon.ts +0 -108
- package/src/components/textfield/index.ts +0 -9
- package/src/components/textfield/textfield.ts +0 -92
- package/src/components/textfield/types.ts +0 -265
- package/src/components/timepicker/README.md +0 -277
- package/src/components/timepicker/api.ts +0 -632
- package/src/components/timepicker/clockdial.ts +0 -479
- package/src/components/timepicker/config.ts +0 -228
- package/src/components/timepicker/index.ts +0 -3
- package/src/components/timepicker/render.ts +0 -613
- package/src/components/timepicker/timepicker.ts +0 -117
- package/src/components/timepicker/types.ts +0 -336
- package/src/components/timepicker/utils.ts +0 -241
- package/src/components/tooltip/api.ts +0 -415
- package/src/components/tooltip/config.ts +0 -80
- package/src/components/tooltip/index.ts +0 -12
- package/src/components/tooltip/tooltip.ts +0 -60
- package/src/components/tooltip/types.ts +0 -223
- package/src/components/top-app-bar/config.ts +0 -83
- package/src/components/top-app-bar/top-app-bar.ts +0 -316
- package/src/components/top-app-bar/types.ts +0 -140
- package/src/core/build/constants.ts +0 -48
- package/src/core/build/icon.ts +0 -137
- package/src/core/build/ripple.ts +0 -193
- package/src/core/build/text.ts +0 -91
- package/src/core/collection/adapters/base.ts +0 -62
- package/src/core/collection/adapters/route.ts +0 -201
- package/src/core/collection/collection.ts +0 -300
- package/src/core/collection/index.ts +0 -57
- package/src/core/collection/list-manager.ts +0 -333
- package/src/core/compose/base.ts +0 -43
- package/src/core/compose/component.ts +0 -255
- package/src/core/compose/features/badge.ts +0 -79
- package/src/core/compose/features/checkable.ts +0 -155
- package/src/core/compose/features/disabled.ts +0 -116
- package/src/core/compose/features/events.ts +0 -65
- package/src/core/compose/features/icon.ts +0 -71
- package/src/core/compose/features/input.ts +0 -174
- package/src/core/compose/features/lifecycle.ts +0 -139
- package/src/core/compose/features/position.ts +0 -94
- package/src/core/compose/features/ripple.ts +0 -58
- package/src/core/compose/features/size.ts +0 -29
- package/src/core/compose/features/style.ts +0 -31
- package/src/core/compose/features/text.ts +0 -44
- package/src/core/compose/features/textinput.ts +0 -238
- package/src/core/compose/features/textlabel.ts +0 -113
- package/src/core/compose/features/track.ts +0 -84
- package/src/core/compose/features/variant.ts +0 -29
- package/src/core/compose/features/withEvents.ts +0 -137
- package/src/core/compose/index.ts +0 -54
- package/src/core/composition/features/dom.ts +0 -45
- package/src/core/composition/features/icon.ts +0 -131
- package/src/core/composition/features/label.ts +0 -155
- package/src/core/config.ts +0 -211
- package/src/core/dom/classes.ts +0 -132
- package/src/core/dom/create.ts +0 -273
- package/src/core/dom/events.ts +0 -209
- package/src/core/dom/utils.ts +0 -97
- package/src/core/index.ts +0 -111
- package/src/core/layout/README.md +0 -715
- package/src/core/layout/array.ts +0 -180
- package/src/core/layout/config.ts +0 -193
- package/src/core/layout/create.ts +0 -54
- package/src/core/layout/index.ts +0 -36
- package/src/core/layout/object.ts +0 -123
- package/src/core/layout/processor.ts +0 -106
- package/src/core/layout/result.ts +0 -84
- package/src/core/layout/types.ts +0 -180
- package/src/core/layout/utils.ts +0 -144
- package/src/core/state/disabled.ts +0 -81
- package/src/core/state/emitter.ts +0 -94
- package/src/core/state/events.ts +0 -88
- package/src/core/state/lifecycle.ts +0 -131
- package/src/core/state/store.ts +0 -197
- package/src/core/utils/index.ts +0 -45
- package/src/core/utils/mobile.ts +0 -98
- package/src/core/utils/object.ts +0 -41
- package/src/core/utils/validate.ts +0 -234
- package/src/index.ts +0 -90
- package/src/styles/abstract/_base.scss +0 -2
- package/src/styles/abstract/_config.scss +0 -28
- package/src/styles/abstract/_functions.scss +0 -124
- package/src/styles/abstract/_mixins.scss +0 -352
- package/src/styles/abstract/_theme.scss +0 -269
- package/src/styles/abstract/_variables.scss +0 -305
- package/src/styles/base/_reset.scss +0 -86
- package/src/styles/base/_typography.scss +0 -155
- package/src/styles/components/_badge.scss +0 -182
- package/src/styles/components/_bottom-app-bar.scss +0 -103
- package/src/styles/components/_button.scss +0 -224
- package/src/styles/components/_card.scss +0 -401
- package/src/styles/components/_carousel.scss +0 -645
- package/src/styles/components/_checkbox.scss +0 -231
- package/src/styles/components/_chips.scss +0 -638
- package/src/styles/components/_datepicker.scss +0 -358
- package/src/styles/components/_dialog.scss +0 -259
- package/src/styles/components/_divider.scss +0 -57
- package/src/styles/components/_extended-fab.scss +0 -267
- package/src/styles/components/_fab.scss +0 -225
- package/src/styles/components/_list.scss +0 -248
- package/src/styles/components/_menu.scss +0 -242
- package/src/styles/components/_navigation-mobile.scss +0 -244
- package/src/styles/components/_navigation-system.scss +0 -151
- package/src/styles/components/_navigation.scss +0 -407
- package/src/styles/components/_progress.scss +0 -151
- package/src/styles/components/_radios.scss +0 -187
- package/src/styles/components/_search.scss +0 -306
- package/src/styles/components/_segmented-button.scss +0 -227
- package/src/styles/components/_select.scss +0 -272
- package/src/styles/components/_sheet.scss +0 -236
- package/src/styles/components/_slider.scss +0 -489
- package/src/styles/components/_snackbar.scss +0 -211
- package/src/styles/components/_switch.scss +0 -298
- package/src/styles/components/_tabs.scss +0 -416
- package/src/styles/components/_textfield.scss +0 -773
- package/src/styles/components/_timepicker.scss +0 -451
- package/src/styles/components/_tooltip.scss +0 -241
- package/src/styles/components/_top-app-bar.scss +0 -225
- package/src/styles/main.scss +0 -175
- package/src/styles/themes/_autumn.scss +0 -105
- package/src/styles/themes/_base-theme.scss +0 -85
- package/src/styles/themes/_baseline.scss +0 -173
- package/src/styles/themes/_bluekhaki.scss +0 -125
- package/src/styles/themes/_brownbeige.scss +0 -125
- package/src/styles/themes/_browngreen.scss +0 -125
- package/src/styles/themes/_forest.scss +0 -77
- package/src/styles/themes/_greenbeige.scss +0 -125
- package/src/styles/themes/_index.scss +0 -6
- package/src/styles/themes/_material.scss +0 -125
- package/src/styles/themes/_ocean.scss +0 -77
- package/src/styles/themes/_sageivory.scss +0 -125
- package/src/styles/themes/_spring.scss +0 -77
- package/src/styles/themes/_summer.scss +0 -87
- package/src/styles/themes/_sunset.scss +0 -60
- package/src/styles/themes/_tealcaramel.scss +0 -125
- package/src/styles/themes/_winter.scss +0 -77
- package/src/styles/utilities/_colors.scss +0 -154
- package/src/styles/utilities/_flexbox.scss +0 -194
- package/src/styles/utilities/_layout.scss +0 -665
- package/src/styles/utilities/_ripple.scss +0 -79
- package/src/styles/utilities/_spacing.scss +0 -139
- package/src/styles/utilities/_typography.scss +0 -178
- package/src/styles/utilities/_visibility.scss +0 -142
|
@@ -1,1423 +0,0 @@
|
|
|
1
|
-
// src/components/menu/features/controller.ts
|
|
2
|
-
|
|
3
|
-
import { MenuConfig, MenuContent, MenuItem, MenuDivider, MenuEvent, MenuSelectEvent } from '../types';
|
|
4
|
-
import { createPositioner } from './position';
|
|
5
|
-
|
|
6
|
-
let ignoreNextDocumentClick = false;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Adds controller functionality to the menu component
|
|
10
|
-
* Manages state, rendering, positioning, and event handling
|
|
11
|
-
*
|
|
12
|
-
* @param config - Menu configuration
|
|
13
|
-
* @returns Component enhancer with menu controller functionality
|
|
14
|
-
*/
|
|
15
|
-
const withController = (config: MenuConfig) => component => {
|
|
16
|
-
if (!component.element) {
|
|
17
|
-
console.warn('Cannot initialize menu controller: missing element');
|
|
18
|
-
return component;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Initialize state
|
|
22
|
-
const state = {
|
|
23
|
-
visible: config.visible || false,
|
|
24
|
-
items: config.items || [],
|
|
25
|
-
position: config.position,
|
|
26
|
-
selectedItemId: null as string | null,
|
|
27
|
-
activeSubmenu: null as HTMLElement,
|
|
28
|
-
activeSubmenuItem: null as HTMLElement,
|
|
29
|
-
activeItemIndex: -1,
|
|
30
|
-
submenuLevel: 0, // Track nesting level of submenus
|
|
31
|
-
activeSubmenus: [] as Array<{
|
|
32
|
-
element: HTMLElement,
|
|
33
|
-
menuItem: HTMLElement,
|
|
34
|
-
level: number,
|
|
35
|
-
isOpening: boolean // Track if submenu is in opening transition
|
|
36
|
-
}>,
|
|
37
|
-
submenuTimer: null,
|
|
38
|
-
hoverIntent: {
|
|
39
|
-
timer: null,
|
|
40
|
-
activeItem: null
|
|
41
|
-
},
|
|
42
|
-
component,
|
|
43
|
-
keyboardNavActive: false // Track if keyboard navigation is active
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Create positioner
|
|
47
|
-
const positioner = createPositioner(component, config);
|
|
48
|
-
|
|
49
|
-
// Create event helpers
|
|
50
|
-
const eventHelpers = {
|
|
51
|
-
triggerEvent(eventName: string, data: any = {}, originalEvent?: Event) {
|
|
52
|
-
const eventData = {
|
|
53
|
-
menu: state.component,
|
|
54
|
-
...data,
|
|
55
|
-
originalEvent,
|
|
56
|
-
preventDefault: () => { eventData.defaultPrevented = true; },
|
|
57
|
-
defaultPrevented: false
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
component.emit(eventName, eventData);
|
|
61
|
-
return eventData;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Gets the anchor element from config
|
|
67
|
-
*/
|
|
68
|
-
const getAnchorElement = (): HTMLElement => {
|
|
69
|
-
// First try to get the resolved anchor from the anchor feature
|
|
70
|
-
if (component.anchor && typeof component.anchor.getAnchor === 'function') {
|
|
71
|
-
return component.anchor.getAnchor();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Fall back to config anchor for initial positioning
|
|
75
|
-
const { anchor } = config;
|
|
76
|
-
|
|
77
|
-
if (typeof anchor === 'string') {
|
|
78
|
-
const element = document.querySelector(anchor);
|
|
79
|
-
if (!element) {
|
|
80
|
-
console.warn(`Menu anchor not found: ${anchor}`);
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
return element as HTMLElement;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Handle component with element property
|
|
87
|
-
if (typeof anchor === 'object' && anchor !== null && 'element' in anchor) {
|
|
88
|
-
return anchor.element;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Handle direct HTML element
|
|
92
|
-
return anchor as HTMLElement;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Creates a DOM element for a menu item
|
|
97
|
-
*/
|
|
98
|
-
const createMenuItem = (item: MenuItem, index: number): HTMLElement => {
|
|
99
|
-
const itemElement = document.createElement('li');
|
|
100
|
-
const itemClass = `${component.getClass('menu-item')}`;
|
|
101
|
-
|
|
102
|
-
itemElement.className = itemClass;
|
|
103
|
-
itemElement.setAttribute('role', 'menuitem');
|
|
104
|
-
itemElement.setAttribute('tabindex', '-1'); // Set to -1 by default, will update when needed
|
|
105
|
-
itemElement.setAttribute('data-id', item.id);
|
|
106
|
-
itemElement.setAttribute('data-index', index.toString());
|
|
107
|
-
|
|
108
|
-
if (item.disabled) {
|
|
109
|
-
itemElement.classList.add(`${itemClass}--disabled`);
|
|
110
|
-
itemElement.setAttribute('aria-disabled', 'true');
|
|
111
|
-
} else {
|
|
112
|
-
itemElement.setAttribute('aria-disabled', 'false');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (state.selectedItemId && item.id === state.selectedItemId) {
|
|
116
|
-
itemElement.classList.add(`${itemClass}--selected`);
|
|
117
|
-
itemElement.setAttribute('aria-selected', 'true');
|
|
118
|
-
} else {
|
|
119
|
-
itemElement.setAttribute('aria-selected', 'false');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (item.hasSubmenu) {
|
|
124
|
-
itemElement.classList.add(`${itemClass}--submenu`);
|
|
125
|
-
itemElement.setAttribute('aria-haspopup', 'true');
|
|
126
|
-
itemElement.setAttribute('aria-expanded', 'false');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Create content container for flexible layout
|
|
130
|
-
const contentContainer = document.createElement('span');
|
|
131
|
-
contentContainer.className = `${component.getClass('menu-item-content')}`;
|
|
132
|
-
|
|
133
|
-
// Add icon if provided
|
|
134
|
-
if (item.icon) {
|
|
135
|
-
const iconElement = document.createElement('span');
|
|
136
|
-
iconElement.className = `${component.getClass('menu-item-icon')}`;
|
|
137
|
-
iconElement.innerHTML = item.icon;
|
|
138
|
-
contentContainer.appendChild(iconElement);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Add text
|
|
142
|
-
const textElement = document.createElement('span');
|
|
143
|
-
textElement.className = `${component.getClass('menu-item-text')}`;
|
|
144
|
-
textElement.textContent = item.text;
|
|
145
|
-
contentContainer.appendChild(textElement);
|
|
146
|
-
|
|
147
|
-
// Add shortcut if provided
|
|
148
|
-
if (item.shortcut) {
|
|
149
|
-
const shortcutElement = document.createElement('span');
|
|
150
|
-
shortcutElement.className = `${component.getClass('menu-item-shortcut')}`;
|
|
151
|
-
shortcutElement.textContent = item.shortcut;
|
|
152
|
-
contentContainer.appendChild(shortcutElement);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
itemElement.appendChild(contentContainer);
|
|
156
|
-
|
|
157
|
-
// Add event listeners
|
|
158
|
-
if (!item.disabled) {
|
|
159
|
-
// Mouse events
|
|
160
|
-
itemElement.addEventListener('click', (e) => handleItemClick(e, item, index));
|
|
161
|
-
|
|
162
|
-
// Focus and blur events for proper focus styling
|
|
163
|
-
itemElement.addEventListener('focus', () => {
|
|
164
|
-
state.activeItemIndex = index;
|
|
165
|
-
state.keyboardNavActive = true;
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// Additional keyboard event handler for accessibility
|
|
169
|
-
itemElement.addEventListener('keydown', (e) => {
|
|
170
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
171
|
-
e.preventDefault();
|
|
172
|
-
handleItemClick(e, item, index);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
if (item.hasSubmenu && config.openSubmenuOnHover) {
|
|
177
|
-
itemElement.addEventListener('mouseenter', () => handleSubmenuHover(item, index, itemElement));
|
|
178
|
-
itemElement.addEventListener('mouseleave', handleSubmenuLeave);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return itemElement;
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Creates a DOM element for a menu divider
|
|
187
|
-
*/
|
|
188
|
-
const createDivider = (divider: MenuDivider, index: number): HTMLElement => {
|
|
189
|
-
const dividerElement = document.createElement('li');
|
|
190
|
-
dividerElement.className = `${component.getClass('menu-divider')}`;
|
|
191
|
-
dividerElement.setAttribute('role', 'separator');
|
|
192
|
-
dividerElement.setAttribute('data-index', index.toString());
|
|
193
|
-
|
|
194
|
-
if (divider.id) {
|
|
195
|
-
dividerElement.setAttribute('id', divider.id);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return dividerElement;
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Renders the menu items
|
|
203
|
-
*/
|
|
204
|
-
const renderMenuItems = (): void => {
|
|
205
|
-
const menuList = document.createElement('ul');
|
|
206
|
-
menuList.className = `${component.getClass('menu-list')}`;
|
|
207
|
-
menuList.setAttribute('role', 'menu');
|
|
208
|
-
|
|
209
|
-
// Create items
|
|
210
|
-
state.items.forEach((item, index) => {
|
|
211
|
-
if ('type' in item && item.type === 'divider') {
|
|
212
|
-
menuList.appendChild(createDivider(item, index));
|
|
213
|
-
} else {
|
|
214
|
-
menuList.appendChild(createMenuItem(item as MenuItem, index));
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Clear and append
|
|
219
|
-
component.element.innerHTML = '';
|
|
220
|
-
component.element.appendChild(menuList);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Clean up hover intent timer
|
|
225
|
-
*/
|
|
226
|
-
const clearHoverIntent = () => {
|
|
227
|
-
if (state.hoverIntent.timer) {
|
|
228
|
-
clearTimeout(state.hoverIntent.timer);
|
|
229
|
-
state.hoverIntent.timer = null;
|
|
230
|
-
state.hoverIntent.activeItem = null;
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Sets focus appropriately based on interaction type
|
|
236
|
-
* For keyboard interactions, focuses the first item
|
|
237
|
-
* For mouse interactions, makes the menu container focusable but doesn't auto-focus
|
|
238
|
-
*
|
|
239
|
-
* @param {'keyboard'|'mouse'} interactionType - Type of interaction that opened the menu
|
|
240
|
-
*/
|
|
241
|
-
const handleFocus = (interactionType: 'keyboard' | 'mouse'): void => {
|
|
242
|
-
// Reset active item index
|
|
243
|
-
state.activeItemIndex = -1;
|
|
244
|
-
|
|
245
|
-
if (interactionType === 'keyboard') {
|
|
246
|
-
// Find all focusable items
|
|
247
|
-
const items = Array.from(
|
|
248
|
-
component.element.querySelectorAll(`.${component.getClass('menu-item')}:not(.${component.getClass('menu-item--disabled')})`)
|
|
249
|
-
) as HTMLElement[];
|
|
250
|
-
|
|
251
|
-
if (items.length > 0) {
|
|
252
|
-
// Set all items to tabindex -1 except the first one
|
|
253
|
-
items.forEach((item, index) => {
|
|
254
|
-
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// Focus the first item for keyboard navigation
|
|
258
|
-
items[0].focus();
|
|
259
|
-
state.activeItemIndex = 0;
|
|
260
|
-
state.keyboardNavActive = true;
|
|
261
|
-
} else {
|
|
262
|
-
// If no items, focus the menu itself
|
|
263
|
-
component.element.setAttribute('tabindex', '0');
|
|
264
|
-
component.element.focus();
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
// For mouse interaction, make the menu focusable but don't auto-focus
|
|
268
|
-
component.element.setAttribute('tabindex', '-1');
|
|
269
|
-
|
|
270
|
-
// Still set up the tabindex correctly for potential keyboard navigation
|
|
271
|
-
const items = Array.from(
|
|
272
|
-
component.element.querySelectorAll(`.${component.getClass('menu-item')}:not(.${component.getClass('menu-item--disabled')})`)
|
|
273
|
-
) as HTMLElement[];
|
|
274
|
-
|
|
275
|
-
if (items.length > 0) {
|
|
276
|
-
// Set all items to tabindex -1 except the first one
|
|
277
|
-
items.forEach((item, index) => {
|
|
278
|
-
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Handles click on a menu item
|
|
286
|
-
*/
|
|
287
|
-
const handleItemClick = (e: MouseEvent, item: MenuItem, index: number): void => {
|
|
288
|
-
e.preventDefault();
|
|
289
|
-
e.stopPropagation();
|
|
290
|
-
|
|
291
|
-
// Don't process if disabled
|
|
292
|
-
if (item.disabled) return;
|
|
293
|
-
|
|
294
|
-
if (item.hasSubmenu) {
|
|
295
|
-
handleSubmenuClick(item, index, e.currentTarget as HTMLElement);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Trigger select event
|
|
300
|
-
const selectEvent = eventHelpers.triggerEvent('select', {
|
|
301
|
-
item,
|
|
302
|
-
itemId: item.id,
|
|
303
|
-
itemData: item.data
|
|
304
|
-
}, e) as MenuSelectEvent;
|
|
305
|
-
|
|
306
|
-
// Close menu if needed
|
|
307
|
-
if (config.closeOnSelect && !selectEvent.defaultPrevented) {
|
|
308
|
-
closeMenu(e);
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Handles click on a submenu item
|
|
314
|
-
*/
|
|
315
|
-
const handleSubmenuClick = (item: MenuItem, index: number, itemElement: HTMLElement, viaKeyboard = false): void => {
|
|
316
|
-
if (!item.submenu || !item.hasSubmenu) return;
|
|
317
|
-
|
|
318
|
-
// Check if the submenu is already open
|
|
319
|
-
const isOpen = itemElement.getAttribute('aria-expanded') === 'true';
|
|
320
|
-
|
|
321
|
-
// Find if any submenu is currently in opening transition
|
|
322
|
-
const anySubmenuTransitioning = state.activeSubmenus.some(s => s.isOpening);
|
|
323
|
-
|
|
324
|
-
// Completely ignore clicks during any submenu transition
|
|
325
|
-
if (anySubmenuTransitioning) {
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (isOpen) {
|
|
330
|
-
// Close submenu - only if fully open
|
|
331
|
-
// Find the closest submenu level
|
|
332
|
-
const currentLevel = parseInt(
|
|
333
|
-
itemElement.closest(`.${component.getClass('menu--submenu')}`)?.getAttribute('data-level') || '0',
|
|
334
|
-
10
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
// Close this level + 1 and deeper
|
|
338
|
-
closeSubmenuAtLevel(currentLevel + 1);
|
|
339
|
-
|
|
340
|
-
// Reset expanded state
|
|
341
|
-
itemElement.setAttribute('aria-expanded', 'false');
|
|
342
|
-
} else {
|
|
343
|
-
// Open new submenu
|
|
344
|
-
openSubmenu(item, index, itemElement, viaKeyboard);
|
|
345
|
-
}
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Handles hover on a submenu item
|
|
350
|
-
*/
|
|
351
|
-
const handleSubmenuHover = (item: MenuItem, index: number, itemElement: HTMLElement): void => {
|
|
352
|
-
if (!config.openSubmenuOnHover || !item.hasSubmenu) return;
|
|
353
|
-
|
|
354
|
-
// If keyboard navigation is active, don't open submenu on hover
|
|
355
|
-
if (state.keyboardNavActive) return;
|
|
356
|
-
|
|
357
|
-
// Clear any existing timers
|
|
358
|
-
clearHoverIntent();
|
|
359
|
-
clearSubmenuTimer();
|
|
360
|
-
|
|
361
|
-
// Set hover intent
|
|
362
|
-
state.hoverIntent.activeItem = itemElement;
|
|
363
|
-
state.hoverIntent.timer = setTimeout(() => {
|
|
364
|
-
const isCurrentlyHovered = itemElement.matches(':hover');
|
|
365
|
-
if (isCurrentlyHovered) {
|
|
366
|
-
// Only close and reopen if this is a different submenu item
|
|
367
|
-
if (state.activeSubmenuItem !== itemElement) {
|
|
368
|
-
openSubmenu(item, index, itemElement);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
state.hoverIntent.timer = null;
|
|
372
|
-
}, 100);
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Handles mouse leave from submenu
|
|
377
|
-
*/
|
|
378
|
-
const handleSubmenuLeave = (e: MouseEvent): void => {
|
|
379
|
-
// If keyboard navigation is active, don't close submenu on mouse leave
|
|
380
|
-
if (state.keyboardNavActive) return;
|
|
381
|
-
|
|
382
|
-
// Clear hover intent
|
|
383
|
-
clearHoverIntent();
|
|
384
|
-
|
|
385
|
-
// Don't close immediately to allow moving to submenu
|
|
386
|
-
clearSubmenuTimer();
|
|
387
|
-
|
|
388
|
-
// Set a timer to close the submenu if not re-entered
|
|
389
|
-
state.submenuTimer = setTimeout(() => {
|
|
390
|
-
// Check if mouse is over the submenu or the parent menu item
|
|
391
|
-
const submenuElement = state.activeSubmenu;
|
|
392
|
-
const menuItemElement = state.activeSubmenuItem;
|
|
393
|
-
|
|
394
|
-
if (submenuElement && menuItemElement) {
|
|
395
|
-
const overSubmenu = submenuElement.matches(':hover');
|
|
396
|
-
const overMenuItem = menuItemElement.matches(':hover');
|
|
397
|
-
|
|
398
|
-
if (!overSubmenu && !overMenuItem) {
|
|
399
|
-
closeSubmenuAtLevel(state.submenuLevel);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
state.submenuTimer = null;
|
|
404
|
-
}, 300);
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Opens a submenu with proper animation and positioning
|
|
409
|
-
*/
|
|
410
|
-
const openSubmenu = (item: MenuItem, index: number, itemElement: HTMLElement, viaKeyboard = false): void => {
|
|
411
|
-
if (!item.submenu || !item.hasSubmenu) return;
|
|
412
|
-
|
|
413
|
-
// If opened via keyboard, update the keyboard navigation state
|
|
414
|
-
if (viaKeyboard) {
|
|
415
|
-
state.keyboardNavActive = true;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Get current level of the submenu we're opening
|
|
419
|
-
const currentLevel = itemElement.closest(`.${component.getClass('menu--submenu')}`)
|
|
420
|
-
? parseInt(itemElement.closest(`.${component.getClass('menu--submenu')}`).getAttribute('data-level') || '0', 10) + 1
|
|
421
|
-
: 1;
|
|
422
|
-
|
|
423
|
-
// Close any deeper level submenus first, preserving the current level
|
|
424
|
-
closeSubmenuAtLevel(currentLevel);
|
|
425
|
-
|
|
426
|
-
// Check if this submenu is already in opening state - if so, do nothing
|
|
427
|
-
const existingSubmenuIndex = state.activeSubmenus.findIndex(
|
|
428
|
-
s => s.menuItem === itemElement && s.isOpening
|
|
429
|
-
);
|
|
430
|
-
if (existingSubmenuIndex >= 0) {
|
|
431
|
-
return; // Already opening this submenu, don't restart the process
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Set expanded state
|
|
435
|
-
itemElement.setAttribute('aria-expanded', 'true');
|
|
436
|
-
|
|
437
|
-
// Create submenu element with proper classes and attributes
|
|
438
|
-
const submenuElement = document.createElement('div');
|
|
439
|
-
submenuElement.className = `${component.getClass('menu')} ${component.getClass('menu--submenu')}`;
|
|
440
|
-
submenuElement.setAttribute('role', 'menu');
|
|
441
|
-
submenuElement.setAttribute('tabindex', '-1');
|
|
442
|
-
submenuElement.setAttribute('data-level', currentLevel.toString());
|
|
443
|
-
submenuElement.setAttribute('data-parent-item', item.id);
|
|
444
|
-
|
|
445
|
-
// Increase z-index for each level of submenu
|
|
446
|
-
submenuElement.style.zIndex = `${1000 + (currentLevel * 10)}`;
|
|
447
|
-
|
|
448
|
-
// Create submenu list
|
|
449
|
-
const submenuList = document.createElement('ul');
|
|
450
|
-
submenuList.className = `${component.getClass('menu-list')}`;
|
|
451
|
-
|
|
452
|
-
// Create submenu items
|
|
453
|
-
const submenuItems = [];
|
|
454
|
-
item.submenu.forEach((subitem, subindex) => {
|
|
455
|
-
if ('type' in subitem && subitem.type === 'divider') {
|
|
456
|
-
submenuList.appendChild(createDivider(subitem, subindex));
|
|
457
|
-
} else {
|
|
458
|
-
const subitemElement = createMenuItem(subitem as MenuItem, subindex);
|
|
459
|
-
submenuList.appendChild(subitemElement);
|
|
460
|
-
if (!(subitem as MenuItem).disabled) {
|
|
461
|
-
submenuItems.push(subitemElement);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
submenuElement.appendChild(submenuList);
|
|
467
|
-
|
|
468
|
-
// Add to DOM to enable measurement and transitions
|
|
469
|
-
document.body.appendChild(submenuElement);
|
|
470
|
-
|
|
471
|
-
// Position the submenu using our positioner with the current nesting level
|
|
472
|
-
positioner.positionSubmenu(submenuElement, itemElement, currentLevel);
|
|
473
|
-
|
|
474
|
-
// Setup keyboard navigation for submenu
|
|
475
|
-
submenuElement.addEventListener('keydown', handleMenuKeydown);
|
|
476
|
-
|
|
477
|
-
// Add mouseenter event to prevent closing
|
|
478
|
-
submenuElement.addEventListener('mouseenter', () => {
|
|
479
|
-
if (!state.keyboardNavActive) {
|
|
480
|
-
clearSubmenuTimer();
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
// Add mouseleave event to handle closing
|
|
485
|
-
submenuElement.addEventListener('mouseleave', (e) => {
|
|
486
|
-
if (!state.keyboardNavActive) {
|
|
487
|
-
handleSubmenuLeave(e);
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
// Setup submenu event handlers for nested submenus
|
|
492
|
-
const setupNestedSubmenuHandlers = (parent: HTMLElement) => {
|
|
493
|
-
const submenuItems = parent.querySelectorAll(`.${component.getClass('menu-item--submenu')}`) as NodeListOf<HTMLElement>;
|
|
494
|
-
|
|
495
|
-
submenuItems.forEach((menuItem) => {
|
|
496
|
-
const itemIndex = parseInt(menuItem.getAttribute('data-index'), 10);
|
|
497
|
-
const menuItemData = item.submenu[itemIndex] as MenuItem;
|
|
498
|
-
|
|
499
|
-
if (menuItemData && menuItemData.hasSubmenu) {
|
|
500
|
-
// Add hover handler for nested submenus
|
|
501
|
-
if (config.openSubmenuOnHover) {
|
|
502
|
-
menuItem.addEventListener('mouseenter', () => {
|
|
503
|
-
handleNestedSubmenuHover(menuItemData, itemIndex, menuItem);
|
|
504
|
-
});
|
|
505
|
-
menuItem.addEventListener('mouseleave', handleSubmenuLeave);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Add click handler for nested submenus
|
|
509
|
-
menuItem.addEventListener('click', (e) => {
|
|
510
|
-
e.preventDefault();
|
|
511
|
-
e.stopPropagation();
|
|
512
|
-
handleNestedSubmenuClick(menuItemData, itemIndex, menuItem, false);
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
// Setup handlers for any nested submenus
|
|
519
|
-
setupNestedSubmenuHandlers(submenuElement);
|
|
520
|
-
|
|
521
|
-
// Update state with active submenu
|
|
522
|
-
state.activeSubmenu = submenuElement;
|
|
523
|
-
state.activeSubmenuItem = itemElement;
|
|
524
|
-
|
|
525
|
-
// Add to active submenus array to maintain hierarchy
|
|
526
|
-
state.activeSubmenus.push({
|
|
527
|
-
element: submenuElement,
|
|
528
|
-
menuItem: itemElement,
|
|
529
|
-
level: currentLevel,
|
|
530
|
-
isOpening: true // Mark as in opening transition
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
// Update submenu level
|
|
534
|
-
state.submenuLevel = currentLevel;
|
|
535
|
-
|
|
536
|
-
// Add document events for this submenu
|
|
537
|
-
document.addEventListener('click', handleDocumentClickForSubmenu);
|
|
538
|
-
window.addEventListener('resize', handleWindowResizeForSubmenu, { passive: true });
|
|
539
|
-
window.addEventListener('scroll', handleWindowScrollForSubmenu, { passive: true });
|
|
540
|
-
|
|
541
|
-
// Make visible with animation
|
|
542
|
-
requestAnimationFrame(() => {
|
|
543
|
-
submenuElement.classList.add(`${component.getClass('menu--visible')}`);
|
|
544
|
-
|
|
545
|
-
// Wait for transition to complete before marking as fully opened
|
|
546
|
-
// This should match your CSS transition duration
|
|
547
|
-
setTimeout(() => {
|
|
548
|
-
// Find this submenu in the active submenus array and update its state
|
|
549
|
-
const index = state.activeSubmenus.findIndex(s => s.element === submenuElement);
|
|
550
|
-
if (index !== -1) {
|
|
551
|
-
state.activeSubmenus[index].isOpening = false;
|
|
552
|
-
}
|
|
553
|
-
}, 300); // Adjust to match your transition duration
|
|
554
|
-
|
|
555
|
-
// If opened via keyboard, focus the first item in the submenu
|
|
556
|
-
if (viaKeyboard && submenuItems.length > 0) {
|
|
557
|
-
submenuItems[0].setAttribute('tabindex', '0');
|
|
558
|
-
|
|
559
|
-
// Set other items to -1
|
|
560
|
-
for (let i = 1; i < submenuItems.length; i++) {
|
|
561
|
-
submenuItems[i].setAttribute('tabindex', '-1');
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Focus with a short delay to allow animation to start
|
|
565
|
-
setTimeout(() => {
|
|
566
|
-
submenuItems[0].focus();
|
|
567
|
-
}, 50);
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Handles hover on a nested submenu item
|
|
574
|
-
*/
|
|
575
|
-
const handleNestedSubmenuHover = (item: MenuItem, index: number, itemElement: HTMLElement): void => {
|
|
576
|
-
if (!config.openSubmenuOnHover || !item.hasSubmenu || state.keyboardNavActive) return;
|
|
577
|
-
|
|
578
|
-
// Clear any existing timers
|
|
579
|
-
clearHoverIntent();
|
|
580
|
-
clearSubmenuTimer();
|
|
581
|
-
|
|
582
|
-
// Set hover intent with a slightly longer delay for nested menus
|
|
583
|
-
state.hoverIntent.activeItem = itemElement;
|
|
584
|
-
state.hoverIntent.timer = setTimeout(() => {
|
|
585
|
-
const isCurrentlyHovered = itemElement.matches(':hover');
|
|
586
|
-
if (isCurrentlyHovered) {
|
|
587
|
-
// Find the closest submenu level of this item
|
|
588
|
-
const currentLevel = parseInt(
|
|
589
|
-
itemElement.closest(`.${component.getClass('menu--submenu')}`)?.getAttribute('data-level') || '1',
|
|
590
|
-
10
|
|
591
|
-
);
|
|
592
|
-
|
|
593
|
-
// Open the nested submenu (will handle closing deeper levels properly)
|
|
594
|
-
handleNestedSubmenuClick(item, index, itemElement, false);
|
|
595
|
-
}
|
|
596
|
-
state.hoverIntent.timer = null;
|
|
597
|
-
}, 120); // Slightly longer delay for nested submenus
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Handles click on a nested submenu item
|
|
602
|
-
*/
|
|
603
|
-
const handleNestedSubmenuClick = (item: MenuItem, index: number, itemElement: HTMLElement, viaKeyboard = false): void => {
|
|
604
|
-
if (!item.submenu || !item.hasSubmenu) return;
|
|
605
|
-
|
|
606
|
-
// Check if the submenu is already open
|
|
607
|
-
const isOpen = itemElement.getAttribute('aria-expanded') === 'true';
|
|
608
|
-
|
|
609
|
-
// Find if any submenu is currently in opening transition
|
|
610
|
-
const anySubmenuTransitioning = state.activeSubmenus.some(s => s.isOpening);
|
|
611
|
-
|
|
612
|
-
// Completely ignore clicks during any submenu transition
|
|
613
|
-
if (anySubmenuTransitioning) {
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (isOpen) {
|
|
618
|
-
// Find the closest submenu level
|
|
619
|
-
const currentLevel = parseInt(
|
|
620
|
-
itemElement.closest(`.${component.getClass('menu--submenu')}`)?.getAttribute('data-level') || '1',
|
|
621
|
-
10
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
// Close submenus at and deeper than the next level
|
|
625
|
-
closeSubmenuAtLevel(currentLevel + 1);
|
|
626
|
-
} else {
|
|
627
|
-
// Open the nested submenu
|
|
628
|
-
openSubmenu(item, index, itemElement, viaKeyboard);
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Clear submenu close timer
|
|
634
|
-
*/
|
|
635
|
-
const clearSubmenuTimer = () => {
|
|
636
|
-
if (state.submenuTimer) {
|
|
637
|
-
clearTimeout(state.submenuTimer);
|
|
638
|
-
state.submenuTimer = null;
|
|
639
|
-
}
|
|
640
|
-
};
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Closes submenus at or deeper than the specified level
|
|
644
|
-
* @param level - The level to start closing from
|
|
645
|
-
*/
|
|
646
|
-
const closeSubmenuAtLevel = (level: number): void => {
|
|
647
|
-
// Clear any hover intent or submenu timers
|
|
648
|
-
clearHoverIntent();
|
|
649
|
-
clearSubmenuTimer();
|
|
650
|
-
|
|
651
|
-
// Find submenus at or deeper than the specified level
|
|
652
|
-
const submenusCopy = [...state.activeSubmenus];
|
|
653
|
-
const submenuIndicesToRemove = [];
|
|
654
|
-
|
|
655
|
-
// Identify which submenus to remove, working from deepest level first
|
|
656
|
-
for (let i = submenusCopy.length - 1; i >= 0; i--) {
|
|
657
|
-
if (submenusCopy[i].level >= level) {
|
|
658
|
-
const submenuToClose = submenusCopy[i];
|
|
659
|
-
|
|
660
|
-
// Set aria-expanded attribute to false on the parent menu item
|
|
661
|
-
if (submenuToClose.menuItem) {
|
|
662
|
-
submenuToClose.menuItem.setAttribute('aria-expanded', 'false');
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Hide with animation
|
|
666
|
-
submenuToClose.element.classList.remove(`${component.getClass('menu--visible')}`);
|
|
667
|
-
|
|
668
|
-
// Schedule for removal
|
|
669
|
-
setTimeout(() => {
|
|
670
|
-
if (submenuToClose.element.parentNode) {
|
|
671
|
-
submenuToClose.element.parentNode.removeChild(submenuToClose.element);
|
|
672
|
-
}
|
|
673
|
-
}, 200);
|
|
674
|
-
|
|
675
|
-
// Mark for removal from state
|
|
676
|
-
submenuIndicesToRemove.push(i);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Remove the closed submenus from state
|
|
681
|
-
submenuIndicesToRemove.forEach(index => {
|
|
682
|
-
state.activeSubmenus.splice(index, 1);
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
// Update active submenu references based on what's left
|
|
686
|
-
if (state.activeSubmenus.length > 0) {
|
|
687
|
-
const deepestRemaining = state.activeSubmenus[state.activeSubmenus.length - 1];
|
|
688
|
-
state.activeSubmenu = deepestRemaining.element;
|
|
689
|
-
state.activeSubmenuItem = deepestRemaining.menuItem;
|
|
690
|
-
state.submenuLevel = deepestRemaining.level;
|
|
691
|
-
} else {
|
|
692
|
-
state.activeSubmenu = null;
|
|
693
|
-
state.activeSubmenuItem = null;
|
|
694
|
-
state.submenuLevel = 0;
|
|
695
|
-
}
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Closes all submenus
|
|
700
|
-
*/
|
|
701
|
-
const closeSubmenu = (): void => {
|
|
702
|
-
// Clear timers
|
|
703
|
-
clearHoverIntent();
|
|
704
|
-
clearSubmenuTimer();
|
|
705
|
-
|
|
706
|
-
if (state.activeSubmenus.length === 0) return;
|
|
707
|
-
|
|
708
|
-
// Close all active submenus
|
|
709
|
-
[...state.activeSubmenus].forEach(submenu => {
|
|
710
|
-
// Remove expanded state from parent item
|
|
711
|
-
if (submenu.menuItem) {
|
|
712
|
-
submenu.menuItem.setAttribute('aria-expanded', 'false');
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Remove submenu element with animation
|
|
716
|
-
submenu.element.classList.remove(`${component.getClass('menu--visible')}`);
|
|
717
|
-
|
|
718
|
-
// Remove after animation
|
|
719
|
-
setTimeout(() => {
|
|
720
|
-
if (submenu.element.parentNode) {
|
|
721
|
-
submenu.element.parentNode.removeChild(submenu.element);
|
|
722
|
-
}
|
|
723
|
-
}, 200);
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
// Clear state
|
|
727
|
-
state.activeSubmenu = null;
|
|
728
|
-
state.activeSubmenuItem = null;
|
|
729
|
-
state.activeSubmenus = [];
|
|
730
|
-
state.submenuLevel = 0;
|
|
731
|
-
|
|
732
|
-
// Remove document events
|
|
733
|
-
document.removeEventListener('click', handleDocumentClickForSubmenu);
|
|
734
|
-
window.removeEventListener('resize', handleWindowResizeForSubmenu);
|
|
735
|
-
window.removeEventListener('scroll', handleWindowScrollForSubmenu);
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
/**
|
|
739
|
-
* Handles document click for submenu
|
|
740
|
-
*/
|
|
741
|
-
const handleDocumentClickForSubmenu = (e: MouseEvent): void => {
|
|
742
|
-
if (!state.activeSubmenu) return;
|
|
743
|
-
|
|
744
|
-
const submenuElement = state.activeSubmenu;
|
|
745
|
-
const menuItemElement = state.activeSubmenuItem;
|
|
746
|
-
|
|
747
|
-
// Check if click was inside submenu or parent menu item
|
|
748
|
-
if (submenuElement.contains(e.target as Node) ||
|
|
749
|
-
(menuItemElement && menuItemElement.contains(e.target as Node))) {
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// Close submenu if clicked outside
|
|
754
|
-
closeSubmenu();
|
|
755
|
-
};
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Handles window resize for submenu
|
|
759
|
-
*/
|
|
760
|
-
const handleWindowResizeForSubmenu = (): void => {
|
|
761
|
-
// Reposition open submenu on resize
|
|
762
|
-
if (state.activeSubmenu && state.activeSubmenuItem) {
|
|
763
|
-
positioner.positionSubmenu(state.activeSubmenu, state.activeSubmenuItem, state.submenuLevel);
|
|
764
|
-
}
|
|
765
|
-
};
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* Handles window scroll for submenu
|
|
769
|
-
* Repositions the submenu to stay attached to its parent during scrolling
|
|
770
|
-
*/
|
|
771
|
-
const handleWindowScrollForSubmenu = (): void => {
|
|
772
|
-
// Use requestAnimationFrame to optimize scroll performance
|
|
773
|
-
window.requestAnimationFrame(() => {
|
|
774
|
-
// Only reposition if we have an active submenu
|
|
775
|
-
if (state.activeSubmenu && state.activeSubmenuItem) {
|
|
776
|
-
positioner.positionSubmenu(state.activeSubmenu, state.activeSubmenuItem, state.submenuLevel);
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
};
|
|
780
|
-
|
|
781
|
-
/**
|
|
782
|
-
* Opens the menu
|
|
783
|
-
* @param {Event} [event] - Optional event that triggered the open
|
|
784
|
-
* @param {'mouse'|'keyboard'} [interactionType='mouse'] - Type of interaction that triggered the open
|
|
785
|
-
*/
|
|
786
|
-
const openMenu = (event?: Event, interactionType: 'mouse' | 'keyboard' = 'mouse'): void => {
|
|
787
|
-
if (state.visible) return;
|
|
788
|
-
|
|
789
|
-
// Set keyboard navigation state based on interaction type
|
|
790
|
-
state.keyboardNavActive = interactionType === 'keyboard';
|
|
791
|
-
|
|
792
|
-
// Update state
|
|
793
|
-
state.visible = true;
|
|
794
|
-
|
|
795
|
-
// First, remove any existing document click listener
|
|
796
|
-
document.removeEventListener('click', handleDocumentClick);
|
|
797
|
-
|
|
798
|
-
// Step 1: Add the menu to the DOM if it's not already there with initial hidden state
|
|
799
|
-
if (!component.element.parentNode) {
|
|
800
|
-
// Apply explicit initial styling to ensure it doesn't flash
|
|
801
|
-
component.element.classList.remove(`${component.getClass('menu--visible')}`);
|
|
802
|
-
component.element.setAttribute('aria-hidden', 'true');
|
|
803
|
-
component.element.style.transform = 'scaleY(0)';
|
|
804
|
-
component.element.style.opacity = '0';
|
|
805
|
-
|
|
806
|
-
// Add to DOM
|
|
807
|
-
document.body.appendChild(component.element);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Step 2: Position the menu (will be invisible)
|
|
811
|
-
const anchorElement = getAnchorElement();
|
|
812
|
-
if (anchorElement) {
|
|
813
|
-
positioner.positionMenu(anchorElement);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// Step 3: Use a small delay to ensure DOM operations are complete
|
|
817
|
-
setTimeout(() => {
|
|
818
|
-
// Set attributes for accessibility
|
|
819
|
-
component.element.setAttribute('aria-hidden', 'false');
|
|
820
|
-
|
|
821
|
-
// Remove the inline styles we added
|
|
822
|
-
component.element.style.transform = '';
|
|
823
|
-
component.element.style.opacity = '';
|
|
824
|
-
|
|
825
|
-
// Force a reflow before adding the visible class
|
|
826
|
-
void component.element.getBoundingClientRect();
|
|
827
|
-
|
|
828
|
-
// Add visible class to start the CSS transition
|
|
829
|
-
component.element.classList.add(`${component.getClass('menu--visible')}`);
|
|
830
|
-
|
|
831
|
-
// Step 4: Focus based on interaction type (after animation starts)
|
|
832
|
-
setTimeout(() => {
|
|
833
|
-
handleFocus(interactionType);
|
|
834
|
-
}, 100);
|
|
835
|
-
|
|
836
|
-
// Add the document click handler on the next event loop
|
|
837
|
-
// after the current click is fully processed
|
|
838
|
-
setTimeout(() => {
|
|
839
|
-
if (config.closeOnClickOutside && state.visible) {
|
|
840
|
-
document.addEventListener('click', handleDocumentClick);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Add other document events normally
|
|
844
|
-
if (config.closeOnEscape) {
|
|
845
|
-
document.addEventListener('keydown', handleDocumentKeydown);
|
|
846
|
-
}
|
|
847
|
-
window.addEventListener('resize', handleWindowResize, { passive: true });
|
|
848
|
-
window.addEventListener('scroll', handleWindowScroll, { passive: true });
|
|
849
|
-
}, 0);
|
|
850
|
-
}, 20); // Short delay for browser to process
|
|
851
|
-
|
|
852
|
-
// Trigger event
|
|
853
|
-
eventHelpers.triggerEvent('open', {}, event);
|
|
854
|
-
};
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Closes the menu
|
|
858
|
-
* @param {Event} [event] - Optional event that triggered the close
|
|
859
|
-
* @param {boolean} [restoreFocus=true] - Whether to restore focus to the anchor element
|
|
860
|
-
* @param {boolean} [skipAnimation=false] - Whether to skip animation (for focus changes)
|
|
861
|
-
*/
|
|
862
|
-
const closeMenu = (event?: Event, restoreFocus: boolean = true, skipAnimation: boolean = false): void => {
|
|
863
|
-
if (!state.visible) return;
|
|
864
|
-
|
|
865
|
-
// Check if we're in a tab navigation - if so, don't restore focus
|
|
866
|
-
const isTabNavigation = document.body.hasAttribute('data-menu-tab-navigation');
|
|
867
|
-
if (isTabNavigation) {
|
|
868
|
-
restoreFocus = false;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// Reset keyboard navigation state on close
|
|
872
|
-
state.keyboardNavActive = false;
|
|
873
|
-
|
|
874
|
-
// Close any open submenu first
|
|
875
|
-
closeSubmenu();
|
|
876
|
-
|
|
877
|
-
// Update state
|
|
878
|
-
state.visible = false;
|
|
879
|
-
|
|
880
|
-
// Set attributes
|
|
881
|
-
component.element.setAttribute('aria-hidden', 'true');
|
|
882
|
-
component.element.classList.remove(`${component.getClass('menu--visible')}`);
|
|
883
|
-
|
|
884
|
-
// Store anchor reference before potentially removing the menu
|
|
885
|
-
const anchorElement = getAnchorElement();
|
|
886
|
-
|
|
887
|
-
// Remove document events
|
|
888
|
-
document.removeEventListener('click', handleDocumentClick);
|
|
889
|
-
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
890
|
-
window.removeEventListener('resize', handleWindowResize);
|
|
891
|
-
window.removeEventListener('scroll', handleWindowScroll);
|
|
892
|
-
|
|
893
|
-
// Trigger event with added data
|
|
894
|
-
eventHelpers.triggerEvent('close', {
|
|
895
|
-
isFocusRelated: event instanceof FocusEvent,
|
|
896
|
-
shouldRestoreFocus: restoreFocus,
|
|
897
|
-
isTabNavigation: isTabNavigation || event?.key === 'Tab'
|
|
898
|
-
}, event);
|
|
899
|
-
|
|
900
|
-
// Determine animation duration - for tab navigation we want to close immediately
|
|
901
|
-
const animationDuration = skipAnimation ? 0 : 300;
|
|
902
|
-
|
|
903
|
-
// Remove from DOM after animation completes (or immediately if skipAnimation)
|
|
904
|
-
setTimeout(() => {
|
|
905
|
-
if (component.element.parentNode && !state.visible) {
|
|
906
|
-
component.element.parentNode.removeChild(component.element);
|
|
907
|
-
|
|
908
|
-
// Only restore focus if explicitly requested AND not in tab navigation
|
|
909
|
-
if (restoreFocus && anchorElement && !isTabNavigation && event?.type !== 'click') {
|
|
910
|
-
// Additional check to make sure we're not in an ongoing tab navigation
|
|
911
|
-
if (!document.body.hasAttribute('data-menu-tab-navigation')) {
|
|
912
|
-
requestAnimationFrame(() => {
|
|
913
|
-
anchorElement.focus();
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}, animationDuration);
|
|
919
|
-
};
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* Toggles the menu
|
|
923
|
-
*/
|
|
924
|
-
const toggleMenu = (event?: Event, interactionType: 'mouse' | 'keyboard' = 'mouse'): void => {
|
|
925
|
-
if (state.visible) {
|
|
926
|
-
closeMenu(event);
|
|
927
|
-
} else {
|
|
928
|
-
// Determine interaction type from event
|
|
929
|
-
if (event) {
|
|
930
|
-
if (event instanceof KeyboardEvent) {
|
|
931
|
-
interactionType = 'keyboard';
|
|
932
|
-
} else if (event instanceof MouseEvent) {
|
|
933
|
-
interactionType = 'mouse';
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
openMenu(event, interactionType);
|
|
937
|
-
}
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* Updates the selected state of menu items
|
|
942
|
-
* @param itemId - The ID of the item to mark as selected, or null to clear selection
|
|
943
|
-
*/
|
|
944
|
-
const updateSelectedState = (itemId: string | null): void => {
|
|
945
|
-
if (!component.element) return;
|
|
946
|
-
|
|
947
|
-
// Get all menu items
|
|
948
|
-
const menuItems = component.element.querySelectorAll(`.${component.getClass('menu-item')}`) as NodeListOf<HTMLElement>;
|
|
949
|
-
|
|
950
|
-
// Update selected state for each item
|
|
951
|
-
menuItems.forEach(item => {
|
|
952
|
-
const currentItemId = item.getAttribute('data-id');
|
|
953
|
-
|
|
954
|
-
if (currentItemId === itemId) {
|
|
955
|
-
item.classList.add(`${component.getClass('menu-item--selected')}`);
|
|
956
|
-
item.setAttribute('aria-selected', 'true');
|
|
957
|
-
} else {
|
|
958
|
-
item.classList.remove(`${component.getClass('menu-item--selected')}`);
|
|
959
|
-
item.setAttribute('aria-selected', 'false');
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
// Also update state
|
|
964
|
-
state.selectedItemId = itemId;
|
|
965
|
-
};
|
|
966
|
-
|
|
967
|
-
/**
|
|
968
|
-
* Handles document click
|
|
969
|
-
*/
|
|
970
|
-
const handleDocumentClick = (e: MouseEvent): void => {
|
|
971
|
-
// If we should ignore this click (happens right after opening), reset the flag and return
|
|
972
|
-
if (ignoreNextDocumentClick) {
|
|
973
|
-
ignoreNextDocumentClick = false;
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// Don't close if clicked inside menu
|
|
978
|
-
if (component.element.contains(e.target as Node)) {
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// Check if clicked on anchor element
|
|
983
|
-
const anchor = getAnchorElement();
|
|
984
|
-
if (anchor && anchor.contains(e.target as Node)) {
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// Close menu
|
|
989
|
-
closeMenu(e);
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
/**
|
|
993
|
-
* Handles document keydown
|
|
994
|
-
*/
|
|
995
|
-
const handleDocumentKeydown = (e: KeyboardEvent): void => {
|
|
996
|
-
if (e.key === 'Escape') {
|
|
997
|
-
// When closing with Escape, always restore focus
|
|
998
|
-
closeMenu(e, true);
|
|
999
|
-
}
|
|
1000
|
-
};
|
|
1001
|
-
|
|
1002
|
-
/**
|
|
1003
|
-
* Handles window resize
|
|
1004
|
-
*/
|
|
1005
|
-
const handleWindowResize = (): void => {
|
|
1006
|
-
if (state.visible) {
|
|
1007
|
-
const anchorElement = getAnchorElement();
|
|
1008
|
-
if (anchorElement) {
|
|
1009
|
-
positioner.positionMenu(anchorElement);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
};
|
|
1013
|
-
|
|
1014
|
-
/**
|
|
1015
|
-
* Handles window scroll
|
|
1016
|
-
* Repositions the menu to stay attached to its anchor during scrolling
|
|
1017
|
-
*/
|
|
1018
|
-
const handleWindowScroll = (): void => {
|
|
1019
|
-
if (state.visible) {
|
|
1020
|
-
// Use requestAnimationFrame to optimize scroll performance
|
|
1021
|
-
window.requestAnimationFrame(() => {
|
|
1022
|
-
// Reposition the main menu to stay attached to anchor when scrolling
|
|
1023
|
-
const anchorElement = getAnchorElement();
|
|
1024
|
-
if (anchorElement) {
|
|
1025
|
-
positioner.positionMenu(anchorElement);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// Also reposition any open submenu relative to its parent menu item
|
|
1029
|
-
if (state.activeSubmenu && state.activeSubmenuItem) {
|
|
1030
|
-
positioner.positionSubmenu(state.activeSubmenu, state.activeSubmenuItem, state.submenuLevel);
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Handles keydown events on the menu or submenu
|
|
1038
|
-
*/
|
|
1039
|
-
const handleMenuKeydown = (e: KeyboardEvent): void => {
|
|
1040
|
-
// Set keyboard navigation active flag
|
|
1041
|
-
state.keyboardNavActive = true;
|
|
1042
|
-
|
|
1043
|
-
// Determine if this event is from the main menu or a submenu
|
|
1044
|
-
const isSubmenu = state.activeSubmenu && state.activeSubmenu.contains(e.target as Node);
|
|
1045
|
-
|
|
1046
|
-
// Get the appropriate menu element
|
|
1047
|
-
const menuElement = isSubmenu ? state.activeSubmenu : component.element;
|
|
1048
|
-
|
|
1049
|
-
// Get all non-disabled menu items from the current menu
|
|
1050
|
-
const items = Array.from(menuElement.querySelectorAll(
|
|
1051
|
-
`.${component.getClass('menu-item')}:not(.${component.getClass('menu-item--disabled')})`
|
|
1052
|
-
)) as HTMLElement[];
|
|
1053
|
-
|
|
1054
|
-
if (items.length === 0) return;
|
|
1055
|
-
|
|
1056
|
-
// Get the currently focused item index
|
|
1057
|
-
let focusedItemIndex = -1;
|
|
1058
|
-
const focusedElement = menuElement.querySelector(':focus') as HTMLElement;
|
|
1059
|
-
if (focusedElement && focusedElement.classList.contains(component.getClass('menu-item'))) {
|
|
1060
|
-
focusedItemIndex = items.indexOf(focusedElement);
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// Function to update tabindex and focus a specific item
|
|
1064
|
-
const focusItem = (index: number) => {
|
|
1065
|
-
// Set all items to tabindex -1
|
|
1066
|
-
items.forEach(item => item.setAttribute('tabindex', '-1'));
|
|
1067
|
-
|
|
1068
|
-
// Set the target item to tabindex 0 and focus it
|
|
1069
|
-
items[index].setAttribute('tabindex', '0');
|
|
1070
|
-
items[index].focus();
|
|
1071
|
-
};
|
|
1072
|
-
|
|
1073
|
-
switch (e.key) {
|
|
1074
|
-
case 'ArrowDown':
|
|
1075
|
-
case 'Down':
|
|
1076
|
-
e.preventDefault();
|
|
1077
|
-
// If no item is active, select the first one
|
|
1078
|
-
if (focusedItemIndex < 0) {
|
|
1079
|
-
focusItem(0);
|
|
1080
|
-
} else if (focusedItemIndex < items.length - 1) {
|
|
1081
|
-
focusItem(focusedItemIndex + 1);
|
|
1082
|
-
} else {
|
|
1083
|
-
// Wrap to first item
|
|
1084
|
-
focusItem(0);
|
|
1085
|
-
}
|
|
1086
|
-
break;
|
|
1087
|
-
|
|
1088
|
-
case 'ArrowUp':
|
|
1089
|
-
case 'Up':
|
|
1090
|
-
e.preventDefault();
|
|
1091
|
-
// If no item is active, select the last one
|
|
1092
|
-
if (focusedItemIndex < 0) {
|
|
1093
|
-
focusItem(items.length - 1);
|
|
1094
|
-
} else if (focusedItemIndex > 0) {
|
|
1095
|
-
focusItem(focusedItemIndex - 1);
|
|
1096
|
-
} else {
|
|
1097
|
-
// Wrap to last item
|
|
1098
|
-
focusItem(items.length - 1);
|
|
1099
|
-
}
|
|
1100
|
-
break;
|
|
1101
|
-
|
|
1102
|
-
case 'Home':
|
|
1103
|
-
e.preventDefault();
|
|
1104
|
-
focusItem(0);
|
|
1105
|
-
break;
|
|
1106
|
-
|
|
1107
|
-
case 'End':
|
|
1108
|
-
e.preventDefault();
|
|
1109
|
-
focusItem(items.length - 1);
|
|
1110
|
-
break;
|
|
1111
|
-
|
|
1112
|
-
case 'Enter':
|
|
1113
|
-
case ' ':
|
|
1114
|
-
e.preventDefault();
|
|
1115
|
-
// If an item is focused, click it
|
|
1116
|
-
if (focusedItemIndex >= 0) {
|
|
1117
|
-
items[focusedItemIndex].click();
|
|
1118
|
-
}
|
|
1119
|
-
break;
|
|
1120
|
-
|
|
1121
|
-
case 'ArrowRight':
|
|
1122
|
-
case 'Right':
|
|
1123
|
-
e.preventDefault();
|
|
1124
|
-
// Handle right arrow in different contexts
|
|
1125
|
-
if (isSubmenu) {
|
|
1126
|
-
// In a submenu, right arrow opens nested submenus
|
|
1127
|
-
if (focusedItemIndex >= 0 && items[focusedItemIndex].classList.contains(`${component.getClass('menu-item--submenu')}`)) {
|
|
1128
|
-
// Simulate click but specifying it's via keyboard
|
|
1129
|
-
const itemElement = items[focusedItemIndex];
|
|
1130
|
-
const itemIndex = parseInt(itemElement.getAttribute('data-index'), 10);
|
|
1131
|
-
|
|
1132
|
-
// Get the parent submenu to find the correct data
|
|
1133
|
-
const parentMenu = itemElement.closest(`.${component.getClass('menu--submenu')}`);
|
|
1134
|
-
const parentItemId = parentMenu?.getAttribute('data-parent-item');
|
|
1135
|
-
|
|
1136
|
-
// Find the parent item in the items array to get its submenu
|
|
1137
|
-
const parentItem = findItemById(parentItemId);
|
|
1138
|
-
if (parentItem && parentItem.submenu) {
|
|
1139
|
-
const itemData = parentItem.submenu[itemIndex] as MenuItem;
|
|
1140
|
-
handleNestedSubmenuClick(itemData, itemIndex, itemElement, true);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
} else {
|
|
1144
|
-
// In main menu, right arrow opens a submenu
|
|
1145
|
-
if (focusedItemIndex >= 0 && items[focusedItemIndex].classList.contains(`${component.getClass('menu-item--submenu')}`)) {
|
|
1146
|
-
// Get the correct menu item data
|
|
1147
|
-
const itemElement = items[focusedItemIndex];
|
|
1148
|
-
const itemIndex = parseInt(itemElement.getAttribute('data-index'), 10);
|
|
1149
|
-
const itemData = state.items[itemIndex] as MenuItem;
|
|
1150
|
-
|
|
1151
|
-
// Open submenu via keyboard
|
|
1152
|
-
handleSubmenuClick(itemData, itemIndex, itemElement, true);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
break;
|
|
1156
|
-
|
|
1157
|
-
case 'ArrowLeft':
|
|
1158
|
-
case 'Left':
|
|
1159
|
-
e.preventDefault();
|
|
1160
|
-
// Handle left arrow in different contexts
|
|
1161
|
-
if (isSubmenu) {
|
|
1162
|
-
// In a submenu, left arrow returns to the parent menu
|
|
1163
|
-
if (state.activeSubmenuItem) {
|
|
1164
|
-
// Store the reference to the parent item before closing the submenu
|
|
1165
|
-
const parentItem = state.activeSubmenuItem;
|
|
1166
|
-
|
|
1167
|
-
// Get the current level
|
|
1168
|
-
const currentLevel = parseInt(
|
|
1169
|
-
menuElement.getAttribute('data-level') || '1',
|
|
1170
|
-
10
|
|
1171
|
-
);
|
|
1172
|
-
|
|
1173
|
-
// Close this level of submenu
|
|
1174
|
-
closeSubmenuAtLevel(currentLevel);
|
|
1175
|
-
|
|
1176
|
-
// Focus the parent item after closing
|
|
1177
|
-
if (parentItem) {
|
|
1178
|
-
parentItem.setAttribute('tabindex', '0');
|
|
1179
|
-
parentItem.focus();
|
|
1180
|
-
}
|
|
1181
|
-
} else {
|
|
1182
|
-
closeSubmenu();
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
break;
|
|
1186
|
-
|
|
1187
|
-
case 'Escape':
|
|
1188
|
-
e.preventDefault();
|
|
1189
|
-
if (isSubmenu) {
|
|
1190
|
-
// In a submenu, Escape closes just the submenu
|
|
1191
|
-
if (state.activeSubmenuItem) {
|
|
1192
|
-
// Store the reference to the parent item before closing the submenu
|
|
1193
|
-
const parentItem = state.activeSubmenuItem;
|
|
1194
|
-
|
|
1195
|
-
// Get the current level
|
|
1196
|
-
const currentLevel = parseInt(
|
|
1197
|
-
menuElement.getAttribute('data-level') || '1',
|
|
1198
|
-
10
|
|
1199
|
-
);
|
|
1200
|
-
|
|
1201
|
-
// Close this level of submenu
|
|
1202
|
-
closeSubmenuAtLevel(currentLevel);
|
|
1203
|
-
|
|
1204
|
-
// Focus the parent item after closing
|
|
1205
|
-
if (parentItem) {
|
|
1206
|
-
parentItem.setAttribute('tabindex', '0');
|
|
1207
|
-
parentItem.focus();
|
|
1208
|
-
}
|
|
1209
|
-
} else {
|
|
1210
|
-
closeSubmenu();
|
|
1211
|
-
}
|
|
1212
|
-
} else {
|
|
1213
|
-
// In main menu, Escape closes the entire menu and restores focus to anchor
|
|
1214
|
-
closeMenu(e, true);
|
|
1215
|
-
}
|
|
1216
|
-
break;
|
|
1217
|
-
|
|
1218
|
-
case 'Tab':
|
|
1219
|
-
// Modified Tab handling - we want to close the menu and move focus to the next focusable element
|
|
1220
|
-
e.preventDefault(); // Prevent default tab behavior
|
|
1221
|
-
|
|
1222
|
-
// Find the focusable elements before closing the menu
|
|
1223
|
-
const focusableElements = getFocusableElements();
|
|
1224
|
-
const anchorElement = getAnchorElement();
|
|
1225
|
-
const anchorIndex = anchorElement ? focusableElements.indexOf(anchorElement) : -1;
|
|
1226
|
-
|
|
1227
|
-
// Calculate the next element to focus
|
|
1228
|
-
let nextElementIndex = -1;
|
|
1229
|
-
if (anchorIndex >= 0) {
|
|
1230
|
-
nextElementIndex = e.shiftKey ?
|
|
1231
|
-
// For Shift+Tab, go to previous element or last element if we're at the start
|
|
1232
|
-
(anchorIndex > 0 ? anchorIndex - 1 : focusableElements.length - 1) :
|
|
1233
|
-
// For Tab, go to next element or first element if we're at the end
|
|
1234
|
-
(anchorIndex < focusableElements.length - 1 ? anchorIndex + 1 : 0);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
// Store the next element to focus before closing the menu
|
|
1238
|
-
const nextElementToFocus = nextElementIndex >= 0 ? focusableElements[nextElementIndex] : null;
|
|
1239
|
-
|
|
1240
|
-
// Create a flag that prevents focus restoration
|
|
1241
|
-
const tabNavigationInProgress = true;
|
|
1242
|
-
|
|
1243
|
-
// Close the menu with focus restoration explicitly disabled
|
|
1244
|
-
closeMenu(e, false, true);
|
|
1245
|
-
|
|
1246
|
-
// Focus the next element if found, with a slight delay to ensure menu is closed
|
|
1247
|
-
if (nextElementToFocus) {
|
|
1248
|
-
// Use setTimeout with a very small delay to ensure this happens after all other operations
|
|
1249
|
-
setTimeout(() => {
|
|
1250
|
-
// Set a flag to prevent any other focus management from interfering
|
|
1251
|
-
document.body.setAttribute('data-menu-tab-navigation', 'true');
|
|
1252
|
-
|
|
1253
|
-
// Focus the element
|
|
1254
|
-
nextElementToFocus.focus();
|
|
1255
|
-
|
|
1256
|
-
// Remove the flag after focus is set
|
|
1257
|
-
setTimeout(() => {
|
|
1258
|
-
document.body.removeAttribute('data-menu-tab-navigation');
|
|
1259
|
-
}, 100);
|
|
1260
|
-
}, 10);
|
|
1261
|
-
}
|
|
1262
|
-
break;
|
|
1263
|
-
}
|
|
1264
|
-
};
|
|
1265
|
-
|
|
1266
|
-
/**
|
|
1267
|
-
* Gets all focusable elements in the document
|
|
1268
|
-
* Useful for Tab navigation management
|
|
1269
|
-
*/
|
|
1270
|
-
const getFocusableElements = (): HTMLElement[] => {
|
|
1271
|
-
// Query all potentially focusable elements
|
|
1272
|
-
const focusableElementsString = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
1273
|
-
const elements = document.querySelectorAll(focusableElementsString) as NodeListOf<HTMLElement>;
|
|
1274
|
-
|
|
1275
|
-
// Convert to array and filter out hidden elements
|
|
1276
|
-
return Array.from(elements).filter(element => {
|
|
1277
|
-
return element.offsetParent !== null && !element.classList.contains('hidden');
|
|
1278
|
-
});
|
|
1279
|
-
};
|
|
1280
|
-
|
|
1281
|
-
/**
|
|
1282
|
-
* Find a menu item by its ID in the items array
|
|
1283
|
-
*/
|
|
1284
|
-
const findItemById = (id: string): MenuItem | null => {
|
|
1285
|
-
// Search in top-level items
|
|
1286
|
-
for (const item of state.items) {
|
|
1287
|
-
if ('id' in item && item.id === id) {
|
|
1288
|
-
return item as MenuItem;
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
// Search in submenu items
|
|
1292
|
-
if ('submenu' in item && Array.isArray((item as MenuItem).submenu)) {
|
|
1293
|
-
for (const subItem of (item as MenuItem).submenu) {
|
|
1294
|
-
if ('id' in subItem && subItem.id === id) {
|
|
1295
|
-
return subItem as MenuItem;
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
return null;
|
|
1302
|
-
};
|
|
1303
|
-
|
|
1304
|
-
/**
|
|
1305
|
-
* Sets up the menu
|
|
1306
|
-
*/
|
|
1307
|
-
const initMenu = () => {
|
|
1308
|
-
// Set up menu structure
|
|
1309
|
-
renderMenuItems();
|
|
1310
|
-
|
|
1311
|
-
// Set up keyboard navigation
|
|
1312
|
-
component.element.addEventListener('keydown', handleMenuKeydown);
|
|
1313
|
-
|
|
1314
|
-
// Position if visible
|
|
1315
|
-
if (state.visible) {
|
|
1316
|
-
const anchorElement = getAnchorElement();
|
|
1317
|
-
if (anchorElement) {
|
|
1318
|
-
positioner.positionMenu(anchorElement);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
// Show immediately
|
|
1322
|
-
component.element.classList.add(`${component.getClass('menu--visible')}`);
|
|
1323
|
-
|
|
1324
|
-
// Set up document events
|
|
1325
|
-
if (config.closeOnClickOutside) {
|
|
1326
|
-
document.addEventListener('click', handleDocumentClick);
|
|
1327
|
-
}
|
|
1328
|
-
if (config.closeOnEscape) {
|
|
1329
|
-
document.addEventListener('keydown', handleDocumentKeydown);
|
|
1330
|
-
}
|
|
1331
|
-
window.addEventListener('resize', handleWindowResize);
|
|
1332
|
-
window.addEventListener('scroll', handleWindowScroll);
|
|
1333
|
-
}
|
|
1334
|
-
};
|
|
1335
|
-
|
|
1336
|
-
// Initialize after DOM is ready
|
|
1337
|
-
setTimeout(initMenu, 0);
|
|
1338
|
-
|
|
1339
|
-
// Register with lifecycle if available
|
|
1340
|
-
if (component.lifecycle) {
|
|
1341
|
-
const originalDestroy = component.lifecycle.destroy || (() => {});
|
|
1342
|
-
component.lifecycle.destroy = () => {
|
|
1343
|
-
// Clean up timers
|
|
1344
|
-
clearHoverIntent();
|
|
1345
|
-
clearSubmenuTimer();
|
|
1346
|
-
|
|
1347
|
-
// Clean up document events
|
|
1348
|
-
document.removeEventListener('click', handleDocumentClick);
|
|
1349
|
-
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
1350
|
-
window.removeEventListener('resize', handleWindowResize);
|
|
1351
|
-
window.removeEventListener('scroll', handleWindowScroll);
|
|
1352
|
-
|
|
1353
|
-
// Clean up submenu events
|
|
1354
|
-
document.removeEventListener('click', handleDocumentClickForSubmenu);
|
|
1355
|
-
window.removeEventListener('resize', handleWindowResizeForSubmenu);
|
|
1356
|
-
window.removeEventListener('scroll', handleWindowScrollForSubmenu);
|
|
1357
|
-
|
|
1358
|
-
// Clean up submenu element
|
|
1359
|
-
if (state.activeSubmenus.length > 0) {
|
|
1360
|
-
state.activeSubmenus.forEach(submenu => {
|
|
1361
|
-
if (submenu.element.parentNode) {
|
|
1362
|
-
submenu.element.parentNode.removeChild(submenu.element);
|
|
1363
|
-
}
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
originalDestroy();
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// Return enhanced component
|
|
1372
|
-
return {
|
|
1373
|
-
...component,
|
|
1374
|
-
menu: {
|
|
1375
|
-
open: (event, interactionType = 'mouse') => {
|
|
1376
|
-
openMenu(event, interactionType);
|
|
1377
|
-
return component;
|
|
1378
|
-
},
|
|
1379
|
-
|
|
1380
|
-
close: (event, restoreFocus = true, skipAnimation = false) => {
|
|
1381
|
-
closeMenu(event, restoreFocus, skipAnimation);
|
|
1382
|
-
return component;
|
|
1383
|
-
},
|
|
1384
|
-
|
|
1385
|
-
toggle: (event, interactionType = 'mouse') => {
|
|
1386
|
-
toggleMenu(event, interactionType);
|
|
1387
|
-
return component;
|
|
1388
|
-
},
|
|
1389
|
-
|
|
1390
|
-
isOpen: () => state.visible,
|
|
1391
|
-
|
|
1392
|
-
setItems: (items) => {
|
|
1393
|
-
state.items = items;
|
|
1394
|
-
renderMenuItems();
|
|
1395
|
-
return component;
|
|
1396
|
-
},
|
|
1397
|
-
|
|
1398
|
-
getItems: () => state.items,
|
|
1399
|
-
|
|
1400
|
-
setPosition: (position) => {
|
|
1401
|
-
state.position = position;
|
|
1402
|
-
if (state.visible) {
|
|
1403
|
-
const anchorElement = getAnchorElement();
|
|
1404
|
-
if (anchorElement) {
|
|
1405
|
-
positioner.positionMenu(anchorElement);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
return component;
|
|
1409
|
-
},
|
|
1410
|
-
|
|
1411
|
-
getPosition: () => state.position,
|
|
1412
|
-
|
|
1413
|
-
setSelected: (itemId: string | null) => {
|
|
1414
|
-
updateSelectedState(itemId);
|
|
1415
|
-
return component;
|
|
1416
|
-
},
|
|
1417
|
-
|
|
1418
|
-
getSelected: () => state.selectedItemId
|
|
1419
|
-
}
|
|
1420
|
-
};
|
|
1421
|
-
};
|
|
1422
|
-
|
|
1423
|
-
export default withController;
|