@udixio/ui-react 2.8.3 → 2.9.3
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/CHANGELOG.md +54 -0
- package/dist/index.cjs +7 -2
- package/dist/index.js +2460 -1972
- package/dist/lib/components/Button.d.ts.map +1 -1
- package/dist/lib/components/Chip.d.ts +9 -0
- package/dist/lib/components/Chip.d.ts.map +1 -0
- package/dist/lib/components/Chips.d.ts +4 -0
- package/dist/lib/components/Chips.d.ts.map +1 -0
- package/dist/lib/components/IconButton.d.ts +1 -1
- package/dist/lib/components/IconButton.d.ts.map +1 -1
- package/dist/lib/components/index.d.ts +2 -0
- package/dist/lib/components/index.d.ts.map +1 -1
- package/dist/lib/effects/block-scroll.effect.d.ts +6 -0
- package/dist/lib/effects/block-scroll.effect.d.ts.map +1 -1
- package/dist/lib/effects/smooth-scroll.effect.d.ts +5 -0
- package/dist/lib/effects/smooth-scroll.effect.d.ts.map +1 -1
- package/dist/lib/icon/icon.d.ts.map +1 -1
- package/dist/lib/interfaces/chip.interface.d.ts +76 -0
- package/dist/lib/interfaces/chip.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/chips.interface.d.ts +29 -0
- package/dist/lib/interfaces/chips.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/index.d.ts +2 -0
- package/dist/lib/interfaces/index.d.ts.map +1 -1
- package/dist/lib/styles/chip.style.d.ts +111 -0
- package/dist/lib/styles/chip.style.d.ts.map +1 -0
- package/dist/lib/styles/chips.style.d.ts +55 -0
- package/dist/lib/styles/chips.style.d.ts.map +1 -0
- package/dist/lib/styles/index.d.ts +2 -0
- package/dist/lib/styles/index.d.ts.map +1 -1
- package/dist/lib/styles/text-field.style.d.ts +2 -2
- package/package.json +4 -4
- package/src/lib/components/Button.tsx +11 -20
- package/src/lib/components/Chip.tsx +333 -0
- package/src/lib/components/Chips.tsx +280 -0
- package/src/lib/components/IconButton.tsx +11 -20
- package/src/lib/components/index.ts +2 -0
- package/src/lib/effects/block-scroll.effect.tsx +89 -11
- package/src/lib/effects/smooth-scroll.effect.tsx +5 -0
- package/src/lib/icon/icon.tsx +7 -1
- package/src/lib/interfaces/chip.interface.ts +97 -0
- package/src/lib/interfaces/chips.interface.ts +37 -0
- package/src/lib/interfaces/index.ts +2 -0
- package/src/lib/styles/chip.style.ts +62 -0
- package/src/lib/styles/chips.style.ts +20 -0
- package/src/lib/styles/index.ts +2 -0
|
@@ -0,0 +1,333 @@
|
|
|
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
|
+
*/
|
|
14
|
+
export const Chip = ({
|
|
15
|
+
variant = 'outlined',
|
|
16
|
+
disabled = false,
|
|
17
|
+
icon,
|
|
18
|
+
href,
|
|
19
|
+
label,
|
|
20
|
+
className,
|
|
21
|
+
onClick,
|
|
22
|
+
onToggle,
|
|
23
|
+
activated,
|
|
24
|
+
ref,
|
|
25
|
+
onRemove,
|
|
26
|
+
editable,
|
|
27
|
+
onEditStart,
|
|
28
|
+
onEditCommit,
|
|
29
|
+
onEditCancel,
|
|
30
|
+
onChange,
|
|
31
|
+
transition,
|
|
32
|
+
children,
|
|
33
|
+
editing,
|
|
34
|
+
...restProps
|
|
35
|
+
}: ReactProps<ChipInterface>) => {
|
|
36
|
+
if (children) label = children;
|
|
37
|
+
// Allow empty string when editable (newly created chips start empty)
|
|
38
|
+
if (label === undefined && !editable) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'Chip component requires either a label prop or children content',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ElementType = href ? 'a' : 'button';
|
|
45
|
+
|
|
46
|
+
const defaultRef = useRef<HTMLDivElement>(null);
|
|
47
|
+
const resolvedRef = ref || defaultRef;
|
|
48
|
+
|
|
49
|
+
const [isActive, setIsActive] = React.useState(activated);
|
|
50
|
+
const [isFocused, setIsFocused] = React.useState(false);
|
|
51
|
+
const [isEditing, setIsEditing] = useState(editing && editable);
|
|
52
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
53
|
+
const [editValue, setEditValue] = React.useState<string>(
|
|
54
|
+
typeof label === 'string' ? label : '',
|
|
55
|
+
);
|
|
56
|
+
const editSpanRef = React.useRef<HTMLSpanElement>(null);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setIsActive(activated);
|
|
59
|
+
}, [activated]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (editing) {
|
|
63
|
+
setIsEditing(editing);
|
|
64
|
+
}
|
|
65
|
+
if (editable && isFocused) {
|
|
66
|
+
// Délai de 1 seconde avant d'activer l'édition
|
|
67
|
+
const timerId = setTimeout(() => {
|
|
68
|
+
// Ignore l'édition si draggable et en cours de dragging
|
|
69
|
+
if ((restProps as any)?.draggable && isDragging) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setIsEditing(true);
|
|
73
|
+
}, 1000);
|
|
74
|
+
|
|
75
|
+
// Cleanup: annule le timer si le focus est perdu avant 1 seconde
|
|
76
|
+
return () => clearTimeout(timerId);
|
|
77
|
+
} else if (!isFocused) {
|
|
78
|
+
// Désactive l'édition immédiatement si le focus est perdu
|
|
79
|
+
setIsEditing(false);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}, [isFocused, editable, isDragging, restProps, editValue]);
|
|
83
|
+
|
|
84
|
+
// Sync edit value and focus caret when entering editing mode
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (isEditing) {
|
|
87
|
+
setEditValue(typeof label === 'string' ? label : '');
|
|
88
|
+
// focus contenteditable span and move caret to end
|
|
89
|
+
const el =
|
|
90
|
+
(labelRef.current as unknown as HTMLSpanElement) || editSpanRef.current;
|
|
91
|
+
if (el) {
|
|
92
|
+
el.focus();
|
|
93
|
+
const range = document.createRange();
|
|
94
|
+
range.selectNodeContents(el);
|
|
95
|
+
range.collapse(false);
|
|
96
|
+
const sel = window.getSelection();
|
|
97
|
+
sel?.removeAllRanges();
|
|
98
|
+
sel?.addRange(range);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, [isEditing]);
|
|
102
|
+
|
|
103
|
+
transition = { duration: 0.3, ...transition };
|
|
104
|
+
|
|
105
|
+
const handleClick = (e: React.MouseEvent<any, MouseEvent>) => {
|
|
106
|
+
if (disabled) {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
}
|
|
109
|
+
if (onToggle) {
|
|
110
|
+
setIsActive(!isActive);
|
|
111
|
+
onToggle(!isActive);
|
|
112
|
+
} else if (onClick) {
|
|
113
|
+
onClick(e);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const isInteractive = !!onToggle || !!onRemove || !!onClick || !!href;
|
|
118
|
+
|
|
119
|
+
if (activated) {
|
|
120
|
+
icon = faCheck;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract potential onFocus/onBlur from rest props to compose handlers
|
|
124
|
+
const {
|
|
125
|
+
onFocus: restOnFocus,
|
|
126
|
+
onBlur: restOnBlur,
|
|
127
|
+
onKeyDown: restOnKeyDown,
|
|
128
|
+
onDragStart: restOnDragStart,
|
|
129
|
+
onDragEnd: restOnDragEnd,
|
|
130
|
+
onDoubleClick: restOnDoubleClick,
|
|
131
|
+
...rest
|
|
132
|
+
} = (restProps as any) ?? {};
|
|
133
|
+
|
|
134
|
+
const styles = useChipStyle({
|
|
135
|
+
href,
|
|
136
|
+
disabled,
|
|
137
|
+
icon,
|
|
138
|
+
variant,
|
|
139
|
+
transition,
|
|
140
|
+
className,
|
|
141
|
+
isActive: isActive ?? false,
|
|
142
|
+
onToggle,
|
|
143
|
+
activated: isActive,
|
|
144
|
+
label,
|
|
145
|
+
isInteractive,
|
|
146
|
+
children: label,
|
|
147
|
+
isFocused: isFocused,
|
|
148
|
+
isDragging,
|
|
149
|
+
onEditCommit,
|
|
150
|
+
isEditing,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const labelRef = useRef(null);
|
|
154
|
+
|
|
155
|
+
const handleCommit = () => {
|
|
156
|
+
const trimmed = (editValue ?? '').trim();
|
|
157
|
+
if (!trimmed) {
|
|
158
|
+
if (onRemove) {
|
|
159
|
+
onRemove();
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
onEditCommit?.(trimmed);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<ElementType
|
|
168
|
+
contentEditable={false}
|
|
169
|
+
ref={resolvedRef}
|
|
170
|
+
href={href}
|
|
171
|
+
className={styles.chip}
|
|
172
|
+
{...(rest as any)}
|
|
173
|
+
onClick={(e: React.MouseEvent<any>) => {
|
|
174
|
+
if (!isEditing) handleClick(e);
|
|
175
|
+
}}
|
|
176
|
+
draggable={!disabled && !!(restProps as any)?.draggable}
|
|
177
|
+
onDragStart={(e: React.DragEvent<any>) => {
|
|
178
|
+
if (!disabled && (restProps as any)?.draggable) {
|
|
179
|
+
setIsDragging(true);
|
|
180
|
+
}
|
|
181
|
+
restOnDragStart?.(e);
|
|
182
|
+
}}
|
|
183
|
+
onDragEnd={(e: React.DragEvent<any>) => {
|
|
184
|
+
if ((restProps as any)?.draggable) {
|
|
185
|
+
setIsDragging(false);
|
|
186
|
+
}
|
|
187
|
+
restOnDragEnd?.(e);
|
|
188
|
+
}}
|
|
189
|
+
onDoubleClick={(e: React.MouseEvent<any>) => {
|
|
190
|
+
if (!disabled && editable && !isEditing) {
|
|
191
|
+
onEditStart?.();
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
}
|
|
195
|
+
restOnDoubleClick?.(e);
|
|
196
|
+
}}
|
|
197
|
+
onFocus={(e: React.FocusEvent<any>) => {
|
|
198
|
+
if (isInteractive) {
|
|
199
|
+
setIsFocused(true);
|
|
200
|
+
}
|
|
201
|
+
restOnFocus?.(e);
|
|
202
|
+
}}
|
|
203
|
+
onBlur={(e: React.FocusEvent<any>) => {
|
|
204
|
+
setIsFocused(false);
|
|
205
|
+
restOnBlur?.(e);
|
|
206
|
+
}}
|
|
207
|
+
onKeyDown={(e: React.KeyboardEvent<any>) => {
|
|
208
|
+
const key = e.key;
|
|
209
|
+
|
|
210
|
+
// While editing: handle commit/cancel locally
|
|
211
|
+
if (!disabled && isEditing) {
|
|
212
|
+
if (key === 'Enter') {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
handleCommit();
|
|
215
|
+
} else if (key === 'Escape') {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
onEditCancel?.();
|
|
218
|
+
} else if (
|
|
219
|
+
onRemove &&
|
|
220
|
+
editValue?.trim() === '' &&
|
|
221
|
+
(key === 'Backspace' || key === 'Delete' || key === 'Del')
|
|
222
|
+
) {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
e.stopPropagation();
|
|
225
|
+
onRemove();
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Only handle keys when focused/selected and not disabled
|
|
231
|
+
if (!disabled && isFocused) {
|
|
232
|
+
// Start editing with F2 or Enter when editable and no toggle behavior
|
|
233
|
+
if (editable && !onToggle && (key === 'F2' || key === 'Enter')) {
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
onEditStart?.();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Toggle active state on Enter or Space when togglable
|
|
240
|
+
if (
|
|
241
|
+
onToggle &&
|
|
242
|
+
(key === 'Enter' || key === ' ' || key === 'Spacebar')
|
|
243
|
+
) {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
const next = !isActive;
|
|
246
|
+
setIsActive(next);
|
|
247
|
+
onToggle(next);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Trigger remove on Backspace or Delete when removable
|
|
251
|
+
if (
|
|
252
|
+
onRemove &&
|
|
253
|
+
(key === 'Backspace' || key === 'Delete' || key === 'Del')
|
|
254
|
+
) {
|
|
255
|
+
e.preventDefault();
|
|
256
|
+
e.stopPropagation();
|
|
257
|
+
onRemove();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Delegate to user handler last
|
|
262
|
+
restOnKeyDown?.(e);
|
|
263
|
+
}}
|
|
264
|
+
disabled={disabled}
|
|
265
|
+
aria-pressed={onToggle ? isActive : undefined}
|
|
266
|
+
style={{ transition: transition.duration + 's' }}
|
|
267
|
+
>
|
|
268
|
+
{isInteractive && !disabled && !isEditing && (
|
|
269
|
+
<State
|
|
270
|
+
style={{ transition: transition.duration + 's' }}
|
|
271
|
+
className={styles.stateLayer}
|
|
272
|
+
colorName={classNames({
|
|
273
|
+
'on-surface-variant': !isActive,
|
|
274
|
+
'on-secondary-container': isActive,
|
|
275
|
+
})}
|
|
276
|
+
stateClassName={'state-ripple-group-[chip]'}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
{icon && <Icon icon={icon} className={styles.leadingIcon} />}
|
|
281
|
+
<span
|
|
282
|
+
ref={labelRef}
|
|
283
|
+
contentEditable={!!editable && !!isEditing}
|
|
284
|
+
suppressContentEditableWarning
|
|
285
|
+
className={styles.label}
|
|
286
|
+
role={editable ? 'textbox' : undefined}
|
|
287
|
+
spellCheck={false}
|
|
288
|
+
onInput={(e) => {
|
|
289
|
+
const text = (e.currentTarget as HTMLSpanElement).innerText;
|
|
290
|
+
setEditValue(text);
|
|
291
|
+
onChange?.(text);
|
|
292
|
+
}}
|
|
293
|
+
onBlur={(e) => {
|
|
294
|
+
if (editable && isEditing) {
|
|
295
|
+
handleCommit();
|
|
296
|
+
}
|
|
297
|
+
}}
|
|
298
|
+
onKeyDown={(e) => {
|
|
299
|
+
// prevent line breaks inside contenteditable
|
|
300
|
+
if (editable && isEditing && e.key === 'Enter') {
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
handleCommit();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (editable && isEditing && e.key === 'Escape') {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
e.stopPropagation();
|
|
309
|
+
onEditCancel?.();
|
|
310
|
+
}
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
{label}
|
|
314
|
+
</span>
|
|
315
|
+
{onRemove && !isEditing && (
|
|
316
|
+
<Icon
|
|
317
|
+
icon={faXmark}
|
|
318
|
+
className={styles.trailingIcon}
|
|
319
|
+
onMouseDown={(e) => {
|
|
320
|
+
e.preventDefault(); // ⬅️ clé
|
|
321
|
+
e.stopPropagation();
|
|
322
|
+
}}
|
|
323
|
+
onClick={(e: React.MouseEvent) => {
|
|
324
|
+
e.stopPropagation();
|
|
325
|
+
if (!disabled) {
|
|
326
|
+
onRemove();
|
|
327
|
+
}
|
|
328
|
+
}}
|
|
329
|
+
/>
|
|
330
|
+
)}
|
|
331
|
+
</ElementType>
|
|
332
|
+
);
|
|
333
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import React, { useEffect, 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
|
+
export const Chips = ({
|
|
10
|
+
variant = 'input',
|
|
11
|
+
className,
|
|
12
|
+
scrollable = true,
|
|
13
|
+
draggable = false,
|
|
14
|
+
items,
|
|
15
|
+
onItemsChange,
|
|
16
|
+
}: ReactProps<ChipsInterface>) => {
|
|
17
|
+
const list = items ?? [];
|
|
18
|
+
|
|
19
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
20
|
+
|
|
21
|
+
const [isFocused, setIsFocused] = React.useState<boolean>(false);
|
|
22
|
+
|
|
23
|
+
// Internal stable ids per item object (since ChipItem no longer exposes id)
|
|
24
|
+
const idMapRef = React.useRef<WeakMap<ChipItem, string>>(new WeakMap());
|
|
25
|
+
const getInternalId = React.useCallback((it: ChipItem) => {
|
|
26
|
+
const map = idMapRef.current;
|
|
27
|
+
let id = map.get(it);
|
|
28
|
+
if (!id) {
|
|
29
|
+
id = v4();
|
|
30
|
+
map.set(it, id);
|
|
31
|
+
}
|
|
32
|
+
return id;
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (isFocused) {
|
|
37
|
+
ref.current?.focus();
|
|
38
|
+
}
|
|
39
|
+
}, [isFocused]);
|
|
40
|
+
|
|
41
|
+
const chipRefs = React.useRef<(HTMLElement | null)[]>([]);
|
|
42
|
+
|
|
43
|
+
const updateItems = React.useCallback(
|
|
44
|
+
(updater: (prev: ChipItem[]) => ChipItem[]) => {
|
|
45
|
+
onItemsChange?.(updater(list));
|
|
46
|
+
},
|
|
47
|
+
[onItemsChange, list],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const removeAt = React.useCallback(
|
|
51
|
+
(index: number) => {
|
|
52
|
+
updateItems((prev) => prev.filter((_, i) => i !== index));
|
|
53
|
+
},
|
|
54
|
+
[updateItems],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const styles = useChipsStyle({
|
|
58
|
+
scrollable,
|
|
59
|
+
className,
|
|
60
|
+
variant,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const createAndStartEdit = React.useCallback(
|
|
64
|
+
(seedLabel = '') => {
|
|
65
|
+
if (variant !== 'input') return;
|
|
66
|
+
|
|
67
|
+
const newItem: ChipItem = {
|
|
68
|
+
label: seedLabel,
|
|
69
|
+
} as ChipItem;
|
|
70
|
+
|
|
71
|
+
// Generate internal ID for the new item
|
|
72
|
+
const newId = getInternalId(newItem);
|
|
73
|
+
|
|
74
|
+
// Ask parent to add as well
|
|
75
|
+
const next = [...list, newItem];
|
|
76
|
+
onItemsChange?.(next);
|
|
77
|
+
|
|
78
|
+
requestAnimationFrame(() => {
|
|
79
|
+
setSelectedChip(newId);
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
[variant, onItemsChange, list, getInternalId],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const [selectedChip, setSelectedChip] = useState<string | null>(null);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (selectedChip) {
|
|
89
|
+
const index = list.findIndex(
|
|
90
|
+
(item) => getInternalId(item) === selectedChip,
|
|
91
|
+
);
|
|
92
|
+
if (index !== -1) {
|
|
93
|
+
const el = chipRefs.current[index] as any;
|
|
94
|
+
el?.focus?.();
|
|
95
|
+
|
|
96
|
+
const chipsEl = ref.current!;
|
|
97
|
+
const scrollLeft =
|
|
98
|
+
el.offsetLeft + el.offsetWidth / 2 - chipsEl.offsetWidth / 2;
|
|
99
|
+
chipsEl.scrollTo({ left: scrollLeft, behavior: 'smooth' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, [selectedChip, list, getInternalId]);
|
|
103
|
+
|
|
104
|
+
// MODE ITEMS (source de vérité locale ou contrôlée)
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
ref={ref}
|
|
108
|
+
role="list"
|
|
109
|
+
aria-label="Chips"
|
|
110
|
+
className={styles.chips}
|
|
111
|
+
tabIndex={variant === 'input' ? 0 : undefined}
|
|
112
|
+
onFocus={(e) => {
|
|
113
|
+
if (e.target === e.currentTarget) {
|
|
114
|
+
setIsFocused(true);
|
|
115
|
+
}
|
|
116
|
+
}}
|
|
117
|
+
onBlur={() => setIsFocused(false)}
|
|
118
|
+
onKeyDown={(e) => {
|
|
119
|
+
if (variant !== 'input') return;
|
|
120
|
+
|
|
121
|
+
const key = e.key;
|
|
122
|
+
const target = e.target as HTMLElement;
|
|
123
|
+
const isContainerFocused = target === e.currentTarget;
|
|
124
|
+
|
|
125
|
+
// If currently editing a chip, let the chip handle keys
|
|
126
|
+
if (!isFocused) return;
|
|
127
|
+
|
|
128
|
+
// Determine focused chip index if any
|
|
129
|
+
const activeEl = document.activeElement as HTMLElement | null;
|
|
130
|
+
const focusedIndex = chipRefs.current.findIndex(
|
|
131
|
+
(el) => el === activeEl,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (key === 'ArrowLeft') {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
const nextIdx = focusedIndex > 0 ? focusedIndex - 1 : list.length - 1;
|
|
137
|
+
const elId = getInternalId(list[nextIdx]);
|
|
138
|
+
setSelectedChip(elId);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (key === 'ArrowRight') {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
const nextIdx =
|
|
144
|
+
focusedIndex >= 0
|
|
145
|
+
? (focusedIndex + 1) % Math.max(1, list.length)
|
|
146
|
+
: 0;
|
|
147
|
+
const elId = getInternalId(list[nextIdx]);
|
|
148
|
+
setSelectedChip(elId);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (key === 'Home') {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
const elId = getInternalId(list[0]);
|
|
154
|
+
setSelectedChip(elId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (key === 'End') {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
const elId = getInternalId(list[list.length - 1]);
|
|
160
|
+
setSelectedChip(elId);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (isContainerFocused) {
|
|
165
|
+
if (key === 'Enter') {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
createAndStartEdit('');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (key === 'Backspace') {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
// Focus last chip if any
|
|
173
|
+
if (list.length > 0) {
|
|
174
|
+
const el = chipRefs.current[list.length - 1] as any;
|
|
175
|
+
el?.focus?.();
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Start creation when typing a printable character
|
|
180
|
+
if (key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey) {
|
|
181
|
+
createAndStartEdit(key);
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{list.map((item, index) => {
|
|
189
|
+
const internalId = getInternalId(item);
|
|
190
|
+
const isInputVariant = variant === 'input';
|
|
191
|
+
const editProps = isInputVariant
|
|
192
|
+
? {
|
|
193
|
+
editable: true,
|
|
194
|
+
editing: selectedChip === internalId,
|
|
195
|
+
onEditCommit: (next: string | undefined) => {
|
|
196
|
+
setIsFocused(true);
|
|
197
|
+
updateItems((prev) =>
|
|
198
|
+
prev.map((it, i) =>
|
|
199
|
+
i === index ? { ...it, label: next as any } : it,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
onEditCancel: () => {
|
|
204
|
+
setIsFocused(true);
|
|
205
|
+
},
|
|
206
|
+
onChange: (next: ChipItem[]) => {
|
|
207
|
+
if (chipRefs.current.length == index + 1) {
|
|
208
|
+
const el = ref.current!;
|
|
209
|
+
requestAnimationFrame(() => {
|
|
210
|
+
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
: {};
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<Chip
|
|
219
|
+
key={internalId}
|
|
220
|
+
ref={(el: any) => (chipRefs.current[index] = el)}
|
|
221
|
+
label={item.label ?? ''}
|
|
222
|
+
icon={item.icon}
|
|
223
|
+
activated={item.activated}
|
|
224
|
+
disabled={item.disabled}
|
|
225
|
+
variant={item.variant}
|
|
226
|
+
href={item.href}
|
|
227
|
+
draggable={draggable}
|
|
228
|
+
{...editProps}
|
|
229
|
+
onToggle={
|
|
230
|
+
item.activated === undefined
|
|
231
|
+
? undefined
|
|
232
|
+
: (next) =>
|
|
233
|
+
updateItems((prev) =>
|
|
234
|
+
prev.map((it, i) =>
|
|
235
|
+
i === index ? { ...it, activated: next } : it,
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
onBlur={() => {
|
|
240
|
+
if (selectedChip === internalId) {
|
|
241
|
+
setSelectedChip(null);
|
|
242
|
+
}
|
|
243
|
+
}}
|
|
244
|
+
onRemove={
|
|
245
|
+
isInputVariant
|
|
246
|
+
? () => {
|
|
247
|
+
setIsFocused(true);
|
|
248
|
+
removeAt(index);
|
|
249
|
+
}
|
|
250
|
+
: undefined
|
|
251
|
+
}
|
|
252
|
+
/>
|
|
253
|
+
);
|
|
254
|
+
})}
|
|
255
|
+
{isFocused && (
|
|
256
|
+
<>
|
|
257
|
+
<Divider
|
|
258
|
+
orientation="vertical"
|
|
259
|
+
className="animate-[var(--animate-blink)] border-outline"
|
|
260
|
+
style={
|
|
261
|
+
{
|
|
262
|
+
'--animate-blink':
|
|
263
|
+
'blink 1s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
264
|
+
} as React.CSSProperties
|
|
265
|
+
}
|
|
266
|
+
/>
|
|
267
|
+
|
|
268
|
+
<style>
|
|
269
|
+
{`
|
|
270
|
+
@keyframes blink {
|
|
271
|
+
0%, 50% { opacity: 1; }
|
|
272
|
+
50.01%, 100% { opacity: 0; }
|
|
273
|
+
}
|
|
274
|
+
`}
|
|
275
|
+
</style>
|
|
276
|
+
</>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
};
|
|
@@ -19,7 +19,6 @@ export const IconButton = ({
|
|
|
19
19
|
variant = 'standard',
|
|
20
20
|
href,
|
|
21
21
|
disabled = false,
|
|
22
|
-
type = 'button',
|
|
23
22
|
title,
|
|
24
23
|
label,
|
|
25
24
|
onToggle,
|
|
@@ -49,26 +48,18 @@ export const IconButton = ({
|
|
|
49
48
|
|
|
50
49
|
const [isActive, setIsActive] = React.useState(activated);
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
if (onClick) {
|
|
59
|
-
onClick(e);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
} else if (onToggle) {
|
|
63
|
-
handleClick = (e: React.MouseEvent<any, MouseEvent>) => {
|
|
64
|
-
if (disabled) {
|
|
65
|
-
e.preventDefault();
|
|
66
|
-
}
|
|
51
|
+
const handleClick = (e: React.MouseEvent<any, MouseEvent>) => {
|
|
52
|
+
if (disabled) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
}
|
|
55
|
+
if (onToggle) {
|
|
67
56
|
setIsActive(!isActive);
|
|
68
|
-
onToggle(
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
onToggle(!isActive);
|
|
58
|
+
} else if (onClick) {
|
|
59
|
+
onClick(e);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
72
63
|
useEffect(() => {
|
|
73
64
|
setIsActive(activated);
|
|
74
65
|
}, [activated]);
|