shadcn_phlexcomponents 0.1.11 → 0.1.14
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.ts +65 -62
- data/app/javascript/controllers/alert_dialog_controller.ts +12 -0
- data/app/javascript/controllers/avatar_controller.ts +7 -2
- data/app/javascript/controllers/checkbox_controller.ts +11 -4
- data/app/javascript/controllers/collapsible_controller.ts +12 -5
- data/app/javascript/controllers/combobox_controller.ts +270 -39
- data/app/javascript/controllers/command_controller.ts +223 -51
- data/app/javascript/controllers/date_picker_controller.ts +185 -125
- data/app/javascript/controllers/date_range_picker_controller.ts +89 -79
- data/app/javascript/controllers/dialog_controller.ts +59 -57
- data/app/javascript/controllers/dropdown_menu_controller.ts +212 -36
- data/app/javascript/controllers/dropdown_menu_sub_controller.ts +31 -29
- data/app/javascript/controllers/form_field_controller.ts +6 -1
- data/app/javascript/controllers/hover_card_controller.ts +36 -26
- data/app/javascript/controllers/loading_button_controller.ts +6 -1
- data/app/javascript/controllers/popover_controller.ts +42 -65
- data/app/javascript/controllers/progress_controller.ts +9 -3
- data/app/javascript/controllers/radio_group_controller.ts +16 -9
- data/app/javascript/controllers/select_controller.ts +206 -65
- data/app/javascript/controllers/slider_controller.ts +23 -16
- data/app/javascript/controllers/switch_controller.ts +11 -4
- data/app/javascript/controllers/tabs_controller.ts +26 -18
- data/app/javascript/controllers/theme_switcher_controller.ts +6 -1
- data/app/javascript/controllers/toast_container_controller.ts +6 -1
- data/app/javascript/controllers/toast_controller.ts +7 -1
- data/app/javascript/controllers/toggle_controller.ts +28 -0
- data/app/javascript/controllers/toggle_group_controller.ts +28 -0
- data/app/javascript/controllers/tooltip_controller.ts +43 -31
- data/app/javascript/shadcn_phlexcomponents.ts +29 -25
- data/app/javascript/utils/command.ts +544 -0
- data/app/javascript/utils/floating_ui.ts +196 -0
- data/app/javascript/utils/index.ts +417 -0
- data/app/stylesheets/date_picker.css +118 -0
- 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 +9 -4
- data/app/javascript/controllers/command_root_controller.ts +0 -355
- data/app/javascript/controllers/dropdown_menu_root_controller.ts +0 -234
- data/app/javascript/utils.ts +0 -437
@@ -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,417 @@
|
|
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
|
+
|
13
|
+
const ANIMATION_OUT_DELAY = 100
|
14
|
+
const ON_OPEN_FOCUS_DELAY = 100
|
15
|
+
const ON_CLOSE_FOCUS_DELAY = 50
|
16
|
+
|
17
|
+
const getScrollbarWidth = () => {
|
18
|
+
// Create a temporary div container and append it into the body
|
19
|
+
const outer = document.createElement('div')
|
20
|
+
outer.style.visibility = 'hidden'
|
21
|
+
outer.style.overflow = 'scroll' // force scrollbars
|
22
|
+
outer.style.width = '100px'
|
23
|
+
outer.style.position = 'absolute'
|
24
|
+
outer.style.top = '-9999px'
|
25
|
+
document.body.appendChild(outer)
|
26
|
+
|
27
|
+
// Create an inner div and place it inside the outer div
|
28
|
+
const inner = document.createElement('div')
|
29
|
+
inner.style.width = '100%'
|
30
|
+
outer.appendChild(inner)
|
31
|
+
|
32
|
+
// Calculate the scrollbar width
|
33
|
+
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
|
34
|
+
|
35
|
+
// Clean up
|
36
|
+
outer.remove()
|
37
|
+
|
38
|
+
return scrollbarWidth
|
39
|
+
}
|
40
|
+
|
41
|
+
const lockScroll = (contentId: string) => {
|
42
|
+
if (window.innerHeight < document.documentElement.scrollHeight) {
|
43
|
+
document.body.dataset.scrollLocked = '1'
|
44
|
+
document.body.classList.add(
|
45
|
+
'data-[scroll-locked]:pointer-events-none',
|
46
|
+
'data-[scroll-locked]:!overflow-hidden',
|
47
|
+
'data-[scroll-locked]:!relative',
|
48
|
+
'data-[scroll-locked]:px-0',
|
49
|
+
'data-[scroll-locked]:pt-0',
|
50
|
+
'data-[scroll-locked]:ml-0',
|
51
|
+
'data-[scroll-locked]:mt-0',
|
52
|
+
)
|
53
|
+
document.body.style.marginRight = `${getScrollbarWidth()}px`
|
54
|
+
|
55
|
+
const contentIdsString =
|
56
|
+
document.body.dataset.scrollLockedContentIds || '[]'
|
57
|
+
const contentIds = JSON.parse(contentIdsString)
|
58
|
+
|
59
|
+
contentIds.push(contentId)
|
60
|
+
document.body.dataset.scrollLockedContentIds = JSON.stringify(contentIds)
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
const unlockScroll = (contentId: string) => {
|
65
|
+
const contentIdsString = document.body.dataset.scrollLockedContentIds || '[]'
|
66
|
+
const contentIds = JSON.parse(contentIdsString)
|
67
|
+
const newContentIds = contentIds.filter((id: string) => id !== contentId)
|
68
|
+
document.body.dataset.scrollLockedContentIds = JSON.stringify(newContentIds)
|
69
|
+
|
70
|
+
if (newContentIds.length === 0) {
|
71
|
+
delete document.body.dataset.scrollLocked
|
72
|
+
document.body.classList.remove(
|
73
|
+
'data-[scroll-locked]:pointer-events-none',
|
74
|
+
'data-[scroll-locked]:!overflow-hidden',
|
75
|
+
'data-[scroll-locked]:!relative',
|
76
|
+
'data-[scroll-locked]:px-0',
|
77
|
+
'data-[scroll-locked]:pt-0',
|
78
|
+
'data-[scroll-locked]:ml-0',
|
79
|
+
'data-[scroll-locked]:mt-0',
|
80
|
+
)
|
81
|
+
|
82
|
+
document.body.style.marginRight = ''
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
const focusTrigger = (triggerTarget: HTMLElement) => {
|
87
|
+
setTimeout(() => {
|
88
|
+
if (triggerTarget.dataset.asChild === 'false') {
|
89
|
+
const childElement = triggerTarget.firstElementChild as HTMLElement
|
90
|
+
|
91
|
+
if (childElement) {
|
92
|
+
childElement.focus()
|
93
|
+
}
|
94
|
+
} else {
|
95
|
+
triggerTarget.focus()
|
96
|
+
}
|
97
|
+
}, ON_CLOSE_FOCUS_DELAY)
|
98
|
+
}
|
99
|
+
|
100
|
+
const focusElement = (element?: HTMLElement | null) => {
|
101
|
+
setTimeout(() => {
|
102
|
+
if (element) {
|
103
|
+
element.focus()
|
104
|
+
}
|
105
|
+
}, ON_OPEN_FOCUS_DELAY)
|
106
|
+
}
|
107
|
+
|
108
|
+
const getFocusableElements = (container: HTMLElement) => {
|
109
|
+
return Array.from(
|
110
|
+
container.querySelectorAll(
|
111
|
+
'button, [href], input:not([type="hidden"]), select:not([tabindex="-1"]), textarea, [tabindex]:not([tabindex="-1"])',
|
112
|
+
),
|
113
|
+
) as HTMLElement[]
|
114
|
+
}
|
115
|
+
|
116
|
+
const getSameLevelItems = ({
|
117
|
+
content,
|
118
|
+
items,
|
119
|
+
closestContentSelector,
|
120
|
+
}: {
|
121
|
+
content: HTMLElement
|
122
|
+
items: HTMLElement[]
|
123
|
+
closestContentSelector: string
|
124
|
+
}) => {
|
125
|
+
const sameLevelItems = [] as HTMLElement[]
|
126
|
+
|
127
|
+
items.forEach((i) => {
|
128
|
+
if (
|
129
|
+
i.closest(closestContentSelector) === content &&
|
130
|
+
i.dataset.disabled === undefined
|
131
|
+
) {
|
132
|
+
sameLevelItems.push(i)
|
133
|
+
}
|
134
|
+
})
|
135
|
+
|
136
|
+
return sameLevelItems
|
137
|
+
}
|
138
|
+
|
139
|
+
const showContent = ({
|
140
|
+
trigger,
|
141
|
+
content,
|
142
|
+
contentContainer,
|
143
|
+
setEqualWidth,
|
144
|
+
overlay,
|
145
|
+
}: {
|
146
|
+
trigger?: HTMLElement
|
147
|
+
content: HTMLElement
|
148
|
+
contentContainer: HTMLElement
|
149
|
+
overlay?: HTMLElement
|
150
|
+
setEqualWidth?: boolean
|
151
|
+
appendToBody?: boolean
|
152
|
+
}) => {
|
153
|
+
contentContainer.style.display = ''
|
154
|
+
|
155
|
+
if (trigger) {
|
156
|
+
if (setEqualWidth) {
|
157
|
+
const triggerWidth = trigger.offsetWidth
|
158
|
+
const contentContainerWidth = contentContainer.offsetWidth
|
159
|
+
|
160
|
+
if (contentContainerWidth < triggerWidth) {
|
161
|
+
contentContainer.style.width = `${triggerWidth}px`
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
trigger.ariaExpanded = 'true'
|
166
|
+
trigger.dataset.state = 'open'
|
167
|
+
}
|
168
|
+
|
169
|
+
content.dataset.state = 'open'
|
170
|
+
|
171
|
+
if (overlay) {
|
172
|
+
overlay.style.display = ''
|
173
|
+
overlay.dataset.state = 'open'
|
174
|
+
lockScroll(content.id)
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
const hideContent = ({
|
179
|
+
trigger,
|
180
|
+
content,
|
181
|
+
contentContainer,
|
182
|
+
overlay,
|
183
|
+
}: {
|
184
|
+
trigger?: HTMLElement
|
185
|
+
content: HTMLElement
|
186
|
+
contentContainer: HTMLElement
|
187
|
+
overlay?: HTMLElement
|
188
|
+
}) => {
|
189
|
+
if (trigger) {
|
190
|
+
trigger.ariaExpanded = 'false'
|
191
|
+
trigger.dataset.state = 'closed'
|
192
|
+
}
|
193
|
+
|
194
|
+
content.dataset.state = 'closed'
|
195
|
+
|
196
|
+
setTimeout(() => {
|
197
|
+
contentContainer.style.display = 'none'
|
198
|
+
|
199
|
+
if (overlay) {
|
200
|
+
overlay.style.display = 'none'
|
201
|
+
overlay.dataset.state = 'closed'
|
202
|
+
unlockScroll(content.id)
|
203
|
+
}
|
204
|
+
}, ANIMATION_OUT_DELAY)
|
205
|
+
}
|
206
|
+
|
207
|
+
const getStimulusInstance = <T>(
|
208
|
+
controller: string,
|
209
|
+
element: HTMLElement | null,
|
210
|
+
) => {
|
211
|
+
if (!element) return
|
212
|
+
|
213
|
+
return window.Stimulus.getControllerForElementAndIdentifier(
|
214
|
+
element,
|
215
|
+
controller,
|
216
|
+
) as T
|
217
|
+
}
|
218
|
+
|
219
|
+
type NestedComponent =
|
220
|
+
| DropdownMenu
|
221
|
+
| Select
|
222
|
+
| Popover
|
223
|
+
| Command
|
224
|
+
| Combobox
|
225
|
+
| Dialog
|
226
|
+
| AlertDialog
|
227
|
+
| HoverCard
|
228
|
+
| Tooltip
|
229
|
+
| DatePicker
|
230
|
+
| DateRangePicker
|
231
|
+
const anyNestedComponentsOpen = (element: HTMLElement) => {
|
232
|
+
const components = [] as NestedComponent[]
|
233
|
+
|
234
|
+
const componentNames = [
|
235
|
+
'dialog',
|
236
|
+
'alert-dialog',
|
237
|
+
'dropdown-menu',
|
238
|
+
'popover',
|
239
|
+
'select',
|
240
|
+
'combobox',
|
241
|
+
'command',
|
242
|
+
'hover-card',
|
243
|
+
'tooltip',
|
244
|
+
'date-picker',
|
245
|
+
'date-range-picker',
|
246
|
+
]
|
247
|
+
|
248
|
+
componentNames.forEach((name) => {
|
249
|
+
const triggers = Array.from(
|
250
|
+
element.querySelectorAll(
|
251
|
+
`[data-shadcn-phlexcomponents="${name}-trigger"]`,
|
252
|
+
),
|
253
|
+
)
|
254
|
+
|
255
|
+
const controllerElements = Array.from(
|
256
|
+
element.querySelectorAll(`[data-controller="${name}"]`),
|
257
|
+
) as HTMLElement[]
|
258
|
+
|
259
|
+
controllerElements.forEach((controller) => {
|
260
|
+
const stimulusInstance = getStimulusInstance<NestedComponent>(
|
261
|
+
name,
|
262
|
+
controller,
|
263
|
+
)
|
264
|
+
|
265
|
+
if (stimulusInstance) {
|
266
|
+
components.push(stimulusInstance)
|
267
|
+
}
|
268
|
+
})
|
269
|
+
|
270
|
+
triggers.forEach((trigger) => {
|
271
|
+
const stimulusInstance = getStimulusInstance<NestedComponent>(
|
272
|
+
name,
|
273
|
+
document.querySelector(`#${trigger.getAttribute('aria-controls')}`),
|
274
|
+
)
|
275
|
+
|
276
|
+
if (stimulusInstance) {
|
277
|
+
components.push(stimulusInstance)
|
278
|
+
}
|
279
|
+
})
|
280
|
+
})
|
281
|
+
|
282
|
+
return components.some((c) => c.isOpenValue)
|
283
|
+
}
|
284
|
+
|
285
|
+
const onClickOutside = (
|
286
|
+
controller: DropdownMenu | Select | Popover | Combobox,
|
287
|
+
event: MouseEvent,
|
288
|
+
) => {
|
289
|
+
const target = event.target as HTMLElement
|
290
|
+
// Let trigger handle state
|
291
|
+
if (target === controller.triggerTarget) return
|
292
|
+
if (controller.triggerTarget.contains(target)) return
|
293
|
+
|
294
|
+
controller.close()
|
295
|
+
}
|
296
|
+
|
297
|
+
const setGroupLabelsId = (controller: Select | Command | Combobox) => {
|
298
|
+
controller.groupTargets.forEach((g) => {
|
299
|
+
const label = g.querySelector(
|
300
|
+
`[data-shadcn-phlexcomponents="${controller.identifier}-label"]`,
|
301
|
+
) as HTMLElement
|
302
|
+
|
303
|
+
if (label) {
|
304
|
+
label.id = g.getAttribute('aria-labelledby') as string
|
305
|
+
}
|
306
|
+
})
|
307
|
+
}
|
308
|
+
|
309
|
+
const getNextEnabledIndex = ({
|
310
|
+
items,
|
311
|
+
currentIndex,
|
312
|
+
wrapAround,
|
313
|
+
filterFn,
|
314
|
+
}: {
|
315
|
+
items: HTMLElement[]
|
316
|
+
currentIndex: number
|
317
|
+
wrapAround: boolean
|
318
|
+
filterFn?: (item: HTMLElement) => boolean
|
319
|
+
}) => {
|
320
|
+
let newIndex = null as number | null
|
321
|
+
|
322
|
+
if (filterFn) {
|
323
|
+
newIndex = items.findIndex(
|
324
|
+
(item, index) => index > currentIndex && filterFn(item),
|
325
|
+
)
|
326
|
+
|
327
|
+
if (newIndex === -1) {
|
328
|
+
newIndex = currentIndex
|
329
|
+
}
|
330
|
+
} else {
|
331
|
+
newIndex = currentIndex + 1
|
332
|
+
}
|
333
|
+
|
334
|
+
if (newIndex > items.length - 1) {
|
335
|
+
if (wrapAround) {
|
336
|
+
newIndex = 0
|
337
|
+
} else {
|
338
|
+
newIndex = items.length - 1
|
339
|
+
}
|
340
|
+
}
|
341
|
+
|
342
|
+
return newIndex
|
343
|
+
}
|
344
|
+
|
345
|
+
const getPreviousEnabledIndex = ({
|
346
|
+
items,
|
347
|
+
currentIndex,
|
348
|
+
wrapAround,
|
349
|
+
filterFn,
|
350
|
+
}: {
|
351
|
+
items: HTMLElement[]
|
352
|
+
currentIndex: number
|
353
|
+
wrapAround: boolean
|
354
|
+
filterFn?: (item: HTMLElement) => boolean
|
355
|
+
}) => {
|
356
|
+
let newIndex = null as number | null
|
357
|
+
|
358
|
+
if (filterFn) {
|
359
|
+
newIndex = items.findLastIndex(
|
360
|
+
(item, index) => index < currentIndex && filterFn(item),
|
361
|
+
)
|
362
|
+
|
363
|
+
if (newIndex === -1) {
|
364
|
+
newIndex = currentIndex
|
365
|
+
}
|
366
|
+
} else {
|
367
|
+
newIndex = currentIndex - 1
|
368
|
+
}
|
369
|
+
|
370
|
+
if (newIndex < 0) {
|
371
|
+
if (wrapAround) {
|
372
|
+
newIndex = items.length - 1
|
373
|
+
} else {
|
374
|
+
newIndex = 0
|
375
|
+
}
|
376
|
+
}
|
377
|
+
|
378
|
+
return newIndex
|
379
|
+
}
|
380
|
+
|
381
|
+
const handleTabNavigation = (element: HTMLElement, event: KeyboardEvent) => {
|
382
|
+
const focusableElements = getFocusableElements(element)
|
383
|
+
const firstElement = focusableElements[0]
|
384
|
+
const lastElement = focusableElements[focusableElements.length - 1]
|
385
|
+
|
386
|
+
// If Shift + Tab pressed on first element, go to last element
|
387
|
+
if (event.shiftKey && document.activeElement === firstElement) {
|
388
|
+
event.preventDefault()
|
389
|
+
lastElement.focus()
|
390
|
+
}
|
391
|
+
// If Tab pressed on last element, go to first element
|
392
|
+
else if (!event.shiftKey && document.activeElement === lastElement) {
|
393
|
+
event.preventDefault()
|
394
|
+
firstElement.focus()
|
395
|
+
}
|
396
|
+
}
|
397
|
+
|
398
|
+
export {
|
399
|
+
ANIMATION_OUT_DELAY,
|
400
|
+
ON_CLOSE_FOCUS_DELAY,
|
401
|
+
ON_OPEN_FOCUS_DELAY,
|
402
|
+
lockScroll,
|
403
|
+
unlockScroll,
|
404
|
+
focusTrigger,
|
405
|
+
focusElement,
|
406
|
+
getFocusableElements,
|
407
|
+
getSameLevelItems,
|
408
|
+
showContent,
|
409
|
+
hideContent,
|
410
|
+
getStimulusInstance,
|
411
|
+
anyNestedComponentsOpen,
|
412
|
+
onClickOutside,
|
413
|
+
setGroupLabelsId,
|
414
|
+
getNextEnabledIndex,
|
415
|
+
getPreviousEnabledIndex,
|
416
|
+
handleTabNavigation,
|
417
|
+
}
|