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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/app/javascript/controllers/accordion_controller.js +107 -0
  3. data/app/javascript/controllers/alert_dialog_controller.js +7 -0
  4. data/app/javascript/controllers/avatar_controller.js +14 -0
  5. data/app/javascript/controllers/checkbox_controller.js +29 -0
  6. data/app/javascript/controllers/collapsible_controller.js +39 -0
  7. data/app/javascript/controllers/combobox_controller.js +278 -0
  8. data/app/javascript/controllers/command_controller.js +207 -0
  9. data/app/javascript/controllers/date_picker_controller.js +258 -0
  10. data/app/javascript/controllers/date_range_picker_controller.js +200 -0
  11. data/app/javascript/controllers/dialog_controller.js +83 -0
  12. data/app/javascript/controllers/dropdown_menu_controller.js +238 -0
  13. data/app/javascript/controllers/dropdown_menu_sub_controller.js +118 -0
  14. data/app/javascript/controllers/form_field_controller.js +20 -0
  15. data/app/javascript/controllers/hover_card_controller.js +73 -0
  16. data/app/javascript/controllers/loading_button_controller.js +14 -0
  17. data/app/javascript/controllers/popover_controller.js +90 -0
  18. data/app/javascript/controllers/progress_controller.js +14 -0
  19. data/app/javascript/controllers/radio_group_controller.js +80 -0
  20. data/app/javascript/controllers/select_controller.js +265 -0
  21. data/app/javascript/controllers/sidebar_controller.js +29 -0
  22. data/app/javascript/controllers/sidebar_trigger_controller.js +15 -0
  23. data/app/javascript/controllers/slider_controller.js +82 -0
  24. data/app/javascript/controllers/switch_controller.js +26 -0
  25. data/app/javascript/controllers/tabs_controller.js +66 -0
  26. data/app/javascript/controllers/theme_switcher_controller.js +32 -0
  27. data/app/javascript/controllers/toast_container_controller.js +48 -0
  28. data/app/javascript/controllers/toast_controller.js +22 -0
  29. data/app/javascript/controllers/toggle_controller.js +20 -0
  30. data/app/javascript/controllers/toggle_group_controller.js +20 -0
  31. data/app/javascript/controllers/tooltip_controller.js +79 -0
  32. data/app/javascript/shadcn_phlexcomponents.js +60 -0
  33. data/app/javascript/utils/command.js +448 -0
  34. data/app/javascript/utils/floating_ui.js +160 -0
  35. data/app/javascript/utils/index.js +288 -0
  36. data/app/stylesheets/date_picker.css +118 -0
  37. data/app/typescript/controllers/accordion_controller.ts +136 -0
  38. data/app/typescript/controllers/alert_dialog_controller.ts +12 -0
  39. data/app/{javascript → typescript}/controllers/avatar_controller.ts +7 -2
  40. data/app/{javascript → typescript}/controllers/checkbox_controller.ts +11 -4
  41. data/app/{javascript → typescript}/controllers/collapsible_controller.ts +12 -5
  42. data/app/typescript/controllers/combobox_controller.ts +376 -0
  43. data/app/typescript/controllers/command_controller.ts +301 -0
  44. data/app/{javascript → typescript}/controllers/date_picker_controller.ts +185 -125
  45. data/app/{javascript → typescript}/controllers/date_range_picker_controller.ts +89 -79
  46. data/app/{javascript → typescript}/controllers/dialog_controller.ts +59 -57
  47. data/app/typescript/controllers/dropdown_menu_controller.ts +309 -0
  48. data/app/{javascript → typescript}/controllers/dropdown_menu_sub_controller.ts +31 -29
  49. data/app/{javascript → typescript}/controllers/form_field_controller.ts +6 -1
  50. data/app/{javascript → typescript}/controllers/hover_card_controller.ts +36 -26
  51. data/app/{javascript → typescript}/controllers/loading_button_controller.ts +6 -1
  52. data/app/{javascript → typescript}/controllers/popover_controller.ts +42 -65
  53. data/app/{javascript → typescript}/controllers/progress_controller.ts +9 -3
  54. data/app/{javascript → typescript}/controllers/radio_group_controller.ts +16 -9
  55. data/app/typescript/controllers/select_controller.ts +341 -0
  56. data/app/{javascript → typescript}/controllers/slider_controller.ts +23 -16
  57. data/app/{javascript → typescript}/controllers/switch_controller.ts +11 -4
  58. data/app/{javascript → typescript}/controllers/tabs_controller.ts +26 -18
  59. data/app/{javascript → typescript}/controllers/theme_switcher_controller.ts +6 -1
  60. data/app/{javascript → typescript}/controllers/toast_container_controller.ts +6 -1
  61. data/app/{javascript → typescript}/controllers/toast_controller.ts +7 -1
  62. data/app/typescript/controllers/toggle_controller.ts +28 -0
  63. data/app/typescript/controllers/toggle_group_controller.ts +28 -0
  64. data/app/{javascript → typescript}/controllers/tooltip_controller.ts +43 -31
  65. data/app/typescript/shadcn_phlexcomponents.ts +61 -0
  66. data/app/typescript/utils/command.ts +544 -0
  67. data/app/typescript/utils/floating_ui.ts +196 -0
  68. data/app/typescript/utils/index.ts +424 -0
  69. data/lib/install/install_shadcn_phlexcomponents.rb +10 -3
  70. data/lib/shadcn_phlexcomponents/alias.rb +3 -0
  71. data/lib/shadcn_phlexcomponents/components/accordion.rb +2 -1
  72. data/lib/shadcn_phlexcomponents/components/alert_dialog.rb +18 -15
  73. data/lib/shadcn_phlexcomponents/components/base.rb +14 -0
  74. data/lib/shadcn_phlexcomponents/components/collapsible.rb +1 -2
  75. data/lib/shadcn_phlexcomponents/components/combobox.rb +87 -57
  76. data/lib/shadcn_phlexcomponents/components/command.rb +77 -47
  77. data/lib/shadcn_phlexcomponents/components/date_picker.rb +25 -81
  78. data/lib/shadcn_phlexcomponents/components/date_range_picker.rb +21 -4
  79. data/lib/shadcn_phlexcomponents/components/dialog.rb +14 -12
  80. data/lib/shadcn_phlexcomponents/components/dropdown_menu.rb +5 -4
  81. data/lib/shadcn_phlexcomponents/components/dropdown_menu_sub.rb +2 -1
  82. data/lib/shadcn_phlexcomponents/components/form/form_combobox.rb +64 -0
  83. data/lib/shadcn_phlexcomponents/components/form.rb +14 -0
  84. data/lib/shadcn_phlexcomponents/components/hover_card.rb +3 -2
  85. data/lib/shadcn_phlexcomponents/components/popover.rb +3 -3
  86. data/lib/shadcn_phlexcomponents/components/select.rb +10 -25
  87. data/lib/shadcn_phlexcomponents/components/sheet.rb +15 -11
  88. data/lib/shadcn_phlexcomponents/components/table.rb +1 -1
  89. data/lib/shadcn_phlexcomponents/components/tabs.rb +1 -1
  90. data/lib/shadcn_phlexcomponents/components/toast_container.rb +1 -1
  91. data/lib/shadcn_phlexcomponents/components/toggle.rb +54 -0
  92. data/lib/shadcn_phlexcomponents/components/tooltip.rb +3 -2
  93. data/lib/shadcn_phlexcomponents/engine.rb +1 -5
  94. data/lib/shadcn_phlexcomponents/version.rb +1 -1
  95. metadata +71 -32
  96. data/app/javascript/controllers/accordion_controller.ts +0 -133
  97. data/app/javascript/controllers/combobox_controller.ts +0 -145
  98. data/app/javascript/controllers/command_controller.ts +0 -129
  99. data/app/javascript/controllers/command_root_controller.ts +0 -355
  100. data/app/javascript/controllers/dropdown_menu_controller.ts +0 -133
  101. data/app/javascript/controllers/dropdown_menu_root_controller.ts +0 -234
  102. data/app/javascript/controllers/select_controller.ts +0 -200
  103. data/app/javascript/shadcn_phlexcomponents.ts +0 -57
  104. data/app/javascript/utils.ts +0 -437
  105. /data/app/{javascript → typescript}/controllers/sidebar_controller.ts +0 -0
  106. /data/app/{javascript → typescript}/controllers/sidebar_trigger_controller.ts +0 -0
@@ -1,113 +1,115 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
- import { useClickOutside } from 'stimulus-use'
3
2
  import {
4
- ON_OPEN_FOCUS_DELAY,
5
- openWithOverlay,
6
- closeWithOverlay,
3
+ focusElement,
7
4
  focusTrigger,
8
5
  showContent,
9
6
  hideContent,
10
7
  getFocusableElements,
8
+ anyNestedComponentsOpen,
9
+ handleTabNavigation,
11
10
  } from '../utils'
12
11
 
13
- export default class extends Controller<HTMLElement> {
14
- static targets = ['trigger', 'content']
12
+ const DialogController = class extends Controller<HTMLElement> {
13
+ // targets
14
+ static targets = ['trigger', 'content', 'overlay']
15
+ declare readonly triggerTarget: HTMLElement
16
+ declare readonly contentTarget: HTMLElement
17
+ declare readonly overlayTarget: HTMLElement
18
+
19
+ // values
15
20
  static values = {
16
21
  isOpen: Boolean,
17
22
  }
18
-
19
- declare readonly triggerTarget: HTMLElement
20
- declare readonly contentTarget: HTMLElement
21
- declare readonly hasContentTarget: boolean
22
23
  declare isOpenValue: boolean
24
+
25
+ // custom properties
26
+ declare trigger: HTMLElement
23
27
  declare DOMKeydownListener: (event: KeyboardEvent) => void
28
+ declare DOMClickListener: (event: MouseEvent) => void
24
29
 
25
30
  connect() {
26
31
  this.DOMKeydownListener = this.onDOMKeydown.bind(this)
27
- useClickOutside(this, { element: this.contentTarget })
32
+ this.DOMClickListener = this.onDOMClick.bind(this)
28
33
  }
29
34
 
30
35
  open() {
31
36
  this.isOpenValue = true
32
-
33
- setTimeout(() => {
34
- this.onOpenFocusedElement().focus()
35
- }, ON_OPEN_FOCUS_DELAY)
36
- }
37
-
38
- onOpenFocusedElement() {
39
- const focusableElements = getFocusableElements(this.contentTarget)
40
- return focusableElements[0]
41
37
  }
42
38
 
43
39
  close() {
44
40
  this.isOpenValue = false
45
41
  }
46
42
 
47
- onDOMKeydown(event: KeyboardEvent) {
48
- if (!this.isOpenValue) return
49
-
50
- const key = event.key
51
-
52
- if (key === 'Escape') {
53
- this.close()
54
- } else if (key === 'Tab') {
55
- const focusableElements = getFocusableElements(this.contentTarget)
56
-
57
- const firstElement = focusableElements[0]
58
- const lastElement = focusableElements[focusableElements.length - 1]
59
-
60
- // If Shift + Tab pressed on first element, go to last element
61
- if (event.shiftKey && document.activeElement === firstElement) {
62
- event.preventDefault()
63
- lastElement.focus()
64
- }
65
- // If Tab pressed on last element, go to first element
66
- else if (!event.shiftKey && document.activeElement === lastElement) {
67
- event.preventDefault()
68
- firstElement.focus()
69
- }
70
- }
71
- }
72
-
73
43
  isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
74
44
  if (isOpen) {
75
- openWithOverlay(this.contentTarget.id)
76
-
77
45
  showContent({
78
46
  trigger: this.triggerTarget,
79
47
  content: this.contentTarget,
80
48
  contentContainer: this.contentTarget,
49
+ appendToBody: true,
50
+ overlay: this.overlayTarget,
81
51
  })
82
52
 
53
+ const focusableElements = getFocusableElements(this.contentTarget)
54
+ focusElement(focusableElements[0])
55
+
83
56
  this.setupEventListeners()
84
57
  } else {
85
- closeWithOverlay(this.contentTarget.id)
86
-
87
58
  hideContent({
88
59
  trigger: this.triggerTarget,
89
60
  content: this.contentTarget,
90
61
  contentContainer: this.contentTarget,
62
+ overlay: this.overlayTarget,
91
63
  })
92
64
 
93
- this.cleanupEventListeners()
94
-
95
- // Only focus trigger when is previously opened
96
65
  if (previousIsOpen) {
97
66
  focusTrigger(this.triggerTarget)
98
67
  }
68
+
69
+ this.cleanupEventListeners()
70
+ }
71
+ }
72
+
73
+ disconnect() {
74
+ this.cleanupEventListeners()
75
+ }
76
+
77
+ protected onDOMClick(event: MouseEvent) {
78
+ if (!this.isOpenValue) return
79
+
80
+ const target = event.target as HTMLElement
81
+ if (target === this.triggerTarget) return
82
+ if (this.contentTarget.contains(target)) return
83
+
84
+ const shouldClose = !anyNestedComponentsOpen(this.contentTarget)
85
+ if (shouldClose) this.close()
86
+ }
87
+
88
+ onDOMKeydown(event: KeyboardEvent) {
89
+ if (!this.isOpenValue) return
90
+
91
+ const key = event.key
92
+
93
+ if (key === 'Escape') {
94
+ const shouldClose = !anyNestedComponentsOpen(this.contentTarget)
95
+ if (shouldClose) this.close()
96
+ } else if (key === 'Tab') {
97
+ handleTabNavigation(this.contentTarget, event)
99
98
  }
100
99
  }
101
100
 
102
101
  setupEventListeners() {
103
102
  document.addEventListener('keydown', this.DOMKeydownListener)
103
+ document.addEventListener('pointerdown', this.DOMClickListener)
104
104
  }
105
105
 
106
106
  cleanupEventListeners() {
107
107
  document.removeEventListener('keydown', this.DOMKeydownListener)
108
- }
109
-
110
- disconnect() {
111
- this.cleanupEventListeners()
108
+ document.removeEventListener('pointerdown', this.DOMClickListener)
112
109
  }
113
110
  }
111
+
112
+ type Dialog = InstanceType<typeof DialogController>
113
+
114
+ export { DialogController }
115
+ export type { Dialog }
@@ -0,0 +1,309 @@
1
+ import { DropdownMenuSub } from './dropdown_menu_sub_controller'
2
+ import { Select } from './select_controller'
3
+ import { Controller } from '@hotwired/stimulus'
4
+ import { useClickOutside } from 'stimulus-use'
5
+ import { initFloatingUi } from '../utils/floating_ui'
6
+ import {
7
+ getSameLevelItems,
8
+ focusTrigger,
9
+ hideContent,
10
+ showContent,
11
+ lockScroll,
12
+ unlockScroll,
13
+ getStimulusInstance,
14
+ onClickOutside,
15
+ getNextEnabledIndex,
16
+ getPreviousEnabledIndex,
17
+ focusElement,
18
+ } from '../utils'
19
+
20
+ const onKeydown = (controller: DropdownMenu | Select, event: KeyboardEvent) => {
21
+ const key = event.key
22
+
23
+ if (['Tab', 'Enter', ' '].includes(key)) event.preventDefault()
24
+ if (key === 'Home') {
25
+ controller.focusItemByIndex(null, 0)
26
+ } else if (key === 'End') {
27
+ controller.focusItemByIndex(null, controller.items.length - 1)
28
+ } else if (key === 'Escape') {
29
+ controller.close()
30
+ }
31
+ }
32
+
33
+ const focusItemByIndex = (
34
+ controller: DropdownMenu | Select,
35
+ event: KeyboardEvent | null = null,
36
+ index: number | null = null,
37
+ ) => {
38
+ if (event !== null) {
39
+ const key = event.key
40
+
41
+ if (key === 'ArrowUp') {
42
+ controller.items[controller.items.length - 1].focus()
43
+ } else {
44
+ controller.items[0].focus()
45
+ }
46
+ } else if (index !== null) {
47
+ controller.items[index].focus()
48
+ }
49
+ }
50
+
51
+ const DropdownMenuController = class extends Controller<HTMLElement> {
52
+ // targets
53
+ static targets = ['trigger', 'contentContainer', 'content', 'item']
54
+ declare readonly triggerTarget: HTMLElement
55
+ declare readonly contentContainerTarget: HTMLElement
56
+ declare readonly contentTarget: HTMLElement
57
+ declare readonly itemTargets: HTMLElement[]
58
+
59
+ // values
60
+ static values = {
61
+ isOpen: Boolean,
62
+ }
63
+ declare isOpenValue: boolean
64
+
65
+ // custom properties
66
+ declare closestContentSelector: string
67
+ declare items: HTMLElement[]
68
+ declare subMenuControllers: DropdownMenuSub[]
69
+ declare DOMKeydownListener: (event: KeyboardEvent) => void
70
+ declare cleanup: () => void
71
+
72
+ connect() {
73
+ this.closestContentSelector =
74
+ '[data-dropdown-menu-target="content"], [data-dropdown-menu-sub-target="content"]'
75
+ this.items = getSameLevelItems({
76
+ content: this.contentTarget,
77
+ items: this.itemTargets,
78
+ closestContentSelector: this.closestContentSelector,
79
+ })
80
+ useClickOutside(this, { element: this.contentTarget, dispatchEvent: false })
81
+ this.DOMKeydownListener = this.onDOMKeydown.bind(this)
82
+ }
83
+
84
+ toggle(event: MouseEvent) {
85
+ if (this.isOpenValue) {
86
+ this.close()
87
+ } else {
88
+ this.open(event)
89
+ }
90
+ }
91
+
92
+ open(event: MouseEvent | KeyboardEvent) {
93
+ this.isOpenValue = true
94
+
95
+ // Sub menus are not connected to the DOM yet when dropdown menu is connected.
96
+ // So we initialize them here instead of in connect().
97
+ if (this.subMenuControllers === undefined) {
98
+ const subMenuControllers = [] as DropdownMenuSub[]
99
+
100
+ const subMenus = Array.from(
101
+ this.contentTarget.querySelectorAll(
102
+ '[data-shadcn-phlexcomponents="dropdown-menu-sub"]',
103
+ ),
104
+ ) as HTMLElement[]
105
+
106
+ subMenus.forEach((subMenu) => {
107
+ const subMenuController = getStimulusInstance<DropdownMenuSub>(
108
+ 'dropdown-menu-sub',
109
+ subMenu,
110
+ )
111
+
112
+ if (subMenuController) {
113
+ subMenuControllers.push(subMenuController)
114
+ }
115
+ })
116
+
117
+ this.subMenuControllers = subMenuControllers
118
+ }
119
+
120
+ let elementToFocus = null as HTMLElement | null
121
+
122
+ if (event instanceof KeyboardEvent) {
123
+ const key = event.key
124
+
125
+ if (['ArrowDown', 'Enter', ' '].includes(key)) {
126
+ elementToFocus = this.items[0]
127
+ }
128
+ } else {
129
+ elementToFocus = this.contentTarget
130
+ }
131
+
132
+ focusElement(elementToFocus)
133
+ }
134
+
135
+ close() {
136
+ this.isOpenValue = false
137
+ this.subMenuControllers.forEach((subMenuController) => {
138
+ subMenuController.closeImmediately()
139
+ })
140
+ }
141
+
142
+ focusItem(event: MouseEvent | KeyboardEvent) {
143
+ const item = event.currentTarget as HTMLElement
144
+ let items = [] as HTMLElement[]
145
+ const content = item.closest(this.closestContentSelector) as HTMLElement
146
+
147
+ const isSubMenu =
148
+ content.dataset.shadcnPhlexcomponents === 'dropdown-menu-sub-content'
149
+
150
+ if (isSubMenu) {
151
+ const subMenu = content.closest(
152
+ '[data-shadcn-phlexcomponents="dropdown-menu-sub"]',
153
+ )
154
+ const subMenuController = this.subMenuControllers.find(
155
+ (subMenuController) => subMenuController.element == subMenu,
156
+ )
157
+ if (subMenuController) {
158
+ items = subMenuController.items
159
+ }
160
+ } else {
161
+ items = this.items
162
+ }
163
+
164
+ const index = items.indexOf(item)
165
+
166
+ if (event instanceof KeyboardEvent) {
167
+ const key = event.key
168
+ let newIndex = 0
169
+
170
+ if (key === 'ArrowUp') {
171
+ newIndex = getPreviousEnabledIndex({
172
+ items,
173
+ currentIndex: index,
174
+ wrapAround: false,
175
+ })
176
+ } else {
177
+ newIndex = getNextEnabledIndex({
178
+ items,
179
+ currentIndex: index,
180
+ wrapAround: false,
181
+ })
182
+ }
183
+
184
+ items[newIndex].focus()
185
+ } else {
186
+ // item mouseover event
187
+ items[index].focus()
188
+ }
189
+
190
+ // Close submenus on the same level
191
+ const subMenusInContent = Array.from(
192
+ content.querySelectorAll(
193
+ '[data-shadcn-phlexcomponents="dropdown-menu-sub"]',
194
+ ),
195
+ ) as HTMLElement[]
196
+
197
+ subMenusInContent.forEach((subMenu) => {
198
+ const subMenuController = this.subMenuControllers.find(
199
+ (subMenuController) => subMenuController.element == subMenu,
200
+ )
201
+
202
+ if (subMenuController) {
203
+ subMenuController.closeImmediately()
204
+ }
205
+ })
206
+ }
207
+
208
+ onItemFocus(event: FocusEvent) {
209
+ const item = event.currentTarget as HTMLElement
210
+ item.tabIndex = 0
211
+ }
212
+
213
+ onItemBlur(event: FocusEvent) {
214
+ const item = event.currentTarget as HTMLElement
215
+ item.tabIndex = -1
216
+ }
217
+
218
+ focusItemByIndex(
219
+ event: KeyboardEvent | null = null,
220
+ index: number | null = null,
221
+ ) {
222
+ focusItemByIndex(this, event, index)
223
+ }
224
+
225
+ focusContent(event: MouseEvent) {
226
+ const item = event.currentTarget as HTMLElement
227
+ const content = item.closest(this.closestContentSelector) as HTMLElement
228
+ content.focus()
229
+ }
230
+
231
+ select(event: MouseEvent | KeyboardEvent) {
232
+ if (event instanceof KeyboardEvent) {
233
+ const key = event.key
234
+ const item = (event.currentTarget || event.target) as HTMLElement
235
+
236
+ // For rails button_to
237
+ if (item && (key === 'Enter' || key === ' ')) {
238
+ item.click()
239
+ }
240
+ }
241
+
242
+ this.close()
243
+ }
244
+
245
+ clickOutside(event: MouseEvent) {
246
+ onClickOutside(this, event)
247
+ }
248
+
249
+ isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
250
+ if (isOpen) {
251
+ lockScroll(this.contentTarget.id)
252
+
253
+ showContent({
254
+ trigger: this.triggerTarget,
255
+ content: this.contentTarget,
256
+ contentContainer: this.contentContainerTarget,
257
+ setEqualWidth: false,
258
+ })
259
+
260
+ this.cleanup = initFloatingUi({
261
+ referenceElement: this.triggerTarget,
262
+ floatingElement: this.contentContainerTarget,
263
+ side: this.contentTarget.dataset.side,
264
+ align: this.contentTarget.dataset.align,
265
+ sideOffset: 4,
266
+ })
267
+
268
+ this.setupEventListeners()
269
+ } else {
270
+ unlockScroll(this.contentTarget.id)
271
+
272
+ hideContent({
273
+ trigger: this.triggerTarget,
274
+ content: this.contentTarget,
275
+ contentContainer: this.contentContainerTarget,
276
+ })
277
+
278
+ if (previousIsOpen) {
279
+ focusTrigger(this.triggerTarget)
280
+ }
281
+
282
+ this.cleanupEventListeners()
283
+ }
284
+ }
285
+
286
+ disconnect() {
287
+ this.cleanupEventListeners()
288
+ }
289
+
290
+ protected onDOMKeydown(event: KeyboardEvent) {
291
+ if (!this.isOpenValue) return
292
+
293
+ onKeydown(this, event)
294
+ }
295
+
296
+ protected setupEventListeners() {
297
+ document.addEventListener('keydown', this.DOMKeydownListener)
298
+ }
299
+
300
+ protected cleanupEventListeners() {
301
+ if (this.cleanup) this.cleanup()
302
+ document.removeEventListener('keydown', this.DOMKeydownListener)
303
+ }
304
+ }
305
+
306
+ type DropdownMenu = InstanceType<typeof DropdownMenuController>
307
+
308
+ export { DropdownMenuController, onKeydown, focusItemByIndex }
309
+ export type { DropdownMenu }
@@ -1,27 +1,31 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
+ import { initFloatingUi } from '../utils/floating_ui'
2
3
  import {
3
- initFloatingUi,
4
4
  ON_OPEN_FOCUS_DELAY,
5
5
  getSameLevelItems,
6
6
  showContent,
7
7
  hideContent,
8
+ getStimulusInstance,
8
9
  } from '../utils'
9
10
 
10
- export default class extends Controller<HTMLElement> {
11
+ const DropdownMenuSubController = class extends Controller<HTMLElement> {
12
+ // targets
11
13
  static targets = ['trigger', 'contentContainer', 'content']
14
+ declare readonly triggerTarget: HTMLElement
15
+ declare readonly contentContainerTarget: HTMLElement
16
+ declare readonly contentTarget: HTMLElement
12
17
 
18
+ // values
13
19
  static values = {
14
20
  isOpen: Boolean,
15
21
  }
16
-
17
22
  declare isOpenValue: boolean
18
- declare readonly triggerTarget: HTMLElement
19
- declare readonly contentContainerTarget: HTMLElement
20
- declare readonly contentTarget: HTMLElement
21
- declare DOMKeydownListener: (event: KeyboardEvent) => void
22
- declare cleanup: () => void
23
+
24
+ // custom properties
23
25
  declare closeTimeout: number
24
26
  declare items: HTMLElement[]
27
+ declare DOMKeydownListener: (event: KeyboardEvent) => void
28
+ declare cleanup: () => void
25
29
 
26
30
  connect() {
27
31
  this.items = getSameLevelItems({
@@ -31,14 +35,10 @@ export default class extends Controller<HTMLElement> {
31
35
  '[data-dropdown-menu-target="item"], [data-dropdown-menu-sub-target="trigger"]',
32
36
  ),
33
37
  ),
34
- closestContentSelector: this.closestContentSelector(),
38
+ closestContentSelector: '[data-dropdown-menu-sub-target="content"]',
35
39
  })
36
40
  }
37
41
 
38
- closestContentSelector() {
39
- return '[data-dropdown-menu-sub-target="content"]'
40
- }
41
-
42
42
  open(event: MouseEvent | KeyboardEvent | null = null) {
43
43
  clearTimeout(this.closeTimeout)
44
44
  this.isOpenValue = true
@@ -60,7 +60,7 @@ export default class extends Controller<HTMLElement> {
60
60
  }, 250)
61
61
  }
62
62
 
63
- closeOnLeftKeydown(event: KeyboardEvent) {
63
+ closeOnLeftKeydown() {
64
64
  this.closeImmediately()
65
65
  this.triggerTarget.focus()
66
66
  }
@@ -87,22 +87,17 @@ export default class extends Controller<HTMLElement> {
87
87
  if (parentContent) {
88
88
  const subMenu = parentContent.closest(
89
89
  '[data-shadcn-phlexcomponents="dropdown-menu-sub"]',
90
- )
90
+ ) as HTMLElement
91
91
 
92
92
  if (subMenu) {
93
- const subMenuController =
94
- window.Stimulus.getControllerForElementAndIdentifier(
95
- subMenu,
96
- 'dropdown-menu-sub',
97
- )
93
+ const subMenuController = getStimulusInstance<DropdownMenuSub>(
94
+ 'dropdown-menu-sub',
95
+ subMenu,
96
+ )
98
97
 
99
98
  if (subMenuController) {
100
- // @ts-ignore
101
99
  subMenuController.closeImmediately()
102
100
  setTimeout(() => {
103
- // @ts-ignore
104
- // weird bug where focus goes to body element after closing, and setting focus
105
- // manually doesn't work
106
101
  subMenuController.triggerTarget.focus()
107
102
  }, 100)
108
103
  }
@@ -114,11 +109,7 @@ export default class extends Controller<HTMLElement> {
114
109
  this.isOpenValue = false
115
110
  }
116
111
 
117
- cleanupEventListeners() {
118
- if (this.cleanup) this.cleanup()
119
- }
120
-
121
- isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
112
+ isOpenValueChanged(isOpen: boolean) {
122
113
  if (isOpen) {
123
114
  showContent({
124
115
  trigger: this.triggerTarget,
@@ -141,10 +132,21 @@ export default class extends Controller<HTMLElement> {
141
132
  contentContainer: this.contentContainerTarget,
142
133
  })
143
134
  })
135
+
136
+ this.cleanupEventListeners()
144
137
  }
145
138
  }
146
139
 
147
140
  disconnect() {
148
141
  this.cleanupEventListeners()
149
142
  }
143
+
144
+ protected cleanupEventListeners() {
145
+ if (this.cleanup) this.cleanup()
146
+ }
150
147
  }
148
+
149
+ type DropdownMenuSub = InstanceType<typeof DropdownMenuSubController>
150
+
151
+ export { DropdownMenuSubController }
152
+ export type { DropdownMenuSub }
@@ -1,6 +1,6 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
 
3
- export default class extends Controller {
3
+ const FormFieldController = class extends Controller {
4
4
  connect() {
5
5
  const hintContainer = this.element.querySelector('[data-remove-label]')
6
6
  const labelContainer = this.element.querySelector('[data-remove-hint]')
@@ -20,3 +20,8 @@ export default class extends Controller {
20
20
  }
21
21
  }
22
22
  }
23
+
24
+ type FormField = InstanceType<typeof FormFieldController>
25
+
26
+ export { FormFieldController }
27
+ export type { FormField }
@@ -1,20 +1,25 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
  import { useHover } from 'stimulus-use'
3
- import { initFloatingUi, showContent, hideContent } from '../utils'
3
+ import { initFloatingUi } from '../utils/floating_ui'
4
+ import { showContent, hideContent } from '../utils'
4
5
 
5
- export default class extends Controller<HTMLElement> {
6
+ const HoverCardController = class extends Controller<HTMLElement> {
7
+ // targets
6
8
  static targets = ['trigger', 'content', 'contentContainer']
9
+ declare readonly triggerTarget: HTMLElement
10
+ declare readonly contentTarget: HTMLElement
11
+ declare readonly contentContainerTarget: HTMLElement
12
+
13
+ // values
7
14
  static values = {
8
15
  isOpen: Boolean,
9
16
  }
10
-
11
17
  declare isOpenValue: boolean
12
- declare readonly triggerTarget: HTMLElement
13
- declare readonly contentTarget: HTMLElement
14
- declare readonly contentContainerTarget: HTMLElement
15
- declare cleanup: () => void
18
+
19
+ // custom properties
16
20
  declare closeTimeout: number
17
21
  declare DOMKeydownListener: (event: KeyboardEvent) => void
22
+ declare cleanup: () => void
18
23
 
19
24
  connect() {
20
25
  this.DOMKeydownListener = this.onDOMKeydown.bind(this)
@@ -42,25 +47,6 @@ export default class extends Controller<HTMLElement> {
42
47
  this.close()
43
48
  }
44
49
 
45
- setupEventListeners() {
46
- document.addEventListener('keydown', this.DOMKeydownListener)
47
- }
48
-
49
- cleanupEventListeners() {
50
- if (this.cleanup) this.cleanup()
51
- document.removeEventListener('keydown', this.DOMKeydownListener)
52
- }
53
-
54
- onDOMKeydown(event: KeyboardEvent) {
55
- if (!this.isOpenValue) return
56
-
57
- const key = event.key
58
-
59
- if (key === 'Escape') {
60
- this.close()
61
- }
62
- }
63
-
64
50
  isOpenValueChanged(isOpen: boolean) {
65
51
  if (isOpen) {
66
52
  showContent({
@@ -90,4 +76,28 @@ export default class extends Controller<HTMLElement> {
90
76
  disconnect() {
91
77
  this.cleanupEventListeners()
92
78
  }
79
+
80
+ protected setupEventListeners() {
81
+ document.addEventListener('keydown', this.DOMKeydownListener)
82
+ }
83
+
84
+ protected cleanupEventListeners() {
85
+ if (this.cleanup) this.cleanup()
86
+ document.removeEventListener('keydown', this.DOMKeydownListener)
87
+ }
88
+
89
+ protected onDOMKeydown(event: KeyboardEvent) {
90
+ if (!this.isOpenValue) return
91
+
92
+ const key = event.key
93
+
94
+ if (key === 'Escape') {
95
+ this.close()
96
+ }
97
+ }
93
98
  }
99
+
100
+ type HoverCard = InstanceType<typeof HoverCardController>
101
+
102
+ export { HoverCardController }
103
+ export type { HoverCard }