@udixio/ui-react 2.10.12 → 2.10.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.
Files changed (179) hide show
  1. package/dist/index.cjs +3 -3
  2. package/dist/index.js +2696 -2710
  3. package/dist/lib/effects/ThemeProvider.d.ts.map +1 -1
  4. package/dist/theme.worker.js +6633 -0
  5. package/package.json +4 -1
  6. package/.eslintrc.mjs +0 -22
  7. package/.storybook/main.ts +0 -20
  8. package/.storybook/preview.ts +0 -1
  9. package/CHANGELOG.md +0 -1130
  10. package/dist/scrollDriven-AP2yWhzi.js +0 -121
  11. package/postcss.config.mjs +0 -5
  12. package/src/index.css +0 -4
  13. package/src/index.ts +0 -1
  14. package/src/lib/components/AnchorPositioner.tsx +0 -185
  15. package/src/lib/components/Button.tsx +0 -208
  16. package/src/lib/components/Card.tsx +0 -47
  17. package/src/lib/components/Carousel.tsx +0 -437
  18. package/src/lib/components/CarouselItem.tsx +0 -61
  19. package/src/lib/components/Checkbox.tsx +0 -120
  20. package/src/lib/components/Chip.tsx +0 -341
  21. package/src/lib/components/Chips.tsx +0 -331
  22. package/src/lib/components/ContextMenu.tsx +0 -109
  23. package/src/lib/components/DatePicker.tsx +0 -432
  24. package/src/lib/components/Divider.tsx +0 -20
  25. package/src/lib/components/Fab.tsx +0 -127
  26. package/src/lib/components/FabMenu.tsx +0 -239
  27. package/src/lib/components/IconButton.tsx +0 -146
  28. package/src/lib/components/Menu.tsx +0 -88
  29. package/src/lib/components/MenuGroup.tsx +0 -34
  30. package/src/lib/components/MenuHeadline.tsx +0 -9
  31. package/src/lib/components/MenuItem.tsx +0 -215
  32. package/src/lib/components/NavigationRail.tsx +0 -186
  33. package/src/lib/components/NavigationRailItem.tsx +0 -227
  34. package/src/lib/components/ProgressIndicator.tsx +0 -214
  35. package/src/lib/components/SideSheet.tsx +0 -135
  36. package/src/lib/components/Slider.tsx +0 -374
  37. package/src/lib/components/Snackbar.tsx +0 -77
  38. package/src/lib/components/Switch.tsx +0 -107
  39. package/src/lib/components/Tab.tsx +0 -123
  40. package/src/lib/components/TabGroup.tsx +0 -66
  41. package/src/lib/components/TabGroupContext.tsx +0 -16
  42. package/src/lib/components/TabPanel.tsx +0 -27
  43. package/src/lib/components/TabPanels.tsx +0 -76
  44. package/src/lib/components/Tabs.tsx +0 -105
  45. package/src/lib/components/TextField.tsx +0 -586
  46. package/src/lib/components/Tooltip.tsx +0 -217
  47. package/src/lib/components/index.ts +0 -34
  48. package/src/lib/config/config.interface.ts +0 -9
  49. package/src/lib/config/define-config.ts +0 -16
  50. package/src/lib/config/index.ts +0 -2
  51. package/src/lib/effects/AnimateOnScroll.ts +0 -391
  52. package/src/lib/effects/State.tsx +0 -90
  53. package/src/lib/effects/SyncedFixedWrapper.tsx +0 -62
  54. package/src/lib/effects/ThemeProvider.tsx +0 -172
  55. package/src/lib/effects/block-scroll.effect.tsx +0 -313
  56. package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +0 -407
  57. package/src/lib/effects/custom-scroll/custom-scroll.interface.ts +0 -29
  58. package/src/lib/effects/custom-scroll/custom-scroll.style.ts +0 -32
  59. package/src/lib/effects/custom-scroll/index.ts +0 -3
  60. package/src/lib/effects/index.ts +0 -7
  61. package/src/lib/effects/ripple/RippleEffect.tsx +0 -116
  62. package/src/lib/effects/ripple/index.tsx +0 -1
  63. package/src/lib/effects/scrollDriven.ts +0 -239
  64. package/src/lib/effects/smooth-scroll.effect.tsx +0 -112
  65. package/src/lib/effects/theme.worker.ts +0 -97
  66. package/src/lib/hooks/index.ts +0 -10
  67. package/src/lib/hooks/useTooltipTrigger.ts +0 -270
  68. package/src/lib/icon/icon.tsx +0 -125
  69. package/src/lib/icon/index.ts +0 -1
  70. package/src/lib/index.ts +0 -8
  71. package/src/lib/interfaces/button.interface.ts +0 -65
  72. package/src/lib/interfaces/card.interface.ts +0 -11
  73. package/src/lib/interfaces/carousel-item.interface.ts +0 -12
  74. package/src/lib/interfaces/carousel.interface.ts +0 -41
  75. package/src/lib/interfaces/checkbox.interface.ts +0 -39
  76. package/src/lib/interfaces/chip.interface.ts +0 -97
  77. package/src/lib/interfaces/chips.interface.ts +0 -37
  78. package/src/lib/interfaces/date-picker.interface.ts +0 -79
  79. package/src/lib/interfaces/divider.interface.ts +0 -7
  80. package/src/lib/interfaces/fab-menu.interface.ts +0 -12
  81. package/src/lib/interfaces/fab.interface.ts +0 -27
  82. package/src/lib/interfaces/icon-button.interface.ts +0 -38
  83. package/src/lib/interfaces/index.ts +0 -26
  84. package/src/lib/interfaces/menu-group.interface.ts +0 -13
  85. package/src/lib/interfaces/menu-item.interface.ts +0 -29
  86. package/src/lib/interfaces/menu.interface.ts +0 -19
  87. package/src/lib/interfaces/navigation-rail-item.interface.ts +0 -39
  88. package/src/lib/interfaces/navigation-rail.interface.ts +0 -39
  89. package/src/lib/interfaces/progress-indicator.interface.ts +0 -41
  90. package/src/lib/interfaces/side-sheet.interface.tsx +0 -28
  91. package/src/lib/interfaces/slider.interface.ts +0 -27
  92. package/src/lib/interfaces/snackbar.interface.ts +0 -13
  93. package/src/lib/interfaces/switch.interface.ts +0 -14
  94. package/src/lib/interfaces/tab-group.interface.ts +0 -13
  95. package/src/lib/interfaces/tab-panels.interface.ts +0 -21
  96. package/src/lib/interfaces/tab.interface.ts +0 -31
  97. package/src/lib/interfaces/tabs.interface.ts +0 -22
  98. package/src/lib/interfaces/text-field.interface.ts +0 -61
  99. package/src/lib/interfaces/tooltip.interface.ts +0 -61
  100. package/src/lib/styles/button.style.ts +0 -136
  101. package/src/lib/styles/card.style.ts +0 -29
  102. package/src/lib/styles/carousel-item.style.ts +0 -24
  103. package/src/lib/styles/carousel.style.ts +0 -22
  104. package/src/lib/styles/checkbox.style.ts +0 -64
  105. package/src/lib/styles/chip.style.ts +0 -62
  106. package/src/lib/styles/chips.style.ts +0 -20
  107. package/src/lib/styles/date-picker.style.ts +0 -43
  108. package/src/lib/styles/divider.style.ts +0 -31
  109. package/src/lib/styles/fab-menu.style.ts +0 -29
  110. package/src/lib/styles/fab.style.ts +0 -49
  111. package/src/lib/styles/icon-button.style.ts +0 -168
  112. package/src/lib/styles/index.ts +0 -25
  113. package/src/lib/styles/menu-group.style.ts +0 -34
  114. package/src/lib/styles/menu-headline.style.ts +0 -20
  115. package/src/lib/styles/menu-item.style.ts +0 -45
  116. package/src/lib/styles/menu.style.ts +0 -32
  117. package/src/lib/styles/navigation-rail-item.style.ts +0 -56
  118. package/src/lib/styles/navigation-rail.style.ts +0 -36
  119. package/src/lib/styles/progress-indicator.style.ts +0 -72
  120. package/src/lib/styles/side-sheet.style.ts +0 -45
  121. package/src/lib/styles/slider.style.ts +0 -41
  122. package/src/lib/styles/snackbar.style.ts +0 -26
  123. package/src/lib/styles/switch.style.ts +0 -67
  124. package/src/lib/styles/tab-panels.style.ts +0 -35
  125. package/src/lib/styles/tab.style.ts +0 -78
  126. package/src/lib/styles/tabs.style.ts +0 -22
  127. package/src/lib/styles/text-field.style.ts +0 -115
  128. package/src/lib/styles/tooltip.style.ts +0 -48
  129. package/src/lib/utils/component-helper.ts +0 -134
  130. package/src/lib/utils/component.ts +0 -34
  131. package/src/lib/utils/index.ts +0 -7
  132. package/src/lib/utils/string.ts +0 -9
  133. package/src/lib/utils/styles/classnames.ts +0 -49
  134. package/src/lib/utils/styles/get-classname.ts +0 -96
  135. package/src/lib/utils/styles/index.ts +0 -4
  136. package/src/lib/utils/styles/use-classnames.ts +0 -25
  137. package/src/stories/action/button.stories.tsx +0 -86
  138. package/src/stories/action/fab.stories.tsx +0 -54
  139. package/src/stories/action/icon-button.stories.tsx +0 -134
  140. package/src/stories/assets/accessibility.png +0 -0
  141. package/src/stories/assets/accessibility.svg +0 -5
  142. package/src/stories/assets/addon-library.png +0 -0
  143. package/src/stories/assets/assets.png +0 -0
  144. package/src/stories/assets/context.png +0 -0
  145. package/src/stories/assets/discord.svg +0 -15
  146. package/src/stories/assets/docs.png +0 -0
  147. package/src/stories/assets/figma-plugin.png +0 -0
  148. package/src/stories/assets/github.svg +0 -3
  149. package/src/stories/assets/share.png +0 -0
  150. package/src/stories/assets/styling.png +0 -0
  151. package/src/stories/assets/testing.png +0 -0
  152. package/src/stories/assets/theming.png +0 -0
  153. package/src/stories/assets/tutorials.svg +0 -12
  154. package/src/stories/assets/youtube.svg +0 -4
  155. package/src/stories/communication/ProgressIndicator.stories.tsx +0 -57
  156. package/src/stories/communication/SnackBar.stories.tsx +0 -32
  157. package/src/stories/communication/tool-tip.stories.tsx +0 -133
  158. package/src/stories/containment/card.stories.tsx +0 -42
  159. package/src/stories/containment/carousel.stories.tsx +0 -65
  160. package/src/stories/containment/divider.stories.tsx +0 -35
  161. package/src/stories/containment/slide-sheet.stories.tsx +0 -45
  162. package/src/stories/effect/smooth-scroll.stories.tsx +0 -54
  163. package/src/stories/navigation/navigation-rail/navigation-rail-item.stories.tsx +0 -65
  164. package/src/stories/navigation/navigation-rail/navigation-rail.stories.tsx +0 -122
  165. package/src/stories/navigation/tabs/tab.stories.tsx +0 -57
  166. package/src/stories/navigation/tabs/tabs.stories.tsx +0 -102
  167. package/src/stories/selection/slider.stories.tsx +0 -85
  168. package/src/stories/selection/switch.stories.tsx +0 -46
  169. package/src/stories/text-inputs/text-field.stories.tsx +0 -135
  170. package/src/tests/Button.spec.tsx +0 -67
  171. package/src/tests/useClassNames.spec.tsx +0 -82
  172. package/src/udixio.css +0 -120
  173. package/theme.config.ts +0 -7
  174. package/tsconfig.json +0 -16
  175. package/tsconfig.lib.json +0 -51
  176. package/tsconfig.spec.json +0 -37
  177. package/tsconfig.storybook.json +0 -38
  178. package/vite.config.ts +0 -82
  179. /package/dist/{scrollDriven-DWAu7CR0.cjs → scrollDriven.js} +0 -0
@@ -1,341 +0,0 @@
1
- import { classNames, ReactProps } from '../utils';
2
- import { ChipInterface } from '../interfaces';
3
- import { useChipStyle } from '../styles';
4
- import { Icon } from '../icon';
5
- import { State } from '../effects';
6
- import React, { useEffect, useRef, useState } from 'react';
7
- import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
8
-
9
- /**
10
- * Chips prompt most actions in a UI
11
- * @status beta
12
- * @category Action
13
- * @devx
14
- * - `editable` relies on contentEditable; label should be a string.
15
- * - `onToggle` uses internal state; pair with `activated` for controlled usage.
16
- * @a11y
17
- * - Uses `aria-pressed` only when togglable.
18
- * @limitations
19
- * - Edit mode starts after a 1s focus delay (no prop to customize).
20
- */
21
- export const Chip = ({
22
- variant = 'outlined',
23
- disabled = false,
24
- icon,
25
- href,
26
- label,
27
- className,
28
- onClick,
29
- onToggle,
30
- activated,
31
- ref,
32
- onRemove,
33
- editable,
34
- onEditStart,
35
- onEditCommit,
36
- onEditCancel,
37
- onChange,
38
- transition,
39
- children,
40
- editing,
41
- ...restProps
42
- }: ReactProps<ChipInterface>) => {
43
- if (children) label = children;
44
- // Allow empty string when editable (newly created chips start empty)
45
- if (label === undefined && !editable) {
46
- throw new Error(
47
- 'Chip component requires either a label prop or children content',
48
- );
49
- }
50
-
51
- const ElementType = href ? 'a' : 'button';
52
-
53
- const defaultRef = useRef<HTMLDivElement>(null);
54
- const resolvedRef = ref || defaultRef;
55
-
56
- const [isActive, setIsActive] = React.useState(activated);
57
- const [isFocused, setIsFocused] = React.useState(false);
58
- const [isEditing, setIsEditing] = useState(editing && editable);
59
- const [isDragging, setIsDragging] = React.useState(false);
60
- const [editValue, setEditValue] = React.useState<string>(
61
- typeof label === 'string' ? label : '',
62
- );
63
- const editSpanRef = React.useRef<HTMLSpanElement>(null);
64
- useEffect(() => {
65
- setIsActive(activated);
66
- }, [activated]);
67
-
68
- useEffect(() => {
69
- if (editing) {
70
- setIsEditing(editing);
71
- }
72
- if (editable && isFocused) {
73
- // Délai de 1 seconde avant d'activer l'édition
74
- const timerId = setTimeout(() => {
75
- // Ignore l'édition si draggable et en cours de dragging
76
- if ((restProps as any)?.draggable && isDragging) {
77
- return;
78
- }
79
- setIsEditing(true);
80
- }, 1000);
81
-
82
- // Cleanup: annule le timer si le focus est perdu avant 1 seconde
83
- return () => clearTimeout(timerId);
84
- } else if (!isFocused) {
85
- // Désactive l'édition immédiatement si le focus est perdu
86
- setIsEditing(false);
87
- }
88
- return;
89
- }, [isFocused, editable, isDragging, restProps, editValue]);
90
-
91
- // Sync edit value and focus caret when entering editing mode
92
- useEffect(() => {
93
- if (isEditing) {
94
- setEditValue(typeof label === 'string' ? label : '');
95
- // focus contenteditable span and move caret to end
96
- const el =
97
- (labelRef.current as unknown as HTMLSpanElement) || editSpanRef.current;
98
- if (el) {
99
- el.focus();
100
- const range = document.createRange();
101
- range.selectNodeContents(el);
102
- range.collapse(false);
103
- const sel = window.getSelection();
104
- sel?.removeAllRanges();
105
- sel?.addRange(range);
106
- }
107
- }
108
- }, [isEditing]);
109
-
110
- transition = { duration: 0.3, ...transition };
111
-
112
- const handleClick = (e: React.MouseEvent<any, MouseEvent>) => {
113
- if (disabled) {
114
- e.preventDefault();
115
- }
116
- if (onToggle) {
117
- setIsActive(!isActive);
118
- onToggle(!isActive);
119
- } else if (onClick) {
120
- onClick(e);
121
- }
122
- };
123
-
124
- const isInteractive =
125
- !!onToggle || !!onRemove || !!onClick || !!href || !!editable;
126
-
127
- if (activated) {
128
- icon = faCheck;
129
- }
130
-
131
- // Extract potential onFocus/onBlur from rest props to compose handlers
132
- const {
133
- onFocus: restOnFocus,
134
- onBlur: restOnBlur,
135
- onKeyDown: restOnKeyDown,
136
- onDragStart: restOnDragStart,
137
- onDragEnd: restOnDragEnd,
138
- onDoubleClick: restOnDoubleClick,
139
- ...rest
140
- } = (restProps as any) ?? {};
141
-
142
- const styles = useChipStyle({
143
- href,
144
- disabled,
145
- icon,
146
- variant,
147
- transition,
148
- className,
149
- isActive: isActive ?? false,
150
- onToggle,
151
- activated: isActive,
152
- label,
153
- isInteractive,
154
- children: label,
155
- isFocused: isFocused,
156
- isDragging,
157
- onEditCommit,
158
- isEditing,
159
- });
160
-
161
- const labelRef = useRef(null);
162
-
163
- const handleCommit = () => {
164
- const trimmed = (editValue ?? '').trim();
165
- if (!trimmed) {
166
- if (onRemove) {
167
- onRemove();
168
- }
169
- return;
170
- }
171
- onEditCommit?.(trimmed);
172
- };
173
-
174
- return (
175
- <ElementType
176
- contentEditable={false}
177
- ref={resolvedRef}
178
- href={href}
179
- className={styles.chip}
180
- {...(rest as any)}
181
- onClick={(e: React.MouseEvent<any>) => {
182
- if (!isEditing) handleClick(e);
183
- }}
184
- draggable={!disabled && !!(restProps as any)?.draggable}
185
- onDragStart={(e: React.DragEvent<any>) => {
186
- if (!disabled && (restProps as any)?.draggable) {
187
- setIsDragging(true);
188
- }
189
- restOnDragStart?.(e);
190
- }}
191
- onDragEnd={(e: React.DragEvent<any>) => {
192
- if ((restProps as any)?.draggable) {
193
- setIsDragging(false);
194
- }
195
- restOnDragEnd?.(e);
196
- }}
197
- onDoubleClick={(e: React.MouseEvent<any>) => {
198
- if (!disabled && editable && !isEditing) {
199
- onEditStart?.();
200
- e.preventDefault();
201
- e.stopPropagation();
202
- }
203
- restOnDoubleClick?.(e);
204
- }}
205
- onFocus={(e: React.FocusEvent<any>) => {
206
- if (isInteractive) {
207
- setIsFocused(true);
208
- }
209
- restOnFocus?.(e);
210
- }}
211
- onBlur={(e: React.FocusEvent<any>) => {
212
- setIsFocused(false);
213
- restOnBlur?.(e);
214
- }}
215
- onKeyDown={(e: React.KeyboardEvent<any>) => {
216
- const key = e.key;
217
-
218
- // While editing: handle commit/cancel locally
219
- if (!disabled && isEditing) {
220
- if (key === 'Enter') {
221
- e.preventDefault();
222
- handleCommit();
223
- } else if (key === 'Escape') {
224
- e.preventDefault();
225
- onEditCancel?.();
226
- } else if (
227
- onRemove &&
228
- editValue?.trim() === '' &&
229
- (key === 'Backspace' || key === 'Delete' || key === 'Del')
230
- ) {
231
- e.preventDefault();
232
- e.stopPropagation();
233
- onRemove();
234
- }
235
- return;
236
- }
237
-
238
- // Only handle keys when focused/selected and not disabled
239
- if (!disabled && isFocused) {
240
- // Start editing with F2 or Enter when editable and no toggle behavior
241
- if (editable && !onToggle && (key === 'F2' || key === 'Enter')) {
242
- e.preventDefault();
243
- onEditStart?.();
244
- return;
245
- }
246
-
247
- // Toggle active state on Enter or Space when togglable
248
- if (
249
- onToggle &&
250
- (key === 'Enter' || key === ' ' || key === 'Spacebar')
251
- ) {
252
- e.preventDefault();
253
- const next = !isActive;
254
- setIsActive(next);
255
- onToggle(next);
256
- }
257
-
258
- // Trigger remove on Backspace or Delete when removable
259
- if (
260
- onRemove &&
261
- (key === 'Backspace' || key === 'Delete' || key === 'Del')
262
- ) {
263
- e.preventDefault();
264
- e.stopPropagation();
265
- onRemove();
266
- }
267
- }
268
-
269
- // Delegate to user handler last
270
- restOnKeyDown?.(e);
271
- }}
272
- disabled={disabled}
273
- aria-pressed={onToggle ? isActive : undefined}
274
- style={{ transition: transition.duration + 's' }}
275
- >
276
- {isInteractive && !disabled && !isEditing && (
277
- <State
278
- style={{ transition: transition.duration + 's' }}
279
- className={styles.stateLayer}
280
- colorName={classNames({
281
- 'on-surface-variant': !isActive,
282
- 'on-secondary-container': isActive,
283
- })}
284
- stateClassName={'state-ripple-group-[chip]'}
285
- />
286
- )}
287
-
288
- {icon && <Icon icon={icon} className={styles.leadingIcon} />}
289
- <span
290
- ref={labelRef}
291
- contentEditable={!!editable && !!isEditing}
292
- suppressContentEditableWarning
293
- className={styles.label}
294
- role={editable ? 'textbox' : undefined}
295
- spellCheck={false}
296
- onInput={(e) => {
297
- const text = (e.currentTarget as HTMLSpanElement).innerText;
298
- setEditValue(text);
299
- onChange?.(text);
300
- }}
301
- onBlur={(e) => {
302
- if (editable && isEditing) {
303
- handleCommit();
304
- }
305
- }}
306
- onKeyDown={(e) => {
307
- // prevent line breaks inside contenteditable
308
- if (editable && isEditing && e.key === 'Enter') {
309
- e.preventDefault();
310
- e.stopPropagation();
311
- handleCommit();
312
- return;
313
- }
314
- if (editable && isEditing && e.key === 'Escape') {
315
- e.preventDefault();
316
- e.stopPropagation();
317
- onEditCancel?.();
318
- }
319
- }}
320
- >
321
- {label}
322
- </span>
323
- {onRemove && !isEditing && (
324
- <Icon
325
- icon={faXmark}
326
- className={styles.trailingIcon}
327
- onMouseDown={(e) => {
328
- e.preventDefault(); // ⬅️ clé
329
- e.stopPropagation();
330
- }}
331
- onClick={(e: React.MouseEvent) => {
332
- e.stopPropagation();
333
- if (!disabled) {
334
- onRemove();
335
- }
336
- }}
337
- />
338
- )}
339
- </ElementType>
340
- );
341
- };
@@ -1,331 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { ReactProps } from '../utils';
3
- import { ChipItem, ChipsInterface } from '../interfaces';
4
- import { useChipsStyle } from '../styles';
5
- import { Chip } from './Chip';
6
- import { Divider } from './Divider';
7
- import { v4 } from 'uuid';
8
-
9
- /**
10
- * Chips group for input or selection lists
11
- * @status beta
12
- * @category Input
13
- * @devx
14
- * - Works best as controlled: pass `items` + `onItemsChange`.
15
- * - Internal ids are derived from object identity; replace items carefully.
16
- * @limitations
17
- * - No virtualization; very large lists can be slow.
18
- */
19
- export const Chips = ({
20
- variant = 'input',
21
- className,
22
- scrollable = true,
23
- draggable = false,
24
- items,
25
- onItemsChange,
26
- }: ReactProps<ChipsInterface>) => {
27
- const list = items ?? [];
28
-
29
- const ref = React.useRef<HTMLDivElement>(null);
30
-
31
- const [isFocused, setIsFocused] = React.useState<boolean>(false);
32
-
33
- // Internal stable ids per item object (since ChipItem no longer exposes id)
34
- const idMapRef = React.useRef<WeakMap<ChipItem, string>>(new WeakMap());
35
- const getInternalId = React.useCallback((it: ChipItem) => {
36
- const map = idMapRef.current;
37
- let id = map.get(it);
38
- if (!id) {
39
- id = v4();
40
- map.set(it, id);
41
- }
42
- return id;
43
- }, []);
44
-
45
- React.useEffect(() => {
46
- if (isFocused) {
47
- if (variant == 'input') {
48
- ghostChipRef.current?.focus();
49
- } else {
50
- ref.current?.focus();
51
- }
52
- }
53
- }, [isFocused]);
54
-
55
- const chipRefs = React.useRef<(HTMLElement | null)[]>([]);
56
-
57
- // Guard to prevent multiple chip creation from fast typing
58
- const isCreatingRef = React.useRef(false);
59
-
60
- const updateItems = React.useCallback(
61
- (updater: (prev: ChipItem[]) => ChipItem[]) => {
62
- onItemsChange?.(updater(list));
63
- },
64
- [onItemsChange, list],
65
- );
66
-
67
- const removeAt = React.useCallback(
68
- (index: number) => {
69
- updateItems((prev) => prev.filter((_, i) => i !== index));
70
- },
71
- [updateItems],
72
- );
73
-
74
- const styles = useChipsStyle({
75
- scrollable,
76
- className,
77
- variant,
78
- });
79
-
80
- const createAndStartEdit = React.useCallback(
81
- (seedLabel = '') => {
82
- if (variant !== 'input') return;
83
-
84
- // Guard against multiple rapid creations
85
- if (isCreatingRef.current) return;
86
- isCreatingRef.current = true;
87
-
88
- const newItem: ChipItem = {
89
- label: seedLabel,
90
- } as ChipItem;
91
-
92
- // Generate internal ID for the new item
93
- const newId = getInternalId(newItem);
94
-
95
- // Ask parent to add as well
96
- const next = [...list, newItem];
97
- onItemsChange?.(next);
98
-
99
- requestAnimationFrame(() => {
100
- setSelectedChip(newId);
101
- // Reset guard after chip is selected
102
- isCreatingRef.current = false;
103
- });
104
- },
105
- [variant, onItemsChange, list, getInternalId],
106
- );
107
-
108
- const [selectedChip, setSelectedChip] = useState<string | null>(null);
109
-
110
- useEffect(() => {
111
- if (selectedChip) {
112
- const index = list.findIndex(
113
- (item) => getInternalId(item) === selectedChip,
114
- );
115
- if (index !== -1) {
116
- const el = chipRefs.current[index] as any;
117
- el?.focus?.();
118
-
119
- const chipsEl = ref.current!;
120
- const scrollLeft =
121
- el.offsetLeft + el.offsetWidth / 2 - chipsEl.offsetWidth / 2;
122
- chipsEl.scrollTo({ left: scrollLeft, behavior: 'smooth' });
123
- }
124
- }
125
- }, [selectedChip, list, getInternalId]);
126
-
127
- // MODE ITEMS (source de vérité locale ou contrôlée)
128
-
129
- const ghostChipRef = useRef<HTMLButtonElement>(null);
130
-
131
- const isGhostChip = (isFocused || list.length === 0) && variant === 'input';
132
-
133
- return (
134
- <div
135
- ref={ref}
136
- role="list"
137
- aria-label="Chips"
138
- className={styles.chips}
139
- tabIndex={variant === 'input' ? 0 : undefined}
140
- onFocus={(e) => {
141
- if (e.target === e.currentTarget) {
142
- setIsFocused(true);
143
- }
144
- }}
145
- onBlur={() => {
146
- setIsFocused(false);
147
- }}
148
- onKeyDown={(e) => {
149
- if (variant !== 'input') return;
150
-
151
- const key = e.key;
152
- const target = e.target as HTMLElement;
153
- const isContainerFocused = target === e.currentTarget;
154
-
155
- // If currently editing a chip, let the chip handle keys
156
- if (!isFocused) return;
157
-
158
- // Determine focused chip index if any
159
- const activeEl = document.activeElement as HTMLElement | null;
160
- const focusedIndex = chipRefs.current.findIndex(
161
- (el) => el === activeEl,
162
- );
163
-
164
- if (key === 'ArrowLeft') {
165
- e.preventDefault();
166
- const nextIdx = focusedIndex > 0 ? focusedIndex - 1 : list.length - 1;
167
- const elId = getInternalId(list[nextIdx]);
168
- setSelectedChip(elId);
169
- return;
170
- }
171
- if (key === 'ArrowRight') {
172
- e.preventDefault();
173
- const nextIdx =
174
- focusedIndex >= 0
175
- ? (focusedIndex + 1) % Math.max(1, list.length)
176
- : 0;
177
- const elId = getInternalId(list[nextIdx]);
178
- setSelectedChip(elId);
179
- return;
180
- }
181
- if (key === 'Home') {
182
- e.preventDefault();
183
- const elId = getInternalId(list[0]);
184
- setSelectedChip(elId);
185
- return;
186
- }
187
- if (key === 'End') {
188
- e.preventDefault();
189
- const elId = getInternalId(list[list.length - 1]);
190
- setSelectedChip(elId);
191
- return;
192
- }
193
- if (isContainerFocused) {
194
- if (key === 'Backspace') {
195
- e.preventDefault();
196
- // Focus last chip if any
197
- if (list.length > 0) {
198
- const el = chipRefs.current[list.length - 1] as any;
199
- el?.focus?.();
200
- }
201
- return;
202
- }
203
- }
204
- }}
205
- >
206
- {list.map((item, index) => {
207
- const internalId = getInternalId(item);
208
- const isInputVariant = variant === 'input';
209
- const editProps = isInputVariant
210
- ? {
211
- editable: true,
212
- editing: selectedChip === internalId,
213
- onEditCommit: (next: string | undefined) => {
214
- setIsFocused(true);
215
- updateItems((prev) =>
216
- prev.map((it, i) =>
217
- i === index ? { ...it, label: next as any } : it,
218
- ),
219
- );
220
- },
221
- onEditCancel: () => {
222
- setIsFocused(true);
223
- },
224
- onChange: () => {
225
- if (chipRefs.current.length == index + 1) {
226
- const el = ref.current!;
227
- requestAnimationFrame(() => {
228
- el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
229
- });
230
- }
231
- },
232
- }
233
- : {};
234
-
235
- return (
236
- <Chip
237
- key={internalId}
238
- ref={(el: any) => (chipRefs.current[index] = el)}
239
- label={item.label ?? ''}
240
- icon={item.icon}
241
- activated={item.activated}
242
- disabled={item.disabled}
243
- variant={item.variant}
244
- href={item.href}
245
- draggable={draggable}
246
- {...editProps}
247
- onToggle={
248
- item.activated === undefined
249
- ? undefined
250
- : (next) =>
251
- updateItems((prev) =>
252
- prev.map((it, i) =>
253
- i === index ? { ...it, activated: next } : it,
254
- ),
255
- )
256
- }
257
- onBlur={() => {
258
- if (selectedChip === internalId) {
259
- setSelectedChip(null);
260
- }
261
- }}
262
- onRemove={
263
- isInputVariant
264
- ? () => {
265
- setIsFocused(true);
266
- removeAt(index);
267
- }
268
- : undefined
269
- }
270
- />
271
- );
272
- })}
273
- {isFocused && (
274
- <>
275
- <Divider
276
- orientation="vertical"
277
- className="animate-[var(--animate-blink)] border-outline"
278
- style={
279
- {
280
- '--animate-blink':
281
- 'blink 1s cubic-bezier(0.4, 0, 0.6, 1) infinite',
282
- } as React.CSSProperties
283
- }
284
- />
285
-
286
- <style>
287
- {`
288
- @keyframes blink {
289
- 0%, 50% { opacity: 1; }
290
- 50.01%, 100% { opacity: 0; }
291
- }
292
- `}
293
- </style>
294
- </>
295
- )}
296
- {isGhostChip && (
297
- <Chip
298
- ref={ghostChipRef}
299
- className="opacity-0"
300
- draggable={draggable}
301
- editable={true}
302
- editing={true}
303
- onChange={(v) => {
304
- v = v.replace(/(&nbsp;)+/g, ' ').trim();
305
- console.log('Ghost chip onChange', v, !!v);
306
- if (v) {
307
- createAndStartEdit(v);
308
- } else {
309
- if (list.length > 0) {
310
- const el = chipRefs.current[list.length - 1] as any;
311
- el?.focus?.();
312
- }
313
- }
314
- }}
315
- onEditCommit={() => {
316
- // Ghost chip doesn't commit - it creates a new chip via onChange
317
- }}
318
- onBlur={() => {
319
- setIsFocused(false);
320
- }}
321
- onFocus={(e: React.FocusEvent) => {
322
- setIsFocused(true);
323
- e.stopPropagation();
324
- }}
325
- >
326
- &nbsp;
327
- </Chip>
328
- )}
329
- </div>
330
- );
331
- };