@udixio/ui-react 2.8.4 → 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 +44 -0
- package/dist/index.cjs +7 -2
- package/dist/index.js +2307 -1818
- 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.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 -19
- 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
|
@@ -22,6 +22,12 @@ type BlockScrollProps = {
|
|
|
22
22
|
el: HTMLElement;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated Potentially blocks scroll events unintentionally (wheel/touch/keyboard)
|
|
27
|
+
* and may interfere with internal scrollable areas (modals, lists, overflow containers).
|
|
28
|
+
* This API will be removed soon. Avoid using it and migrate to native behaviors
|
|
29
|
+
* or to local scroll handling at the component level.
|
|
30
|
+
*/
|
|
25
31
|
export const BlockScroll: React.FC<BlockScrollProps> = ({
|
|
26
32
|
onScroll,
|
|
27
33
|
el,
|
|
@@ -143,11 +149,73 @@ export const BlockScroll: React.FC<BlockScrollProps> = ({
|
|
|
143
149
|
lastTouch.current = null;
|
|
144
150
|
};
|
|
145
151
|
|
|
152
|
+
const interactiveRoles = new Set([
|
|
153
|
+
'textbox',
|
|
154
|
+
'listbox',
|
|
155
|
+
'menu',
|
|
156
|
+
'menubar',
|
|
157
|
+
'grid',
|
|
158
|
+
'tree',
|
|
159
|
+
'tablist',
|
|
160
|
+
'toolbar',
|
|
161
|
+
'radiogroup',
|
|
162
|
+
'combobox',
|
|
163
|
+
'spinbutton',
|
|
164
|
+
'slider',
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const isEditableOrInteractive = (node: HTMLElement | null) => {
|
|
168
|
+
if (!node) return false;
|
|
169
|
+
const tag = node.tagName.toLowerCase();
|
|
170
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select')
|
|
171
|
+
return true;
|
|
172
|
+
if ((node as HTMLInputElement).isContentEditable) return true;
|
|
173
|
+
if (tag === 'button') return true;
|
|
174
|
+
if (tag === 'a' && (node as HTMLAnchorElement).href) return true;
|
|
175
|
+
const role = node.getAttribute('role');
|
|
176
|
+
if (role && interactiveRoles.has(role)) return true;
|
|
177
|
+
return false;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const hasInteractiveAncestor = (el: HTMLElement | null) => {
|
|
181
|
+
let n: HTMLElement | null = el;
|
|
182
|
+
while (n && n !== document.body && n !== document.documentElement) {
|
|
183
|
+
if (isEditableOrInteractive(n)) return true;
|
|
184
|
+
n = n.parentElement;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const canAncestorScroll = (start: HTMLElement | null, dy: number) => {
|
|
190
|
+
let n: HTMLElement | null = start;
|
|
191
|
+
while (n && n !== el) {
|
|
192
|
+
const style = window.getComputedStyle(n);
|
|
193
|
+
const overflowY = style.overflowY || style.overflow;
|
|
194
|
+
const canScrollY =
|
|
195
|
+
(overflowY === 'auto' || overflowY === 'scroll') &&
|
|
196
|
+
n.scrollHeight > n.clientHeight;
|
|
197
|
+
if (canScrollY) {
|
|
198
|
+
const canDown = n.scrollTop < n.scrollHeight - n.clientHeight;
|
|
199
|
+
const canUp = n.scrollTop > 0;
|
|
200
|
+
if ((dy > 0 && canDown) || (dy < 0 && canUp)) return true;
|
|
201
|
+
}
|
|
202
|
+
n = n.parentElement;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
};
|
|
206
|
+
|
|
146
207
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
208
|
+
if (e.defaultPrevented) return;
|
|
209
|
+
|
|
210
|
+
// Garder les comportements natifs pour les éléments interactifs
|
|
211
|
+
const target = e.target as HTMLElement | null;
|
|
212
|
+
if (isEditableOrInteractive(target) || hasInteractiveAncestor(target)) {
|
|
213
|
+
return; // ne pas empêcher
|
|
214
|
+
}
|
|
215
|
+
|
|
147
216
|
const line = 40;
|
|
148
217
|
const page = el.clientHeight * 0.9;
|
|
149
|
-
let
|
|
150
|
-
dy = 0;
|
|
218
|
+
let dy = 0;
|
|
151
219
|
|
|
152
220
|
switch (e.key) {
|
|
153
221
|
case 'ArrowDown':
|
|
@@ -156,12 +224,6 @@ export const BlockScroll: React.FC<BlockScrollProps> = ({
|
|
|
156
224
|
case 'ArrowUp':
|
|
157
225
|
dy = -line;
|
|
158
226
|
break;
|
|
159
|
-
case 'ArrowRight':
|
|
160
|
-
dx = line;
|
|
161
|
-
break;
|
|
162
|
-
case 'ArrowLeft':
|
|
163
|
-
dx = -line;
|
|
164
|
-
break;
|
|
165
227
|
case 'PageDown':
|
|
166
228
|
dy = page;
|
|
167
229
|
break;
|
|
@@ -174,17 +236,33 @@ export const BlockScroll: React.FC<BlockScrollProps> = ({
|
|
|
174
236
|
case 'End':
|
|
175
237
|
dy = Number.POSITIVE_INFINITY;
|
|
176
238
|
break;
|
|
177
|
-
case ' ':
|
|
239
|
+
case ' ': {
|
|
240
|
+
// Espace: laisser passer sur boutons/inputs/etc. déjà filtrés ci-dessus
|
|
178
241
|
dy = e.shiftKey ? -page : page;
|
|
179
242
|
break;
|
|
243
|
+
}
|
|
180
244
|
default:
|
|
181
|
-
return;
|
|
245
|
+
return; // ne pas gérer, laisser natif
|
|
182
246
|
}
|
|
247
|
+
|
|
248
|
+
// Si un ancêtre (≠ el) peut scroller dans ce sens, laisser le natif
|
|
249
|
+
if (canAncestorScroll(target, dy)) return;
|
|
250
|
+
|
|
251
|
+
// Ne gérer que si focus est sur body/html ou dans el
|
|
252
|
+
const ae = document.activeElement as HTMLElement | null;
|
|
253
|
+
const focusInsideEl =
|
|
254
|
+
ae &&
|
|
255
|
+
(ae === document.body ||
|
|
256
|
+
ae === document.documentElement ||
|
|
257
|
+
el.contains(ae));
|
|
258
|
+
if (!focusInsideEl) return;
|
|
259
|
+
|
|
260
|
+
// OK on prend en charge: empêcher le natif et émettre l’intention
|
|
183
261
|
e.preventDefault();
|
|
184
262
|
emitIntent({
|
|
185
263
|
type: 'intent',
|
|
186
264
|
source: 'keyboard',
|
|
187
|
-
deltaX:
|
|
265
|
+
deltaX: 0,
|
|
188
266
|
deltaY: dy,
|
|
189
267
|
originalEvent: e,
|
|
190
268
|
});
|
|
@@ -4,6 +4,11 @@ import { ReactProps } from '../utils';
|
|
|
4
4
|
import { BlockScroll } from './block-scroll.effect';
|
|
5
5
|
import { animate, AnimationPlaybackControls } from 'motion';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* WARNING: using this component is not recommended for now.
|
|
9
|
+
* It may block or alter certain scroll events (wheel/touch/keyboard) depending on the context.
|
|
10
|
+
* Rework it later (e.g., via Lenis or another solution) before using it in production.
|
|
11
|
+
*/
|
|
7
12
|
export const SmoothScroll = ({
|
|
8
13
|
transition,
|
|
9
14
|
orientation = 'vertical',
|
package/src/lib/icon/icon.tsx
CHANGED
|
@@ -17,7 +17,12 @@ interface Props {
|
|
|
17
17
|
className?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export const Icon: React.FC<Props> = ({
|
|
20
|
+
export const Icon: React.FC<Props> = ({
|
|
21
|
+
icon,
|
|
22
|
+
colors = [],
|
|
23
|
+
className,
|
|
24
|
+
...restProps
|
|
25
|
+
}) => {
|
|
21
26
|
// Si c'est une chaîne de caractères (SVG raw)
|
|
22
27
|
if (typeof icon === 'string') {
|
|
23
28
|
// Modifier la couleur du SVG en remplaçant les attributs fill/stroke
|
|
@@ -98,6 +103,7 @@ export const Icon: React.FC<Props> = ({ icon, colors = [], className }) => {
|
|
|
98
103
|
viewBox={`0 0 ${width} ${height}`}
|
|
99
104
|
role="img"
|
|
100
105
|
aria-hidden="true"
|
|
106
|
+
{...restProps}
|
|
101
107
|
>
|
|
102
108
|
{typeof svgPathData === 'string' ? (
|
|
103
109
|
<path className={'fill-current'} d={svgPathData} />
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ActionOrLink } from '../utils';
|
|
2
|
+
import { Transition } from 'motion';
|
|
3
|
+
import { Icon } from '../icon';
|
|
4
|
+
|
|
5
|
+
type ChipVariant = 'outlined' | 'elevated';
|
|
6
|
+
|
|
7
|
+
export type ChipProps = {
|
|
8
|
+
/**
|
|
9
|
+
* The label is the text that is displayed on the chip.
|
|
10
|
+
*/
|
|
11
|
+
label?: string;
|
|
12
|
+
|
|
13
|
+
children?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The chip variant determines the style.
|
|
17
|
+
*/
|
|
18
|
+
variant?: ChipVariant;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Disables the chip if set to true.
|
|
22
|
+
*/
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An optional icon to display in the chip.
|
|
27
|
+
*/
|
|
28
|
+
icon?: Icon;
|
|
29
|
+
|
|
30
|
+
transition?: Transition;
|
|
31
|
+
|
|
32
|
+
onToggle?: (isActive: boolean) => void;
|
|
33
|
+
|
|
34
|
+
activated?: boolean;
|
|
35
|
+
|
|
36
|
+
onRemove?: () => void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Enable native HTML drag and drop on the chip.
|
|
40
|
+
*/
|
|
41
|
+
draggable?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Called when drag starts (composed with internal handler that sets isDragging).
|
|
45
|
+
*/
|
|
46
|
+
onDragStart?: (e: React.DragEvent) => void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Called when drag ends (composed with internal handler that clears isDragging).
|
|
50
|
+
*/
|
|
51
|
+
onDragEnd?: (e: React.DragEvent) => void;
|
|
52
|
+
} & (
|
|
53
|
+
| {
|
|
54
|
+
editable?: false;
|
|
55
|
+
editing?: never;
|
|
56
|
+
onEditStart?: never;
|
|
57
|
+
onEditCommit: never;
|
|
58
|
+
onEditCancel?: never;
|
|
59
|
+
onChange?: never;
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
/** Enable label inline edition for this chip (used by Chips variant="input"). */
|
|
63
|
+
editable?: true;
|
|
64
|
+
|
|
65
|
+
/** Affirms that the chip is currently being edited. */
|
|
66
|
+
editing?: boolean;
|
|
67
|
+
|
|
68
|
+
/** Request to start editing (e.g., double-click, Enter/F2). */
|
|
69
|
+
onEditStart?: () => void;
|
|
70
|
+
|
|
71
|
+
/** Commit edition with the new label. */
|
|
72
|
+
onEditCommit: (nextLabel: string) => void;
|
|
73
|
+
|
|
74
|
+
/** Cancel edition and restore previous label. */
|
|
75
|
+
onEditCancel?: () => void;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fired on each edit keystroke when content changes (only while editing).
|
|
79
|
+
* Useful for live formatting, suggestions, validation, etc.
|
|
80
|
+
*/
|
|
81
|
+
onChange?: (nextLabel: string) => void;
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
type Elements = ['chip', 'stateLayer', 'leadingIcon', 'trailingIcon', 'label'];
|
|
86
|
+
|
|
87
|
+
export type ChipInterface = ActionOrLink<ChipProps> & {
|
|
88
|
+
elements: Elements;
|
|
89
|
+
states: {
|
|
90
|
+
isActive: boolean;
|
|
91
|
+
trailingIcon?: boolean;
|
|
92
|
+
isFocused: boolean;
|
|
93
|
+
isInteractive: boolean;
|
|
94
|
+
isDragging?: boolean;
|
|
95
|
+
isEditing?: boolean;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ActionOrLink } from '../utils';
|
|
2
|
+
import type { Icon } from '../icon';
|
|
3
|
+
|
|
4
|
+
// Ce que Chips a besoin de connaître pour (re)construire un Chip
|
|
5
|
+
export type ChipItem = {
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: Icon;
|
|
8
|
+
activated?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
variant?: 'outlined' | 'elevated';
|
|
11
|
+
href?: string; // si tu utilises ActionOrLink côté Chip
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ChipsVariant = 'input';
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
/** Style du conteneur de chips */
|
|
18
|
+
variant?: ChipsVariant;
|
|
19
|
+
|
|
20
|
+
/** Active/masse un comportement de container (si utile) */
|
|
21
|
+
scrollable?: boolean;
|
|
22
|
+
|
|
23
|
+
draggable?: boolean; // optionnel
|
|
24
|
+
|
|
25
|
+
/** Mode contrôlé: la source de vérité */
|
|
26
|
+
items?: ChipItem[];
|
|
27
|
+
|
|
28
|
+
/** Notifie toute modification de la liste (remove, toggle, etc.) */
|
|
29
|
+
onItemsChange?: (next: ChipItem[]) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type Elements = ['chips'];
|
|
33
|
+
|
|
34
|
+
export type ChipsInterface = ActionOrLink<Props> & {
|
|
35
|
+
elements: Elements;
|
|
36
|
+
states: {};
|
|
37
|
+
};
|
|
@@ -2,6 +2,8 @@ export * from './button.interface';
|
|
|
2
2
|
export * from './card.interface';
|
|
3
3
|
export * from './carousel-item.interface';
|
|
4
4
|
export * from './carousel.interface';
|
|
5
|
+
export * from './chip.interface';
|
|
6
|
+
export * from './chips.interface';
|
|
5
7
|
export * from './divider.interface';
|
|
6
8
|
export * from './fab.interface';
|
|
7
9
|
export * from './fab-menu.interface';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ClassNameComponent } from '../utils';
|
|
2
|
+
import { classNames, createUseClassNames, defaultClassNames } from '../utils';
|
|
3
|
+
import { ChipInterface } from '../interfaces';
|
|
4
|
+
|
|
5
|
+
const chipConfig: ClassNameComponent<ChipInterface> = ({
|
|
6
|
+
variant,
|
|
7
|
+
|
|
8
|
+
disabled,
|
|
9
|
+
trailingIcon,
|
|
10
|
+
icon,
|
|
11
|
+
isActive,
|
|
12
|
+
isInteractive,
|
|
13
|
+
activated,
|
|
14
|
+
isFocused,
|
|
15
|
+
isDragging,
|
|
16
|
+
isEditing,
|
|
17
|
+
}) => ({
|
|
18
|
+
chip: classNames(
|
|
19
|
+
' group/chip px-3 py-1.5 rounded-lg flex items-center gap-2 outline-none',
|
|
20
|
+
{
|
|
21
|
+
'pl-2': icon,
|
|
22
|
+
'pr-2': trailingIcon,
|
|
23
|
+
'cursor-pointer': !disabled && isInteractive,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
' text-on-surface-variant': (!activated && !isFocused) || isEditing,
|
|
27
|
+
'bg-secondary-container text-on-secondary-container':
|
|
28
|
+
(activated || isFocused) && !isEditing,
|
|
29
|
+
},
|
|
30
|
+
// Dragging feedback
|
|
31
|
+
isDragging && ['opacity-100 cursor-grabbing shadow-3'],
|
|
32
|
+
variant === 'outlined' && [
|
|
33
|
+
'border border-outline-variant',
|
|
34
|
+
{
|
|
35
|
+
'border-transparent': isEditing,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
variant === 'elevated' &&
|
|
39
|
+
!isEditing && [
|
|
40
|
+
'shadow-1 bg-surface-container-low',
|
|
41
|
+
'border border-outline-variant',
|
|
42
|
+
],
|
|
43
|
+
),
|
|
44
|
+
|
|
45
|
+
stateLayer: classNames('rounded-lg overflow-hidden', {}),
|
|
46
|
+
label: classNames('outline-none text-nowrap', {
|
|
47
|
+
'opacity-[0.38]': disabled,
|
|
48
|
+
}),
|
|
49
|
+
leadingIcon: classNames('text-primary size-[18px]', {
|
|
50
|
+
'opacity-[0.38]': disabled,
|
|
51
|
+
}),
|
|
52
|
+
trailingIcon: classNames('cursor-pointer size-[18px]', {
|
|
53
|
+
'opacity-[0.38]': disabled,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const chipStyle = defaultClassNames<ChipInterface>('chip', chipConfig);
|
|
58
|
+
|
|
59
|
+
export const useChipStyle = createUseClassNames<ChipInterface>(
|
|
60
|
+
'chip',
|
|
61
|
+
chipConfig,
|
|
62
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ClassNameComponent } from '../utils';
|
|
2
|
+
import { classNames, createUseClassNames, defaultClassNames } from '../utils';
|
|
3
|
+
import { ChipsInterface } from '../interfaces';
|
|
4
|
+
|
|
5
|
+
const chipsConfig: ClassNameComponent<ChipsInterface> = ({ scrollable }) => ({
|
|
6
|
+
chips: classNames(' flex gap-3 outline-none', {
|
|
7
|
+
'flex-wrap': !scrollable,
|
|
8
|
+
'overflow-x-auto': scrollable,
|
|
9
|
+
}),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const chipsStyle = defaultClassNames<ChipsInterface>(
|
|
13
|
+
'chips',
|
|
14
|
+
chipsConfig,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const useChipsStyle = createUseClassNames<ChipsInterface>(
|
|
18
|
+
'chips',
|
|
19
|
+
chipsConfig,
|
|
20
|
+
);
|
package/src/lib/styles/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export * from './button.style';
|
|
|
2
2
|
export * from './card.style';
|
|
3
3
|
export * from './carousel-item.style';
|
|
4
4
|
export * from './carousel.style';
|
|
5
|
+
export * from './chip.style';
|
|
6
|
+
export * from './chips.style';
|
|
5
7
|
export * from './divider.style';
|
|
6
8
|
export * from './fab.style';
|
|
7
9
|
export * from './fab-menu.style';
|