@udixio/ui-react 2.10.13 → 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.
- package/package.json +4 -1
- package/.eslintrc.mjs +0 -22
- package/.storybook/main.ts +0 -20
- package/.storybook/preview.ts +0 -1
- package/CHANGELOG.md +0 -1144
- package/postcss.config.mjs +0 -5
- package/src/index.css +0 -4
- package/src/index.ts +0 -1
- package/src/lib/components/AnchorPositioner.tsx +0 -185
- package/src/lib/components/Button.tsx +0 -208
- package/src/lib/components/Card.tsx +0 -47
- package/src/lib/components/Carousel.tsx +0 -437
- package/src/lib/components/CarouselItem.tsx +0 -61
- package/src/lib/components/Checkbox.tsx +0 -120
- package/src/lib/components/Chip.tsx +0 -341
- package/src/lib/components/Chips.tsx +0 -331
- package/src/lib/components/ContextMenu.tsx +0 -109
- package/src/lib/components/DatePicker.tsx +0 -432
- package/src/lib/components/Divider.tsx +0 -20
- package/src/lib/components/Fab.tsx +0 -127
- package/src/lib/components/FabMenu.tsx +0 -239
- package/src/lib/components/IconButton.tsx +0 -146
- package/src/lib/components/Menu.tsx +0 -88
- package/src/lib/components/MenuGroup.tsx +0 -34
- package/src/lib/components/MenuHeadline.tsx +0 -9
- package/src/lib/components/MenuItem.tsx +0 -215
- package/src/lib/components/NavigationRail.tsx +0 -186
- package/src/lib/components/NavigationRailItem.tsx +0 -227
- package/src/lib/components/ProgressIndicator.tsx +0 -214
- package/src/lib/components/SideSheet.tsx +0 -135
- package/src/lib/components/Slider.tsx +0 -374
- package/src/lib/components/Snackbar.tsx +0 -77
- package/src/lib/components/Switch.tsx +0 -107
- package/src/lib/components/Tab.tsx +0 -123
- package/src/lib/components/TabGroup.tsx +0 -66
- package/src/lib/components/TabGroupContext.tsx +0 -16
- package/src/lib/components/TabPanel.tsx +0 -27
- package/src/lib/components/TabPanels.tsx +0 -76
- package/src/lib/components/Tabs.tsx +0 -105
- package/src/lib/components/TextField.tsx +0 -586
- package/src/lib/components/Tooltip.tsx +0 -217
- package/src/lib/components/index.ts +0 -34
- package/src/lib/config/config.interface.ts +0 -9
- package/src/lib/config/define-config.ts +0 -16
- package/src/lib/config/index.ts +0 -2
- package/src/lib/effects/AnimateOnScroll.ts +0 -391
- package/src/lib/effects/State.tsx +0 -90
- package/src/lib/effects/SyncedFixedWrapper.tsx +0 -62
- package/src/lib/effects/ThemeProvider.tsx +0 -174
- package/src/lib/effects/block-scroll.effect.tsx +0 -313
- package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +0 -407
- package/src/lib/effects/custom-scroll/custom-scroll.interface.ts +0 -29
- package/src/lib/effects/custom-scroll/custom-scroll.style.ts +0 -32
- package/src/lib/effects/custom-scroll/index.ts +0 -3
- package/src/lib/effects/index.ts +0 -7
- package/src/lib/effects/ripple/RippleEffect.tsx +0 -116
- package/src/lib/effects/ripple/index.tsx +0 -1
- package/src/lib/effects/scrollDriven.ts +0 -239
- package/src/lib/effects/smooth-scroll.effect.tsx +0 -112
- package/src/lib/effects/theme.worker.ts +0 -97
- package/src/lib/hooks/index.ts +0 -10
- package/src/lib/hooks/useTooltipTrigger.ts +0 -270
- package/src/lib/icon/icon.tsx +0 -125
- package/src/lib/icon/index.ts +0 -1
- package/src/lib/index.ts +0 -8
- package/src/lib/interfaces/button.interface.ts +0 -65
- package/src/lib/interfaces/card.interface.ts +0 -11
- package/src/lib/interfaces/carousel-item.interface.ts +0 -12
- package/src/lib/interfaces/carousel.interface.ts +0 -41
- package/src/lib/interfaces/checkbox.interface.ts +0 -39
- package/src/lib/interfaces/chip.interface.ts +0 -97
- package/src/lib/interfaces/chips.interface.ts +0 -37
- package/src/lib/interfaces/date-picker.interface.ts +0 -79
- package/src/lib/interfaces/divider.interface.ts +0 -7
- package/src/lib/interfaces/fab-menu.interface.ts +0 -12
- package/src/lib/interfaces/fab.interface.ts +0 -27
- package/src/lib/interfaces/icon-button.interface.ts +0 -38
- package/src/lib/interfaces/index.ts +0 -26
- package/src/lib/interfaces/menu-group.interface.ts +0 -13
- package/src/lib/interfaces/menu-item.interface.ts +0 -29
- package/src/lib/interfaces/menu.interface.ts +0 -19
- package/src/lib/interfaces/navigation-rail-item.interface.ts +0 -39
- package/src/lib/interfaces/navigation-rail.interface.ts +0 -39
- package/src/lib/interfaces/progress-indicator.interface.ts +0 -41
- package/src/lib/interfaces/side-sheet.interface.tsx +0 -28
- package/src/lib/interfaces/slider.interface.ts +0 -27
- package/src/lib/interfaces/snackbar.interface.ts +0 -13
- package/src/lib/interfaces/switch.interface.ts +0 -14
- package/src/lib/interfaces/tab-group.interface.ts +0 -13
- package/src/lib/interfaces/tab-panels.interface.ts +0 -21
- package/src/lib/interfaces/tab.interface.ts +0 -31
- package/src/lib/interfaces/tabs.interface.ts +0 -22
- package/src/lib/interfaces/text-field.interface.ts +0 -61
- package/src/lib/interfaces/tooltip.interface.ts +0 -61
- package/src/lib/styles/button.style.ts +0 -136
- package/src/lib/styles/card.style.ts +0 -29
- package/src/lib/styles/carousel-item.style.ts +0 -24
- package/src/lib/styles/carousel.style.ts +0 -22
- package/src/lib/styles/checkbox.style.ts +0 -64
- package/src/lib/styles/chip.style.ts +0 -62
- package/src/lib/styles/chips.style.ts +0 -20
- package/src/lib/styles/date-picker.style.ts +0 -43
- package/src/lib/styles/divider.style.ts +0 -31
- package/src/lib/styles/fab-menu.style.ts +0 -29
- package/src/lib/styles/fab.style.ts +0 -49
- package/src/lib/styles/icon-button.style.ts +0 -168
- package/src/lib/styles/index.ts +0 -25
- package/src/lib/styles/menu-group.style.ts +0 -34
- package/src/lib/styles/menu-headline.style.ts +0 -20
- package/src/lib/styles/menu-item.style.ts +0 -45
- package/src/lib/styles/menu.style.ts +0 -32
- package/src/lib/styles/navigation-rail-item.style.ts +0 -56
- package/src/lib/styles/navigation-rail.style.ts +0 -36
- package/src/lib/styles/progress-indicator.style.ts +0 -72
- package/src/lib/styles/side-sheet.style.ts +0 -45
- package/src/lib/styles/slider.style.ts +0 -41
- package/src/lib/styles/snackbar.style.ts +0 -26
- package/src/lib/styles/switch.style.ts +0 -67
- package/src/lib/styles/tab-panels.style.ts +0 -35
- package/src/lib/styles/tab.style.ts +0 -78
- package/src/lib/styles/tabs.style.ts +0 -22
- package/src/lib/styles/text-field.style.ts +0 -115
- package/src/lib/styles/tooltip.style.ts +0 -48
- package/src/lib/utils/component-helper.ts +0 -134
- package/src/lib/utils/component.ts +0 -34
- package/src/lib/utils/index.ts +0 -7
- package/src/lib/utils/string.ts +0 -9
- package/src/lib/utils/styles/classnames.ts +0 -49
- package/src/lib/utils/styles/get-classname.ts +0 -96
- package/src/lib/utils/styles/index.ts +0 -4
- package/src/lib/utils/styles/use-classnames.ts +0 -25
- package/src/stories/action/button.stories.tsx +0 -86
- package/src/stories/action/fab.stories.tsx +0 -54
- package/src/stories/action/icon-button.stories.tsx +0 -134
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +0 -5
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +0 -15
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +0 -3
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +0 -12
- package/src/stories/assets/youtube.svg +0 -4
- package/src/stories/communication/ProgressIndicator.stories.tsx +0 -57
- package/src/stories/communication/SnackBar.stories.tsx +0 -32
- package/src/stories/communication/tool-tip.stories.tsx +0 -133
- package/src/stories/containment/card.stories.tsx +0 -42
- package/src/stories/containment/carousel.stories.tsx +0 -65
- package/src/stories/containment/divider.stories.tsx +0 -35
- package/src/stories/containment/slide-sheet.stories.tsx +0 -45
- package/src/stories/effect/smooth-scroll.stories.tsx +0 -54
- package/src/stories/navigation/navigation-rail/navigation-rail-item.stories.tsx +0 -65
- package/src/stories/navigation/navigation-rail/navigation-rail.stories.tsx +0 -122
- package/src/stories/navigation/tabs/tab.stories.tsx +0 -57
- package/src/stories/navigation/tabs/tabs.stories.tsx +0 -102
- package/src/stories/selection/slider.stories.tsx +0 -85
- package/src/stories/selection/switch.stories.tsx +0 -46
- package/src/stories/text-inputs/text-field.stories.tsx +0 -135
- package/src/tests/Button.spec.tsx +0 -67
- package/src/tests/useClassNames.spec.tsx +0 -82
- package/src/udixio.css +0 -120
- package/theme.config.ts +0 -7
- package/tsconfig.json +0 -16
- package/tsconfig.lib.json +0 -51
- package/tsconfig.spec.json +0 -37
- package/tsconfig.storybook.json +0 -38
- 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(/( )+/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
|
-
|
|
327
|
-
</Chip>
|
|
328
|
-
)}
|
|
329
|
-
</div>
|
|
330
|
-
);
|
|
331
|
-
};
|