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,307 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Custom select dropdown with keyboard navigation and form integration.
|
|
7
|
+
* Renders a trigger button, hidden listbox, and syncs selection to a hidden input.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* <div data-controller="kiso--select" data-slot="select">
|
|
11
|
+
* <button data-kiso--select-target="trigger"
|
|
12
|
+
* data-action="click->kiso--select#toggle keydown->kiso--select#triggerKeydown">
|
|
13
|
+
* <span data-kiso--select-target="valueDisplay" data-placeholder="Pick one...">
|
|
14
|
+
* <span class="text-muted-foreground">Pick one...</span>
|
|
15
|
+
* </span>
|
|
16
|
+
* </button>
|
|
17
|
+
* <div data-kiso--select-target="content" role="listbox" hidden>
|
|
18
|
+
* <div data-kiso--select-target="item" data-value="apple"
|
|
19
|
+
* data-action="click->kiso--select#selectItem" role="option">
|
|
20
|
+
* <span data-kiso--select-target="indicator" hidden>✓</span>
|
|
21
|
+
* <span>Apple</span>
|
|
22
|
+
* </div>
|
|
23
|
+
* </div>
|
|
24
|
+
* <input type="hidden" data-kiso--select-target="hiddenInput" name="fruit">
|
|
25
|
+
* </div>
|
|
26
|
+
*
|
|
27
|
+
* @property {HTMLElement} triggerTarget - Button that opens/closes the dropdown
|
|
28
|
+
* @property {HTMLElement} contentTarget - The dropdown panel (listbox)
|
|
29
|
+
* @property {HTMLElement[]} itemTargets - Selectable option elements
|
|
30
|
+
* @property {HTMLElement[]} indicatorTargets - Checkmark indicators inside items
|
|
31
|
+
* @property {HTMLInputElement} hiddenInputTarget - Hidden input for form submission
|
|
32
|
+
* @property {HTMLElement} valueDisplayTarget - Span showing the current selection text
|
|
33
|
+
*
|
|
34
|
+
* @fires kiso--select:change - When selection changes. Detail: `{ value: string }`.
|
|
35
|
+
*/
|
|
36
|
+
export default class extends Controller {
|
|
37
|
+
static targets = ["trigger", "content", "item", "indicator", "hiddenInput", "valueDisplay"]
|
|
38
|
+
|
|
39
|
+
connect() {
|
|
40
|
+
this._open = false
|
|
41
|
+
this._highlightedIndex = -1
|
|
42
|
+
this._handleOutsideClick = this._handleOutsideClick.bind(this)
|
|
43
|
+
this._handleKeydown = this._handleKeydown.bind(this)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
disconnect() {
|
|
47
|
+
this._cleanupPosition?.()
|
|
48
|
+
this._removeGlobalListeners()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Toggles the dropdown open or closed.
|
|
53
|
+
* Skips if the click was auto-generated by a Space keyup (already handled in triggerKeydown).
|
|
54
|
+
*
|
|
55
|
+
* @param {Event} event
|
|
56
|
+
*/
|
|
57
|
+
toggle(event) {
|
|
58
|
+
if (this._ignoreNextClick) {
|
|
59
|
+
this._ignoreNextClick = false
|
|
60
|
+
event.preventDefault()
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
event.preventDefault()
|
|
64
|
+
if (this._open) {
|
|
65
|
+
this.close()
|
|
66
|
+
} else {
|
|
67
|
+
this.open()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Opens the dropdown, positions it, and highlights the selected or first item. */
|
|
72
|
+
open() {
|
|
73
|
+
if (this._open || this.triggerTarget.disabled) return
|
|
74
|
+
|
|
75
|
+
this._open = true
|
|
76
|
+
this.contentTarget.hidden = false
|
|
77
|
+
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
78
|
+
this._positionContent()
|
|
79
|
+
this._addGlobalListeners()
|
|
80
|
+
|
|
81
|
+
// Highlight the currently selected item, or the first item
|
|
82
|
+
const selectedIndex = this._enabledItems.findIndex(
|
|
83
|
+
(item) => item.getAttribute("aria-selected") === "true",
|
|
84
|
+
)
|
|
85
|
+
this._highlightIndex(selectedIndex >= 0 ? selectedIndex : 0)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Closes the dropdown and returns focus to the trigger. */
|
|
89
|
+
close() {
|
|
90
|
+
if (!this._open) return
|
|
91
|
+
|
|
92
|
+
this._cleanupPosition?.()
|
|
93
|
+
this._cleanupPosition = null
|
|
94
|
+
this._open = false
|
|
95
|
+
this.contentTarget.hidden = true
|
|
96
|
+
this.triggerTarget.setAttribute("aria-expanded", "false")
|
|
97
|
+
this._highlightIndex(-1)
|
|
98
|
+
this._removeGlobalListeners()
|
|
99
|
+
this.triggerTarget.focus()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Selects an item when clicked.
|
|
104
|
+
*
|
|
105
|
+
* @param {Event} event - Click event from an item element
|
|
106
|
+
*/
|
|
107
|
+
selectItem(event) {
|
|
108
|
+
const item = event.currentTarget
|
|
109
|
+
if (item.dataset.disabled === "true") return
|
|
110
|
+
|
|
111
|
+
const value = item.dataset.value
|
|
112
|
+
const text = item.querySelector("[data-slot='select-item-text']")?.textContent?.trim() || value
|
|
113
|
+
|
|
114
|
+
this._setValue(value, text)
|
|
115
|
+
this.close()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Opens the dropdown on ArrowDown, ArrowUp, Space, or Enter when trigger is focused.
|
|
120
|
+
*
|
|
121
|
+
* @param {KeyboardEvent} event
|
|
122
|
+
*/
|
|
123
|
+
triggerKeydown(event) {
|
|
124
|
+
switch (event.key) {
|
|
125
|
+
case "ArrowDown":
|
|
126
|
+
case "ArrowUp":
|
|
127
|
+
case " ":
|
|
128
|
+
case "Enter":
|
|
129
|
+
event.preventDefault()
|
|
130
|
+
if (!this._open) {
|
|
131
|
+
// Stop propagation so the global _handleKeydown (added during open())
|
|
132
|
+
// does not also process this same keydown event — otherwise Space/Enter
|
|
133
|
+
// would immediately select the highlighted item and close the dropdown.
|
|
134
|
+
event.stopPropagation()
|
|
135
|
+
this._ignoreNextClick = true
|
|
136
|
+
this.open()
|
|
137
|
+
}
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Private ---
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Updates the hidden input, display text, aria-selected states, and indicators.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} value - The selected value
|
|
148
|
+
* @param {string} text - The display text for the selection
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
_setValue(value, text) {
|
|
152
|
+
// Update hidden input
|
|
153
|
+
if (this.hasHiddenInputTarget) {
|
|
154
|
+
this.hiddenInputTarget.value = value
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update displayed value
|
|
158
|
+
if (this.hasValueDisplayTarget) {
|
|
159
|
+
this.valueDisplayTarget.textContent = text
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Update aria-selected on items and show/hide indicators
|
|
163
|
+
this.itemTargets.forEach((item) => {
|
|
164
|
+
const isSelected = item.dataset.value === value
|
|
165
|
+
item.setAttribute("aria-selected", isSelected)
|
|
166
|
+
|
|
167
|
+
// Find the indicator within this item
|
|
168
|
+
const indicator = item.querySelector("[data-slot='select-item-indicator']")
|
|
169
|
+
if (indicator) {
|
|
170
|
+
indicator.hidden = !isSelected
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Dispatch change event
|
|
175
|
+
this.dispatch("change", { detail: { value } })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Returns items that are not disabled.
|
|
180
|
+
*
|
|
181
|
+
* @returns {HTMLElement[]}
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
get _enabledItems() {
|
|
185
|
+
return this.itemTargets.filter((item) => item.dataset.disabled !== "true")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Highlights an item at the given index and scrolls it into view.
|
|
190
|
+
* Pass -1 to clear all highlights.
|
|
191
|
+
*
|
|
192
|
+
* @param {number} index - Index within enabled items, or -1 to clear
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
_highlightIndex(index) {
|
|
196
|
+
this._highlightedIndex = index
|
|
197
|
+
highlightItem(this.itemTargets, this._enabledItems, index)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Positions the dropdown relative to the trigger with matching width.
|
|
202
|
+
* Starts auto-updating on scroll/resize.
|
|
203
|
+
*
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
_positionContent() {
|
|
207
|
+
this._cleanupPosition = startPositioning(this.triggerTarget, this.contentTarget, {
|
|
208
|
+
matchWidth: true,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Closes the dropdown when clicking outside the component.
|
|
214
|
+
*
|
|
215
|
+
* @param {MouseEvent} event
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
_handleOutsideClick(event) {
|
|
219
|
+
if (!this.element.contains(event.target)) {
|
|
220
|
+
this.close()
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Handles keyboard navigation while the dropdown is open.
|
|
226
|
+
* Supports ArrowDown/Up, Enter, Space, Escape, Home, End, Tab, and type-ahead.
|
|
227
|
+
*
|
|
228
|
+
* @param {KeyboardEvent} event
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
_handleKeydown(event) {
|
|
232
|
+
if (!this._open) return
|
|
233
|
+
|
|
234
|
+
const items = this._enabledItems
|
|
235
|
+
|
|
236
|
+
switch (event.key) {
|
|
237
|
+
case "ArrowDown":
|
|
238
|
+
event.preventDefault()
|
|
239
|
+
this._highlightIndex(wrapIndex(this._highlightedIndex, 1, items.length))
|
|
240
|
+
break
|
|
241
|
+
case "ArrowUp":
|
|
242
|
+
event.preventDefault()
|
|
243
|
+
this._highlightIndex(wrapIndex(this._highlightedIndex, -1, items.length))
|
|
244
|
+
break
|
|
245
|
+
case "Enter":
|
|
246
|
+
case " ":
|
|
247
|
+
event.preventDefault()
|
|
248
|
+
if (this._highlightedIndex >= 0 && this._highlightedIndex < items.length) {
|
|
249
|
+
const item = items[this._highlightedIndex]
|
|
250
|
+
if (item.dataset.disabled !== "true") {
|
|
251
|
+
const value = item.dataset.value
|
|
252
|
+
const text =
|
|
253
|
+
item.querySelector("[data-slot='select-item-text']")?.textContent?.trim() || value
|
|
254
|
+
this._setValue(value, text)
|
|
255
|
+
this.close()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
break
|
|
259
|
+
case "Escape":
|
|
260
|
+
event.preventDefault()
|
|
261
|
+
this.close()
|
|
262
|
+
break
|
|
263
|
+
case "Home":
|
|
264
|
+
event.preventDefault()
|
|
265
|
+
this._highlightIndex(0)
|
|
266
|
+
break
|
|
267
|
+
case "End":
|
|
268
|
+
event.preventDefault()
|
|
269
|
+
this._highlightIndex(items.length - 1)
|
|
270
|
+
break
|
|
271
|
+
case "Tab":
|
|
272
|
+
this.close()
|
|
273
|
+
break
|
|
274
|
+
default:
|
|
275
|
+
// Type-ahead: focus first item starting with typed character
|
|
276
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
277
|
+
const char = event.key.toLowerCase()
|
|
278
|
+
const startIndex = this._highlightedIndex + 1
|
|
279
|
+
const matchIndex = items.findIndex((item, i) => {
|
|
280
|
+
const actualIndex = (startIndex + i) % items.length
|
|
281
|
+
const text = items[actualIndex]
|
|
282
|
+
.querySelector("[data-slot='select-item-text']")
|
|
283
|
+
?.textContent?.trim()
|
|
284
|
+
.toLowerCase()
|
|
285
|
+
return text?.startsWith(char)
|
|
286
|
+
})
|
|
287
|
+
if (matchIndex >= 0) {
|
|
288
|
+
const actualIndex = (startIndex + matchIndex) % items.length
|
|
289
|
+
this._highlightIndex(actualIndex)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** @private */
|
|
297
|
+
_addGlobalListeners() {
|
|
298
|
+
document.addEventListener("click", this._handleOutsideClick, true)
|
|
299
|
+
document.addEventListener("keydown", this._handleKeydown)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** @private */
|
|
303
|
+
_removeGlobalListeners() {
|
|
304
|
+
document.removeEventListener("click", this._handleOutsideClick, true)
|
|
305
|
+
document.removeEventListener("keydown", this._handleKeydown)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Kiso sidebar toggle controller.
|
|
5
|
+
*
|
|
6
|
+
* Manages the dashboard sidebar open/closed state via a single
|
|
7
|
+
* `data-sidebar-open` attribute on the controller element. CSS cascade
|
|
8
|
+
* handles all visual changes — this controller only manages the
|
|
9
|
+
* boolean attribute and persists the preference to a cookie for
|
|
10
|
+
* FOUC-free server-side restoration on the next page load.
|
|
11
|
+
*
|
|
12
|
+
* Register as `kiso--sidebar` (the engine index does this automatically).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <div data-slot="dashboard-group"
|
|
16
|
+
* data-controller="kiso--sidebar"
|
|
17
|
+
* data-sidebar-open="true">
|
|
18
|
+
* <header data-slot="dashboard-navbar">
|
|
19
|
+
* <button data-kiso--sidebar-target="trigger"
|
|
20
|
+
* data-action="click->kiso--sidebar#toggle"
|
|
21
|
+
* aria-expanded="true"
|
|
22
|
+
* aria-controls="dashboard-sidebar">
|
|
23
|
+
* <!-- hamburger icon -->
|
|
24
|
+
* </button>
|
|
25
|
+
* </header>
|
|
26
|
+
* <aside data-slot="dashboard-sidebar" id="dashboard-sidebar">
|
|
27
|
+
* <div data-slot="dashboard-sidebar-inner">
|
|
28
|
+
* <!-- sidebar content -->
|
|
29
|
+
* </div>
|
|
30
|
+
* </aside>
|
|
31
|
+
* <main data-slot="dashboard-panel"><!-- page content --></main>
|
|
32
|
+
* <div data-slot="dashboard-scrim"
|
|
33
|
+
* data-kiso--sidebar-target="scrim"
|
|
34
|
+
* data-action="click->kiso--sidebar#closeOnMobile"
|
|
35
|
+
* aria-hidden="true"></div>
|
|
36
|
+
* </div>
|
|
37
|
+
*
|
|
38
|
+
* @property {Element[]} triggerTargets - Toggle/collapse buttons that control the sidebar
|
|
39
|
+
* @property {Element} scrimTarget - The mobile overlay scrim
|
|
40
|
+
*/
|
|
41
|
+
export default class extends Controller {
|
|
42
|
+
static targets = ["trigger", "scrim"]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Toggles the sidebar open/closed state.
|
|
46
|
+
*
|
|
47
|
+
* Flips `data-sidebar-open` on the controller element, syncs
|
|
48
|
+
* `aria-expanded` on the trigger target, and persists the new
|
|
49
|
+
* state to a one-year cookie for FOUC-free server-side restoration.
|
|
50
|
+
*/
|
|
51
|
+
toggle() {
|
|
52
|
+
const isOpen = this.element.dataset.sidebarOpen !== "false"
|
|
53
|
+
this.#setState(!isOpen)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Closes the sidebar on mobile viewports only.
|
|
58
|
+
*
|
|
59
|
+
* Connected to the scrim click event. Tapping the overlay outside
|
|
60
|
+
* the mobile sidebar dismisses it without affecting desktop layout.
|
|
61
|
+
*/
|
|
62
|
+
closeOnMobile() {
|
|
63
|
+
if (matchMedia("(max-width: 767px)").matches) {
|
|
64
|
+
this.#setState(false)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sets the sidebar state, syncs aria-expanded, and persists to cookie.
|
|
70
|
+
*
|
|
71
|
+
* @param {boolean} open - Whether the sidebar should be open
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
#setState(open) {
|
|
75
|
+
const value = String(open)
|
|
76
|
+
|
|
77
|
+
this.element.dataset.sidebarOpen = value
|
|
78
|
+
document.cookie = `sidebar_open=${value};path=/;max-age=31536000;SameSite=Lax`
|
|
79
|
+
|
|
80
|
+
for (const trigger of this.triggerTargets) {
|
|
81
|
+
trigger.setAttribute("aria-expanded", value)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Kiso theme toggle controller.
|
|
5
|
+
*
|
|
6
|
+
* Toggles the `.dark` class on `<html>` and persists the preference
|
|
7
|
+
* to both `localStorage` and a cookie. Works in concert with the
|
|
8
|
+
* `kiso_theme_script` helper which prevents FOUC on initial page load.
|
|
9
|
+
*
|
|
10
|
+
* Supports three modes: "light", "dark", "system". The `toggle` action
|
|
11
|
+
* cycles light ↔ dark. The `set` action accepts any of the three values.
|
|
12
|
+
*
|
|
13
|
+
* Register as `kiso--theme` (the engine index does this automatically).
|
|
14
|
+
*
|
|
15
|
+
* @example Toggle button (light ↔ dark)
|
|
16
|
+
* <button data-controller="kiso--theme"
|
|
17
|
+
* data-action="click->kiso--theme#toggle"
|
|
18
|
+
* aria-label="Toggle dark mode">
|
|
19
|
+
* <!-- sun / moon icon -->
|
|
20
|
+
* </button>
|
|
21
|
+
*
|
|
22
|
+
* @example Set a specific mode via kiso--select:change
|
|
23
|
+
* <div data-controller="kiso--theme"
|
|
24
|
+
* data-action="kiso--select:change->kiso--theme#set">
|
|
25
|
+
* <!-- kui(:select) with light/dark/system items -->
|
|
26
|
+
* </div>
|
|
27
|
+
*
|
|
28
|
+
* @fires kiso:theme-change on document.documentElement when theme changes
|
|
29
|
+
*/
|
|
30
|
+
export default class extends Controller {
|
|
31
|
+
/**
|
|
32
|
+
* Toggles dark mode on the document root.
|
|
33
|
+
*
|
|
34
|
+
* Cycles between light and dark. Persists the preference
|
|
35
|
+
* to `localStorage` and a one-year cookie.
|
|
36
|
+
*/
|
|
37
|
+
toggle() {
|
|
38
|
+
const dark = document.documentElement.classList.toggle("dark")
|
|
39
|
+
this.#persist(dark ? "dark" : "light")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sets a specific theme preference.
|
|
44
|
+
*
|
|
45
|
+
* Accepts "light", "dark", or "system" via event detail.
|
|
46
|
+
* When "system", resolves to the OS preference via matchMedia.
|
|
47
|
+
*
|
|
48
|
+
* @param {CustomEvent} event - Event with detail.value
|
|
49
|
+
*/
|
|
50
|
+
set(event) {
|
|
51
|
+
const preference = event.detail?.value
|
|
52
|
+
if (!preference) return
|
|
53
|
+
|
|
54
|
+
this.#apply(preference)
|
|
55
|
+
this.#persist(preference)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolves a preference to an actual theme and applies it.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} preference - "light", "dark", or "system"
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
#apply(preference) {
|
|
65
|
+
let resolved = preference
|
|
66
|
+
if (preference === "system") {
|
|
67
|
+
resolved = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
68
|
+
}
|
|
69
|
+
document.documentElement.classList.toggle("dark", resolved === "dark")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Persists preference and dispatches change event.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} preference - "light", "dark", or "system"
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
#persist(preference) {
|
|
79
|
+
localStorage.setItem("theme", preference)
|
|
80
|
+
document.cookie = `theme=${preference};path=/;max-age=31536000;SameSite=Lax`
|
|
81
|
+
|
|
82
|
+
document.documentElement.dispatchEvent(
|
|
83
|
+
new CustomEvent("kiso:theme-change", {
|
|
84
|
+
detail: { theme: preference },
|
|
85
|
+
bubbles: true,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages pressed state for a standalone toggle button.
|
|
5
|
+
* Toggles `data-state` between "on" and "off" and updates `aria-pressed`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <button data-controller="kiso--toggle"
|
|
9
|
+
* data-action="click->kiso--toggle#toggle"
|
|
10
|
+
* data-state="off" aria-pressed="false">
|
|
11
|
+
* Bold
|
|
12
|
+
* </button>
|
|
13
|
+
*/
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
/**
|
|
16
|
+
* Toggles the pressed state of the element.
|
|
17
|
+
* Flips `data-state` between "on"/"off" and syncs `aria-pressed`.
|
|
18
|
+
*/
|
|
19
|
+
toggle() {
|
|
20
|
+
const pressed = this.element.dataset.state === "on"
|
|
21
|
+
this.element.dataset.state = pressed ? "off" : "on"
|
|
22
|
+
this.element.setAttribute("aria-pressed", !pressed)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages selection state for a group of toggle buttons.
|
|
5
|
+
* Supports single (radio-like) and multiple (checkbox-like) selection modes.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <div data-controller="kiso--toggle-group"
|
|
9
|
+
* data-kiso--toggle-group-type-value="single">
|
|
10
|
+
* <button data-kiso--toggle-group-target="item"
|
|
11
|
+
* data-action="click->kiso--toggle-group#toggle"
|
|
12
|
+
* data-value="left" data-state="off" aria-pressed="false">
|
|
13
|
+
* Left
|
|
14
|
+
* </button>
|
|
15
|
+
* </div>
|
|
16
|
+
*
|
|
17
|
+
* @property {HTMLElement[]} itemTargets - Toggle buttons in the group
|
|
18
|
+
* @property {string} typeValue - Selection mode: "single" or "multiple"
|
|
19
|
+
* @property {string} variantValue - Inherited variant for styling context
|
|
20
|
+
* @property {string} sizeValue - Inherited size for styling context
|
|
21
|
+
*
|
|
22
|
+
* @fires kiso--toggle-group:change - When selection changes.
|
|
23
|
+
* Detail: `{ value: string | null }` (single) or `{ value: string[] }` (multiple).
|
|
24
|
+
*/
|
|
25
|
+
export default class extends Controller {
|
|
26
|
+
static targets = ["item"]
|
|
27
|
+
static values = {
|
|
28
|
+
type: { type: String, default: "single" },
|
|
29
|
+
variant: { type: String, default: "default" },
|
|
30
|
+
size: { type: String, default: "default" },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Handles a toggle click on one of the group items.
|
|
35
|
+
* In single mode, deselects all others first (allows deselect).
|
|
36
|
+
* In multiple mode, toggles the clicked item independently.
|
|
37
|
+
*
|
|
38
|
+
* @param {Event} event - The click event from a group item
|
|
39
|
+
*/
|
|
40
|
+
toggle(event) {
|
|
41
|
+
const item = event.currentTarget
|
|
42
|
+
const pressed = item.dataset.state === "on"
|
|
43
|
+
|
|
44
|
+
if (this.typeValue === "single") {
|
|
45
|
+
// In single mode, deselect all others first
|
|
46
|
+
this.itemTargets.forEach((target) => {
|
|
47
|
+
target.dataset.state = "off"
|
|
48
|
+
target.setAttribute("aria-pressed", "false")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Toggle the clicked item (allow deselect in single mode)
|
|
52
|
+
if (!pressed) {
|
|
53
|
+
item.dataset.state = "on"
|
|
54
|
+
item.setAttribute("aria-pressed", "true")
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// In multiple mode, toggle independently
|
|
58
|
+
item.dataset.state = pressed ? "off" : "on"
|
|
59
|
+
item.setAttribute("aria-pressed", !pressed)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.#dispatchChange()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Sets up arrow-key navigation between items. */
|
|
66
|
+
connect() {
|
|
67
|
+
this.element.addEventListener("keydown", this.#handleKeydown)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Tears down the keydown listener. */
|
|
71
|
+
disconnect() {
|
|
72
|
+
this.element.removeEventListener("keydown", this.#handleKeydown)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handles arrow key, Home, and End navigation between items.
|
|
77
|
+
* Wraps around at boundaries.
|
|
78
|
+
*
|
|
79
|
+
* @param {KeyboardEvent} event
|
|
80
|
+
*/
|
|
81
|
+
#handleKeydown = (event) => {
|
|
82
|
+
const items = this.itemTargets.filter((item) => !item.disabled)
|
|
83
|
+
const currentIndex = items.indexOf(document.activeElement)
|
|
84
|
+
|
|
85
|
+
if (currentIndex === -1) return
|
|
86
|
+
|
|
87
|
+
let nextIndex
|
|
88
|
+
switch (event.key) {
|
|
89
|
+
case "ArrowRight":
|
|
90
|
+
case "ArrowDown":
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
nextIndex = (currentIndex + 1) % items.length
|
|
93
|
+
items[nextIndex].focus()
|
|
94
|
+
break
|
|
95
|
+
case "ArrowLeft":
|
|
96
|
+
case "ArrowUp":
|
|
97
|
+
event.preventDefault()
|
|
98
|
+
nextIndex = (currentIndex - 1 + items.length) % items.length
|
|
99
|
+
items[nextIndex].focus()
|
|
100
|
+
break
|
|
101
|
+
case "Home":
|
|
102
|
+
event.preventDefault()
|
|
103
|
+
items[0].focus()
|
|
104
|
+
break
|
|
105
|
+
case "End":
|
|
106
|
+
event.preventDefault()
|
|
107
|
+
items[items.length - 1].focus()
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Dispatches a "change" event with the currently selected value(s).
|
|
114
|
+
* Single mode emits `{ value: string | null }`,
|
|
115
|
+
* multiple mode emits `{ value: string[] }`.
|
|
116
|
+
*/
|
|
117
|
+
#dispatchChange() {
|
|
118
|
+
const selectedValues = this.itemTargets
|
|
119
|
+
.filter((item) => item.dataset.state === "on")
|
|
120
|
+
.map((item) => item.dataset.value)
|
|
121
|
+
|
|
122
|
+
this.dispatch("change", {
|
|
123
|
+
detail: {
|
|
124
|
+
value: this.typeValue === "single" ? selectedValues[0] || null : selectedValues,
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS selector for all natively focusable elements that are not disabled
|
|
3
|
+
* or explicitly removed from the tab order.
|
|
4
|
+
*
|
|
5
|
+
* @type {string}
|
|
6
|
+
*/
|
|
7
|
+
export const FOCUSABLE_SELECTOR =
|
|
8
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared highlight and index utilities for list-based components.
|
|
3
|
+
* Used by select, combobox, command, and dropdown_menu controllers.
|
|
4
|
+
*
|
|
5
|
+
* @module utils/highlight
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Highlights an item at the given index. Clears the attribute from all
|
|
10
|
+
* clearItems, then sets it on the item at `index` within `items` and
|
|
11
|
+
* scrolls it into view.
|
|
12
|
+
*
|
|
13
|
+
* @param {HTMLElement[]} clearItems - Items to remove the attribute from
|
|
14
|
+
* @param {HTMLElement[]} items - Items to index into for highlighting
|
|
15
|
+
* @param {number} index - Index to highlight, or -1 to clear only
|
|
16
|
+
* @param {Object} [options]
|
|
17
|
+
* @param {string} [options.attr="data-highlighted"] - The attribute to toggle
|
|
18
|
+
*/
|
|
19
|
+
export function highlightItem(clearItems, items, index, { attr = "data-highlighted" } = {}) {
|
|
20
|
+
clearItems.forEach((item) => item.removeAttribute(attr))
|
|
21
|
+
|
|
22
|
+
if (index >= 0 && index < items.length) {
|
|
23
|
+
items[index].setAttribute(attr, "")
|
|
24
|
+
items[index].scrollIntoView({ block: "nearest" })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wraps an index within a range, cycling from end to start and vice versa.
|
|
30
|
+
*
|
|
31
|
+
* @param {number} current - Current index
|
|
32
|
+
* @param {number} direction - +1 for next, -1 for previous
|
|
33
|
+
* @param {number} length - Total number of items
|
|
34
|
+
* @returns {number} The wrapped index, or -1 if length is 0
|
|
35
|
+
*/
|
|
36
|
+
export function wrapIndex(current, direction, length) {
|
|
37
|
+
if (length === 0) return -1
|
|
38
|
+
|
|
39
|
+
let next = current + direction
|
|
40
|
+
if (next < 0) next = length - 1
|
|
41
|
+
if (next >= length) next = 0
|
|
42
|
+
return next
|
|
43
|
+
}
|