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
@@ -0,0 +1,196 @@
1
+ import {
2
+ computePosition,
3
+ flip,
4
+ shift,
5
+ offset,
6
+ autoUpdate,
7
+ size,
8
+ Placement,
9
+ Middleware,
10
+ arrow,
11
+ } from '@floating-ui/dom'
12
+
13
+ const OPPOSITE_SIDE = {
14
+ top: 'bottom',
15
+ right: 'left',
16
+ bottom: 'top',
17
+ left: 'right',
18
+ }
19
+
20
+ const ARROW_TRANSFORM_ORIGIN = {
21
+ top: '',
22
+ right: '0 0',
23
+ bottom: 'center 0',
24
+ left: '100% 0',
25
+ }
26
+
27
+ const ARROW_TRANSFORM = {
28
+ top: 'translateY(100%)',
29
+ right: 'translateY(50%) rotate(90deg) translateX(-50%)',
30
+ bottom: `rotate(180deg)`,
31
+ left: 'translateY(50%) rotate(-90deg) translateX(50%)',
32
+ }
33
+
34
+ const initFloatingUi = ({
35
+ referenceElement,
36
+ floatingElement,
37
+ side = 'bottom',
38
+ align = 'center',
39
+ sideOffset = 0,
40
+ alignOffset = 0,
41
+ arrowElement,
42
+ }: {
43
+ referenceElement: HTMLElement
44
+ floatingElement: HTMLElement
45
+ side?: string
46
+ align?: string
47
+ sideOffset?: number
48
+ alignOffset?: number
49
+ offsetPx?: number
50
+ arrowElement?: HTMLElement
51
+ }) => {
52
+ let placement = `${side}-${align}`
53
+ placement = placement.replace(/-center/g, '')
54
+
55
+ let arrowHeight = 0,
56
+ arrowWidth = 0
57
+
58
+ if (arrowElement) {
59
+ const rect = arrowElement.getBoundingClientRect()
60
+ arrowWidth = rect.width
61
+ arrowHeight = rect.height
62
+ }
63
+
64
+ const middleware = [
65
+ transformOrigin({ arrowHeight, arrowWidth }),
66
+ offset({ mainAxis: sideOffset, alignmentAxis: alignOffset }),
67
+ size({
68
+ apply: ({ elements, rects, availableWidth, availableHeight }) => {
69
+ const { width: anchorWidth, height: anchorHeight } = rects.reference
70
+ const contentStyle = elements.floating.style
71
+ contentStyle.setProperty(
72
+ '--radix-popper-available-width',
73
+ `${availableWidth}px`,
74
+ )
75
+ contentStyle.setProperty(
76
+ '--radix-popper-available-height',
77
+ `${availableHeight}px`,
78
+ )
79
+ contentStyle.setProperty(
80
+ '--radix-popper-anchor-width',
81
+ `${anchorWidth}px`,
82
+ )
83
+ contentStyle.setProperty(
84
+ '--radix-popper-anchor-height',
85
+ `${anchorHeight}px`,
86
+ )
87
+ },
88
+ }),
89
+ ]
90
+
91
+ const flipMiddleware = flip({
92
+ // Ensure we flip to the perpendicular axis if it doesn't fit
93
+ // on narrow viewports.
94
+ crossAxis: 'alignment',
95
+ fallbackAxisSideDirection: 'end', // or 'start'
96
+ })
97
+ const shiftMiddleware = shift()
98
+
99
+ // Prioritize flip over shift for edge-aligned placements only.
100
+ if (placement.includes('-')) {
101
+ middleware.push(flipMiddleware, shiftMiddleware)
102
+ } else {
103
+ middleware.push(shiftMiddleware, flipMiddleware)
104
+ }
105
+
106
+ if (arrowElement) {
107
+ middleware.push(arrow({ element: arrowElement, padding: 0 }))
108
+ }
109
+
110
+ return autoUpdate(referenceElement, floatingElement, () => {
111
+ computePosition(referenceElement, floatingElement, {
112
+ placement: placement as Placement,
113
+ strategy: 'fixed',
114
+ middleware,
115
+ }).then(({ middlewareData, x, y }) => {
116
+ const arrowX = middlewareData.arrow?.x
117
+ const arrowY = middlewareData.arrow?.y
118
+ const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0
119
+
120
+ floatingElement.style.setProperty(
121
+ '--radix-popper-transform-origin',
122
+ `${middlewareData.transformOrigin?.x} ${middlewareData.transformOrigin?.y}`,
123
+ )
124
+ if (arrowElement) {
125
+ const baseSide = OPPOSITE_SIDE[side as keyof typeof OPPOSITE_SIDE]
126
+
127
+ const arrowStyle = {
128
+ position: 'absolute',
129
+ left: arrowX ? `${arrowX}px` : undefined,
130
+ top: arrowY ? `${arrowY}px` : undefined,
131
+ [baseSide]: 0,
132
+ transformOrigin:
133
+ ARROW_TRANSFORM_ORIGIN[side as keyof typeof ARROW_TRANSFORM_ORIGIN],
134
+ transform: ARROW_TRANSFORM[side as keyof typeof ARROW_TRANSFORM],
135
+ visibility: cannotCenterArrow ? 'hidden' : undefined,
136
+ }
137
+
138
+ Object.assign(arrowElement.style, arrowStyle)
139
+ }
140
+ Object.assign(floatingElement.style, {
141
+ left: `${x}px`,
142
+ top: `${y}px`,
143
+ })
144
+ })
145
+ })
146
+ }
147
+
148
+ const transformOrigin = (options: {
149
+ arrowWidth: number
150
+ arrowHeight: number
151
+ }): Middleware => {
152
+ return {
153
+ name: 'transformOrigin',
154
+ options,
155
+ fn(data) {
156
+ const { placement, rects, middlewareData } = data
157
+ const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0
158
+ const isArrowHidden = cannotCenterArrow
159
+ const arrowWidth = isArrowHidden ? 0 : options.arrowWidth
160
+ const arrowHeight = isArrowHidden ? 0 : options.arrowHeight
161
+
162
+ const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement)
163
+ const noArrowAlign = { start: '0%', center: '50%', end: '100%' }[
164
+ placedAlign
165
+ ] as string
166
+
167
+ const arrowXCenter = (middlewareData.arrow?.x ?? 0) + arrowWidth / 2
168
+ const arrowYCenter = (middlewareData.arrow?.y ?? 0) + arrowHeight / 2
169
+
170
+ let x = ''
171
+ let y = ''
172
+
173
+ if (placedSide === 'bottom') {
174
+ x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`
175
+ y = `${-arrowHeight}px`
176
+ } else if (placedSide === 'top') {
177
+ x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`
178
+ y = `${rects.floating.height + arrowHeight}px`
179
+ } else if (placedSide === 'right') {
180
+ x = `${-arrowHeight}px`
181
+ y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`
182
+ } else if (placedSide === 'left') {
183
+ x = `${rects.floating.width + arrowHeight}px`
184
+ y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`
185
+ }
186
+ return { data: { x, y } }
187
+ },
188
+ }
189
+ }
190
+
191
+ function getSideAndAlignFromPlacement(placement: Placement) {
192
+ const [side, align = 'center'] = placement.split('-')
193
+ return [side, align] as const
194
+ }
195
+
196
+ export { initFloatingUi }
@@ -0,0 +1,424 @@
1
+ import type { DropdownMenu } from '../controllers/dropdown_menu_controller'
2
+ import type { Select } from '../controllers/select_controller'
3
+ import type { Popover } from '../controllers/popover_controller'
4
+ import type { Command } from '../controllers/command_controller'
5
+ import type { Combobox } from '../controllers/combobox_controller'
6
+ import type { Dialog } from '../controllers/dialog_controller'
7
+ import type { AlertDialog } from '../controllers/alert_dialog_controller'
8
+ import type { HoverCard } from '../controllers/hover_card_controller'
9
+ import type { Tooltip } from '../controllers/tooltip_controller'
10
+ import type { DatePicker } from '../controllers/date_picker_controller'
11
+ import type { DateRangePicker } from '../controllers/date_range_picker_controller'
12
+ import { Application } from '@hotwired/stimulus'
13
+
14
+ declare global {
15
+ interface Window {
16
+ Stimulus: Application
17
+ }
18
+ }
19
+
20
+ const ANIMATION_OUT_DELAY = 100
21
+ const ON_OPEN_FOCUS_DELAY = 100
22
+ const ON_CLOSE_FOCUS_DELAY = 50
23
+
24
+ const getScrollbarWidth = () => {
25
+ // Create a temporary div container and append it into the body
26
+ const outer = document.createElement('div')
27
+ outer.style.visibility = 'hidden'
28
+ outer.style.overflow = 'scroll' // force scrollbars
29
+ outer.style.width = '100px'
30
+ outer.style.position = 'absolute'
31
+ outer.style.top = '-9999px'
32
+ document.body.appendChild(outer)
33
+
34
+ // Create an inner div and place it inside the outer div
35
+ const inner = document.createElement('div')
36
+ inner.style.width = '100%'
37
+ outer.appendChild(inner)
38
+
39
+ // Calculate the scrollbar width
40
+ const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
41
+
42
+ // Clean up
43
+ outer.remove()
44
+
45
+ return scrollbarWidth
46
+ }
47
+
48
+ const lockScroll = (contentId: string) => {
49
+ if (window.innerHeight < document.documentElement.scrollHeight) {
50
+ document.body.dataset.scrollLocked = '1'
51
+ document.body.classList.add(
52
+ 'data-[scroll-locked]:pointer-events-none',
53
+ 'data-[scroll-locked]:!overflow-hidden',
54
+ 'data-[scroll-locked]:!relative',
55
+ 'data-[scroll-locked]:px-0',
56
+ 'data-[scroll-locked]:pt-0',
57
+ 'data-[scroll-locked]:ml-0',
58
+ 'data-[scroll-locked]:mt-0',
59
+ )
60
+ document.body.style.marginRight = `${getScrollbarWidth()}px`
61
+
62
+ const contentIdsString =
63
+ document.body.dataset.scrollLockedContentIds || '[]'
64
+ const contentIds = JSON.parse(contentIdsString)
65
+
66
+ contentIds.push(contentId)
67
+ document.body.dataset.scrollLockedContentIds = JSON.stringify(contentIds)
68
+ }
69
+ }
70
+
71
+ const unlockScroll = (contentId: string) => {
72
+ const contentIdsString = document.body.dataset.scrollLockedContentIds || '[]'
73
+ const contentIds = JSON.parse(contentIdsString)
74
+ const newContentIds = contentIds.filter((id: string) => id !== contentId)
75
+ document.body.dataset.scrollLockedContentIds = JSON.stringify(newContentIds)
76
+
77
+ if (newContentIds.length === 0) {
78
+ delete document.body.dataset.scrollLocked
79
+ document.body.classList.remove(
80
+ 'data-[scroll-locked]:pointer-events-none',
81
+ 'data-[scroll-locked]:!overflow-hidden',
82
+ 'data-[scroll-locked]:!relative',
83
+ 'data-[scroll-locked]:px-0',
84
+ 'data-[scroll-locked]:pt-0',
85
+ 'data-[scroll-locked]:ml-0',
86
+ 'data-[scroll-locked]:mt-0',
87
+ )
88
+
89
+ document.body.style.marginRight = ''
90
+ }
91
+ }
92
+
93
+ const focusTrigger = (triggerTarget: HTMLElement) => {
94
+ setTimeout(() => {
95
+ if (triggerTarget.dataset.asChild === 'false') {
96
+ const childElement = triggerTarget.firstElementChild as HTMLElement
97
+
98
+ if (childElement) {
99
+ childElement.focus()
100
+ }
101
+ } else {
102
+ triggerTarget.focus()
103
+ }
104
+ }, ON_CLOSE_FOCUS_DELAY)
105
+ }
106
+
107
+ const focusElement = (element?: HTMLElement | null) => {
108
+ setTimeout(() => {
109
+ if (element) {
110
+ element.focus()
111
+ }
112
+ }, ON_OPEN_FOCUS_DELAY)
113
+ }
114
+
115
+ const getFocusableElements = (container: HTMLElement) => {
116
+ return Array.from(
117
+ container.querySelectorAll(
118
+ 'button, [href], input:not([type="hidden"]), select:not([tabindex="-1"]), textarea, [tabindex]:not([tabindex="-1"])',
119
+ ),
120
+ ) as HTMLElement[]
121
+ }
122
+
123
+ const getSameLevelItems = ({
124
+ content,
125
+ items,
126
+ closestContentSelector,
127
+ }: {
128
+ content: HTMLElement
129
+ items: HTMLElement[]
130
+ closestContentSelector: string
131
+ }) => {
132
+ const sameLevelItems = [] as HTMLElement[]
133
+
134
+ items.forEach((i) => {
135
+ if (
136
+ i.closest(closestContentSelector) === content &&
137
+ i.dataset.disabled === undefined
138
+ ) {
139
+ sameLevelItems.push(i)
140
+ }
141
+ })
142
+
143
+ return sameLevelItems
144
+ }
145
+
146
+ const showContent = ({
147
+ trigger,
148
+ content,
149
+ contentContainer,
150
+ setEqualWidth,
151
+ overlay,
152
+ }: {
153
+ trigger?: HTMLElement
154
+ content: HTMLElement
155
+ contentContainer: HTMLElement
156
+ overlay?: HTMLElement
157
+ setEqualWidth?: boolean
158
+ appendToBody?: boolean
159
+ }) => {
160
+ contentContainer.style.display = ''
161
+
162
+ if (trigger) {
163
+ if (setEqualWidth) {
164
+ const triggerWidth = trigger.offsetWidth
165
+ const contentContainerWidth = contentContainer.offsetWidth
166
+
167
+ if (contentContainerWidth < triggerWidth) {
168
+ contentContainer.style.width = `${triggerWidth}px`
169
+ }
170
+ }
171
+
172
+ trigger.ariaExpanded = 'true'
173
+ trigger.dataset.state = 'open'
174
+ }
175
+
176
+ content.dataset.state = 'open'
177
+
178
+ if (overlay) {
179
+ overlay.style.display = ''
180
+ overlay.dataset.state = 'open'
181
+ lockScroll(content.id)
182
+ }
183
+ }
184
+
185
+ const hideContent = ({
186
+ trigger,
187
+ content,
188
+ contentContainer,
189
+ overlay,
190
+ }: {
191
+ trigger?: HTMLElement
192
+ content: HTMLElement
193
+ contentContainer: HTMLElement
194
+ overlay?: HTMLElement
195
+ }) => {
196
+ if (trigger) {
197
+ trigger.ariaExpanded = 'false'
198
+ trigger.dataset.state = 'closed'
199
+ }
200
+
201
+ content.dataset.state = 'closed'
202
+
203
+ setTimeout(() => {
204
+ contentContainer.style.display = 'none'
205
+
206
+ if (overlay) {
207
+ overlay.style.display = 'none'
208
+ overlay.dataset.state = 'closed'
209
+ unlockScroll(content.id)
210
+ }
211
+ }, ANIMATION_OUT_DELAY)
212
+ }
213
+
214
+ const getStimulusInstance = <T>(
215
+ controller: string,
216
+ element: HTMLElement | null,
217
+ ) => {
218
+ if (!element) return
219
+
220
+ return window.Stimulus.getControllerForElementAndIdentifier(
221
+ element,
222
+ controller,
223
+ ) as T
224
+ }
225
+
226
+ type NestedComponent =
227
+ | DropdownMenu
228
+ | Select
229
+ | Popover
230
+ | Command
231
+ | Combobox
232
+ | Dialog
233
+ | AlertDialog
234
+ | HoverCard
235
+ | Tooltip
236
+ | DatePicker
237
+ | DateRangePicker
238
+ const anyNestedComponentsOpen = (element: HTMLElement) => {
239
+ const components = [] as NestedComponent[]
240
+
241
+ const componentNames = [
242
+ 'dialog',
243
+ 'alert-dialog',
244
+ 'dropdown-menu',
245
+ 'popover',
246
+ 'select',
247
+ 'combobox',
248
+ 'command',
249
+ 'hover-card',
250
+ 'tooltip',
251
+ 'date-picker',
252
+ 'date-range-picker',
253
+ ]
254
+
255
+ componentNames.forEach((name) => {
256
+ const triggers = Array.from(
257
+ element.querySelectorAll(
258
+ `[data-shadcn-phlexcomponents="${name}-trigger"]`,
259
+ ),
260
+ )
261
+
262
+ const controllerElements = Array.from(
263
+ element.querySelectorAll(`[data-controller="${name}"]`),
264
+ ) as HTMLElement[]
265
+
266
+ controllerElements.forEach((controller) => {
267
+ const stimulusInstance = getStimulusInstance<NestedComponent>(
268
+ name,
269
+ controller,
270
+ )
271
+
272
+ if (stimulusInstance) {
273
+ components.push(stimulusInstance)
274
+ }
275
+ })
276
+
277
+ triggers.forEach((trigger) => {
278
+ const stimulusInstance = getStimulusInstance<NestedComponent>(
279
+ name,
280
+ document.querySelector(`#${trigger.getAttribute('aria-controls')}`),
281
+ )
282
+
283
+ if (stimulusInstance) {
284
+ components.push(stimulusInstance)
285
+ }
286
+ })
287
+ })
288
+
289
+ return components.some((c) => c.isOpenValue)
290
+ }
291
+
292
+ const onClickOutside = (
293
+ controller: DropdownMenu | Select | Popover | Combobox,
294
+ event: MouseEvent,
295
+ ) => {
296
+ const target = event.target as HTMLElement
297
+ // Let trigger handle state
298
+ if (target === controller.triggerTarget) return
299
+ if (controller.triggerTarget.contains(target)) return
300
+
301
+ controller.close()
302
+ }
303
+
304
+ const setGroupLabelsId = (controller: Select | Command | Combobox) => {
305
+ controller.groupTargets.forEach((g) => {
306
+ const label = g.querySelector(
307
+ `[data-shadcn-phlexcomponents="${controller.identifier}-label"]`,
308
+ ) as HTMLElement
309
+
310
+ if (label) {
311
+ label.id = g.getAttribute('aria-labelledby') as string
312
+ }
313
+ })
314
+ }
315
+
316
+ const getNextEnabledIndex = ({
317
+ items,
318
+ currentIndex,
319
+ wrapAround,
320
+ filterFn,
321
+ }: {
322
+ items: HTMLElement[]
323
+ currentIndex: number
324
+ wrapAround: boolean
325
+ filterFn?: (item: HTMLElement) => boolean
326
+ }) => {
327
+ let newIndex = null as number | null
328
+
329
+ if (filterFn) {
330
+ newIndex = items.findIndex(
331
+ (item, index) => index > currentIndex && filterFn(item),
332
+ )
333
+
334
+ if (newIndex === -1) {
335
+ newIndex = currentIndex
336
+ }
337
+ } else {
338
+ newIndex = currentIndex + 1
339
+ }
340
+
341
+ if (newIndex > items.length - 1) {
342
+ if (wrapAround) {
343
+ newIndex = 0
344
+ } else {
345
+ newIndex = items.length - 1
346
+ }
347
+ }
348
+
349
+ return newIndex
350
+ }
351
+
352
+ const getPreviousEnabledIndex = ({
353
+ items,
354
+ currentIndex,
355
+ wrapAround,
356
+ filterFn,
357
+ }: {
358
+ items: HTMLElement[]
359
+ currentIndex: number
360
+ wrapAround: boolean
361
+ filterFn?: (item: HTMLElement) => boolean
362
+ }) => {
363
+ let newIndex = null as number | null
364
+
365
+ if (filterFn) {
366
+ newIndex = items.findLastIndex(
367
+ (item, index) => index < currentIndex && filterFn(item),
368
+ )
369
+
370
+ if (newIndex === -1) {
371
+ newIndex = currentIndex
372
+ }
373
+ } else {
374
+ newIndex = currentIndex - 1
375
+ }
376
+
377
+ if (newIndex < 0) {
378
+ if (wrapAround) {
379
+ newIndex = items.length - 1
380
+ } else {
381
+ newIndex = 0
382
+ }
383
+ }
384
+
385
+ return newIndex
386
+ }
387
+
388
+ const handleTabNavigation = (element: HTMLElement, event: KeyboardEvent) => {
389
+ const focusableElements = getFocusableElements(element)
390
+ const firstElement = focusableElements[0]
391
+ const lastElement = focusableElements[focusableElements.length - 1]
392
+
393
+ // If Shift + Tab pressed on first element, go to last element
394
+ if (event.shiftKey && document.activeElement === firstElement) {
395
+ event.preventDefault()
396
+ lastElement.focus()
397
+ }
398
+ // If Tab pressed on last element, go to first element
399
+ else if (!event.shiftKey && document.activeElement === lastElement) {
400
+ event.preventDefault()
401
+ firstElement.focus()
402
+ }
403
+ }
404
+
405
+ export {
406
+ ANIMATION_OUT_DELAY,
407
+ ON_CLOSE_FOCUS_DELAY,
408
+ ON_OPEN_FOCUS_DELAY,
409
+ lockScroll,
410
+ unlockScroll,
411
+ focusTrigger,
412
+ focusElement,
413
+ getFocusableElements,
414
+ getSameLevelItems,
415
+ showContent,
416
+ hideContent,
417
+ getStimulusInstance,
418
+ anyNestedComponentsOpen,
419
+ onClickOutside,
420
+ setGroupLabelsId,
421
+ getNextEnabledIndex,
422
+ getPreviousEnabledIndex,
423
+ handleTabNavigation,
424
+ }
@@ -2,7 +2,8 @@
2
2
 
3
3
  components_path = File.expand_path("../shadcn_phlexcomponents/components", __dir__)
4
4
  components_install_path = Rails.root.join("vendor/shadcn_phlexcomponents/components")
5
- stimulus_controllers_path = File.expand_path("../../app/javascript", __dir__)
5
+ stimulus_js_controllers_path = File.expand_path("../../app/javascript", __dir__)
6
+ stimulus_ts_controllers_path = File.expand_path("../../app/typescript", __dir__)
6
7
  stimulus_controllers_install_path = Rails.root.join("vendor/shadcn_phlexcomponents/javascript")
7
8
  css_path = File.expand_path("../../app/stylesheets", __dir__)
8
9
  css_install_path = Rails.root.join("vendor/shadcn_phlexcomponents/stylesheets")
@@ -14,12 +15,18 @@ say "Please make sure to commit or stash your existing changes in your working d
14
15
 
15
16
  if ENV["ENVIRONMENT"] == "test"
16
17
  directory(components_path, components_install_path)
17
- directory(stimulus_controllers_path, stimulus_controllers_install_path)
18
+ directory(stimulus_js_controllers_path, stimulus_controllers_install_path)
18
19
  directory(css_path, css_install_path)
19
20
  copy_file(initializer_file_path, initializer_file_install_path)
20
21
  elsif yes?("Do you want to continue? (y/n)")
22
+ using_typescript = yes?("Are you using Typescript?")
23
+
24
+ if using_typescript
25
+ directory(stimulus_ts_controllers_path, stimulus_controllers_install_path)
26
+ else
27
+ directory(stimulus_js_controllers_path, stimulus_controllers_install_path)
28
+ end
21
29
  directory(components_path, components_install_path)
22
- directory(stimulus_controllers_path, stimulus_controllers_install_path)
23
30
  directory(css_path, css_install_path)
24
31
  copy_file(initializer_file_path, initializer_file_install_path)
25
32
  end
@@ -14,7 +14,9 @@ CheckboxGroup = ShadcnPhlexcomponents::CheckboxGroup
14
14
  Checkbox = ShadcnPhlexcomponents::Checkbox
15
15
  Collapsible = ShadcnPhlexcomponents::Collapsible
16
16
  Command = ShadcnPhlexcomponents::Command
17
+ CommandItem = ShadcnPhlexcomponents::CommandItem
17
18
  Combobox = ShadcnPhlexcomponents::Combobox
19
+ ComboboxItem = ShadcnPhlexcomponents::ComboboxItem
18
20
  DatePicker = ShadcnPhlexcomponents::DatePicker
19
21
  DateRangePicker = ShadcnPhlexcomponents::DateRangePicker
20
22
  Dialog = ShadcnPhlexcomponents::Dialog
@@ -42,4 +44,5 @@ Textarea = ShadcnPhlexcomponents::Textarea
42
44
  ThemeSwitcher = ShadcnPhlexcomponents::ThemeSwitcher
43
45
  Toast = ShadcnPhlexcomponents::Toast
44
46
  ToastContainer = ShadcnPhlexcomponents::ToastContainer
47
+ Toggle = ShadcnPhlexcomponents::Toggle
45
48
  Tooltip = ShadcnPhlexcomponents::Tooltip
@@ -81,6 +81,7 @@ module ShadcnPhlexcomponents
81
81
  },
82
82
  data: {
83
83
  state: "closed",
84
+ accordion_target: "trigger",
84
85
  action: <<~HEREDOC,
85
86
  click->accordion#toggle
86
87
  keydown.up->accordion#focusTrigger:prevent
@@ -119,7 +120,7 @@ module ShadcnPhlexcomponents
119
120
  },
120
121
  data: {
121
122
  state: "closed",
122
- shadcn_phlexcomponents: "accordion-content-container",
123
+ accordion_target: "content"
123
124
  },
124
125
  ) do
125
126
  div(**@attributes, &)