kiso 0.1.0.pre → 0.2.0.pre
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -2
- data/README.md +67 -27
- data/Rakefile +8 -0
- data/app/assets/tailwind/kiso/checkbox.css +18 -0
- data/app/assets/tailwind/kiso/color-mode.css +9 -0
- data/app/assets/tailwind/kiso/dashboard.css +194 -0
- data/app/assets/tailwind/kiso/engine.css +117 -0
- data/app/assets/tailwind/kiso/input-otp.css +10 -0
- data/app/assets/tailwind/kiso/radio-group.css +17 -0
- data/app/helpers/kiso/component_helper.rb +46 -27
- data/app/helpers/kiso/icon_helper.rb +53 -9
- data/app/helpers/kiso/theme_helper.rb +38 -0
- data/app/javascript/controllers/kiso/combobox_controller.js +616 -0
- data/app/javascript/controllers/kiso/command_controller.js +184 -0
- data/app/javascript/controllers/kiso/command_dialog_controller.js +104 -0
- data/app/javascript/controllers/kiso/dropdown_menu_controller.js +684 -0
- data/app/javascript/controllers/kiso/index.d.ts +12 -0
- data/app/javascript/controllers/kiso/index.js +42 -0
- data/app/javascript/controllers/kiso/input_otp_controller.js +195 -0
- data/app/javascript/controllers/kiso/popover_controller.js +254 -0
- data/app/javascript/controllers/kiso/select_controller.js +307 -0
- data/app/javascript/controllers/kiso/sidebar_controller.js +84 -0
- data/app/javascript/controllers/kiso/theme_controller.js +89 -0
- data/app/javascript/controllers/kiso/toggle_controller.js +24 -0
- data/app/javascript/controllers/kiso/toggle_group_controller.js +128 -0
- data/app/javascript/kiso/utils/focusable.js +8 -0
- data/app/javascript/kiso/utils/highlight.js +43 -0
- data/app/javascript/kiso/utils/positioning.js +86 -0
- data/app/javascript/kiso/vendor/floating-ui-core.js +1 -0
- data/app/javascript/kiso/vendor/floating-ui-dom.js +1 -0
- data/app/views/kiso/components/_alert.html.erb +1 -1
- data/app/views/kiso/components/_avatar.html.erb +23 -0
- data/app/views/kiso/components/_badge.html.erb +1 -1
- data/app/views/kiso/components/_breadcrumb.html.erb +8 -0
- data/app/views/kiso/components/_button.html.erb +1 -1
- data/app/views/kiso/components/_card.html.erb +1 -1
- data/app/views/kiso/components/_checkbox.html.erb +7 -0
- data/app/views/kiso/components/_color_mode_button.html.erb +14 -0
- data/app/views/kiso/components/_color_mode_select.html.erb +24 -0
- data/app/views/kiso/components/_combobox.html.erb +12 -0
- data/app/views/kiso/components/_command.html.erb +7 -0
- data/app/views/kiso/components/_dashboard_group.html.erb +14 -0
- data/app/views/kiso/components/_dashboard_navbar.html.erb +7 -0
- data/app/views/kiso/components/_dashboard_panel.html.erb +7 -0
- data/app/views/kiso/components/_dashboard_sidebar.html.erb +11 -0
- data/app/views/kiso/components/_dashboard_toolbar.html.erb +7 -0
- data/app/views/kiso/components/_dropdown_menu.html.erb +7 -0
- data/app/views/kiso/components/{_empty_state.html.erb → _empty.html.erb} +2 -2
- data/app/views/kiso/components/_field.html.erb +12 -0
- data/app/views/kiso/components/_field_group.html.erb +7 -0
- data/app/views/kiso/components/_field_set.html.erb +7 -0
- data/app/views/kiso/components/_input.html.erb +8 -0
- data/app/views/kiso/components/_input_group.html.erb +8 -0
- data/app/views/kiso/components/_input_otp.html.erb +22 -0
- data/app/views/kiso/components/_kbd.html.erb +7 -0
- data/app/views/kiso/components/_label.html.erb +5 -0
- data/app/views/kiso/components/_nav.html.erb +7 -0
- data/app/views/kiso/components/_pagination.html.erb +9 -0
- data/app/views/kiso/components/_popover.html.erb +8 -0
- data/app/views/kiso/components/_radio_group.html.erb +8 -0
- data/app/views/kiso/components/_select.html.erb +8 -0
- data/app/views/kiso/components/_select_native.html.erb +16 -0
- data/app/views/kiso/components/_separator.html.erb +1 -1
- data/app/views/kiso/components/_stats_card.html.erb +1 -1
- data/app/views/kiso/components/_stats_grid.html.erb +1 -1
- data/app/views/kiso/components/_switch.html.erb +10 -0
- data/app/views/kiso/components/_table.html.erb +2 -1
- data/app/views/kiso/components/_textarea.html.erb +9 -0
- data/app/views/kiso/components/_toggle.html.erb +12 -0
- data/app/views/kiso/components/_toggle_group.html.erb +12 -0
- data/app/views/kiso/components/alert/_description.html.erb +1 -1
- data/app/views/kiso/components/alert/_title.html.erb +1 -1
- data/app/views/kiso/components/avatar/_badge.html.erb +7 -0
- data/app/views/kiso/components/avatar/_fallback.html.erb +7 -0
- data/app/views/kiso/components/avatar/_group.html.erb +7 -0
- data/app/views/kiso/components/avatar/_group_count.html.erb +7 -0
- data/app/views/kiso/components/avatar/_image.html.erb +6 -0
- data/app/views/kiso/components/breadcrumb/_ellipsis.html.erb +10 -0
- data/app/views/kiso/components/breadcrumb/_item.html.erb +7 -0
- data/app/views/kiso/components/breadcrumb/_link.html.erb +7 -0
- data/app/views/kiso/components/breadcrumb/_list.html.erb +7 -0
- data/app/views/kiso/components/breadcrumb/_page.html.erb +9 -0
- data/app/views/kiso/components/breadcrumb/_separator.html.erb +9 -0
- data/app/views/kiso/components/card/_action.html.erb +7 -0
- data/app/views/kiso/components/card/_content.html.erb +1 -1
- data/app/views/kiso/components/card/_description.html.erb +1 -1
- data/app/views/kiso/components/card/_footer.html.erb +1 -1
- data/app/views/kiso/components/card/_header.html.erb +1 -1
- data/app/views/kiso/components/card/_title.html.erb +1 -1
- data/app/views/kiso/components/combobox/_chip.html.erb +19 -0
- data/app/views/kiso/components/combobox/_chips.html.erb +20 -0
- data/app/views/kiso/components/combobox/_chips_input.html.erb +10 -0
- data/app/views/kiso/components/combobox/_content.html.erb +9 -0
- data/app/views/kiso/components/combobox/_empty.html.erb +9 -0
- data/app/views/kiso/components/combobox/_group.html.erb +8 -0
- data/app/views/kiso/components/combobox/_input.html.erb +23 -0
- data/app/views/kiso/components/combobox/_item.html.erb +19 -0
- data/app/views/kiso/components/combobox/_label.html.erb +7 -0
- data/app/views/kiso/components/combobox/_list.html.erb +10 -0
- data/app/views/kiso/components/combobox/_separator.html.erb +6 -0
- data/app/views/kiso/components/command/_dialog.html.erb +11 -0
- data/app/views/kiso/components/command/_empty.html.erb +9 -0
- data/app/views/kiso/components/command/_group.html.erb +14 -0
- data/app/views/kiso/components/command/_input.html.erb +16 -0
- data/app/views/kiso/components/command/_item.html.erb +13 -0
- data/app/views/kiso/components/command/_list.html.erb +10 -0
- data/app/views/kiso/components/command/_separator.html.erb +7 -0
- data/app/views/kiso/components/command/_shortcut.html.erb +7 -0
- data/app/views/kiso/components/dashboard_navbar/_toggle.html.erb +11 -0
- data/app/views/kiso/components/dashboard_sidebar/_collapse.html.erb +12 -0
- data/app/views/kiso/components/dashboard_sidebar/_footer.html.erb +7 -0
- data/app/views/kiso/components/dashboard_sidebar/_header.html.erb +7 -0
- data/app/views/kiso/components/dashboard_sidebar/_toggle.html.erb +11 -0
- data/app/views/kiso/components/dashboard_toolbar/_left.html.erb +7 -0
- data/app/views/kiso/components/dashboard_toolbar/_right.html.erb +7 -0
- data/app/views/kiso/components/dropdown_menu/_checkbox_item.html.erb +18 -0
- data/app/views/kiso/components/dropdown_menu/_content.html.erb +10 -0
- data/app/views/kiso/components/dropdown_menu/_group.html.erb +8 -0
- data/app/views/kiso/components/dropdown_menu/_item.html.erb +15 -0
- data/app/views/kiso/components/dropdown_menu/_label.html.erb +8 -0
- data/app/views/kiso/components/dropdown_menu/_radio_group.html.erb +10 -0
- data/app/views/kiso/components/dropdown_menu/_radio_item.html.erb +19 -0
- data/app/views/kiso/components/dropdown_menu/_separator.html.erb +6 -0
- data/app/views/kiso/components/dropdown_menu/_shortcut.html.erb +7 -0
- data/app/views/kiso/components/dropdown_menu/_sub.html.erb +8 -0
- data/app/views/kiso/components/dropdown_menu/_sub_content.html.erb +10 -0
- data/app/views/kiso/components/dropdown_menu/_sub_trigger.html.erb +12 -0
- data/app/views/kiso/components/dropdown_menu/_trigger.html.erb +9 -0
- data/app/views/kiso/components/empty/_content.html.erb +7 -0
- data/app/views/kiso/components/empty/_description.html.erb +7 -0
- data/app/views/kiso/components/empty/_header.html.erb +7 -0
- data/app/views/kiso/components/empty/_media.html.erb +7 -0
- data/app/views/kiso/components/empty/_title.html.erb +7 -0
- data/app/views/kiso/components/field/_content.html.erb +7 -0
- data/app/views/kiso/components/field/_description.html.erb +7 -0
- data/app/views/kiso/components/field/_error.html.erb +22 -0
- data/app/views/kiso/components/field/_label.html.erb +5 -0
- data/app/views/kiso/components/field/_separator.html.erb +15 -0
- data/app/views/kiso/components/field/_title.html.erb +7 -0
- data/app/views/kiso/components/field_set/_legend.html.erb +9 -0
- data/app/views/kiso/components/input_group/_addon.html.erb +7 -0
- data/app/views/kiso/components/input_otp/_group.html.erb +7 -0
- data/app/views/kiso/components/input_otp/_separator.html.erb +8 -0
- data/app/views/kiso/components/input_otp/_slot.html.erb +11 -0
- data/app/views/kiso/components/kbd/_group.html.erb +7 -0
- data/app/views/kiso/components/nav/_item.html.erb +15 -0
- data/app/views/kiso/components/nav/_section.html.erb +37 -0
- data/app/views/kiso/components/nav/_section_title.html.erb +7 -0
- data/app/views/kiso/components/pagination/_content.html.erb +7 -0
- data/app/views/kiso/components/pagination/_ellipsis.html.erb +9 -0
- data/app/views/kiso/components/pagination/_item.html.erb +7 -0
- data/app/views/kiso/components/pagination/_link.html.erb +9 -0
- data/app/views/kiso/components/pagination/_next.html.erb +12 -0
- data/app/views/kiso/components/pagination/_previous.html.erb +12 -0
- data/app/views/kiso/components/popover/_anchor.html.erb +8 -0
- data/app/views/kiso/components/popover/_content.html.erb +11 -0
- data/app/views/kiso/components/popover/_description.html.erb +7 -0
- data/app/views/kiso/components/popover/_header.html.erb +7 -0
- data/app/views/kiso/components/popover/_title.html.erb +7 -0
- data/app/views/kiso/components/popover/_trigger.html.erb +9 -0
- data/app/views/kiso/components/radio_group/_item.html.erb +6 -0
- data/app/views/kiso/components/select/_content.html.erb +10 -0
- data/app/views/kiso/components/select/_group.html.erb +8 -0
- data/app/views/kiso/components/select/_item.html.erb +19 -0
- data/app/views/kiso/components/select/_label.html.erb +7 -0
- data/app/views/kiso/components/select/_separator.html.erb +6 -0
- data/app/views/kiso/components/select/_trigger.html.erb +13 -0
- data/app/views/kiso/components/select/_value.html.erb +11 -0
- data/app/views/kiso/components/stats_card/_description.html.erb +1 -1
- data/app/views/kiso/components/stats_card/_header.html.erb +1 -1
- data/app/views/kiso/components/stats_card/_label.html.erb +1 -1
- data/app/views/kiso/components/stats_card/_value.html.erb +1 -1
- data/app/views/kiso/components/table/_body.html.erb +1 -1
- data/app/views/kiso/components/table/_caption.html.erb +1 -1
- data/app/views/kiso/components/table/_cell.html.erb +1 -1
- data/app/views/kiso/components/table/_footer.html.erb +1 -1
- data/app/views/kiso/components/table/_head.html.erb +1 -1
- data/app/views/kiso/components/table/_header.html.erb +1 -1
- data/app/views/kiso/components/table/_row.html.erb +1 -1
- data/app/views/kiso/components/toggle_group/_item.html.erb +13 -0
- data/config/deploy.docs.yml +31 -0
- data/config/deploy.yml +34 -0
- data/config/importmap.rb +10 -0
- data/lib/kiso/cli/base.rb +15 -0
- data/lib/kiso/cli/icons.rb +2 -1
- data/lib/kiso/cli/main.rb +6 -0
- data/lib/kiso/cli/make.rb +22 -12
- data/lib/kiso/configuration.rb +54 -0
- data/lib/kiso/engine.rb +36 -1
- data/lib/kiso/theme_overrides.rb +130 -0
- data/lib/kiso/themes/alert.rb +16 -1
- data/lib/kiso/themes/avatar.rb +53 -0
- data/lib/kiso/themes/badge.rb +15 -5
- data/lib/kiso/themes/breadcrumb.rb +44 -0
- data/lib/kiso/themes/button.rb +15 -2
- data/lib/kiso/themes/card.rb +18 -2
- data/lib/kiso/themes/checkbox.rb +33 -0
- data/lib/kiso/themes/color_mode_button.rb +15 -0
- data/lib/kiso/themes/color_mode_select.rb +7 -0
- data/lib/kiso/themes/combobox.rb +97 -0
- data/lib/kiso/themes/command.rb +79 -0
- data/lib/kiso/themes/dashboard.rb +51 -0
- data/lib/kiso/themes/dropdown_menu.rb +108 -0
- data/lib/kiso/themes/empty.rb +54 -0
- data/lib/kiso/themes/field.rb +76 -0
- data/lib/kiso/themes/field_group.rb +15 -0
- data/lib/kiso/themes/field_set.rb +32 -0
- data/lib/kiso/themes/input.rb +33 -0
- data/lib/kiso/themes/input_group.rb +39 -0
- data/lib/kiso/themes/input_otp.rb +46 -0
- data/lib/kiso/themes/kbd.rb +31 -0
- data/lib/kiso/themes/label.rb +16 -0
- data/lib/kiso/themes/nav.rb +27 -0
- data/lib/kiso/themes/pagination.rb +73 -0
- data/lib/kiso/themes/popover.rb +32 -0
- data/lib/kiso/themes/radio_group.rb +43 -0
- data/lib/kiso/themes/select.rb +78 -0
- data/lib/kiso/themes/select_native.rb +49 -0
- data/lib/kiso/themes/separator.rb +8 -2
- data/lib/kiso/themes/shared.rb +51 -0
- data/lib/kiso/themes/stats_card.rb +26 -14
- data/lib/kiso/themes/switch.rb +56 -0
- data/lib/kiso/themes/table.rb +18 -15
- data/lib/kiso/themes/textarea.rb +33 -0
- data/lib/kiso/themes/toggle.rb +71 -0
- data/lib/kiso/themes/toggle_group.rb +13 -0
- data/lib/kiso/version.rb +4 -1
- data/lib/kiso.rb +70 -2
- metadata +183 -22
- data/app/views/kiso/components/empty_state/_content.html.erb +0 -7
- data/app/views/kiso/components/empty_state/_description.html.erb +0 -7
- data/app/views/kiso/components/empty_state/_header.html.erb +0 -7
- data/app/views/kiso/components/empty_state/_media.html.erb +0 -7
- data/app/views/kiso/components/empty_state/_title.html.erb +0 -7
- data/lib/kiso/themes/empty_state.rb +0 -42
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { highlightItem, wrapIndex } from "kiso-ui/utils/highlight"
|
|
3
|
+
import { startPositioning } from "kiso-ui/utils/positioning"
|
|
4
|
+
|
|
5
|
+
/** @type {WeakMap<HTMLElement, {enterHandler: Function, leaveHandler: Function}>} */
|
|
6
|
+
const _subHandlers = new WeakMap()
|
|
7
|
+
|
|
8
|
+
/** @type {WeakMap<HTMLElement, Function>} */
|
|
9
|
+
const _subPositionCleanups = new WeakMap()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Dropdown menu with keyboard navigation, sub-menus, checkbox items, and radio items.
|
|
13
|
+
* Supports nested sub-menus with hover-to-open, type-ahead search, and full
|
|
14
|
+
* arrow-key navigation including ArrowRight/Left for sub-menu enter/exit.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <div data-controller="kiso--dropdown-menu" data-slot="dropdown-menu">
|
|
18
|
+
* <div data-kiso--dropdown-menu-target="trigger"
|
|
19
|
+
* data-action="click->kiso--dropdown-menu#toggle keydown->kiso--dropdown-menu#triggerKeydown">
|
|
20
|
+
* <button>Open Menu</button>
|
|
21
|
+
* </div>
|
|
22
|
+
* <div data-kiso--dropdown-menu-target="content" role="menu" hidden>
|
|
23
|
+
* <div data-kiso--dropdown-menu-target="item" data-slot="dropdown-menu-item"
|
|
24
|
+
* data-action="click->kiso--dropdown-menu#selectItem" role="menuitem">
|
|
25
|
+
* Profile
|
|
26
|
+
* </div>
|
|
27
|
+
* </div>
|
|
28
|
+
* </div>
|
|
29
|
+
*
|
|
30
|
+
* @property {HTMLElement} triggerTarget - Button that opens/closes the menu
|
|
31
|
+
* @property {HTMLElement} contentTarget - The dropdown panel (menu)
|
|
32
|
+
* @property {HTMLElement[]} itemTargets - Standard menu items (role="menuitem")
|
|
33
|
+
* @property {HTMLElement[]} checkboxItemTargets - Checkbox toggle items (role="menuitemcheckbox")
|
|
34
|
+
* @property {HTMLElement[]} radioGroupTargets - Radio group containers
|
|
35
|
+
* @property {HTMLElement[]} radioItemTargets - Radio selection items (role="menuitemradio")
|
|
36
|
+
* @property {HTMLElement[]} subTargets - Sub-menu wrappers (contain subTrigger + subContent)
|
|
37
|
+
* @property {HTMLElement[]} subTriggerTargets - Elements that open nested sub-menus
|
|
38
|
+
* @property {HTMLElement[]} subContentTargets - Nested sub-menu panels
|
|
39
|
+
*
|
|
40
|
+
* @fires kiso--dropdown-menu:select - When a standard item is selected. Detail: `{ item: HTMLElement }`.
|
|
41
|
+
* @fires kiso--dropdown-menu:checkbox-change - When a checkbox item is toggled. Detail: `{ item: HTMLElement, checked: boolean }`.
|
|
42
|
+
* @fires kiso--dropdown-menu:radio-change - When a radio item is selected. Detail: `{ item: HTMLElement, value: string }`.
|
|
43
|
+
*/
|
|
44
|
+
export default class extends Controller {
|
|
45
|
+
static targets = [
|
|
46
|
+
"trigger",
|
|
47
|
+
"content",
|
|
48
|
+
"item",
|
|
49
|
+
"checkboxItem",
|
|
50
|
+
"radioGroup",
|
|
51
|
+
"radioItem",
|
|
52
|
+
"sub",
|
|
53
|
+
"subTrigger",
|
|
54
|
+
"subContent",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
connect() {
|
|
58
|
+
this._open = false
|
|
59
|
+
this._handleOutsideClick = this._handleOutsideClick.bind(this)
|
|
60
|
+
this._handleKeydown = this._handleKeydown.bind(this)
|
|
61
|
+
this._handleMouseover = this._handleMouseover.bind(this)
|
|
62
|
+
this._closeSubTimeout = null
|
|
63
|
+
|
|
64
|
+
// Set ARIA attrs on the interactive element inside the trigger wrapper
|
|
65
|
+
this._triggerButton =
|
|
66
|
+
this.triggerTarget.querySelector("button, [tabindex]") || this.triggerTarget
|
|
67
|
+
this._triggerButton.setAttribute("aria-haspopup", "menu")
|
|
68
|
+
this._triggerButton.setAttribute("aria-expanded", "false")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
disconnect() {
|
|
72
|
+
this._cleanupPosition?.()
|
|
73
|
+
this._closeAllSubs()
|
|
74
|
+
this._removeGlobalListeners()
|
|
75
|
+
if (this._closeSubTimeout) {
|
|
76
|
+
clearTimeout(this._closeSubTimeout)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Toggles the dropdown menu open or closed.
|
|
82
|
+
*
|
|
83
|
+
* @param {Event} event
|
|
84
|
+
*/
|
|
85
|
+
toggle(event) {
|
|
86
|
+
event.preventDefault()
|
|
87
|
+
event.stopPropagation()
|
|
88
|
+
if (this._open) {
|
|
89
|
+
this.close()
|
|
90
|
+
} else {
|
|
91
|
+
this.open()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Opens the dropdown, positions it below the trigger, highlights the first
|
|
97
|
+
* item, and attaches mouse hover delegation.
|
|
98
|
+
*/
|
|
99
|
+
open() {
|
|
100
|
+
if (this._open) return
|
|
101
|
+
|
|
102
|
+
this._open = true
|
|
103
|
+
this.contentTarget.hidden = false
|
|
104
|
+
this._triggerButton.setAttribute("aria-expanded", "true")
|
|
105
|
+
this._positionContent()
|
|
106
|
+
this._addGlobalListeners()
|
|
107
|
+
this._highlightIndex(0)
|
|
108
|
+
|
|
109
|
+
// Mouse hover highlighting via event delegation
|
|
110
|
+
this.contentTarget.addEventListener("mouseover", this._handleMouseover)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Closes the dropdown, all sub-menus, removes listeners,
|
|
115
|
+
* and returns focus to the trigger.
|
|
116
|
+
*/
|
|
117
|
+
close() {
|
|
118
|
+
if (!this._open) return
|
|
119
|
+
|
|
120
|
+
this._cleanupPosition?.()
|
|
121
|
+
this._cleanupPosition = null
|
|
122
|
+
this._open = false
|
|
123
|
+
this._lastHoveredItem = null
|
|
124
|
+
this._closeAllSubs()
|
|
125
|
+
this.contentTarget.hidden = true
|
|
126
|
+
this._triggerButton.setAttribute("aria-expanded", "false")
|
|
127
|
+
this._highlightIndex(-1)
|
|
128
|
+
this._removeGlobalListeners()
|
|
129
|
+
this.contentTarget.removeEventListener("mouseover", this._handleMouseover)
|
|
130
|
+
|
|
131
|
+
if (this._closeSubTimeout) {
|
|
132
|
+
clearTimeout(this._closeSubTimeout)
|
|
133
|
+
this._closeSubTimeout = null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Focus the button inside the trigger wrapper, or the trigger itself
|
|
137
|
+
const btn = this.triggerTarget.querySelector("button, [tabindex]")
|
|
138
|
+
;(btn || this.triggerTarget).focus()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Dispatches a "select" event for a standard menu item and closes the menu.
|
|
143
|
+
*
|
|
144
|
+
* @param {Event} event - Click event from an item element
|
|
145
|
+
*/
|
|
146
|
+
selectItem(event) {
|
|
147
|
+
const item = event.currentTarget
|
|
148
|
+
if (item.dataset.disabled === "true") return
|
|
149
|
+
|
|
150
|
+
this.dispatch("select", { detail: { item } })
|
|
151
|
+
this.close()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Toggles a checkbox menu item's checked state and updates its indicator icon.
|
|
156
|
+
*
|
|
157
|
+
* @param {Event} event - Click event from a checkbox item element
|
|
158
|
+
*/
|
|
159
|
+
toggleCheckboxItem(event) {
|
|
160
|
+
const item = event.currentTarget
|
|
161
|
+
if (item.dataset.disabled === "true") return
|
|
162
|
+
|
|
163
|
+
const currentChecked = item.getAttribute("aria-checked") === "true"
|
|
164
|
+
const newChecked = !currentChecked
|
|
165
|
+
item.setAttribute("aria-checked", newChecked)
|
|
166
|
+
|
|
167
|
+
// Toggle the indicator visibility
|
|
168
|
+
const indicator = item.querySelector("[data-slot='dropdown-menu-item-indicator']")
|
|
169
|
+
if (indicator) {
|
|
170
|
+
indicator.hidden = !newChecked
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.dispatch("checkbox-change", {
|
|
174
|
+
detail: { item, checked: newChecked },
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Selects a radio item within its group, deselecting all siblings.
|
|
180
|
+
* Updates aria-checked and indicator icons.
|
|
181
|
+
*
|
|
182
|
+
* @param {Event} event - Click event from a radio item element
|
|
183
|
+
*/
|
|
184
|
+
selectRadioItem(event) {
|
|
185
|
+
const item = event.currentTarget
|
|
186
|
+
if (item.dataset.disabled === "true") return
|
|
187
|
+
|
|
188
|
+
const value = item.dataset.value
|
|
189
|
+
const group = item.closest("[data-slot='dropdown-menu-radio-group']")
|
|
190
|
+
|
|
191
|
+
if (group) {
|
|
192
|
+
// Deselect all radio items in this group
|
|
193
|
+
const radioItems = group.querySelectorAll("[data-slot='dropdown-menu-radio-item']")
|
|
194
|
+
radioItems.forEach((radio) => {
|
|
195
|
+
radio.setAttribute("aria-checked", "false")
|
|
196
|
+
const indicator = radio.querySelector("[data-slot='dropdown-menu-item-indicator']")
|
|
197
|
+
if (indicator) indicator.hidden = true
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// Select the clicked item
|
|
201
|
+
item.setAttribute("aria-checked", "true")
|
|
202
|
+
const indicator = item.querySelector("[data-slot='dropdown-menu-item-indicator']")
|
|
203
|
+
if (indicator) indicator.hidden = false
|
|
204
|
+
|
|
205
|
+
// Update group value
|
|
206
|
+
group.dataset.value = value
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.dispatch("radio-change", { detail: { item, value } })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Toggles a sub-menu open or closed when its trigger is clicked.
|
|
214
|
+
*
|
|
215
|
+
* @param {Event} event - Click event from a sub-trigger element
|
|
216
|
+
*/
|
|
217
|
+
toggleSub(event) {
|
|
218
|
+
event.stopPropagation()
|
|
219
|
+
const subTrigger = event.currentTarget
|
|
220
|
+
const sub = subTrigger.closest("[data-slot='dropdown-menu-sub']")
|
|
221
|
+
if (!sub) return
|
|
222
|
+
|
|
223
|
+
const subContent = sub.querySelector("[data-slot='dropdown-menu-sub-content']")
|
|
224
|
+
if (!subContent) return
|
|
225
|
+
|
|
226
|
+
if (subContent.hidden) {
|
|
227
|
+
this._openSub(sub, subTrigger, subContent)
|
|
228
|
+
} else {
|
|
229
|
+
this._closeSub(sub, subTrigger, subContent)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Opens a sub-menu on hover, closing sibling sub-menus first.
|
|
235
|
+
* Cancels any pending close timeout.
|
|
236
|
+
*
|
|
237
|
+
* @param {Event} event - Mouseenter event from a sub-trigger element
|
|
238
|
+
*/
|
|
239
|
+
openSubOnHover(event) {
|
|
240
|
+
if (this._closeSubTimeout) {
|
|
241
|
+
clearTimeout(this._closeSubTimeout)
|
|
242
|
+
this._closeSubTimeout = null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const subTrigger = event.currentTarget
|
|
246
|
+
const sub = subTrigger.closest("[data-slot='dropdown-menu-sub']")
|
|
247
|
+
if (!sub) return
|
|
248
|
+
|
|
249
|
+
const subContent = sub.querySelector("[data-slot='dropdown-menu-sub-content']")
|
|
250
|
+
if (!subContent || !subContent.hidden) return
|
|
251
|
+
|
|
252
|
+
// Close any other open sub-menus at the same level
|
|
253
|
+
const parent = sub.parentElement
|
|
254
|
+
if (parent) {
|
|
255
|
+
parent.querySelectorAll(":scope > [data-slot='dropdown-menu-sub']").forEach((otherSub) => {
|
|
256
|
+
if (otherSub !== sub) {
|
|
257
|
+
const otherContent = otherSub.querySelector("[data-slot='dropdown-menu-sub-content']")
|
|
258
|
+
const otherTrigger = otherSub.querySelector("[data-slot='dropdown-menu-sub-trigger']")
|
|
259
|
+
if (otherContent && !otherContent.hidden) {
|
|
260
|
+
this._closeSub(otherSub, otherTrigger, otherContent)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this._openSub(sub, subTrigger, subContent)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Opens the dropdown on ArrowDown, Space, Enter, or ArrowUp when
|
|
271
|
+
* the trigger is focused. ArrowUp highlights the last item.
|
|
272
|
+
*
|
|
273
|
+
* @param {KeyboardEvent} event
|
|
274
|
+
*/
|
|
275
|
+
triggerKeydown(event) {
|
|
276
|
+
switch (event.key) {
|
|
277
|
+
case "ArrowDown":
|
|
278
|
+
case " ":
|
|
279
|
+
case "Enter":
|
|
280
|
+
event.preventDefault()
|
|
281
|
+
if (!this._open) {
|
|
282
|
+
this.open()
|
|
283
|
+
}
|
|
284
|
+
break
|
|
285
|
+
case "ArrowUp":
|
|
286
|
+
event.preventDefault()
|
|
287
|
+
if (!this._open) {
|
|
288
|
+
this.open()
|
|
289
|
+
// Highlight last item
|
|
290
|
+
const items = this._allMenuItems(this.contentTarget)
|
|
291
|
+
this._highlightIndex(items.length - 1)
|
|
292
|
+
}
|
|
293
|
+
break
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- Private ---
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Opens a sub-menu, positions it, and attaches mouseenter/mouseleave
|
|
301
|
+
* listeners for auto-close with a delay for gap crossing.
|
|
302
|
+
*
|
|
303
|
+
* @param {HTMLElement} sub - The sub wrapper element
|
|
304
|
+
* @param {HTMLElement} subTrigger - The sub-trigger element
|
|
305
|
+
* @param {HTMLElement} subContent - The sub-content panel
|
|
306
|
+
* @private
|
|
307
|
+
*/
|
|
308
|
+
_openSub(sub, subTrigger, subContent) {
|
|
309
|
+
subContent.hidden = false
|
|
310
|
+
subTrigger.setAttribute("data-state", "open")
|
|
311
|
+
this._positionSubContent(subTrigger, subContent)
|
|
312
|
+
|
|
313
|
+
// Auto-close when mouse leaves sub-content (with delay for gap crossing)
|
|
314
|
+
const enterHandler = () => {
|
|
315
|
+
if (this._closeSubTimeout) {
|
|
316
|
+
clearTimeout(this._closeSubTimeout)
|
|
317
|
+
this._closeSubTimeout = null
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const leaveHandler = () => {
|
|
321
|
+
this._closeSubTimeout = setTimeout(() => {
|
|
322
|
+
if (!subContent.hidden) {
|
|
323
|
+
this._closeSub(sub, subTrigger, subContent)
|
|
324
|
+
}
|
|
325
|
+
}, 150)
|
|
326
|
+
}
|
|
327
|
+
_subHandlers.set(subContent, { enterHandler, leaveHandler })
|
|
328
|
+
subContent.addEventListener("mouseenter", enterHandler)
|
|
329
|
+
subContent.addEventListener("mouseleave", leaveHandler)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Closes a sub-menu, cleans up hover listeners, and recursively
|
|
334
|
+
* closes any nested sub-menus.
|
|
335
|
+
*
|
|
336
|
+
* @param {HTMLElement} sub - The sub wrapper element
|
|
337
|
+
* @param {HTMLElement} subTrigger - The sub-trigger element
|
|
338
|
+
* @param {HTMLElement} subContent - The sub-content panel
|
|
339
|
+
* @private
|
|
340
|
+
*/
|
|
341
|
+
_closeSub(sub, subTrigger, subContent) {
|
|
342
|
+
subContent.hidden = true
|
|
343
|
+
subTrigger.removeAttribute("data-state")
|
|
344
|
+
|
|
345
|
+
this._stopSubPositioning(subContent)
|
|
346
|
+
this._removeSubContentListeners(subContent)
|
|
347
|
+
|
|
348
|
+
// Close nested sub-menus recursively
|
|
349
|
+
subContent.querySelectorAll("[data-slot='dropdown-menu-sub-content']").forEach((nested) => {
|
|
350
|
+
nested.hidden = true
|
|
351
|
+
this._stopSubPositioning(nested)
|
|
352
|
+
this._removeSubContentListeners(nested)
|
|
353
|
+
})
|
|
354
|
+
subContent.querySelectorAll("[data-slot='dropdown-menu-sub-trigger']").forEach((nested) => {
|
|
355
|
+
nested.removeAttribute("data-state")
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Closes all open sub-menus and cleans up their listeners.
|
|
361
|
+
*
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
_closeAllSubs() {
|
|
365
|
+
this.subContentTargets.forEach((subContent) => {
|
|
366
|
+
subContent.hidden = true
|
|
367
|
+
this._stopSubPositioning(subContent)
|
|
368
|
+
this._removeSubContentListeners(subContent)
|
|
369
|
+
})
|
|
370
|
+
this.subTriggerTargets.forEach((subTrigger) => {
|
|
371
|
+
subTrigger.removeAttribute("data-state")
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Removes mouseenter/mouseleave handlers from a sub-content element.
|
|
377
|
+
*
|
|
378
|
+
* @param {HTMLElement} subContent
|
|
379
|
+
* @private
|
|
380
|
+
*/
|
|
381
|
+
_removeSubContentListeners(subContent) {
|
|
382
|
+
const handlers = _subHandlers.get(subContent)
|
|
383
|
+
if (handlers) {
|
|
384
|
+
subContent.removeEventListener("mouseenter", handlers.enterHandler)
|
|
385
|
+
subContent.removeEventListener("mouseleave", handlers.leaveHandler)
|
|
386
|
+
_subHandlers.delete(subContent)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Stops positioning for a sub-content element and removes its cleanup entry.
|
|
392
|
+
*
|
|
393
|
+
* @param {HTMLElement} subContent
|
|
394
|
+
* @private
|
|
395
|
+
*/
|
|
396
|
+
_stopSubPositioning(subContent) {
|
|
397
|
+
_subPositionCleanups.get(subContent)?.()
|
|
398
|
+
_subPositionCleanups.delete(subContent)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Collects all focusable menu items within a container, skipping hidden
|
|
403
|
+
* sub-content panels and disabled items. Walks the DOM tree recursively.
|
|
404
|
+
*
|
|
405
|
+
* @param {HTMLElement} container - The menu or sub-content container to search
|
|
406
|
+
* @returns {HTMLElement[]} Ordered list of focusable items
|
|
407
|
+
* @private
|
|
408
|
+
*/
|
|
409
|
+
_allMenuItems(container) {
|
|
410
|
+
const items = []
|
|
411
|
+
const walk = (el) => {
|
|
412
|
+
for (const child of el.children) {
|
|
413
|
+
const slot = child.dataset?.slot
|
|
414
|
+
// Skip hidden sub-content
|
|
415
|
+
if (slot === "dropdown-menu-sub-content" && child.hidden) {
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
if (
|
|
419
|
+
slot === "dropdown-menu-item" ||
|
|
420
|
+
slot === "dropdown-menu-checkbox-item" ||
|
|
421
|
+
slot === "dropdown-menu-radio-item" ||
|
|
422
|
+
slot === "dropdown-menu-sub-trigger"
|
|
423
|
+
) {
|
|
424
|
+
if (child.dataset.disabled !== "true") {
|
|
425
|
+
items.push(child)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Recurse into groups, subs, radio-groups, etc.
|
|
429
|
+
if (child.children && child.children.length > 0) {
|
|
430
|
+
walk(child)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
walk(container)
|
|
435
|
+
return items
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Highlights a menu item at the given index and scrolls it into view.
|
|
440
|
+
* Pass -1 to clear all highlights.
|
|
441
|
+
*
|
|
442
|
+
* @param {number} index - Index within all menu items, or -1 to clear
|
|
443
|
+
* @private
|
|
444
|
+
*/
|
|
445
|
+
_highlightIndex(index) {
|
|
446
|
+
const allItems = this._allMenuItems(this.contentTarget)
|
|
447
|
+
highlightItem(allItems, allItems, index)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Positions the dropdown content relative to the trigger.
|
|
452
|
+
* Starts auto-updating on scroll/resize.
|
|
453
|
+
*
|
|
454
|
+
* @private
|
|
455
|
+
*/
|
|
456
|
+
_positionContent() {
|
|
457
|
+
this._cleanupPosition = startPositioning(this.triggerTarget, this.contentTarget)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Positions a sub-content panel to the right of its trigger using
|
|
462
|
+
* Floating UI with fixed positioning to escape parent overflow clipping.
|
|
463
|
+
*
|
|
464
|
+
* @param {HTMLElement} subTrigger - The sub-trigger element
|
|
465
|
+
* @param {HTMLElement} subContent - The sub-content panel to position
|
|
466
|
+
* @private
|
|
467
|
+
*/
|
|
468
|
+
_positionSubContent(subTrigger, subContent) {
|
|
469
|
+
const cleanup = startPositioning(subTrigger, subContent, {
|
|
470
|
+
placement: "right-start",
|
|
471
|
+
strategy: "fixed",
|
|
472
|
+
})
|
|
473
|
+
_subPositionCleanups.set(subContent, cleanup)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Handles mouseover events via delegation on the content panel.
|
|
478
|
+
* Highlights hovered items and closes sibling sub-menus when hovering
|
|
479
|
+
* non-sub-trigger items.
|
|
480
|
+
*
|
|
481
|
+
* @param {MouseEvent} event
|
|
482
|
+
* @private
|
|
483
|
+
*/
|
|
484
|
+
_handleMouseover(event) {
|
|
485
|
+
const item = event.target.closest(
|
|
486
|
+
"[data-slot='dropdown-menu-item'], " +
|
|
487
|
+
"[data-slot='dropdown-menu-checkbox-item'], " +
|
|
488
|
+
"[data-slot='dropdown-menu-radio-item'], " +
|
|
489
|
+
"[data-slot='dropdown-menu-sub-trigger']",
|
|
490
|
+
)
|
|
491
|
+
if (!item || !this.element.contains(item)) return
|
|
492
|
+
if (item.dataset.disabled === "true") return
|
|
493
|
+
if (item === this._lastHoveredItem) return
|
|
494
|
+
this._lastHoveredItem = item
|
|
495
|
+
|
|
496
|
+
this._clearAllHighlights()
|
|
497
|
+
item.setAttribute("data-highlighted", "")
|
|
498
|
+
|
|
499
|
+
// When hovering a regular item, close open subs at the same level
|
|
500
|
+
if (item.dataset.slot !== "dropdown-menu-sub-trigger") {
|
|
501
|
+
const parentContainer = item.closest(
|
|
502
|
+
"[data-slot='dropdown-menu-content'], [data-slot='dropdown-menu-sub-content']",
|
|
503
|
+
)
|
|
504
|
+
if (parentContainer) {
|
|
505
|
+
parentContainer.querySelectorAll("[data-slot='dropdown-menu-sub']").forEach((sub) => {
|
|
506
|
+
// Only close subs whose nearest content ancestor is this container
|
|
507
|
+
if (
|
|
508
|
+
sub.closest(
|
|
509
|
+
"[data-slot='dropdown-menu-content'], [data-slot='dropdown-menu-sub-content']",
|
|
510
|
+
) === parentContainer
|
|
511
|
+
) {
|
|
512
|
+
const sc = sub.querySelector("[data-slot='dropdown-menu-sub-content']")
|
|
513
|
+
const st = sub.querySelector("[data-slot='dropdown-menu-sub-trigger']")
|
|
514
|
+
if (sc && !sc.hidden) {
|
|
515
|
+
this._closeSub(sub, st, sc)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Closes the dropdown when clicking outside the component,
|
|
525
|
+
* including outside any open fixed-positioned sub-content.
|
|
526
|
+
*
|
|
527
|
+
* @param {MouseEvent} event
|
|
528
|
+
* @private
|
|
529
|
+
*/
|
|
530
|
+
_handleOutsideClick(event) {
|
|
531
|
+
// Check both the root element and any open fixed-positioned sub-contents
|
|
532
|
+
if (this.element.contains(event.target)) return
|
|
533
|
+
for (const subContent of this.subContentTargets) {
|
|
534
|
+
if (!subContent.hidden && subContent.contains(event.target)) return
|
|
535
|
+
}
|
|
536
|
+
this.close()
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Handles keyboard navigation while the dropdown is open.
|
|
541
|
+
* Operates on the deepest open sub-menu container.
|
|
542
|
+
* Supports ArrowDown/Up, ArrowRight (enter sub), ArrowLeft (exit sub),
|
|
543
|
+
* Enter/Space (activate), Escape, Home, End, Tab, and type-ahead.
|
|
544
|
+
*
|
|
545
|
+
* @param {KeyboardEvent} event
|
|
546
|
+
* @private
|
|
547
|
+
*/
|
|
548
|
+
_handleKeydown(event) {
|
|
549
|
+
if (!this._open) return
|
|
550
|
+
|
|
551
|
+
// Find the currently active sub-content (deepest open sub)
|
|
552
|
+
let activeContainer = this.contentTarget
|
|
553
|
+
const openSubs = Array.from(
|
|
554
|
+
this.element.querySelectorAll("[data-slot='dropdown-menu-sub-content']:not([hidden])"),
|
|
555
|
+
)
|
|
556
|
+
if (openSubs.length > 0) {
|
|
557
|
+
activeContainer = openSubs[openSubs.length - 1]
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const items = this._allMenuItems(activeContainer)
|
|
561
|
+
|
|
562
|
+
// Find current highlighted in active container
|
|
563
|
+
let currentIndex = items.findIndex((item) => item.hasAttribute("data-highlighted"))
|
|
564
|
+
|
|
565
|
+
switch (event.key) {
|
|
566
|
+
case "ArrowDown":
|
|
567
|
+
event.preventDefault()
|
|
568
|
+
highlightItem(items, items, wrapIndex(currentIndex, 1, items.length))
|
|
569
|
+
break
|
|
570
|
+
case "ArrowUp":
|
|
571
|
+
event.preventDefault()
|
|
572
|
+
highlightItem(items, items, wrapIndex(currentIndex, -1, items.length))
|
|
573
|
+
break
|
|
574
|
+
case "ArrowRight":
|
|
575
|
+
event.preventDefault()
|
|
576
|
+
// If highlighted item is a sub-trigger, open it
|
|
577
|
+
if (currentIndex >= 0) {
|
|
578
|
+
const current = items[currentIndex]
|
|
579
|
+
if (current.dataset.slot === "dropdown-menu-sub-trigger") {
|
|
580
|
+
const sub = current.closest("[data-slot='dropdown-menu-sub']")
|
|
581
|
+
const subContent = sub?.querySelector("[data-slot='dropdown-menu-sub-content']")
|
|
582
|
+
if (sub && subContent && subContent.hidden) {
|
|
583
|
+
this._openSub(sub, current, subContent)
|
|
584
|
+
// Highlight first item in sub
|
|
585
|
+
const subItems = this._allMenuItems(subContent)
|
|
586
|
+
this._clearAllHighlights()
|
|
587
|
+
if (subItems[0]) {
|
|
588
|
+
subItems[0].setAttribute("data-highlighted", "")
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
break
|
|
594
|
+
case "ArrowLeft":
|
|
595
|
+
event.preventDefault()
|
|
596
|
+
// Close the current sub-menu if we're in one
|
|
597
|
+
if (activeContainer !== this.contentTarget) {
|
|
598
|
+
const sub = activeContainer.closest("[data-slot='dropdown-menu-sub']")
|
|
599
|
+
const subTrigger = sub?.querySelector("[data-slot='dropdown-menu-sub-trigger']")
|
|
600
|
+
if (sub && subTrigger) {
|
|
601
|
+
this._closeSub(sub, subTrigger, activeContainer)
|
|
602
|
+
this._clearAllHighlights()
|
|
603
|
+
subTrigger.setAttribute("data-highlighted", "")
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
break
|
|
607
|
+
case "Enter":
|
|
608
|
+
case " ":
|
|
609
|
+
event.preventDefault()
|
|
610
|
+
if (currentIndex >= 0 && currentIndex < items.length) {
|
|
611
|
+
const current = items[currentIndex]
|
|
612
|
+
// Trigger click on the highlighted item
|
|
613
|
+
current.click()
|
|
614
|
+
}
|
|
615
|
+
break
|
|
616
|
+
case "Escape":
|
|
617
|
+
event.preventDefault()
|
|
618
|
+
// If in a sub-menu, close just that sub
|
|
619
|
+
if (activeContainer !== this.contentTarget) {
|
|
620
|
+
const sub = activeContainer.closest("[data-slot='dropdown-menu-sub']")
|
|
621
|
+
const subTrigger = sub?.querySelector("[data-slot='dropdown-menu-sub-trigger']")
|
|
622
|
+
if (sub && subTrigger) {
|
|
623
|
+
this._closeSub(sub, subTrigger, activeContainer)
|
|
624
|
+
this._clearAllHighlights()
|
|
625
|
+
subTrigger.setAttribute("data-highlighted", "")
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
this.close()
|
|
629
|
+
}
|
|
630
|
+
break
|
|
631
|
+
case "Home":
|
|
632
|
+
event.preventDefault()
|
|
633
|
+
highlightItem(items, items, 0)
|
|
634
|
+
break
|
|
635
|
+
case "End":
|
|
636
|
+
event.preventDefault()
|
|
637
|
+
highlightItem(items, items, items.length - 1)
|
|
638
|
+
break
|
|
639
|
+
case "Tab":
|
|
640
|
+
this.close()
|
|
641
|
+
break
|
|
642
|
+
default:
|
|
643
|
+
// Type-ahead
|
|
644
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
645
|
+
const char = event.key.toLowerCase()
|
|
646
|
+
const startIndex = currentIndex + 1
|
|
647
|
+
for (let i = 0; i < items.length; i++) {
|
|
648
|
+
const idx = (startIndex + i) % items.length
|
|
649
|
+
const text = items[idx].textContent?.trim().toLowerCase()
|
|
650
|
+
if (text?.startsWith(char)) {
|
|
651
|
+
this._clearAllHighlights()
|
|
652
|
+
items[idx].setAttribute("data-highlighted", "")
|
|
653
|
+
items[idx].scrollIntoView({ block: "nearest" })
|
|
654
|
+
break
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
break
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Removes `data-highlighted` from all elements in the dropdown.
|
|
664
|
+
*
|
|
665
|
+
* @private
|
|
666
|
+
*/
|
|
667
|
+
_clearAllHighlights() {
|
|
668
|
+
this.element
|
|
669
|
+
.querySelectorAll("[data-highlighted]")
|
|
670
|
+
.forEach((el) => el.removeAttribute("data-highlighted"))
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** @private */
|
|
674
|
+
_addGlobalListeners() {
|
|
675
|
+
document.addEventListener("click", this._handleOutsideClick, true)
|
|
676
|
+
document.addEventListener("keydown", this._handleKeydown)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** @private */
|
|
680
|
+
_removeGlobalListeners() {
|
|
681
|
+
document.removeEventListener("click", this._handleOutsideClick, true)
|
|
682
|
+
document.removeEventListener("keydown", this._handleKeydown)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Application, Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
declare const KisoUi: {
|
|
4
|
+
start(application: Application): void
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default KisoUi
|
|
8
|
+
export const KisoComboboxController: typeof Controller
|
|
9
|
+
export const KisoDropdownMenuController: typeof Controller
|
|
10
|
+
export const KisoSelectController: typeof Controller
|
|
11
|
+
export const KisoToggleController: typeof Controller
|
|
12
|
+
export const KisoToggleGroupController: typeof Controller
|