@agility/plenum-ui 2.0.0-rc44 → 2.0.0-rc46

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