@udixio/ui-react 2.10.13 → 2.10.15

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 (173) hide show
  1. package/package.json +4 -2
  2. package/.eslintrc.mjs +0 -22
  3. package/.storybook/main.ts +0 -20
  4. package/.storybook/preview.ts +0 -1
  5. package/CHANGELOG.md +0 -1144
  6. package/postcss.config.mjs +0 -5
  7. package/src/index.css +0 -4
  8. package/src/index.ts +0 -1
  9. package/src/lib/components/AnchorPositioner.tsx +0 -185
  10. package/src/lib/components/Button.tsx +0 -208
  11. package/src/lib/components/Card.tsx +0 -47
  12. package/src/lib/components/Carousel.tsx +0 -437
  13. package/src/lib/components/CarouselItem.tsx +0 -61
  14. package/src/lib/components/Checkbox.tsx +0 -120
  15. package/src/lib/components/Chip.tsx +0 -341
  16. package/src/lib/components/Chips.tsx +0 -331
  17. package/src/lib/components/ContextMenu.tsx +0 -109
  18. package/src/lib/components/DatePicker.tsx +0 -432
  19. package/src/lib/components/Divider.tsx +0 -20
  20. package/src/lib/components/Fab.tsx +0 -127
  21. package/src/lib/components/FabMenu.tsx +0 -239
  22. package/src/lib/components/IconButton.tsx +0 -146
  23. package/src/lib/components/Menu.tsx +0 -88
  24. package/src/lib/components/MenuGroup.tsx +0 -34
  25. package/src/lib/components/MenuHeadline.tsx +0 -9
  26. package/src/lib/components/MenuItem.tsx +0 -215
  27. package/src/lib/components/NavigationRail.tsx +0 -186
  28. package/src/lib/components/NavigationRailItem.tsx +0 -227
  29. package/src/lib/components/ProgressIndicator.tsx +0 -214
  30. package/src/lib/components/SideSheet.tsx +0 -135
  31. package/src/lib/components/Slider.tsx +0 -374
  32. package/src/lib/components/Snackbar.tsx +0 -77
  33. package/src/lib/components/Switch.tsx +0 -107
  34. package/src/lib/components/Tab.tsx +0 -123
  35. package/src/lib/components/TabGroup.tsx +0 -66
  36. package/src/lib/components/TabGroupContext.tsx +0 -16
  37. package/src/lib/components/TabPanel.tsx +0 -27
  38. package/src/lib/components/TabPanels.tsx +0 -76
  39. package/src/lib/components/Tabs.tsx +0 -105
  40. package/src/lib/components/TextField.tsx +0 -586
  41. package/src/lib/components/Tooltip.tsx +0 -217
  42. package/src/lib/components/index.ts +0 -34
  43. package/src/lib/config/config.interface.ts +0 -9
  44. package/src/lib/config/define-config.ts +0 -16
  45. package/src/lib/config/index.ts +0 -2
  46. package/src/lib/effects/AnimateOnScroll.ts +0 -391
  47. package/src/lib/effects/State.tsx +0 -90
  48. package/src/lib/effects/SyncedFixedWrapper.tsx +0 -62
  49. package/src/lib/effects/ThemeProvider.tsx +0 -174
  50. package/src/lib/effects/block-scroll.effect.tsx +0 -313
  51. package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +0 -407
  52. package/src/lib/effects/custom-scroll/custom-scroll.interface.ts +0 -29
  53. package/src/lib/effects/custom-scroll/custom-scroll.style.ts +0 -32
  54. package/src/lib/effects/custom-scroll/index.ts +0 -3
  55. package/src/lib/effects/index.ts +0 -7
  56. package/src/lib/effects/ripple/RippleEffect.tsx +0 -116
  57. package/src/lib/effects/ripple/index.tsx +0 -1
  58. package/src/lib/effects/scrollDriven.ts +0 -239
  59. package/src/lib/effects/smooth-scroll.effect.tsx +0 -112
  60. package/src/lib/effects/theme.worker.ts +0 -97
  61. package/src/lib/hooks/index.ts +0 -10
  62. package/src/lib/hooks/useTooltipTrigger.ts +0 -270
  63. package/src/lib/icon/icon.tsx +0 -125
  64. package/src/lib/icon/index.ts +0 -1
  65. package/src/lib/index.ts +0 -8
  66. package/src/lib/interfaces/button.interface.ts +0 -65
  67. package/src/lib/interfaces/card.interface.ts +0 -11
  68. package/src/lib/interfaces/carousel-item.interface.ts +0 -12
  69. package/src/lib/interfaces/carousel.interface.ts +0 -41
  70. package/src/lib/interfaces/checkbox.interface.ts +0 -39
  71. package/src/lib/interfaces/chip.interface.ts +0 -97
  72. package/src/lib/interfaces/chips.interface.ts +0 -37
  73. package/src/lib/interfaces/date-picker.interface.ts +0 -79
  74. package/src/lib/interfaces/divider.interface.ts +0 -7
  75. package/src/lib/interfaces/fab-menu.interface.ts +0 -12
  76. package/src/lib/interfaces/fab.interface.ts +0 -27
  77. package/src/lib/interfaces/icon-button.interface.ts +0 -38
  78. package/src/lib/interfaces/index.ts +0 -26
  79. package/src/lib/interfaces/menu-group.interface.ts +0 -13
  80. package/src/lib/interfaces/menu-item.interface.ts +0 -29
  81. package/src/lib/interfaces/menu.interface.ts +0 -19
  82. package/src/lib/interfaces/navigation-rail-item.interface.ts +0 -39
  83. package/src/lib/interfaces/navigation-rail.interface.ts +0 -39
  84. package/src/lib/interfaces/progress-indicator.interface.ts +0 -41
  85. package/src/lib/interfaces/side-sheet.interface.tsx +0 -28
  86. package/src/lib/interfaces/slider.interface.ts +0 -27
  87. package/src/lib/interfaces/snackbar.interface.ts +0 -13
  88. package/src/lib/interfaces/switch.interface.ts +0 -14
  89. package/src/lib/interfaces/tab-group.interface.ts +0 -13
  90. package/src/lib/interfaces/tab-panels.interface.ts +0 -21
  91. package/src/lib/interfaces/tab.interface.ts +0 -31
  92. package/src/lib/interfaces/tabs.interface.ts +0 -22
  93. package/src/lib/interfaces/text-field.interface.ts +0 -61
  94. package/src/lib/interfaces/tooltip.interface.ts +0 -61
  95. package/src/lib/styles/button.style.ts +0 -136
  96. package/src/lib/styles/card.style.ts +0 -29
  97. package/src/lib/styles/carousel-item.style.ts +0 -24
  98. package/src/lib/styles/carousel.style.ts +0 -22
  99. package/src/lib/styles/checkbox.style.ts +0 -64
  100. package/src/lib/styles/chip.style.ts +0 -62
  101. package/src/lib/styles/chips.style.ts +0 -20
  102. package/src/lib/styles/date-picker.style.ts +0 -43
  103. package/src/lib/styles/divider.style.ts +0 -31
  104. package/src/lib/styles/fab-menu.style.ts +0 -29
  105. package/src/lib/styles/fab.style.ts +0 -49
  106. package/src/lib/styles/icon-button.style.ts +0 -168
  107. package/src/lib/styles/index.ts +0 -25
  108. package/src/lib/styles/menu-group.style.ts +0 -34
  109. package/src/lib/styles/menu-headline.style.ts +0 -20
  110. package/src/lib/styles/menu-item.style.ts +0 -45
  111. package/src/lib/styles/menu.style.ts +0 -32
  112. package/src/lib/styles/navigation-rail-item.style.ts +0 -56
  113. package/src/lib/styles/navigation-rail.style.ts +0 -36
  114. package/src/lib/styles/progress-indicator.style.ts +0 -72
  115. package/src/lib/styles/side-sheet.style.ts +0 -45
  116. package/src/lib/styles/slider.style.ts +0 -41
  117. package/src/lib/styles/snackbar.style.ts +0 -26
  118. package/src/lib/styles/switch.style.ts +0 -67
  119. package/src/lib/styles/tab-panels.style.ts +0 -35
  120. package/src/lib/styles/tab.style.ts +0 -78
  121. package/src/lib/styles/tabs.style.ts +0 -22
  122. package/src/lib/styles/text-field.style.ts +0 -115
  123. package/src/lib/styles/tooltip.style.ts +0 -48
  124. package/src/lib/utils/component-helper.ts +0 -134
  125. package/src/lib/utils/component.ts +0 -34
  126. package/src/lib/utils/index.ts +0 -7
  127. package/src/lib/utils/string.ts +0 -9
  128. package/src/lib/utils/styles/classnames.ts +0 -49
  129. package/src/lib/utils/styles/get-classname.ts +0 -96
  130. package/src/lib/utils/styles/index.ts +0 -4
  131. package/src/lib/utils/styles/use-classnames.ts +0 -25
  132. package/src/stories/action/button.stories.tsx +0 -86
  133. package/src/stories/action/fab.stories.tsx +0 -54
  134. package/src/stories/action/icon-button.stories.tsx +0 -134
  135. package/src/stories/assets/accessibility.png +0 -0
  136. package/src/stories/assets/accessibility.svg +0 -5
  137. package/src/stories/assets/addon-library.png +0 -0
  138. package/src/stories/assets/assets.png +0 -0
  139. package/src/stories/assets/context.png +0 -0
  140. package/src/stories/assets/discord.svg +0 -15
  141. package/src/stories/assets/docs.png +0 -0
  142. package/src/stories/assets/figma-plugin.png +0 -0
  143. package/src/stories/assets/github.svg +0 -3
  144. package/src/stories/assets/share.png +0 -0
  145. package/src/stories/assets/styling.png +0 -0
  146. package/src/stories/assets/testing.png +0 -0
  147. package/src/stories/assets/theming.png +0 -0
  148. package/src/stories/assets/tutorials.svg +0 -12
  149. package/src/stories/assets/youtube.svg +0 -4
  150. package/src/stories/communication/ProgressIndicator.stories.tsx +0 -57
  151. package/src/stories/communication/SnackBar.stories.tsx +0 -32
  152. package/src/stories/communication/tool-tip.stories.tsx +0 -133
  153. package/src/stories/containment/card.stories.tsx +0 -42
  154. package/src/stories/containment/carousel.stories.tsx +0 -65
  155. package/src/stories/containment/divider.stories.tsx +0 -35
  156. package/src/stories/containment/slide-sheet.stories.tsx +0 -45
  157. package/src/stories/effect/smooth-scroll.stories.tsx +0 -54
  158. package/src/stories/navigation/navigation-rail/navigation-rail-item.stories.tsx +0 -65
  159. package/src/stories/navigation/navigation-rail/navigation-rail.stories.tsx +0 -122
  160. package/src/stories/navigation/tabs/tab.stories.tsx +0 -57
  161. package/src/stories/navigation/tabs/tabs.stories.tsx +0 -102
  162. package/src/stories/selection/slider.stories.tsx +0 -85
  163. package/src/stories/selection/switch.stories.tsx +0 -46
  164. package/src/stories/text-inputs/text-field.stories.tsx +0 -135
  165. package/src/tests/Button.spec.tsx +0 -67
  166. package/src/tests/useClassNames.spec.tsx +0 -82
  167. package/src/udixio.css +0 -120
  168. package/theme.config.ts +0 -7
  169. package/tsconfig.json +0 -16
  170. package/tsconfig.lib.json +0 -51
  171. package/tsconfig.spec.json +0 -37
  172. package/tsconfig.storybook.json +0 -38
  173. package/vite.config.ts +0 -96
@@ -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
- };