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,200 +0,0 @@
1
- import DropdownMenuRootController from './dropdown_menu_root_controller'
2
-
3
- export default class extends DropdownMenuRootController {
4
- static targets = [
5
- 'trigger',
6
- 'contentContainer',
7
- 'content',
8
- 'item',
9
- 'triggerText',
10
- 'group',
11
- 'label',
12
- 'select',
13
- ]
14
-
15
- static values = {
16
- isOpen: Boolean,
17
- selected: String,
18
- setEqualWidth: { type: Boolean, default: true },
19
- closestContentSelector: {
20
- type: String,
21
- default: '[data-select-target="content"]',
22
- },
23
- }
24
-
25
- declare selectedValue: string
26
- declare searchString: string
27
- declare searchTimeout: number
28
- declare groupTargets: HTMLElement[]
29
- declare triggerTextTarget: HTMLElement
30
- declare selectTarget: HTMLSelectElement
31
- declare itemsInnerText: string[]
32
-
33
- connect() {
34
- super.connect()
35
- this.itemsInnerText = this.items.map((i) => i.innerText.trim())
36
- this.setAriaLabelledby()
37
- this.searchString = ''
38
- }
39
-
40
- setAriaLabelledby() {
41
- this.groupTargets.forEach((g) => {
42
- const label = g.querySelector(
43
- '[data-select-target="label"]',
44
- ) as HTMLElement
45
-
46
- if (label) {
47
- label.id = g.getAttribute('aria-labelledby') as string
48
- }
49
- })
50
- }
51
-
52
- onOpenFocusedElement(event: MouseEvent | KeyboardEvent) {
53
- let itemIndex = null as number | null
54
-
55
- if (this.selectedValue) {
56
- const item = this.itemTargets.find(
57
- (i) => i.dataset.value === this.selectedValue,
58
- )
59
-
60
- if (item && !item.dataset.disabled) {
61
- itemIndex = this.items.indexOf(item)
62
- }
63
- } else {
64
- if (event instanceof KeyboardEvent) {
65
- const key = event.key
66
-
67
- if (['ArrowDown', 'Enter', ' '].includes(key)) {
68
- itemIndex = 0
69
- }
70
- }
71
- }
72
-
73
- if (itemIndex !== null) {
74
- return this.items[itemIndex]
75
- } else {
76
- return this.contentTarget
77
- }
78
- }
79
-
80
- onSelect(event: MouseEvent | KeyboardEvent) {
81
- const item = event.currentTarget as HTMLElement
82
- const value = item.dataset.value as string
83
- this.selectedValue = value
84
- }
85
-
86
- onDOMKeydown(event: KeyboardEvent) {
87
- super.onDOMKeydown(event)
88
-
89
- const { key, altKey, ctrlKey, metaKey } = event
90
-
91
- if (
92
- key === 'Backspace' ||
93
- key === 'Clear' ||
94
- (key.length === 1 && key !== ' ' && !altKey && !ctrlKey && !metaKey)
95
- ) {
96
- this.handleSearch(key)
97
- }
98
- }
99
-
100
- // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
101
- handleSearch(char: string) {
102
- const searchString = this.getSearchString(char)
103
- const focusedItem = this.items.find(
104
- (item) => document.activeElement === item,
105
- )
106
- const focusedIndex = focusedItem ? this.items.indexOf(focusedItem) : 0
107
- const searchIndex = this.getIndexByLetter(searchString, focusedIndex + 1)
108
-
109
- // if a match was found, go to it
110
- if (searchIndex >= 0) {
111
- this.focusItemByIndex(null, searchIndex)
112
- }
113
- // if no matches, clear the timeout and search string
114
- else {
115
- window.clearTimeout(this.searchTimeout)
116
- this.searchString = ''
117
- }
118
- }
119
-
120
- filterItemsInnerText(items: string[], filter: string) {
121
- return items.filter((item) => {
122
- const matches = item.toLowerCase().indexOf(filter.toLowerCase()) === 0
123
- return matches
124
- })
125
- }
126
-
127
- getSearchString(char: string) {
128
- // reset typing timeout and start new timeout
129
- // this allows us to make multiple-letter matches, like a native select
130
- if (typeof this.searchTimeout === 'number') {
131
- window.clearTimeout(this.searchTimeout)
132
- }
133
-
134
- this.searchTimeout = window.setTimeout(() => {
135
- this.searchString = ''
136
- }, 500)
137
-
138
- // add most recent letter to saved search string
139
- this.searchString += char
140
- return this.searchString
141
- }
142
-
143
- // return the index of an option from an array of options, based on a search string
144
- // if the filter is multiple iterations of the same letter (e.g "aaa"), then cycle through first-letter matches
145
- getIndexByLetter(filter: string, startIndex: number) {
146
- const orderedItems = [
147
- ...this.itemsInnerText.slice(startIndex),
148
- ...this.itemsInnerText.slice(0, startIndex),
149
- ]
150
-
151
- const firstMatch = this.filterItemsInnerText(orderedItems, filter)[0]
152
-
153
- const allSameLetter = (array: string[]) =>
154
- array.every((letter) => letter === array[0])
155
-
156
- // first check if there is an exact match for the typed string
157
- if (firstMatch) {
158
- const index = this.itemsInnerText.indexOf(firstMatch)
159
- return index
160
- }
161
-
162
- // if the same letter is being repeated, cycle through first-letter matches
163
- else if (allSameLetter(filter.split(''))) {
164
- const matches = this.filterItemsInnerText(orderedItems, filter[0])
165
- const index = this.itemsInnerText.indexOf(matches[0])
166
- return index
167
- }
168
-
169
- // if no matches, return -1
170
- else {
171
- return -1
172
- }
173
- }
174
-
175
- selectedValueChanged(value: string) {
176
- const item = this.itemTargets.find((i) => i.dataset.value === value)
177
-
178
- if (item) {
179
- this.triggerTextTarget.textContent = item.textContent
180
-
181
- this.itemTargets.forEach((i) => {
182
- if (i.dataset.value === value) {
183
- i.setAttribute('aria-selected', 'true')
184
- } else {
185
- i.setAttribute('aria-selected', 'false')
186
- }
187
- })
188
-
189
- this.selectTarget.value = value
190
- }
191
-
192
- this.triggerTarget.dataset.hasValue = `${!!value && value.length > 0}`
193
-
194
- const placeholder = this.triggerTarget.dataset.placeholder
195
-
196
- if (placeholder && this.triggerTarget.dataset.hasValue === 'false') {
197
- this.triggerTextTarget.textContent = placeholder
198
- }
199
- }
200
- }
@@ -1,57 +0,0 @@
1
- import AccordionController from './controllers/accordion_controller'
2
- import AvatarController from './controllers/avatar_controller'
3
- import CheckboxController from './controllers/checkbox_controller'
4
- import CollapsibleController from './controllers/collapsible_controller'
5
- import ComboboxController from './controllers/combobox_controller'
6
- import CommandController from './controllers/command_controller'
7
- import DatePickerController from './controllers/date_picker_controller'
8
- import DateRangePickerController from './controllers/date_range_picker_controller'
9
- import DialogController from './controllers/dialog_controller'
10
- import DropdownMenuController from './controllers/dropdown_menu_controller'
11
- import DropdownMenuSubController from './controllers/dropdown_menu_sub_controller'
12
- import FormFieldController from './controllers/form_field_controller'
13
- import HoverCardController from './controllers/hover_card_controller'
14
- import LoadingButtonController from './controllers/loading_button_controller'
15
- import PopoverController from './controllers/popover_controller'
16
- import ProgressController from './controllers/progress_controller'
17
- import RadioGroupController from './controllers/radio_group_controller'
18
- import SelectController from './controllers/select_controller'
19
- import SidebarController from './controllers/sidebar_controller'
20
- import SidebarTriggerController from './controllers/sidebar_trigger_controller'
21
- import SliderController from './controllers/slider_controller'
22
- import SwitchController from './controllers/switch_controller'
23
- import TabsController from './controllers/tabs_controller'
24
- import ThemeSwitcherController from './controllers/theme_switcher_controller'
25
- import ToastContainerController from './controllers/toast_container_controller'
26
- import ToastController from './controllers/toast_controller'
27
- import TooltipController from './controllers/tooltip_controller'
28
-
29
- export default {
30
- accordion: AccordionController,
31
- avatar: AvatarController,
32
- checkbox: CheckboxController,
33
- collapsible: CollapsibleController,
34
- combobox: ComboboxController,
35
- command: CommandController,
36
- 'date-picker': DatePickerController,
37
- 'date-range-picker': DateRangePickerController,
38
- dialog: DialogController,
39
- 'dropdown-menu': DropdownMenuController,
40
- 'dropdown-menu-sub': DropdownMenuSubController,
41
- 'form-field': FormFieldController,
42
- 'hover-card': HoverCardController,
43
- 'loading-button': LoadingButtonController,
44
- popover: PopoverController,
45
- progress: ProgressController,
46
- 'radio-group': RadioGroupController,
47
- select: SelectController,
48
- sidebar: SidebarController,
49
- 'sidebar-trigger': SidebarTriggerController,
50
- slider: SliderController,
51
- switch: SwitchController,
52
- tabs: TabsController,
53
- 'theme-switcher': ThemeSwitcherController,
54
- 'toast-container': ToastContainerController,
55
- toast: ToastController,
56
- tooltip: TooltipController,
57
- }
@@ -1,437 +0,0 @@
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 ANIMATION_OUT_DELAY = 100
14
- const ON_OPEN_FOCUS_DELAY = 100
15
- const ON_CLOSE_FOCUS_DELAY = 50
16
-
17
- const OPPOSITE_SIDE = {
18
- top: 'bottom',
19
- right: 'left',
20
- bottom: 'top',
21
- left: 'right',
22
- }
23
-
24
- const ARROW_TRANSFORM_ORIGIN = {
25
- top: '',
26
- right: '0 0',
27
- bottom: 'center 0',
28
- left: '100% 0',
29
- }
30
-
31
- const ARROW_TRANSFORM = {
32
- top: 'translateY(100%)',
33
- right: 'translateY(50%) rotate(90deg) translateX(-50%)',
34
- bottom: `rotate(180deg)`,
35
- left: 'translateY(50%) rotate(-90deg) translateX(50%)',
36
- }
37
-
38
- const getScrollbarWidth = () => {
39
- // Create a temporary div container and append it into the body
40
- const outer = document.createElement('div')
41
- outer.style.visibility = 'hidden'
42
- outer.style.overflow = 'scroll' // force scrollbars
43
- outer.style.width = '100px'
44
- outer.style.position = 'absolute'
45
- outer.style.top = '-9999px'
46
- document.body.appendChild(outer)
47
-
48
- // Create an inner div and place it inside the outer div
49
- const inner = document.createElement('div')
50
- inner.style.width = '100%'
51
- outer.appendChild(inner)
52
-
53
- // Calculate the scrollbar width
54
- const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
55
-
56
- // Clean up
57
- outer.remove()
58
-
59
- return scrollbarWidth
60
- }
61
-
62
- const showOverlay = ({
63
- classNames = '',
64
- elementId,
65
- }: {
66
- classNames?: string
67
- elementId: string
68
- }) => {
69
- const element = document.createElement('div')
70
-
71
- let defaultClassNames = [
72
- 'data-[state=open]:animate-in',
73
- 'data-[state=closed]:animate-out',
74
- 'data-[state=closed]:fade-out-0',
75
- 'data-[state=open]:fade-in-0',
76
- 'fixed',
77
- 'inset-0',
78
- 'z-[48]',
79
- 'bg-black/50',
80
- 'pointer-events-auto',
81
- ]
82
-
83
- defaultClassNames = defaultClassNames.concat(
84
- classNames.split(' ').filter((c) => !!c),
85
- )
86
-
87
- element.classList.add(...defaultClassNames)
88
- element.dataset.state = 'open'
89
- element.dataset.shadcnPhlexcomponentsOverlay = elementId
90
- element.ariaHidden = 'true'
91
-
92
- document.body.appendChild(element)
93
- }
94
-
95
- const hideOverlay = (elementId: string) => {
96
- const element = document.querySelector(
97
- `[data-shadcn-phlexcomponents-overlay=${elementId}]`,
98
- )
99
-
100
- if (element && element instanceof HTMLElement) {
101
- element.dataset.state = 'closed'
102
-
103
- setTimeout(() => {
104
- element.remove()
105
- }, ANIMATION_OUT_DELAY)
106
- }
107
- }
108
-
109
- const lockScroll = () => {
110
- if (window.innerHeight < document.documentElement.scrollHeight) {
111
- document.body.dataset.scrollLocked = '1'
112
- document.body.classList.add(
113
- 'data-[scroll-locked]:pointer-events-none',
114
- 'data-[scroll-locked]:!overflow-hidden',
115
- 'data-[scroll-locked]:!relative',
116
- 'data-[scroll-locked]:px-0',
117
- 'data-[scroll-locked]:pt-0',
118
- 'data-[scroll-locked]:ml-0',
119
- 'data-[scroll-locked]:mt-0',
120
- )
121
- const style = getComputedStyle(document.body)
122
- const originalMarginRight = style.marginRight
123
- document.body.dataset.marginRight = originalMarginRight
124
- document.body.style.marginRight = `${getScrollbarWidth()}px`
125
- }
126
- }
127
-
128
- const unlockScroll = () => {
129
- if (document.body.dataset.scrollLocked) {
130
- delete document.body.dataset.scrollLocked
131
- document.body.classList.remove(
132
- 'data-[scroll-locked]:pointer-events-none',
133
- 'data-[scroll-locked]:!overflow-hidden',
134
- 'data-[scroll-locked]:!relative',
135
- 'data-[scroll-locked]:px-0',
136
- 'data-[scroll-locked]:pt-0',
137
- 'data-[scroll-locked]:ml-0',
138
- 'data-[scroll-locked]:mt-0',
139
- )
140
-
141
- const originalMarginRight = document.body.dataset.marginRight
142
-
143
- if (originalMarginRight && parseInt(originalMarginRight) === 0) {
144
- document.body.style.marginRight = ''
145
- } else {
146
- document.body.style.marginRight = `${originalMarginRight}`
147
- }
148
-
149
- delete document.body.dataset.marginRight
150
- }
151
- }
152
-
153
- const openWithOverlay = (elementId: string) => {
154
- showOverlay({ elementId })
155
- lockScroll()
156
- }
157
-
158
- const closeWithOverlay = (elementId: string) => {
159
- hideOverlay(elementId)
160
- unlockScroll()
161
- }
162
-
163
- const initFloatingUi = ({
164
- referenceElement,
165
- floatingElement,
166
- side = 'bottom',
167
- align = 'center',
168
- sideOffset = 0,
169
- alignOffset = 0,
170
- arrowElement,
171
- }: {
172
- referenceElement: HTMLElement
173
- floatingElement: HTMLElement
174
- side?: string
175
- align?: string
176
- sideOffset?: number
177
- alignOffset?: number
178
- offsetPx?: number
179
- arrowElement?: HTMLElement
180
- }) => {
181
- let placement = `${side}-${align}`
182
- placement = placement.replace(/-center/g, '')
183
-
184
- let arrowHeight = 0,
185
- arrowWidth = 0
186
-
187
- if (arrowElement) {
188
- const rect = arrowElement.getBoundingClientRect()
189
- arrowWidth = rect.width
190
- arrowHeight = rect.height
191
- }
192
-
193
- const middleware = [
194
- transformOrigin({ arrowHeight, arrowWidth }),
195
- offset({ mainAxis: sideOffset, alignmentAxis: alignOffset }),
196
- size({
197
- apply: ({ elements, rects, availableWidth, availableHeight }) => {
198
- const { width: anchorWidth, height: anchorHeight } = rects.reference
199
- const contentStyle = elements.floating.style
200
- contentStyle.setProperty(
201
- '--radix-popper-available-width',
202
- `${availableWidth}px`,
203
- )
204
- contentStyle.setProperty(
205
- '--radix-popper-available-height',
206
- `${availableHeight}px`,
207
- )
208
- contentStyle.setProperty(
209
- '--radix-popper-anchor-width',
210
- `${anchorWidth}px`,
211
- )
212
- contentStyle.setProperty(
213
- '--radix-popper-anchor-height',
214
- `${anchorHeight}px`,
215
- )
216
- },
217
- }),
218
- ]
219
-
220
- const flipMiddleware = flip({
221
- // Ensure we flip to the perpendicular axis if it doesn't fit
222
- // on narrow viewports.
223
- crossAxis: 'alignment',
224
- fallbackAxisSideDirection: 'end', // or 'start'
225
- })
226
- const shiftMiddleware = shift()
227
-
228
- // Prioritize flip over shift for edge-aligned placements only.
229
- if (placement.includes('-')) {
230
- middleware.push(flipMiddleware, shiftMiddleware)
231
- } else {
232
- middleware.push(shiftMiddleware, flipMiddleware)
233
- }
234
-
235
- if (arrowElement) {
236
- middleware.push(arrow({ element: arrowElement, padding: 0 }))
237
- }
238
-
239
- return autoUpdate(referenceElement, floatingElement, () => {
240
- computePosition(referenceElement, floatingElement, {
241
- placement: placement as Placement,
242
- strategy: 'fixed',
243
- middleware,
244
- }).then(({ middlewareData, x, y }) => {
245
- const arrowX = middlewareData.arrow?.x
246
- const arrowY = middlewareData.arrow?.y
247
- const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0
248
-
249
- floatingElement.style.setProperty(
250
- '--radix-popper-transform-origin',
251
- `${middlewareData.transformOrigin?.x} ${middlewareData.transformOrigin?.y}`,
252
- )
253
- if (arrowElement) {
254
- const baseSide = OPPOSITE_SIDE[side as keyof typeof OPPOSITE_SIDE]
255
-
256
- const arrowStyle = {
257
- position: 'absolute',
258
- left: arrowX ? `${arrowX}px` : undefined,
259
- top: arrowY ? `${arrowY}px` : undefined,
260
- [baseSide]: 0,
261
- transformOrigin:
262
- ARROW_TRANSFORM_ORIGIN[side as keyof typeof ARROW_TRANSFORM_ORIGIN],
263
- transform: ARROW_TRANSFORM[side as keyof typeof ARROW_TRANSFORM],
264
- visibility: cannotCenterArrow ? 'hidden' : undefined,
265
- }
266
-
267
- Object.assign(arrowElement.style, arrowStyle)
268
- }
269
- Object.assign(floatingElement.style, {
270
- left: `${x}px`,
271
- top: `${y}px`,
272
- })
273
- })
274
- })
275
- }
276
-
277
- const transformOrigin = (options: {
278
- arrowWidth: number
279
- arrowHeight: number
280
- }): Middleware => {
281
- return {
282
- name: 'transformOrigin',
283
- options,
284
- fn(data) {
285
- const { placement, rects, middlewareData } = data
286
- const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0
287
- const isArrowHidden = cannotCenterArrow
288
- const arrowWidth = isArrowHidden ? 0 : options.arrowWidth
289
- const arrowHeight = isArrowHidden ? 0 : options.arrowHeight
290
-
291
- const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement)
292
- const noArrowAlign = { start: '0%', center: '50%', end: '100%' }[
293
- placedAlign
294
- ] as string
295
-
296
- const arrowXCenter = (middlewareData.arrow?.x ?? 0) + arrowWidth / 2
297
- const arrowYCenter = (middlewareData.arrow?.y ?? 0) + arrowHeight / 2
298
-
299
- let x = ''
300
- let y = ''
301
-
302
- if (placedSide === 'bottom') {
303
- x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`
304
- y = `${-arrowHeight}px`
305
- } else if (placedSide === 'top') {
306
- x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`
307
- y = `${rects.floating.height + arrowHeight}px`
308
- } else if (placedSide === 'right') {
309
- x = `${-arrowHeight}px`
310
- y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`
311
- } else if (placedSide === 'left') {
312
- x = `${rects.floating.width + arrowHeight}px`
313
- y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`
314
- }
315
- return { data: { x, y } }
316
- },
317
- }
318
- }
319
-
320
- function getSideAndAlignFromPlacement(placement: Placement) {
321
- const [side, align = 'center'] = placement.split('-')
322
- return [side, align] as const
323
- }
324
-
325
- const focusTrigger = (triggerTarget: HTMLElement) => {
326
- setTimeout(() => {
327
- if (triggerTarget.dataset.asChild === 'false') {
328
- const childElement = triggerTarget.firstElementChild as HTMLElement
329
-
330
- if (childElement) {
331
- childElement.focus()
332
- }
333
- } else {
334
- triggerTarget.focus()
335
- }
336
- }, ON_CLOSE_FOCUS_DELAY)
337
- }
338
-
339
- const getFocusableElements = (container: HTMLElement) => {
340
- return Array.from(
341
- container.querySelectorAll(
342
- 'button, [href], input:not([type="hidden"]), select:not([tabindex="-1"]), textarea, [tabindex]:not([tabindex="-1"])',
343
- ),
344
- ) as HTMLElement[]
345
- }
346
-
347
- const getSameLevelItems = ({
348
- content,
349
- items,
350
- closestContentSelector,
351
- }: {
352
- content: HTMLElement
353
- items: HTMLElement[]
354
- closestContentSelector: string
355
- }) => {
356
- let sameLevelItems = [] as HTMLElement[]
357
-
358
- items.forEach((i) => {
359
- if (
360
- i.closest(closestContentSelector) === content &&
361
- i.dataset.disabled === undefined
362
- ) {
363
- sameLevelItems.push(i)
364
- }
365
- })
366
-
367
- return sameLevelItems
368
- }
369
-
370
- const showContent = ({
371
- trigger,
372
- content,
373
- contentContainer,
374
- setEqualWidth,
375
- }: {
376
- trigger?: HTMLElement
377
- content: HTMLElement
378
- contentContainer: HTMLElement
379
- setEqualWidth?: boolean
380
- }) => {
381
- contentContainer.classList.remove('hidden')
382
-
383
- if (trigger) {
384
- if (setEqualWidth) {
385
- const triggerWidth = trigger.offsetWidth
386
- const contentContainerWidth = contentContainer.offsetWidth
387
-
388
- if (contentContainerWidth < triggerWidth) {
389
- contentContainer.style.width = `${triggerWidth}px`
390
- }
391
- }
392
-
393
- trigger.ariaExpanded = 'true'
394
- trigger.dataset.state = 'open'
395
- }
396
-
397
- content.dataset.state = 'open'
398
- }
399
-
400
- const hideContent = ({
401
- trigger,
402
- content,
403
- contentContainer,
404
- }: {
405
- trigger?: HTMLElement
406
- content: HTMLElement
407
- contentContainer: HTMLElement
408
- }) => {
409
- if (trigger) {
410
- trigger.ariaExpanded = 'false'
411
- trigger.dataset.state = 'closed'
412
- }
413
-
414
- content.dataset.state = 'closed'
415
-
416
- setTimeout(() => {
417
- contentContainer.classList.add('hidden')
418
- }, ANIMATION_OUT_DELAY)
419
- }
420
-
421
- export {
422
- ANIMATION_OUT_DELAY,
423
- ON_CLOSE_FOCUS_DELAY,
424
- ON_OPEN_FOCUS_DELAY,
425
- showOverlay,
426
- hideOverlay,
427
- lockScroll,
428
- unlockScroll,
429
- openWithOverlay,
430
- closeWithOverlay,
431
- initFloatingUi,
432
- focusTrigger,
433
- getFocusableElements,
434
- getSameLevelItems,
435
- showContent,
436
- hideContent,
437
- }