@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/index.cjs +7 -2
  3. package/dist/index.js +2307 -1818
  4. package/dist/lib/components/Button.d.ts.map +1 -1
  5. package/dist/lib/components/Chip.d.ts +9 -0
  6. package/dist/lib/components/Chip.d.ts.map +1 -0
  7. package/dist/lib/components/Chips.d.ts +4 -0
  8. package/dist/lib/components/Chips.d.ts.map +1 -0
  9. package/dist/lib/components/IconButton.d.ts.map +1 -1
  10. package/dist/lib/components/index.d.ts +2 -0
  11. package/dist/lib/components/index.d.ts.map +1 -1
  12. package/dist/lib/effects/block-scroll.effect.d.ts +6 -0
  13. package/dist/lib/effects/block-scroll.effect.d.ts.map +1 -1
  14. package/dist/lib/effects/smooth-scroll.effect.d.ts +5 -0
  15. package/dist/lib/effects/smooth-scroll.effect.d.ts.map +1 -1
  16. package/dist/lib/icon/icon.d.ts.map +1 -1
  17. package/dist/lib/interfaces/chip.interface.d.ts +76 -0
  18. package/dist/lib/interfaces/chip.interface.d.ts.map +1 -0
  19. package/dist/lib/interfaces/chips.interface.d.ts +29 -0
  20. package/dist/lib/interfaces/chips.interface.d.ts.map +1 -0
  21. package/dist/lib/interfaces/index.d.ts +2 -0
  22. package/dist/lib/interfaces/index.d.ts.map +1 -1
  23. package/dist/lib/styles/chip.style.d.ts +111 -0
  24. package/dist/lib/styles/chip.style.d.ts.map +1 -0
  25. package/dist/lib/styles/chips.style.d.ts +55 -0
  26. package/dist/lib/styles/chips.style.d.ts.map +1 -0
  27. package/dist/lib/styles/index.d.ts +2 -0
  28. package/dist/lib/styles/index.d.ts.map +1 -1
  29. package/dist/lib/styles/text-field.style.d.ts +2 -2
  30. package/package.json +4 -4
  31. package/src/lib/components/Button.tsx +11 -20
  32. package/src/lib/components/Chip.tsx +333 -0
  33. package/src/lib/components/Chips.tsx +280 -0
  34. package/src/lib/components/IconButton.tsx +11 -19
  35. package/src/lib/components/index.ts +2 -0
  36. package/src/lib/effects/block-scroll.effect.tsx +89 -11
  37. package/src/lib/effects/smooth-scroll.effect.tsx +5 -0
  38. package/src/lib/icon/icon.tsx +7 -1
  39. package/src/lib/interfaces/chip.interface.ts +97 -0
  40. package/src/lib/interfaces/chips.interface.ts +37 -0
  41. package/src/lib/interfaces/index.ts +2 -0
  42. package/src/lib/styles/chip.style.ts +62 -0
  43. package/src/lib/styles/chips.style.ts +20 -0
  44. 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 dx = 0,
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: dx,
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',
@@ -17,7 +17,12 @@ interface Props {
17
17
  className?: string;
18
18
  }
19
19
 
20
- export const Icon: React.FC<Props> = ({ icon, colors = [], className }) => {
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
+ );
@@ -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';