@agility/plenum-ui 2.0.0-rc47 → 2.0.0-rc48

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 (139) hide show
  1. package/.eslintrc.json +6 -6
  2. package/.prettierrc +13 -13
  3. package/.storybook/Layout.jsx +12 -12
  4. package/.storybook/head.tsx +4 -4
  5. package/.storybook/main.ts +18 -18
  6. package/.storybook/manager-head.html +1 -1
  7. package/.storybook/manager.ts +25 -25
  8. package/.storybook/plenumTheme.ts +8 -8
  9. package/.storybook/preview-head.html +3 -3
  10. package/.storybook/preview.tsx +28 -28
  11. package/.vscode/settings.json +3 -3
  12. package/README.md +271 -271
  13. package/app/globals.css +99 -99
  14. package/app/head.tsx +59 -59
  15. package/app/layout.tsx +28 -28
  16. package/app/page.tsx +7 -7
  17. package/build.js +45 -45
  18. package/dist/index.d.ts +230 -230
  19. package/dist/index.js.map +1 -1
  20. package/local.sh +100 -100
  21. package/next.config.js +8 -8
  22. package/package.json +83 -83
  23. package/pages/api/hello.ts +13 -13
  24. package/postcss.config.js +6 -6
  25. package/rollup.config.mjs +41 -41
  26. package/scripts/create-component.js +97 -97
  27. package/stories/Introduction.mdx +314 -314
  28. package/stories/assets/stackalt.svg +1 -1
  29. package/stories/atoms/Avatar/Avatar.stories.tsx +96 -96
  30. package/stories/atoms/Avatar/Avatar.tsx +123 -123
  31. package/stories/atoms/Avatar/index.ts +3 -3
  32. package/stories/atoms/badges/Badge.tsx +127 -127
  33. package/stories/atoms/badges/Pill/Pill.stories.tsx +75 -75
  34. package/stories/atoms/badges/Rounded/Rounded.stories.tsx +75 -75
  35. package/stories/atoms/badges/index.ts +3 -3
  36. package/stories/atoms/buttons/Button/Alternative/Alternative.stories.ts +86 -86
  37. package/stories/atoms/buttons/Button/Button.tsx +232 -232
  38. package/stories/atoms/buttons/Button/Danger/Danger.stories.ts +90 -90
  39. package/stories/atoms/buttons/Button/Primary/Primary.stories.ts +97 -97
  40. package/stories/atoms/buttons/Button/Secondary/Secondary.stories.ts +93 -93
  41. package/stories/atoms/buttons/Button/defaultArgs.ts +9 -9
  42. package/stories/atoms/buttons/Button/index.ts +3 -3
  43. package/stories/atoms/buttons/Capsule/Alternative/Alternative.stories.ts +27 -27
  44. package/stories/atoms/buttons/Capsule/Capsule.tsx +88 -88
  45. package/stories/atoms/buttons/Capsule/Danger/Danger.stories.ts +27 -27
  46. package/stories/atoms/buttons/Capsule/Primary/Primary.stories.ts +27 -27
  47. package/stories/atoms/buttons/Capsule/Secondary/Secondary.stories.ts +27 -27
  48. package/stories/atoms/buttons/Capsule/index.ts +3 -3
  49. package/stories/atoms/buttons/FloatingActionButton/FloatingActionButton.stories.tsx +15 -15
  50. package/stories/atoms/buttons/FloatingActionButton/FloatingActionButton.tsx +22 -22
  51. package/stories/atoms/buttons/FloatingActionButton/index.tsx +3 -3
  52. package/stories/atoms/buttons/index.ts +4 -4
  53. package/stories/atoms/crumb/Crumb.stories.tsx +18 -18
  54. package/stories/atoms/crumb/Crumb.tsx +22 -22
  55. package/stories/atoms/crumb/index.tsx +3 -3
  56. package/stories/atoms/icons/DynamicIcon.stories.ts +43 -43
  57. package/stories/atoms/icons/DynamicIcon.tsx +90 -90
  58. package/stories/atoms/icons/IconWithShadow.stories.ts +43 -43
  59. package/stories/atoms/icons/IconWithShadow.tsx +16 -16
  60. package/stories/atoms/icons/TablerIcon.tsx +22 -22
  61. package/stories/atoms/icons/index.tsx +14 -14
  62. package/stories/atoms/icons/tablerIconNames.ts +4336 -4336
  63. package/stories/atoms/index.ts +46 -46
  64. package/stories/atoms/loaders/Loader.stories.ts +15 -15
  65. package/stories/atoms/loaders/Loader.tsx +21 -21
  66. package/stories/atoms/loaders/NProgress/RadialProgress.stories.tsx +19 -19
  67. package/stories/atoms/loaders/NProgress/RadialProgress.tsx +74 -74
  68. package/stories/atoms/loaders/NProgress/index.ts +3 -3
  69. package/stories/atoms/loaders/index.ts +4 -4
  70. package/stories/index.ts +136 -136
  71. package/stories/molecules/index.ts +51 -51
  72. package/stories/molecules/inputs/InputCounter/InputCounter.stories.tsx +18 -18
  73. package/stories/molecules/inputs/InputCounter/InputCounter.tsx +24 -24
  74. package/stories/molecules/inputs/InputCounter/index.tsx +3 -3
  75. package/stories/molecules/inputs/InputField/InputField.stories.tsx +29 -29
  76. package/stories/molecules/inputs/InputField/InputField.tsx +96 -96
  77. package/stories/molecules/inputs/InputField/index.tsx +3 -3
  78. package/stories/molecules/inputs/InputLabel/InputLabel.stories.tsx +19 -19
  79. package/stories/molecules/inputs/InputLabel/InputLabel.tsx +45 -45
  80. package/stories/molecules/inputs/InputLabel/index.tsx +3 -3
  81. package/stories/molecules/inputs/NestedInputButton/NestedInputButton.stories.tsx +52 -52
  82. package/stories/molecules/inputs/NestedInputButton/NestedInputButton.tsx +64 -64
  83. package/stories/molecules/inputs/NestedInputButton/index.tsx +3 -3
  84. package/stories/molecules/inputs/TextInput/TextInput.stories.tsx +32 -32
  85. package/stories/molecules/inputs/TextInput/TextInput.tsx +165 -165
  86. package/stories/molecules/inputs/TextInput/index.tsx +5 -5
  87. package/stories/molecules/inputs/checkbox/Checkbox.stories.ts +23 -23
  88. package/stories/molecules/inputs/checkbox/Checkbox.tsx +98 -98
  89. package/stories/molecules/inputs/checkbox/index.ts +3 -3
  90. package/stories/molecules/inputs/combobox/ComboBox.stories.ts +41 -41
  91. package/stories/molecules/inputs/combobox/ComboBox.tsx +185 -185
  92. package/stories/molecules/inputs/combobox/index.ts +3 -3
  93. package/stories/molecules/inputs/index.ts +38 -38
  94. package/stories/molecules/inputs/radio/Radio.stories.ts +27 -27
  95. package/stories/molecules/inputs/radio/Radio.tsx +92 -92
  96. package/stories/molecules/inputs/radio/index.ts +3 -3
  97. package/stories/molecules/inputs/select/Select.stories.ts +23 -23
  98. package/stories/molecules/inputs/select/Select.tsx +100 -100
  99. package/stories/molecules/inputs/select/index.ts +3 -3
  100. package/stories/molecules/inputs/textArea/TextArea.stories.ts +22 -22
  101. package/stories/molecules/inputs/textArea/TextArea.tsx +158 -158
  102. package/stories/molecules/inputs/textArea/index.ts +3 -3
  103. package/stories/molecules/inputs/toggleSwitch/ToggleSwitch.stories.tsx +118 -118
  104. package/stories/molecules/inputs/toggleSwitch/ToggleSwitch.tsx +81 -81
  105. package/stories/molecules/inputs/toggleSwitch/index.ts +3 -3
  106. package/stories/molecules/tabs/Tabs.stories.tsx +18 -18
  107. package/stories/molecules/tabs/Tabs.tsx +22 -22
  108. package/stories/molecules/tabs/index.tsx +2 -2
  109. package/stories/organisms/AnimatedLabelInput/AnimatedLabelInput.stories.tsx +30 -30
  110. package/stories/organisms/AnimatedLabelInput/AnimatedLabelInput.tsx +66 -66
  111. package/stories/organisms/AnimatedLabelInput/index.tsx +3 -3
  112. package/stories/organisms/AnimatedLabelTextArea/AnimatedLabelTextArea.stories.tsx +26 -26
  113. package/stories/organisms/AnimatedLabelTextArea/AnimatedLabelTextArea.tsx +61 -61
  114. package/stories/organisms/AnimatedLabelTextArea/index.tsx +3 -3
  115. package/stories/organisms/ButtonDropdown/ButtonDropdown.stories.tsx +125 -125
  116. package/stories/organisms/ButtonDropdown/ButtonDropdown.tsx +86 -86
  117. package/stories/organisms/ButtonDropdown/index.tsx +3 -3
  118. package/stories/organisms/DropdownComponent/Dropdown.stories.tsx +73 -73
  119. package/stories/organisms/DropdownComponent/DropdownComponent.tsx +346 -346
  120. package/stories/organisms/DropdownComponent/dropdownItems.ts +122 -122
  121. package/stories/organisms/DropdownComponent/index.ts +4 -4
  122. package/stories/organisms/EmptySectionPlaceholder/EmptySectionPlaceholder.stories.tsx +76 -76
  123. package/stories/organisms/EmptySectionPlaceholder/EmptySectionPlaceholder.tsx +52 -52
  124. package/stories/organisms/EmptySectionPlaceholder/index.tsx +4 -4
  125. package/stories/organisms/FormInputWithAddons/FormInputWithAddons.stories.tsx +29 -29
  126. package/stories/organisms/FormInputWithAddons/FormInputWithAddons.tsx +145 -145
  127. package/stories/organisms/FormInputWithAddons/index.tsx +3 -3
  128. package/stories/organisms/TextInputSelect/InputSelect.tsx +59 -59
  129. package/stories/organisms/TextInputSelect/TextInputSelect.stories.tsx +33 -33
  130. package/stories/organisms/TextInputSelect/TextInputSelect.tsx +186 -186
  131. package/stories/organisms/TextInputSelect/index.tsx +3 -3
  132. package/stories/organisms/index.ts +27 -27
  133. package/tailwind.config.js +192 -192
  134. package/tsconfig.json +29 -29
  135. package/tsconfig.lib.json +25 -25
  136. package/utils/types.d.ts +2 -2
  137. package/utils/types.ts +3 -3
  138. package/utils/useId.d.ts +1 -1
  139. package/utils/useId.tsx +16 -16
@@ -1,346 +1,346 @@
1
- import React, { HTMLAttributes, useEffect, useMemo, useRef, useState } from "react"
2
- import { default as cn } from "classnames"
3
- import {
4
- useFloating,
5
- autoUpdate,
6
- offset,
7
- useDismiss,
8
- useRole,
9
- useClick,
10
- useInteractions,
11
- FloatingFocusManager,
12
- autoPlacement,
13
- shift,
14
- FloatingPortal,
15
- FloatingList,
16
- useTransitionStyles,
17
- Placement,
18
- useListNavigation
19
- } from "@floating-ui/react"
20
-
21
- import { ClassNameWithAutocomplete } from "utils/types"
22
- import { DynamicIcon, IDynamicIconProps, UnifiedIconName } from "@/stories/atoms/icons"
23
- import { list } from "postcss"
24
-
25
- export interface IItemProp {
26
- //Don't think this needs to extend HtmlButton... extends HTMLAttributes<HTMLButtonElement> {
27
- icon?: IDynamicIconProps
28
- iconPosition?: "trailing" | "leading"
29
- label: string | JSX.Element
30
- onClick?(): void
31
- isEmphasized?: boolean
32
- key: React.Key
33
- iconObj?: JSX.Element
34
- }
35
-
36
- export interface IDropdownProps extends HTMLAttributes<HTMLDivElement> {
37
- items: IItemProp[][]
38
- label: string
39
- CustomDropdownTrigger?: React.ReactNode
40
- id: string
41
- groupClassname?: ClassNameWithAutocomplete
42
- itemsClassname?: ClassNameWithAutocomplete
43
- itemClassname?: ClassNameWithAutocomplete
44
- activeItemClassname?: ClassNameWithAutocomplete
45
- buttonClassname?: ClassNameWithAutocomplete
46
- iconClassname?: ClassNameWithAutocomplete
47
- iconSpacingClassname?: ClassNameWithAutocomplete
48
- placement?: Placement
49
- offsetOptions?: Partial<{
50
- mainAxis: number
51
- crossAxis: number
52
- alignmentAxis: number | null
53
- }>
54
- disabled?: boolean
55
- onFocus?: () => void
56
- onBlur?: () => void
57
- }
58
- export const defaultClassNames = {
59
- groupClassname: "flex inline-block text-left",
60
- itemsClassname:
61
- "mt-2 origin-bottom-right rounded bg-white shadow-lg z-[99999] divide-y divide-gray-100 border border-gray-300 ",
62
- itemClassname:
63
- "group flex font-muli cursor-pointer items-center px-4 py-2 text-sm transition-all hover:bg-gray-100 hover:text-gray-900 justify-between gap-4 ",
64
- activeItemClassname: "block px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 hover:text-gray-900",
65
- buttonClassname:
66
- "py-[2px] flex items-center rounded outline-purple-500 transition-all text-gray-400 hover:text-gray-600 ",
67
- iconClassname: "ml-1 h-5 w-6",
68
- iconSpacingClassname: "flex items-center gap-x-4"
69
- }
70
-
71
- /** Comment */
72
- const Dropdown: React.FC<IDropdownProps> = ({
73
- items,
74
- id,
75
- label,
76
- groupClassname,
77
- itemsClassname,
78
- itemClassname,
79
- activeItemClassname,
80
- buttonClassname,
81
- iconClassname,
82
- iconSpacingClassname,
83
- CustomDropdownTrigger,
84
- placement = "bottom-start",
85
- offsetOptions,
86
- disabled,
87
- onFocus,
88
- onBlur,
89
- ...props
90
- }: IDropdownProps): JSX.Element | null => {
91
- const [isOpen, setIsOpen] = useState(false)
92
- const [activeItem, setActiveItem] = useState<React.Key | null>(null)
93
- const [activeIndex, setActiveIndex] = useState<number | null>(null)
94
-
95
- const listRef = useRef<(HTMLButtonElement | null)[]>([])
96
-
97
- // Floating UI logic
98
- const { refs, floatingStyles, context } = useFloating({
99
- open: isOpen,
100
- onOpenChange: (bool: boolean) => {
101
- listRef.current = []
102
- setActiveIndex(null)
103
- setIsOpen(bool)
104
- },
105
- placement,
106
- middleware: [
107
- offset(offsetOptions ?? 10),
108
- autoPlacement({
109
- allowedPlacements: [placement, "bottom-start", "bottom-end", "bottom"]
110
- }),
111
- shift({ rootBoundary: "document" })
112
- ],
113
- whileElementsMounted: autoUpdate
114
- })
115
- const click = useClick(context)
116
- const dismiss = useDismiss(context)
117
- const role = useRole(context)
118
- const listNavigation = useListNavigation(context, {
119
- listRef,
120
- activeIndex,
121
- onNavigate: (index: number | null) => {
122
- if (index !== null && listRef.current[index]) {
123
- setActiveIndex(index)
124
- listRef.current[index]?.focus()
125
- }
126
- }
127
- })
128
-
129
- const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
130
- click,
131
- dismiss,
132
- role,
133
- listNavigation
134
- ])
135
-
136
- useEffect(() => {
137
- if (isOpen) {
138
- onFocus && onFocus()
139
- } else {
140
- onBlur && onBlur()
141
- }
142
- }, [isOpen, onBlur, onFocus])
143
-
144
- const ItemComponents = useMemo(
145
- () =>
146
- items.map((itemStack, stackIndex) => {
147
- return itemStack.map((item, itemIndex) => {
148
- const { key, label, icon, iconObj, iconPosition, isEmphasized, onClick, ...rest } = item
149
- const active = activeItem && activeItem === key
150
- const itemClass = cn(
151
- defaultClassNames.itemClassname,
152
- itemClassname,
153
- "group flex cursor-pointer items-center px-4 py-2 text-sm transition-all",
154
- {
155
- "text-red-500": isEmphasized
156
- },
157
- {
158
- "text-gray-900": !isEmphasized
159
- },
160
- {
161
- "bg-gray-100 text-gray-900": active
162
- },
163
- active ? cn(defaultClassNames.activeItemClassname, activeItemClassname) : "",
164
- {
165
- "bg-gray-100 text-red-500 hover:text-red-500": active && isEmphasized
166
- }
167
- )
168
- return (
169
- <button
170
- {...{
171
- key: key,
172
- id: key.toString(),
173
- className: cn(itemClass, "w-full"),
174
- ...rest,
175
- ...getItemProps(),
176
- onClick: () => {
177
- onClick && onClick()
178
- setTimeout(() => {
179
- //hide the dropdown after click
180
- setIsOpen(false)
181
- }, 150)
182
- }
183
- }}
184
- ref={(node) => {
185
- //If the list ref already contains a node with the same id do nothing, otherwise add it
186
- if (listRef.current.some((item) => item?.id === key)) {
187
- return
188
- }
189
- listRef.current.push(node)
190
- }}
191
- key={key}
192
- >
193
- <div className={cn(defaultClassNames.iconSpacingClassname, iconSpacingClassname)}>
194
- {iconObj && (iconPosition === "leading" || iconPosition === undefined) && (
195
- <>{iconObj}</>
196
- )}
197
- {icon &&
198
- (iconPosition === "leading" || iconPosition === undefined) &&
199
- (typeof icon === "string" ? (
200
- <DynamicIcon
201
- {...{
202
- icon: icon,
203
- className: cn(
204
- {
205
- "text-red-500": isEmphasized
206
- },
207
- "opacity-60 group"
208
- )
209
- }}
210
- />
211
- ) : (
212
- <DynamicIcon
213
- {...{
214
- ...icon,
215
- className: cn(
216
- icon.className,
217
- {
218
- "text-red-500": isEmphasized
219
- },
220
- "opacity-60 group"
221
- )
222
- }}
223
- />
224
- ))}
225
- <div className="whitespace-nowrap">{label}</div>
226
- {iconObj && iconPosition === "trailing" && <>{iconObj}</>}
227
- {icon &&
228
- iconPosition === "trailing" &&
229
- (typeof icon === "string" ? (
230
- <DynamicIcon
231
- {...{
232
- icon: icon,
233
- className: cn(
234
- {
235
- "text-red-500": isEmphasized
236
- },
237
- "opacity-60 group"
238
- )
239
- }}
240
- />
241
- ) : (
242
- <DynamicIcon
243
- {...{
244
- ...icon,
245
- className: cn(
246
- icon.className,
247
- {
248
- "text-red-500": isEmphasized
249
- },
250
- "opacity-60 group"
251
- )
252
- }}
253
- />
254
- ))}
255
- </div>
256
- </button>
257
- )
258
- })
259
- }),
260
- [activeItem, activeItemClassname, getItemProps, iconSpacingClassname, itemClassname, items]
261
- )
262
-
263
- const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
264
- duration: {
265
- open: 200,
266
- close: 200
267
- },
268
- initial: {
269
- opacity: 0,
270
- scale: 95
271
- },
272
- open: {
273
- opacity: 1,
274
- scale: 100
275
- }
276
- })
277
- return (
278
- <div
279
- {...{
280
- className: cn(defaultClassNames.groupClassname, groupClassname),
281
- role: "combobox",
282
- "aria-owns": `${id}-list`,
283
- "aria-expanded": isOpen,
284
- "aria-haspopup": "listbox",
285
- ...props
286
- }}
287
- >
288
- <button
289
- {...{
290
- ref: refs.setReference,
291
- className: cn(defaultClassNames.buttonClassname, buttonClassname),
292
- onClick: () => {
293
- setIsOpen(!isOpen)
294
- },
295
- type: "button",
296
- disabled: disabled,
297
- ...getReferenceProps()
298
- }}
299
- >
300
- {CustomDropdownTrigger ? (
301
- <span className="">{CustomDropdownTrigger}</span>
302
- ) : (
303
- <>
304
- <span className="pl-1">{label}</span>
305
- <DynamicIcon
306
- icon="IconChevronDown"
307
- className={cn(defaultClassNames.iconClassname, iconClassname)}
308
- />
309
- </>
310
- )}
311
- </button>
312
-
313
- {isMounted && items.length > 0 && isOpen && (
314
- <FloatingList
315
- {...{
316
- elementsRef: listRef
317
- }}
318
- >
319
- <FloatingPortal>
320
- <FloatingFocusManager context={context} modal={true}>
321
- <div
322
- {...getFloatingProps()}
323
- className={cn(defaultClassNames.itemsClassname, itemsClassname)}
324
- ref={refs.setFloating}
325
- aria-labelledby={label}
326
- style={{
327
- position: context.strategy,
328
- top: Math.round(context.y ?? 0),
329
- left: Math.round(context.x ?? 0),
330
- width: "max-content",
331
- maxWidth: "min(calc(100vw - 10px), 25rem)",
332
- ...floatingStyles,
333
- ...transitionStyles
334
- }}
335
- >
336
- {ItemComponents}
337
- </div>
338
- </FloatingFocusManager>
339
- </FloatingPortal>
340
- </FloatingList>
341
- )}
342
- </div>
343
- )
344
- }
345
-
346
- export default Dropdown
1
+ import React, { HTMLAttributes, useEffect, useMemo, useRef, useState } from "react"
2
+ import { default as cn } from "classnames"
3
+ import {
4
+ useFloating,
5
+ autoUpdate,
6
+ offset,
7
+ useDismiss,
8
+ useRole,
9
+ useClick,
10
+ useInteractions,
11
+ FloatingFocusManager,
12
+ autoPlacement,
13
+ shift,
14
+ FloatingPortal,
15
+ FloatingList,
16
+ useTransitionStyles,
17
+ Placement,
18
+ useListNavigation
19
+ } from "@floating-ui/react"
20
+
21
+ import { ClassNameWithAutocomplete } from "utils/types"
22
+ import { DynamicIcon, IDynamicIconProps, UnifiedIconName } from "@/stories/atoms/icons"
23
+ import { list } from "postcss"
24
+
25
+ export interface IItemProp {
26
+ //Don't think this needs to extend HtmlButton... extends HTMLAttributes<HTMLButtonElement> {
27
+ icon?: IDynamicIconProps
28
+ iconPosition?: "trailing" | "leading"
29
+ label: string | JSX.Element
30
+ onClick?(): void
31
+ isEmphasized?: boolean
32
+ key: React.Key
33
+ iconObj?: JSX.Element
34
+ }
35
+
36
+ export interface IDropdownProps extends HTMLAttributes<HTMLDivElement> {
37
+ items: IItemProp[][]
38
+ label: string
39
+ CustomDropdownTrigger?: React.ReactNode
40
+ id: string
41
+ groupClassname?: ClassNameWithAutocomplete
42
+ itemsClassname?: ClassNameWithAutocomplete
43
+ itemClassname?: ClassNameWithAutocomplete
44
+ activeItemClassname?: ClassNameWithAutocomplete
45
+ buttonClassname?: ClassNameWithAutocomplete
46
+ iconClassname?: ClassNameWithAutocomplete
47
+ iconSpacingClassname?: ClassNameWithAutocomplete
48
+ placement?: Placement
49
+ offsetOptions?: Partial<{
50
+ mainAxis: number
51
+ crossAxis: number
52
+ alignmentAxis: number | null
53
+ }>
54
+ disabled?: boolean
55
+ onFocus?: () => void
56
+ onBlur?: () => void
57
+ }
58
+ export const defaultClassNames = {
59
+ groupClassname: "flex inline-block text-left",
60
+ itemsClassname:
61
+ "mt-2 origin-bottom-right rounded bg-white shadow-lg z-[99999] divide-y divide-gray-100 border border-gray-300 ",
62
+ itemClassname:
63
+ "group flex font-muli cursor-pointer items-center px-4 py-2 text-sm transition-all hover:bg-gray-100 hover:text-gray-900 justify-between gap-4 ",
64
+ activeItemClassname: "block px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 hover:text-gray-900",
65
+ buttonClassname:
66
+ "py-[2px] flex items-center rounded outline-purple-500 transition-all text-gray-400 hover:text-gray-600 ",
67
+ iconClassname: "ml-1 h-5 w-6",
68
+ iconSpacingClassname: "flex items-center gap-x-4"
69
+ }
70
+
71
+ /** Comment */
72
+ const Dropdown: React.FC<IDropdownProps> = ({
73
+ items,
74
+ id,
75
+ label,
76
+ groupClassname,
77
+ itemsClassname,
78
+ itemClassname,
79
+ activeItemClassname,
80
+ buttonClassname,
81
+ iconClassname,
82
+ iconSpacingClassname,
83
+ CustomDropdownTrigger,
84
+ placement = "bottom-start",
85
+ offsetOptions,
86
+ disabled,
87
+ onFocus,
88
+ onBlur,
89
+ ...props
90
+ }: IDropdownProps): JSX.Element | null => {
91
+ const [isOpen, setIsOpen] = useState(false)
92
+ const [activeItem, setActiveItem] = useState<React.Key | null>(null)
93
+ const [activeIndex, setActiveIndex] = useState<number | null>(null)
94
+
95
+ const listRef = useRef<(HTMLButtonElement | null)[]>([])
96
+
97
+ // Floating UI logic
98
+ const { refs, floatingStyles, context } = useFloating({
99
+ open: isOpen,
100
+ onOpenChange: (bool: boolean) => {
101
+ listRef.current = []
102
+ setActiveIndex(null)
103
+ setIsOpen(bool)
104
+ },
105
+ placement,
106
+ middleware: [
107
+ offset(offsetOptions ?? 10),
108
+ autoPlacement({
109
+ allowedPlacements: [placement, "bottom-start", "bottom-end", "bottom"]
110
+ }),
111
+ shift({ rootBoundary: "document" })
112
+ ],
113
+ whileElementsMounted: autoUpdate
114
+ })
115
+ const click = useClick(context)
116
+ const dismiss = useDismiss(context)
117
+ const role = useRole(context)
118
+ const listNavigation = useListNavigation(context, {
119
+ listRef,
120
+ activeIndex,
121
+ onNavigate: (index: number | null) => {
122
+ if (index !== null && listRef.current[index]) {
123
+ setActiveIndex(index)
124
+ listRef.current[index]?.focus()
125
+ }
126
+ }
127
+ })
128
+
129
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
130
+ click,
131
+ dismiss,
132
+ role,
133
+ listNavigation
134
+ ])
135
+
136
+ useEffect(() => {
137
+ if (isOpen) {
138
+ onFocus && onFocus()
139
+ } else {
140
+ onBlur && onBlur()
141
+ }
142
+ }, [isOpen, onBlur, onFocus])
143
+
144
+ const ItemComponents = useMemo(
145
+ () =>
146
+ items.map((itemStack, stackIndex) => {
147
+ return itemStack.map((item, itemIndex) => {
148
+ const { key, label, icon, iconObj, iconPosition, isEmphasized, onClick, ...rest } = item
149
+ const active = activeItem && activeItem === key
150
+ const itemClass = cn(
151
+ defaultClassNames.itemClassname,
152
+ itemClassname,
153
+ "group flex cursor-pointer items-center px-4 py-2 text-sm transition-all",
154
+ {
155
+ "text-red-500": isEmphasized
156
+ },
157
+ {
158
+ "text-gray-900": !isEmphasized
159
+ },
160
+ {
161
+ "bg-gray-100 text-gray-900": active
162
+ },
163
+ active ? cn(defaultClassNames.activeItemClassname, activeItemClassname) : "",
164
+ {
165
+ "bg-gray-100 text-red-500 hover:text-red-500": active && isEmphasized
166
+ }
167
+ )
168
+ return (
169
+ <button
170
+ {...{
171
+ key: key,
172
+ id: key.toString(),
173
+ className: cn(itemClass, "w-full"),
174
+ ...rest,
175
+ ...getItemProps(),
176
+ onClick: () => {
177
+ onClick && onClick()
178
+ setTimeout(() => {
179
+ //hide the dropdown after click
180
+ setIsOpen(false)
181
+ }, 150)
182
+ }
183
+ }}
184
+ ref={(node) => {
185
+ //If the list ref already contains a node with the same id do nothing, otherwise add it
186
+ if (listRef.current.some((item) => item?.id === key)) {
187
+ return
188
+ }
189
+ listRef.current.push(node)
190
+ }}
191
+ key={key}
192
+ >
193
+ <div className={cn(defaultClassNames.iconSpacingClassname, iconSpacingClassname)}>
194
+ {iconObj && (iconPosition === "leading" || iconPosition === undefined) && (
195
+ <>{iconObj}</>
196
+ )}
197
+ {icon &&
198
+ (iconPosition === "leading" || iconPosition === undefined) &&
199
+ (typeof icon === "string" ? (
200
+ <DynamicIcon
201
+ {...{
202
+ icon: icon,
203
+ className: cn(
204
+ {
205
+ "text-red-500": isEmphasized
206
+ },
207
+ "opacity-60 group"
208
+ )
209
+ }}
210
+ />
211
+ ) : (
212
+ <DynamicIcon
213
+ {...{
214
+ ...icon,
215
+ className: cn(
216
+ icon.className,
217
+ {
218
+ "text-red-500": isEmphasized
219
+ },
220
+ "opacity-60 group"
221
+ )
222
+ }}
223
+ />
224
+ ))}
225
+ <div className="whitespace-nowrap">{label}</div>
226
+ {iconObj && iconPosition === "trailing" && <>{iconObj}</>}
227
+ {icon &&
228
+ iconPosition === "trailing" &&
229
+ (typeof icon === "string" ? (
230
+ <DynamicIcon
231
+ {...{
232
+ icon: icon,
233
+ className: cn(
234
+ {
235
+ "text-red-500": isEmphasized
236
+ },
237
+ "opacity-60 group"
238
+ )
239
+ }}
240
+ />
241
+ ) : (
242
+ <DynamicIcon
243
+ {...{
244
+ ...icon,
245
+ className: cn(
246
+ icon.className,
247
+ {
248
+ "text-red-500": isEmphasized
249
+ },
250
+ "opacity-60 group"
251
+ )
252
+ }}
253
+ />
254
+ ))}
255
+ </div>
256
+ </button>
257
+ )
258
+ })
259
+ }),
260
+ [activeItem, activeItemClassname, getItemProps, iconSpacingClassname, itemClassname, items]
261
+ )
262
+
263
+ const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
264
+ duration: {
265
+ open: 200,
266
+ close: 200
267
+ },
268
+ initial: {
269
+ opacity: 0,
270
+ scale: 95
271
+ },
272
+ open: {
273
+ opacity: 1,
274
+ scale: 100
275
+ }
276
+ })
277
+ return (
278
+ <div
279
+ {...{
280
+ className: cn(defaultClassNames.groupClassname, groupClassname),
281
+ role: "combobox",
282
+ "aria-owns": `${id}-list`,
283
+ "aria-expanded": isOpen,
284
+ "aria-haspopup": "listbox",
285
+ ...props
286
+ }}
287
+ >
288
+ <button
289
+ {...{
290
+ ref: refs.setReference,
291
+ className: cn(defaultClassNames.buttonClassname, buttonClassname),
292
+ onClick: () => {
293
+ setIsOpen(!isOpen)
294
+ },
295
+ type: "button",
296
+ disabled: disabled,
297
+ ...getReferenceProps()
298
+ }}
299
+ >
300
+ {CustomDropdownTrigger ? (
301
+ <span className="">{CustomDropdownTrigger}</span>
302
+ ) : (
303
+ <>
304
+ <span className="pl-1">{label}</span>
305
+ <DynamicIcon
306
+ icon="IconChevronDown"
307
+ className={cn(defaultClassNames.iconClassname, iconClassname)}
308
+ />
309
+ </>
310
+ )}
311
+ </button>
312
+
313
+ {isMounted && items.length > 0 && isOpen && (
314
+ <FloatingList
315
+ {...{
316
+ elementsRef: listRef
317
+ }}
318
+ >
319
+ <FloatingPortal>
320
+ <FloatingFocusManager context={context} modal={true}>
321
+ <div
322
+ {...getFloatingProps()}
323
+ className={cn(defaultClassNames.itemsClassname, itemsClassname)}
324
+ ref={refs.setFloating}
325
+ aria-labelledby={label}
326
+ style={{
327
+ position: context.strategy,
328
+ top: Math.round(context.y ?? 0),
329
+ left: Math.round(context.x ?? 0),
330
+ width: "max-content",
331
+ maxWidth: "min(calc(100vw - 10px), 25rem)",
332
+ ...floatingStyles,
333
+ ...transitionStyles
334
+ }}
335
+ >
336
+ {ItemComponents}
337
+ </div>
338
+ </FloatingFocusManager>
339
+ </FloatingPortal>
340
+ </FloatingList>
341
+ )}
342
+ </div>
343
+ )
344
+ }
345
+
346
+ export default Dropdown