shadcn_phlexcomponents 0.1.11 → 0.1.16
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/app/javascript/controllers/accordion_controller.js +107 -0
- data/app/javascript/controllers/alert_dialog_controller.js +7 -0
- data/app/javascript/controllers/avatar_controller.js +14 -0
- data/app/javascript/controllers/checkbox_controller.js +29 -0
- data/app/javascript/controllers/collapsible_controller.js +39 -0
- data/app/javascript/controllers/combobox_controller.js +278 -0
- data/app/javascript/controllers/command_controller.js +207 -0
- data/app/javascript/controllers/date_picker_controller.js +258 -0
- data/app/javascript/controllers/date_range_picker_controller.js +200 -0
- data/app/javascript/controllers/dialog_controller.js +83 -0
- data/app/javascript/controllers/dropdown_menu_controller.js +238 -0
- data/app/javascript/controllers/dropdown_menu_sub_controller.js +118 -0
- data/app/javascript/controllers/form_field_controller.js +20 -0
- data/app/javascript/controllers/hover_card_controller.js +73 -0
- data/app/javascript/controllers/loading_button_controller.js +14 -0
- data/app/javascript/controllers/popover_controller.js +90 -0
- data/app/javascript/controllers/progress_controller.js +14 -0
- data/app/javascript/controllers/radio_group_controller.js +80 -0
- data/app/javascript/controllers/select_controller.js +265 -0
- data/app/javascript/controllers/sidebar_controller.js +29 -0
- data/app/javascript/controllers/sidebar_trigger_controller.js +15 -0
- data/app/javascript/controllers/slider_controller.js +82 -0
- data/app/javascript/controllers/switch_controller.js +26 -0
- data/app/javascript/controllers/tabs_controller.js +66 -0
- data/app/javascript/controllers/theme_switcher_controller.js +32 -0
- data/app/javascript/controllers/toast_container_controller.js +48 -0
- data/app/javascript/controllers/toast_controller.js +22 -0
- data/app/javascript/controllers/toggle_controller.js +20 -0
- data/app/javascript/controllers/toggle_group_controller.js +20 -0
- data/app/javascript/controllers/tooltip_controller.js +79 -0
- data/app/javascript/shadcn_phlexcomponents.js +60 -0
- data/app/javascript/utils/command.js +448 -0
- data/app/javascript/utils/floating_ui.js +160 -0
- data/app/javascript/utils/index.js +288 -0
- data/app/stylesheets/date_picker.css +118 -0
- data/app/typescript/controllers/accordion_controller.ts +136 -0
- data/app/typescript/controllers/alert_dialog_controller.ts +12 -0
- data/app/{javascript → typescript}/controllers/avatar_controller.ts +7 -2
- data/app/{javascript → typescript}/controllers/checkbox_controller.ts +11 -4
- data/app/{javascript → typescript}/controllers/collapsible_controller.ts +12 -5
- data/app/typescript/controllers/combobox_controller.ts +376 -0
- data/app/typescript/controllers/command_controller.ts +301 -0
- data/app/{javascript → typescript}/controllers/date_picker_controller.ts +185 -125
- data/app/{javascript → typescript}/controllers/date_range_picker_controller.ts +89 -79
- data/app/{javascript → typescript}/controllers/dialog_controller.ts +59 -57
- data/app/typescript/controllers/dropdown_menu_controller.ts +309 -0
- data/app/{javascript → typescript}/controllers/dropdown_menu_sub_controller.ts +31 -29
- data/app/{javascript → typescript}/controllers/form_field_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/hover_card_controller.ts +36 -26
- data/app/{javascript → typescript}/controllers/loading_button_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/popover_controller.ts +42 -65
- data/app/{javascript → typescript}/controllers/progress_controller.ts +9 -3
- data/app/{javascript → typescript}/controllers/radio_group_controller.ts +16 -9
- data/app/typescript/controllers/select_controller.ts +341 -0
- data/app/{javascript → typescript}/controllers/slider_controller.ts +23 -16
- data/app/{javascript → typescript}/controllers/switch_controller.ts +11 -4
- data/app/{javascript → typescript}/controllers/tabs_controller.ts +26 -18
- data/app/{javascript → typescript}/controllers/theme_switcher_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/toast_container_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/toast_controller.ts +7 -1
- data/app/typescript/controllers/toggle_controller.ts +28 -0
- data/app/typescript/controllers/toggle_group_controller.ts +28 -0
- data/app/{javascript → typescript}/controllers/tooltip_controller.ts +43 -31
- data/app/typescript/shadcn_phlexcomponents.ts +61 -0
- data/app/typescript/utils/command.ts +544 -0
- data/app/typescript/utils/floating_ui.ts +196 -0
- data/app/typescript/utils/index.ts +424 -0
- data/lib/install/install_shadcn_phlexcomponents.rb +10 -3
- data/lib/shadcn_phlexcomponents/alias.rb +3 -0
- data/lib/shadcn_phlexcomponents/components/accordion.rb +2 -1
- data/lib/shadcn_phlexcomponents/components/alert_dialog.rb +18 -15
- data/lib/shadcn_phlexcomponents/components/base.rb +14 -0
- data/lib/shadcn_phlexcomponents/components/collapsible.rb +1 -2
- data/lib/shadcn_phlexcomponents/components/combobox.rb +87 -57
- data/lib/shadcn_phlexcomponents/components/command.rb +77 -47
- data/lib/shadcn_phlexcomponents/components/date_picker.rb +25 -81
- data/lib/shadcn_phlexcomponents/components/date_range_picker.rb +21 -4
- data/lib/shadcn_phlexcomponents/components/dialog.rb +14 -12
- data/lib/shadcn_phlexcomponents/components/dropdown_menu.rb +5 -4
- data/lib/shadcn_phlexcomponents/components/dropdown_menu_sub.rb +2 -1
- data/lib/shadcn_phlexcomponents/components/form/form_combobox.rb +64 -0
- data/lib/shadcn_phlexcomponents/components/form.rb +14 -0
- data/lib/shadcn_phlexcomponents/components/hover_card.rb +3 -2
- data/lib/shadcn_phlexcomponents/components/popover.rb +3 -3
- data/lib/shadcn_phlexcomponents/components/select.rb +10 -25
- data/lib/shadcn_phlexcomponents/components/sheet.rb +15 -11
- data/lib/shadcn_phlexcomponents/components/table.rb +1 -1
- data/lib/shadcn_phlexcomponents/components/tabs.rb +1 -1
- data/lib/shadcn_phlexcomponents/components/toast_container.rb +1 -1
- data/lib/shadcn_phlexcomponents/components/toggle.rb +54 -0
- data/lib/shadcn_phlexcomponents/components/tooltip.rb +3 -2
- data/lib/shadcn_phlexcomponents/engine.rb +1 -5
- data/lib/shadcn_phlexcomponents/version.rb +1 -1
- metadata +71 -32
- data/app/javascript/controllers/accordion_controller.ts +0 -133
- data/app/javascript/controllers/combobox_controller.ts +0 -145
- data/app/javascript/controllers/command_controller.ts +0 -129
- data/app/javascript/controllers/command_root_controller.ts +0 -355
- data/app/javascript/controllers/dropdown_menu_controller.ts +0 -133
- data/app/javascript/controllers/dropdown_menu_root_controller.ts +0 -234
- data/app/javascript/controllers/select_controller.ts +0 -200
- data/app/javascript/shadcn_phlexcomponents.ts +0 -57
- data/app/javascript/utils.ts +0 -437
- /data/app/{javascript → typescript}/controllers/sidebar_controller.ts +0 -0
- /data/app/{javascript → typescript}/controllers/sidebar_trigger_controller.ts +0 -0
@@ -1,355 +0,0 @@
|
|
1
|
-
import { Controller } from '@hotwired/stimulus'
|
2
|
-
import { useClickOutside, useDebounce } from 'stimulus-use'
|
3
|
-
import Fuse from 'fuse.js'
|
4
|
-
import { ON_OPEN_FOCUS_DELAY } from '../utils'
|
5
|
-
|
6
|
-
export default class extends Controller<HTMLElement> {
|
7
|
-
static targets = [
|
8
|
-
'trigger',
|
9
|
-
'content',
|
10
|
-
'item',
|
11
|
-
'triggerText',
|
12
|
-
'group',
|
13
|
-
'label',
|
14
|
-
'searchInput',
|
15
|
-
'results',
|
16
|
-
'empty',
|
17
|
-
'list',
|
18
|
-
]
|
19
|
-
|
20
|
-
static values = {
|
21
|
-
isOpen: Boolean,
|
22
|
-
filteredItemIndexes: Array,
|
23
|
-
}
|
24
|
-
|
25
|
-
static debounces = ['search']
|
26
|
-
|
27
|
-
declare readonly emptyTarget: HTMLElement
|
28
|
-
declare readonly triggerTarget: HTMLElement
|
29
|
-
declare readonly listTarget: HTMLElement
|
30
|
-
declare readonly groupTargets: HTMLElement[]
|
31
|
-
declare readonly hasEmptyTarget: boolean
|
32
|
-
declare readonly contentTarget: HTMLElement
|
33
|
-
declare readonly searchInputTarget: HTMLInputElement
|
34
|
-
declare readonly itemTargets: HTMLInputElement[]
|
35
|
-
declare items: HTMLElement[]
|
36
|
-
declare filteredItemIndexesValue: number[]
|
37
|
-
declare itemsInnerText: string[]
|
38
|
-
declare filteredItems: HTMLElement[]
|
39
|
-
declare fuse: Fuse<string>
|
40
|
-
declare isOpenValue: boolean
|
41
|
-
declare resultsTarget: HTMLElement
|
42
|
-
declare DOMKeydownListener: (event: KeyboardEvent) => void
|
43
|
-
declare scrollingViaKeyboard: boolean
|
44
|
-
declare keyboardScrollTimeout: number
|
45
|
-
|
46
|
-
connect() {
|
47
|
-
this.DOMKeydownListener = this.onDOMKeydown.bind(this)
|
48
|
-
useClickOutside(this, { element: this.contentTarget })
|
49
|
-
useDebounce(this)
|
50
|
-
this.items = this.itemTargets.filter(
|
51
|
-
(i) => i.dataset.disabled === undefined,
|
52
|
-
)
|
53
|
-
this.itemsInnerText = this.items.map((i) => i.innerText.trim())
|
54
|
-
this.setAriaLabelledby()
|
55
|
-
this.setItemsGroupIds()
|
56
|
-
this.fuse = new Fuse(this.items.map((i) => i.innerText.trim()))
|
57
|
-
this.filteredItemIndexesValue = Array.from(
|
58
|
-
{ length: this.items.length },
|
59
|
-
(_, i) => i,
|
60
|
-
)
|
61
|
-
|
62
|
-
this.filteredItems = this.items
|
63
|
-
}
|
64
|
-
|
65
|
-
setItemsGroupIds() {
|
66
|
-
this.items.forEach((item, index) => {
|
67
|
-
const parent = item.parentElement
|
68
|
-
|
69
|
-
if (parent?.dataset[`${this.identifier}Target`] === 'group') {
|
70
|
-
item.dataset.groupId = parent.getAttribute('aria-labelledby') as string
|
71
|
-
}
|
72
|
-
})
|
73
|
-
}
|
74
|
-
|
75
|
-
setAriaLabelledby() {
|
76
|
-
this.groupTargets.forEach((g) => {
|
77
|
-
const label = g.querySelector(
|
78
|
-
`[data-${this.identifier}-target="label"]`,
|
79
|
-
) as HTMLElement
|
80
|
-
|
81
|
-
if (label) {
|
82
|
-
label.id = g.getAttribute('aria-labelledby') as string
|
83
|
-
}
|
84
|
-
})
|
85
|
-
}
|
86
|
-
|
87
|
-
scrollToItem(index: number) {
|
88
|
-
const item = this.filteredItems[index]
|
89
|
-
const containerRect = this.contentTarget.getBoundingClientRect()
|
90
|
-
const itemRect = item.getBoundingClientRect()
|
91
|
-
const listRect = this.listTarget.getBoundingClientRect()
|
92
|
-
let newScrollTop = null as number | null
|
93
|
-
|
94
|
-
const maxScrollTop =
|
95
|
-
this.listTarget.scrollHeight - this.listTarget.clientHeight
|
96
|
-
|
97
|
-
// scroll to bottom
|
98
|
-
if (itemRect.bottom - containerRect.bottom > 0) {
|
99
|
-
if (index === this.filteredItems.length - 1) {
|
100
|
-
newScrollTop = maxScrollTop
|
101
|
-
} else {
|
102
|
-
newScrollTop =
|
103
|
-
this.listTarget.scrollTop + (itemRect.bottom - containerRect.bottom)
|
104
|
-
}
|
105
|
-
} else if (listRect.top - itemRect.top > 0) {
|
106
|
-
// scroll to top
|
107
|
-
if (index === 0) {
|
108
|
-
newScrollTop = 0
|
109
|
-
} else {
|
110
|
-
newScrollTop = this.listTarget.scrollTop - (listRect.top - itemRect.top)
|
111
|
-
}
|
112
|
-
}
|
113
|
-
|
114
|
-
if (newScrollTop !== null) {
|
115
|
-
this.scrollingViaKeyboard = true
|
116
|
-
|
117
|
-
if (newScrollTop >= 0 && newScrollTop <= maxScrollTop) {
|
118
|
-
this.listTarget.scrollTop = newScrollTop
|
119
|
-
}
|
120
|
-
|
121
|
-
// Clear the flag after scroll settles
|
122
|
-
clearTimeout(this.keyboardScrollTimeout)
|
123
|
-
this.keyboardScrollTimeout = window.setTimeout(() => {
|
124
|
-
this.scrollingViaKeyboard = false
|
125
|
-
}, 200)
|
126
|
-
}
|
127
|
-
}
|
128
|
-
|
129
|
-
highlightItem(
|
130
|
-
event: MouseEvent | KeyboardEvent | null = null,
|
131
|
-
index: number | null = null,
|
132
|
-
) {
|
133
|
-
if (event !== null) {
|
134
|
-
if (event instanceof KeyboardEvent) {
|
135
|
-
const key = event.key
|
136
|
-
const item = this.filteredItems.find(
|
137
|
-
(i) => i.dataset.highlighted === 'true',
|
138
|
-
)
|
139
|
-
|
140
|
-
if (item) {
|
141
|
-
const index = this.filteredItems.indexOf(item)
|
142
|
-
|
143
|
-
let newIndex = 0
|
144
|
-
if (key === 'ArrowUp') {
|
145
|
-
newIndex = index - 1
|
146
|
-
|
147
|
-
if (newIndex < 0) {
|
148
|
-
newIndex = 0
|
149
|
-
}
|
150
|
-
} else {
|
151
|
-
newIndex = index + 1
|
152
|
-
|
153
|
-
if (newIndex > this.filteredItems.length - 1) {
|
154
|
-
newIndex = this.filteredItems.length - 1
|
155
|
-
}
|
156
|
-
}
|
157
|
-
|
158
|
-
this.highlightItemByIndex(newIndex)
|
159
|
-
this.scrollToItem(newIndex)
|
160
|
-
} else {
|
161
|
-
if (key === 'ArrowUp') {
|
162
|
-
this.highlightItemByIndex(this.filteredItems.length - 1)
|
163
|
-
} else {
|
164
|
-
this.highlightItemByIndex(0)
|
165
|
-
}
|
166
|
-
}
|
167
|
-
} else {
|
168
|
-
// mouse event
|
169
|
-
if (this.scrollingViaKeyboard) {
|
170
|
-
event.stopImmediatePropagation()
|
171
|
-
return
|
172
|
-
} else {
|
173
|
-
const item = event.currentTarget as HTMLElement
|
174
|
-
const index = this.filteredItems.indexOf(item)
|
175
|
-
this.highlightItemByIndex(index)
|
176
|
-
}
|
177
|
-
}
|
178
|
-
} else if (index !== null) {
|
179
|
-
this.highlightItemByIndex(index)
|
180
|
-
}
|
181
|
-
}
|
182
|
-
|
183
|
-
highlightItemByIndex(index: number) {
|
184
|
-
this.filteredItems.forEach((item, i) => {
|
185
|
-
if (i === index) {
|
186
|
-
item.dataset.highlighted = 'true'
|
187
|
-
} else {
|
188
|
-
item.dataset.highlighted = 'false'
|
189
|
-
}
|
190
|
-
})
|
191
|
-
}
|
192
|
-
|
193
|
-
open() {
|
194
|
-
this.isOpenValue = true
|
195
|
-
this.highlightItemByIndex(0)
|
196
|
-
|
197
|
-
setTimeout(() => {
|
198
|
-
this.searchInputTarget.focus()
|
199
|
-
}, ON_OPEN_FOCUS_DELAY)
|
200
|
-
}
|
201
|
-
|
202
|
-
close() {
|
203
|
-
this.isOpenValue = false
|
204
|
-
this.searchInputTarget.value = ''
|
205
|
-
this.filteredItemIndexesValue = Array.from(
|
206
|
-
{ length: this.items.length },
|
207
|
-
(_, i) => i,
|
208
|
-
)
|
209
|
-
}
|
210
|
-
|
211
|
-
select(event: MouseEvent | KeyboardEvent) {
|
212
|
-
if (!this.isOpenValue) return
|
213
|
-
|
214
|
-
if (event instanceof KeyboardEvent) {
|
215
|
-
const item = this.filteredItems.find(
|
216
|
-
(i) => i.dataset.highlighted === 'true',
|
217
|
-
)
|
218
|
-
|
219
|
-
if (item) {
|
220
|
-
this.onSelect(item.dataset.value as string)
|
221
|
-
this.close()
|
222
|
-
}
|
223
|
-
} else {
|
224
|
-
// mouse event
|
225
|
-
const item = event.currentTarget as HTMLElement
|
226
|
-
this.onSelect(item.dataset.value as string)
|
227
|
-
this.close()
|
228
|
-
}
|
229
|
-
}
|
230
|
-
|
231
|
-
onSelect(value: string) {}
|
232
|
-
|
233
|
-
onDOMKeydown(event: KeyboardEvent) {
|
234
|
-
if (!this.isOpenValue) return
|
235
|
-
|
236
|
-
const key = event.key
|
237
|
-
|
238
|
-
if (['Tab', 'Enter', ' '].includes(key)) event.preventDefault()
|
239
|
-
|
240
|
-
if (key === 'Escape') {
|
241
|
-
this.close()
|
242
|
-
}
|
243
|
-
}
|
244
|
-
|
245
|
-
setupEventListeners() {
|
246
|
-
document.addEventListener('keydown', this.DOMKeydownListener)
|
247
|
-
}
|
248
|
-
|
249
|
-
cleanupEventListeners() {
|
250
|
-
document.removeEventListener('keydown', this.DOMKeydownListener)
|
251
|
-
}
|
252
|
-
|
253
|
-
disconnect() {
|
254
|
-
this.cleanupEventListeners()
|
255
|
-
}
|
256
|
-
|
257
|
-
search(event: InputEvent) {
|
258
|
-
const input = event.target as HTMLInputElement
|
259
|
-
const value = input.value
|
260
|
-
|
261
|
-
if (value.length > 0) {
|
262
|
-
const results = this.fuse.search(value)
|
263
|
-
|
264
|
-
this.filteredItemIndexesValue = results.map((result) => result.refIndex)
|
265
|
-
} else {
|
266
|
-
this.filteredItemIndexesValue = Array.from(
|
267
|
-
{ length: this.items.length },
|
268
|
-
(_, i) => i,
|
269
|
-
)
|
270
|
-
}
|
271
|
-
}
|
272
|
-
|
273
|
-
filteredItemIndexesValueChanged(filteredItemIndexes: number[]) {
|
274
|
-
if (this.items) {
|
275
|
-
const filteredItems = filteredItemIndexes.map((i) => this.items[i])
|
276
|
-
|
277
|
-
// 1. Toggle visibility of items
|
278
|
-
this.items.forEach((item) => {
|
279
|
-
if (filteredItems.includes(item)) {
|
280
|
-
item.ariaHidden = 'false'
|
281
|
-
item.classList.remove('hidden')
|
282
|
-
} else {
|
283
|
-
item.ariaHidden = 'true'
|
284
|
-
item.classList.add('hidden')
|
285
|
-
}
|
286
|
-
})
|
287
|
-
|
288
|
-
// 2. Get groups based on order of filtered items
|
289
|
-
const groupIds = filteredItems.map((item) => item.dataset.groupId)
|
290
|
-
const uniqueGroupIds = [...new Set(groupIds)].filter(
|
291
|
-
(groupId) => !!groupId,
|
292
|
-
)
|
293
|
-
const orderedGroups = uniqueGroupIds.map((groupId) => {
|
294
|
-
return this.resultsTarget.querySelector(
|
295
|
-
`[aria-labelledby=${groupId}]`,
|
296
|
-
) as HTMLElement
|
297
|
-
})
|
298
|
-
|
299
|
-
// 3. Append items and groups based on filtered items
|
300
|
-
const appendedGroupIds = [] as string[]
|
301
|
-
|
302
|
-
filteredItems.forEach((item, index) => {
|
303
|
-
const groupId = item.dataset.groupId
|
304
|
-
|
305
|
-
if (groupId) {
|
306
|
-
const group = orderedGroups.find(
|
307
|
-
(g) => g.getAttribute('aria-labelledby') === groupId,
|
308
|
-
)
|
309
|
-
|
310
|
-
if (group) {
|
311
|
-
group.appendChild(item)
|
312
|
-
|
313
|
-
if (!appendedGroupIds.includes(groupId)) {
|
314
|
-
this.resultsTarget.appendChild(group)
|
315
|
-
appendedGroupIds.push(groupId)
|
316
|
-
}
|
317
|
-
}
|
318
|
-
} else {
|
319
|
-
this.resultsTarget.appendChild(item)
|
320
|
-
}
|
321
|
-
})
|
322
|
-
|
323
|
-
// 4. Toggle visibility of groups
|
324
|
-
this.groupTargets.forEach((group) => {
|
325
|
-
const itemsCount = group.querySelectorAll(
|
326
|
-
`[data-${this.identifier}-target=item][aria-hidden=false]`,
|
327
|
-
).length
|
328
|
-
if (itemsCount > 0) {
|
329
|
-
group.classList.remove('hidden')
|
330
|
-
} else {
|
331
|
-
group.classList.add('hidden')
|
332
|
-
}
|
333
|
-
})
|
334
|
-
|
335
|
-
// 5. Assign filteredItems based on the order it is displayed in the DOM
|
336
|
-
this.filteredItems = Array.from(
|
337
|
-
this.resultsTarget.querySelectorAll(
|
338
|
-
`[data-${this.identifier}-target=item][aria-hidden=false]`,
|
339
|
-
),
|
340
|
-
)
|
341
|
-
|
342
|
-
// 6. Highlight first item
|
343
|
-
this.highlightItemByIndex(0)
|
344
|
-
|
345
|
-
// 7. Toggle visibility of empty
|
346
|
-
if (this.hasEmptyTarget) {
|
347
|
-
if (this.filteredItems.length > 0) {
|
348
|
-
this.emptyTarget.classList.add('hidden')
|
349
|
-
} else {
|
350
|
-
this.emptyTarget.classList.remove('hidden')
|
351
|
-
}
|
352
|
-
}
|
353
|
-
}
|
354
|
-
}
|
355
|
-
}
|
@@ -1,133 +0,0 @@
|
|
1
|
-
import DropdownMenuSubController from './dropdown_menu_sub_controller'
|
2
|
-
import DropdownMenuRootController from './dropdown_menu_root_controller'
|
3
|
-
|
4
|
-
export default class extends DropdownMenuRootController {
|
5
|
-
static values = {
|
6
|
-
isOpen: Boolean,
|
7
|
-
setEqualWidth: { type: Boolean, default: false },
|
8
|
-
closestContentSelector: {
|
9
|
-
type: String,
|
10
|
-
default:
|
11
|
-
'[data-dropdown-menu-target="content"], [data-dropdown-menu-sub-target="content"]',
|
12
|
-
},
|
13
|
-
}
|
14
|
-
|
15
|
-
declare DOMKeydownListener: (event: KeyboardEvent) => void
|
16
|
-
declare subMenuControllers: DropdownMenuSubController[]
|
17
|
-
|
18
|
-
connect() {
|
19
|
-
super.connect()
|
20
|
-
}
|
21
|
-
|
22
|
-
onOpen(_event: MouseEvent | KeyboardEvent) {
|
23
|
-
// Sub menus are not connected to the DOM yet when dropdown menu is connected.
|
24
|
-
// So we initialize them here instead of in connect().
|
25
|
-
if (this.subMenuControllers === undefined) {
|
26
|
-
let subMenuControllers = [] as DropdownMenuSubController[]
|
27
|
-
|
28
|
-
const subMenus = Array.from(
|
29
|
-
this.contentTarget.querySelectorAll(
|
30
|
-
'[data-shadcn-phlexcomponents="dropdown-menu-sub"]',
|
31
|
-
),
|
32
|
-
)
|
33
|
-
|
34
|
-
subMenus.forEach((subMenu) => {
|
35
|
-
const subMenuController =
|
36
|
-
window.Stimulus.getControllerForElementAndIdentifier(
|
37
|
-
subMenu,
|
38
|
-
'dropdown-menu-sub',
|
39
|
-
) as DropdownMenuSubController
|
40
|
-
|
41
|
-
if (subMenuController) {
|
42
|
-
subMenuControllers.push(subMenuController)
|
43
|
-
}
|
44
|
-
})
|
45
|
-
|
46
|
-
this.subMenuControllers = subMenuControllers
|
47
|
-
}
|
48
|
-
}
|
49
|
-
|
50
|
-
focusItem(event: MouseEvent | KeyboardEvent) {
|
51
|
-
const item = event.currentTarget as HTMLElement
|
52
|
-
let items = [] as HTMLElement[]
|
53
|
-
const content = item.closest(
|
54
|
-
this.closestContentSelectorValue,
|
55
|
-
) as HTMLElement
|
56
|
-
|
57
|
-
const isSubMenu =
|
58
|
-
content.dataset.shadcnPhlexcomponents === 'dropdown-menu-sub-content'
|
59
|
-
|
60
|
-
if (isSubMenu) {
|
61
|
-
const subMenu = content.closest(
|
62
|
-
'[data-shadcn-phlexcomponents="dropdown-menu-sub"]',
|
63
|
-
)
|
64
|
-
const subMenuController = this.subMenuControllers.find(
|
65
|
-
(subMenuController) => subMenuController.element == subMenu,
|
66
|
-
)
|
67
|
-
if (subMenuController) {
|
68
|
-
items = subMenuController.items
|
69
|
-
}
|
70
|
-
} else {
|
71
|
-
items = this.items
|
72
|
-
}
|
73
|
-
|
74
|
-
let index = items.indexOf(item)
|
75
|
-
|
76
|
-
if (event instanceof KeyboardEvent) {
|
77
|
-
const key = event.key
|
78
|
-
let newIndex = 0
|
79
|
-
|
80
|
-
if (key === 'ArrowUp') {
|
81
|
-
newIndex = index - 1
|
82
|
-
if (newIndex < 0) {
|
83
|
-
newIndex = 0
|
84
|
-
}
|
85
|
-
} else {
|
86
|
-
newIndex = index + 1
|
87
|
-
if (newIndex > items.length - 1) {
|
88
|
-
newIndex = items.length - 1
|
89
|
-
}
|
90
|
-
}
|
91
|
-
|
92
|
-
items[newIndex].focus()
|
93
|
-
} else {
|
94
|
-
// item mouseover event
|
95
|
-
items[index].focus()
|
96
|
-
}
|
97
|
-
|
98
|
-
// Close submenus on the same level
|
99
|
-
const subMenusInContent = Array.from(
|
100
|
-
content.querySelectorAll(
|
101
|
-
'[data-shadcn-phlexcomponents="dropdown-menu-sub"]',
|
102
|
-
),
|
103
|
-
) as HTMLElement[]
|
104
|
-
|
105
|
-
subMenusInContent.forEach((subMenu) => {
|
106
|
-
const subMenuController = this.subMenuControllers.find(
|
107
|
-
(subMenuController) => subMenuController.element == subMenu,
|
108
|
-
)
|
109
|
-
|
110
|
-
if (subMenuController) {
|
111
|
-
subMenuController.closeImmediately()
|
112
|
-
}
|
113
|
-
})
|
114
|
-
}
|
115
|
-
|
116
|
-
onClose() {
|
117
|
-
this.subMenuControllers.forEach((subMenuController) => {
|
118
|
-
subMenuController.closeImmediately()
|
119
|
-
})
|
120
|
-
}
|
121
|
-
|
122
|
-
onSelect(event: MouseEvent | KeyboardEvent) {
|
123
|
-
if (event instanceof KeyboardEvent) {
|
124
|
-
const key = event.key
|
125
|
-
const item = (event.currentTarget || event.target) as HTMLElement
|
126
|
-
|
127
|
-
// For rails button_to
|
128
|
-
if (item && (key === 'Enter' || key === ' ')) {
|
129
|
-
item.click()
|
130
|
-
}
|
131
|
-
}
|
132
|
-
}
|
133
|
-
}
|
@@ -1,234 +0,0 @@
|
|
1
|
-
import { Controller } from '@hotwired/stimulus'
|
2
|
-
import { useClickOutside } from 'stimulus-use'
|
3
|
-
import {
|
4
|
-
getSameLevelItems,
|
5
|
-
ON_OPEN_FOCUS_DELAY,
|
6
|
-
lockScroll,
|
7
|
-
unlockScroll,
|
8
|
-
showContent,
|
9
|
-
hideContent,
|
10
|
-
initFloatingUi,
|
11
|
-
focusTrigger,
|
12
|
-
} from '../utils'
|
13
|
-
|
14
|
-
export default class extends Controller<HTMLElement> {
|
15
|
-
static targets = ['trigger', 'contentContainer', 'content', 'item']
|
16
|
-
static values = {
|
17
|
-
isOpen: Boolean,
|
18
|
-
setEqualWidth: { type: Boolean, default: false },
|
19
|
-
closestContentSelector: { type: String, default: '' },
|
20
|
-
}
|
21
|
-
|
22
|
-
declare isOpenValue: boolean
|
23
|
-
declare setEqualWidthValue: boolean
|
24
|
-
declare closestContentSelectorValue: string
|
25
|
-
declare readonly triggerTarget: HTMLElement
|
26
|
-
declare readonly contentContainerTarget: HTMLElement
|
27
|
-
declare readonly contentTarget: HTMLElement
|
28
|
-
declare readonly itemTargets: HTMLElement[]
|
29
|
-
declare items: HTMLElement[]
|
30
|
-
declare DOMKeydownListener: (event: KeyboardEvent) => void
|
31
|
-
declare cleanup: () => void
|
32
|
-
|
33
|
-
connect() {
|
34
|
-
this.DOMKeydownListener = this.onDOMKeydown.bind(this)
|
35
|
-
this.items = getSameLevelItems({
|
36
|
-
content: this.contentTarget,
|
37
|
-
items: this.itemTargets,
|
38
|
-
closestContentSelector: this.closestContentSelectorValue,
|
39
|
-
})
|
40
|
-
useClickOutside(this, { element: this.contentTarget, dispatchEvent: false })
|
41
|
-
}
|
42
|
-
|
43
|
-
toggle(event: MouseEvent) {
|
44
|
-
if (this.isOpenValue) {
|
45
|
-
this.close()
|
46
|
-
} else {
|
47
|
-
this.open(event)
|
48
|
-
}
|
49
|
-
}
|
50
|
-
|
51
|
-
open(event: MouseEvent | KeyboardEvent) {
|
52
|
-
this.isOpenValue = true
|
53
|
-
this.onOpen(event)
|
54
|
-
|
55
|
-
setTimeout(() => {
|
56
|
-
this.onOpenFocusedElement(event).focus()
|
57
|
-
}, ON_OPEN_FOCUS_DELAY)
|
58
|
-
}
|
59
|
-
|
60
|
-
onOpen(_event: MouseEvent | KeyboardEvent) {}
|
61
|
-
|
62
|
-
onOpenFocusedElement(event: MouseEvent | KeyboardEvent) {
|
63
|
-
let itemIndex = null as number | null
|
64
|
-
|
65
|
-
if (event instanceof KeyboardEvent) {
|
66
|
-
const key = event.key
|
67
|
-
|
68
|
-
if (['ArrowDown', 'Enter', ' '].includes(key)) {
|
69
|
-
itemIndex = 0
|
70
|
-
}
|
71
|
-
}
|
72
|
-
|
73
|
-
if (itemIndex !== null) {
|
74
|
-
return this.items[itemIndex]
|
75
|
-
} else {
|
76
|
-
return this.contentTarget
|
77
|
-
}
|
78
|
-
}
|
79
|
-
|
80
|
-
close() {
|
81
|
-
this.isOpenValue = false
|
82
|
-
this.onClose()
|
83
|
-
}
|
84
|
-
|
85
|
-
onClose() {}
|
86
|
-
|
87
|
-
onItemFocus(event: FocusEvent) {
|
88
|
-
const item = event.currentTarget as HTMLElement
|
89
|
-
item.tabIndex = 0
|
90
|
-
}
|
91
|
-
|
92
|
-
onItemBlur(event: FocusEvent) {
|
93
|
-
const item = event.currentTarget as HTMLElement
|
94
|
-
item.tabIndex = -1
|
95
|
-
}
|
96
|
-
|
97
|
-
focusItemByIndex(
|
98
|
-
event: KeyboardEvent | null = null,
|
99
|
-
index: number | null = null,
|
100
|
-
) {
|
101
|
-
if (event !== null) {
|
102
|
-
const key = event.key
|
103
|
-
|
104
|
-
if (key === 'ArrowUp') {
|
105
|
-
this.items[this.items.length - 1].focus()
|
106
|
-
} else {
|
107
|
-
this.items[0].focus()
|
108
|
-
}
|
109
|
-
} else if (index !== null) {
|
110
|
-
this.items[index].focus()
|
111
|
-
}
|
112
|
-
}
|
113
|
-
|
114
|
-
focusItem(event: MouseEvent | KeyboardEvent) {
|
115
|
-
let item = event.currentTarget as HTMLElement
|
116
|
-
const index = this.items.indexOf(item)
|
117
|
-
|
118
|
-
if (event instanceof KeyboardEvent) {
|
119
|
-
const key = event.key
|
120
|
-
let newIndex = 0
|
121
|
-
|
122
|
-
if (key === 'ArrowUp') {
|
123
|
-
newIndex = index - 1
|
124
|
-
|
125
|
-
if (newIndex < 0) {
|
126
|
-
newIndex = 0
|
127
|
-
}
|
128
|
-
} else {
|
129
|
-
newIndex = index + 1
|
130
|
-
|
131
|
-
if (newIndex > this.items.length - 1) {
|
132
|
-
newIndex = this.items.length - 1
|
133
|
-
}
|
134
|
-
}
|
135
|
-
|
136
|
-
this.items[newIndex].focus()
|
137
|
-
} else {
|
138
|
-
// item mouseover event
|
139
|
-
this.items[index].focus()
|
140
|
-
}
|
141
|
-
}
|
142
|
-
|
143
|
-
focusContent(event: MouseEvent) {
|
144
|
-
const item = event.currentTarget as HTMLElement
|
145
|
-
const content = item.closest(
|
146
|
-
this.closestContentSelectorValue,
|
147
|
-
) as HTMLElement
|
148
|
-
content.focus()
|
149
|
-
}
|
150
|
-
|
151
|
-
select(event: MouseEvent | KeyboardEvent) {
|
152
|
-
if (!this.isOpenValue) return
|
153
|
-
|
154
|
-
this.onSelect(event)
|
155
|
-
this.close()
|
156
|
-
}
|
157
|
-
|
158
|
-
onSelect(_event: MouseEvent | KeyboardEvent) {}
|
159
|
-
|
160
|
-
isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
|
161
|
-
if (isOpen) {
|
162
|
-
lockScroll()
|
163
|
-
|
164
|
-
showContent({
|
165
|
-
trigger: this.triggerTarget,
|
166
|
-
content: this.contentTarget,
|
167
|
-
contentContainer: this.contentContainerTarget,
|
168
|
-
setEqualWidth: this.setEqualWidthValue,
|
169
|
-
})
|
170
|
-
|
171
|
-
this.cleanup = initFloatingUi({
|
172
|
-
referenceElement: this.triggerTarget,
|
173
|
-
floatingElement: this.contentContainerTarget,
|
174
|
-
side: this.contentTarget.dataset.side,
|
175
|
-
align: this.contentTarget.dataset.align,
|
176
|
-
sideOffset: 4,
|
177
|
-
})
|
178
|
-
|
179
|
-
this.setupEventListeners()
|
180
|
-
} else {
|
181
|
-
unlockScroll()
|
182
|
-
|
183
|
-
hideContent({
|
184
|
-
trigger: this.triggerTarget,
|
185
|
-
content: this.contentTarget,
|
186
|
-
contentContainer: this.contentContainerTarget,
|
187
|
-
})
|
188
|
-
|
189
|
-
this.cleanupEventListeners()
|
190
|
-
|
191
|
-
// Only focus trigger when is previously opened
|
192
|
-
if (previousIsOpen) {
|
193
|
-
focusTrigger(this.triggerTarget)
|
194
|
-
}
|
195
|
-
}
|
196
|
-
}
|
197
|
-
|
198
|
-
clickOutside(event: MouseEvent) {
|
199
|
-
const target = event.target
|
200
|
-
// Let #toggle to handle state when clicked on trigger
|
201
|
-
if (target === this.triggerTarget) return
|
202
|
-
|
203
|
-
this.close()
|
204
|
-
}
|
205
|
-
|
206
|
-
onDOMKeydown(event: KeyboardEvent) {
|
207
|
-
if (!this.isOpenValue) return
|
208
|
-
|
209
|
-
const key = event.key
|
210
|
-
|
211
|
-
if (['Tab', 'Enter', ' '].includes(key)) event.preventDefault()
|
212
|
-
|
213
|
-
if (key === 'Home') {
|
214
|
-
this.focusItemByIndex(null, 0)
|
215
|
-
} else if (key === 'End') {
|
216
|
-
this.focusItemByIndex(null, this.items.length - 1)
|
217
|
-
} else if (key === 'Escape') {
|
218
|
-
this.close()
|
219
|
-
}
|
220
|
-
}
|
221
|
-
|
222
|
-
setupEventListeners() {
|
223
|
-
document.addEventListener('keydown', this.DOMKeydownListener)
|
224
|
-
}
|
225
|
-
|
226
|
-
cleanupEventListeners() {
|
227
|
-
if (this.cleanup) this.cleanup()
|
228
|
-
document.removeEventListener('keydown', this.DOMKeydownListener)
|
229
|
-
}
|
230
|
-
|
231
|
-
disconnect() {
|
232
|
-
this.cleanupEventListeners()
|
233
|
-
}
|
234
|
-
}
|