@udixio/ui-react 2.9.7 → 2.9.9

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 (38) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.cjs +3 -3
  3. package/dist/index.js +2158 -2042
  4. package/dist/lib/components/Tooltip.d.ts +9 -0
  5. package/dist/lib/components/Tooltip.d.ts.map +1 -0
  6. package/dist/lib/components/index.d.ts +1 -1
  7. package/dist/lib/effects/State.d.ts.map +1 -1
  8. package/dist/lib/hooks/index.d.ts +5 -0
  9. package/dist/lib/hooks/index.d.ts.map +1 -0
  10. package/dist/lib/hooks/useTooltipPosition.d.ts +22 -0
  11. package/dist/lib/hooks/useTooltipPosition.d.ts.map +1 -0
  12. package/dist/lib/hooks/useTooltipTrigger.d.ts +44 -0
  13. package/dist/lib/hooks/useTooltipTrigger.d.ts.map +1 -0
  14. package/dist/lib/index.d.ts +1 -0
  15. package/dist/lib/index.d.ts.map +1 -1
  16. package/dist/lib/interfaces/tooltip.interface.d.ts +24 -2
  17. package/dist/lib/interfaces/tooltip.interface.d.ts.map +1 -1
  18. package/dist/lib/styles/card.style.d.ts.map +1 -1
  19. package/dist/lib/styles/tooltip.style.d.ts +32 -4
  20. package/dist/lib/styles/tooltip.style.d.ts.map +1 -1
  21. package/package.json +3 -3
  22. package/src/lib/components/Fab.tsx +2 -2
  23. package/src/lib/components/IconButton.tsx +3 -3
  24. package/src/lib/components/Tooltip.tsx +172 -0
  25. package/src/lib/components/index.ts +1 -1
  26. package/src/lib/effects/State.tsx +6 -2
  27. package/src/lib/hooks/index.ts +11 -0
  28. package/src/lib/hooks/useTooltipPosition.ts +95 -0
  29. package/src/lib/hooks/useTooltipTrigger.ts +270 -0
  30. package/src/lib/index.ts +1 -0
  31. package/src/lib/interfaces/tooltip.interface.ts +24 -2
  32. package/src/lib/styles/card.style.ts +4 -1
  33. package/src/lib/styles/tooltip.style.ts +1 -0
  34. package/src/stories/communication/tool-tip.stories.tsx +19 -19
  35. package/tsconfig.json +0 -6
  36. package/dist/lib/components/ToolTip.d.ts +0 -9
  37. package/dist/lib/components/ToolTip.d.ts.map +0 -1
  38. package/src/lib/components/ToolTip.tsx +0 -256
@@ -0,0 +1,270 @@
1
+ import { useCallback, useEffect, useId, useRef, useState } from 'react';
2
+
3
+ type Trigger = 'hover' | 'click' | 'focus' | null;
4
+
5
+ type TooltipState = 'hidden' | 'hovered' | 'focused' | 'clicked';
6
+
7
+ export interface UseTooltipTriggerOptions {
8
+ trigger?: Trigger | Trigger[];
9
+ isOpen?: boolean;
10
+ defaultOpen?: boolean;
11
+ onOpenChange?: (open: boolean) => void;
12
+ openDelay?: number;
13
+ closeDelay?: number;
14
+ id?: string;
15
+ }
16
+
17
+ export interface UseTooltipTriggerReturn {
18
+ triggerProps: {
19
+ 'aria-describedby': string | undefined;
20
+ onMouseEnter: () => void;
21
+ onMouseLeave: () => void;
22
+ onFocus: () => void;
23
+ onBlur: () => void;
24
+ onClick: () => void;
25
+ onKeyDown: (event: React.KeyboardEvent) => void;
26
+ };
27
+ tooltipProps: {
28
+ id: string;
29
+ role: 'tooltip';
30
+ 'aria-hidden': boolean;
31
+ onMouseEnter: () => void;
32
+ onMouseLeave: () => void;
33
+ };
34
+ isOpen: boolean;
35
+ state: TooltipState;
36
+ }
37
+
38
+ /**
39
+ * Hook to manage tooltip trigger state machine, events, and accessibility props.
40
+ *
41
+ * State Machine:
42
+ * - States: hidden | hovered | focused | clicked
43
+ * - Priority: clicked > focused > hovered > hidden
44
+ * - Focus takes priority over hover (don't close on mouse leave if focused)
45
+ * - Escape key closes tooltip from any open state
46
+ * - Click toggles for 'click' trigger
47
+ */
48
+ export function useTooltipTrigger({
49
+ trigger = ['hover', 'focus'],
50
+ isOpen: isOpenProp,
51
+ defaultOpen = false,
52
+ onOpenChange,
53
+ openDelay = 400,
54
+ closeDelay = 150,
55
+ id: idProp,
56
+ }: UseTooltipTriggerOptions = {}): UseTooltipTriggerReturn {
57
+ const generatedId = useId();
58
+ const tooltipId = idProp ?? `tooltip-${generatedId}`;
59
+
60
+ // Normalize trigger to array
61
+ const triggers = Array.isArray(trigger) ? trigger : [trigger];
62
+
63
+ // Controlled vs uncontrolled state
64
+ const isControlled = typeof isOpenProp === 'boolean';
65
+ const [internalState, setInternalState] = useState<TooltipState>(
66
+ defaultOpen ? 'hovered' : 'hidden',
67
+ );
68
+
69
+ // Track if tooltip content is being hovered (for pointer intent)
70
+ const [isTooltipHovered, setIsTooltipHovered] = useState(false);
71
+
72
+ // Timeout refs for delayed open/close
73
+ const openTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
74
+ const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
75
+
76
+ // Clear all timeouts
77
+ const clearTimeouts = useCallback(() => {
78
+ if (openTimeoutRef.current) {
79
+ clearTimeout(openTimeoutRef.current);
80
+ openTimeoutRef.current = null;
81
+ }
82
+ if (closeTimeoutRef.current) {
83
+ clearTimeout(closeTimeoutRef.current);
84
+ closeTimeoutRef.current = null;
85
+ }
86
+ }, []);
87
+
88
+ // Cleanup on unmount
89
+ useEffect(() => {
90
+ return () => clearTimeouts();
91
+ }, [clearTimeouts]);
92
+
93
+ // State transition function
94
+ const transition = useCallback(
95
+ (newState: TooltipState) => {
96
+ if (isControlled) {
97
+ // In controlled mode, notify parent of desired state change
98
+ const shouldBeOpen = newState !== 'hidden';
99
+ onOpenChange?.(shouldBeOpen);
100
+ } else {
101
+ setInternalState(newState);
102
+ const shouldBeOpen = newState !== 'hidden';
103
+ onOpenChange?.(shouldBeOpen);
104
+ }
105
+ },
106
+ [isControlled, onOpenChange],
107
+ );
108
+
109
+ // Compute actual state and isOpen
110
+ const state = isControlled
111
+ ? isOpenProp
112
+ ? 'hovered' // Simplified: in controlled mode, we just track open/closed
113
+ : 'hidden'
114
+ : internalState;
115
+
116
+ const isOpen = state !== 'hidden';
117
+
118
+ // Get state priority for comparison
119
+ const getStatePriority = (s: TooltipState): number => {
120
+ switch (s) {
121
+ case 'hidden':
122
+ return 0;
123
+ case 'hovered':
124
+ return 1;
125
+ case 'focused':
126
+ return 2;
127
+ case 'clicked':
128
+ return 3;
129
+ default:
130
+ return 0;
131
+ }
132
+ };
133
+
134
+ // Schedule opening with delay
135
+ const scheduleOpen = useCallback(
136
+ (targetState: TooltipState) => {
137
+ clearTimeouts();
138
+
139
+ // Only transition if new state has higher priority
140
+ if (getStatePriority(targetState) <= getStatePriority(state)) {
141
+ return;
142
+ }
143
+
144
+ openTimeoutRef.current = setTimeout(() => {
145
+ transition(targetState);
146
+ }, openDelay);
147
+ },
148
+ [clearTimeouts, openDelay, state, transition],
149
+ );
150
+
151
+ // Schedule closing with delay
152
+ const scheduleClose = useCallback(
153
+ (fromState: TooltipState) => {
154
+ clearTimeouts();
155
+
156
+ closeTimeoutRef.current = setTimeout(() => {
157
+ // Only close if we're still in the same state (or lower priority)
158
+ if (
159
+ !isControlled &&
160
+ getStatePriority(internalState) <= getStatePriority(fromState)
161
+ ) {
162
+ transition('hidden');
163
+ } else if (isControlled) {
164
+ transition('hidden');
165
+ }
166
+ }, closeDelay);
167
+ },
168
+ [clearTimeouts, closeDelay, internalState, isControlled, transition],
169
+ );
170
+
171
+ // Event handlers for trigger element
172
+ const handleMouseEnter = useCallback(() => {
173
+ if (!triggers.includes('hover')) return;
174
+ scheduleOpen('hovered');
175
+ }, [triggers, scheduleOpen]);
176
+
177
+ const handleMouseLeave = useCallback(() => {
178
+ if (!triggers.includes('hover')) return;
179
+
180
+ // Don't close if focused (focus has higher priority)
181
+ if (state === 'focused' || state === 'clicked') return;
182
+
183
+ // Don't close immediately if tooltip itself is hovered
184
+ if (isTooltipHovered) return;
185
+
186
+ scheduleClose('hovered');
187
+ }, [triggers, state, isTooltipHovered, scheduleClose]);
188
+
189
+ const handleFocus = useCallback(() => {
190
+ if (!triggers.includes('focus')) return;
191
+ clearTimeouts();
192
+ transition('focused');
193
+ }, [triggers, clearTimeouts, transition]);
194
+
195
+ const handleBlur = useCallback(() => {
196
+ if (!triggers.includes('focus')) return;
197
+
198
+ // Don't close if clicked (clicked has higher priority)
199
+ if (state === 'clicked') return;
200
+
201
+ // If also hovering, transition to hovered state
202
+ if (triggers.includes('hover') && isTooltipHovered) {
203
+ transition('hovered');
204
+ return;
205
+ }
206
+
207
+ scheduleClose('focused');
208
+ }, [triggers, state, isTooltipHovered, scheduleClose, transition]);
209
+
210
+ const handleClick = useCallback(() => {
211
+ if (!triggers.includes('click')) return;
212
+
213
+ clearTimeouts();
214
+
215
+ // Toggle behavior for click trigger
216
+ if (state === 'clicked') {
217
+ transition('hidden');
218
+ } else {
219
+ transition('clicked');
220
+ }
221
+ }, [triggers, state, clearTimeouts, transition]);
222
+
223
+ const handleKeyDown = useCallback(
224
+ (event: React.KeyboardEvent) => {
225
+ // Escape closes tooltip from any open state
226
+ if (event.key === 'Escape' && isOpen) {
227
+ clearTimeouts();
228
+ transition('hidden');
229
+ event.preventDefault();
230
+ }
231
+ },
232
+ [isOpen, clearTimeouts, transition],
233
+ );
234
+
235
+ // Event handlers for tooltip element (pointer intent)
236
+ const handleTooltipMouseEnter = useCallback(() => {
237
+ setIsTooltipHovered(true);
238
+ clearTimeouts();
239
+ }, [clearTimeouts]);
240
+
241
+ const handleTooltipMouseLeave = useCallback(() => {
242
+ setIsTooltipHovered(false);
243
+
244
+ // If trigger includes hover and we're in hover state, schedule close
245
+ if (triggers.includes('hover') && state === 'hovered') {
246
+ scheduleClose('hovered');
247
+ }
248
+ }, [triggers, state, scheduleClose]);
249
+
250
+ return {
251
+ triggerProps: {
252
+ 'aria-describedby': isOpen ? tooltipId : undefined,
253
+ onMouseEnter: handleMouseEnter,
254
+ onMouseLeave: handleMouseLeave,
255
+ onFocus: handleFocus,
256
+ onBlur: handleBlur,
257
+ onClick: handleClick,
258
+ onKeyDown: handleKeyDown,
259
+ },
260
+ tooltipProps: {
261
+ id: tooltipId,
262
+ role: 'tooltip',
263
+ 'aria-hidden': !isOpen,
264
+ onMouseEnter: handleTooltipMouseEnter,
265
+ onMouseLeave: handleTooltipMouseLeave,
266
+ },
267
+ isOpen,
268
+ state,
269
+ };
270
+ }
package/src/lib/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './components';
2
2
  export * from './effects';
3
+ export * from './hooks';
3
4
  export * from './icon';
4
5
  export * from './interfaces';
5
6
  export * from './styles';
@@ -10,7 +10,10 @@ export type ToolTipInterface<T extends HTMLElement = any> = {
10
10
  props: {
11
11
  variant?: 'plain' | 'rich';
12
12
  title?: string;
13
- text: string;
13
+ /** Supporting text for the tooltip. Optional when using `content` prop. */
14
+ text?: string;
15
+ /** Custom content slot that replaces title/text/buttons when provided */
16
+ content?: ReactNode;
14
17
  buttons?: ReactProps<ButtonInterface> | ReactProps<ButtonInterface>[];
15
18
  position?:
16
19
  | 'top'
@@ -23,6 +26,18 @@ export type ToolTipInterface<T extends HTMLElement = any> = {
23
26
  | 'bottom-right';
24
27
  trigger?: Trigger | Trigger[];
25
28
  transition?: Transition;
29
+ /** Delay in milliseconds before showing the tooltip. Default: 400ms */
30
+ openDelay?: number;
31
+ /** Delay in milliseconds before hiding the tooltip. Default: 150ms */
32
+ closeDelay?: number;
33
+ /** Controlled mode: explicitly control whether the tooltip is open */
34
+ isOpen?: boolean;
35
+ /** Uncontrolled mode: default open state */
36
+ defaultOpen?: boolean;
37
+ /** Callback when the open state changes */
38
+ onOpenChange?: (open: boolean) => void;
39
+ /** Custom ID for accessibility linking. Auto-generated if not provided. */
40
+ id?: string;
26
41
  } & (
27
42
  | {
28
43
  children?: never;
@@ -33,5 +48,12 @@ export type ToolTipInterface<T extends HTMLElement = any> = {
33
48
  targetRef?: never;
34
49
  }
35
50
  );
36
- elements: ['toolTip', 'container', 'subHead', 'supportingText', 'actions'];
51
+ elements: [
52
+ 'toolTip',
53
+ 'container',
54
+ 'subHead',
55
+ 'supportingText',
56
+ 'actions',
57
+ 'content',
58
+ ];
37
59
  };
@@ -11,10 +11,13 @@ const cardConfig: ClassNameComponent<CardInterface> = ({
11
11
  isInteractive,
12
12
  }) => ({
13
13
  card: classNames(
14
- 'group/card rounded-xl overflow-hidden z-10',
14
+ ' rounded-xl overflow-hidden ',
15
15
  variant === 'outlined' && 'bg-surface border border-outline-variant',
16
16
  variant === 'elevated' && 'bg-surface-container-low shadow-1',
17
17
  variant === 'filled' && 'bg-surface-container-highest',
18
+ {
19
+ 'group/card': isInteractive,
20
+ },
18
21
  ),
19
22
  });
20
23
 
@@ -34,6 +34,7 @@ const toolTipConfig: ClassNameComponent<ToolTipInterface> = ({
34
34
  actions: classNames('flex gap-10 px-1 mt-2', variant == 'plain' && 'hidden'),
35
35
  subHead: classNames('text-title-small mb-1', variant == 'plain' && 'hidden'),
36
36
  supportingText: classNames(''),
37
+ content: classNames('w-full'),
37
38
  });
38
39
 
39
40
  export const toolStyle = defaultClassNames<ToolTipInterface>(
@@ -1,10 +1,10 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
- import { Button, ReactProps, ToolTip, ToolTipInterface } from '../../lib';
2
+ import { Button, ReactProps, Tooltip, ToolTipInterface } from '../../lib';
3
3
 
4
4
  // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
5
5
  const meta = {
6
6
  title: 'Communication/ToolTip',
7
- component: ToolTip,
7
+ component: Tooltip,
8
8
  parameters: {
9
9
  // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
10
10
  },
@@ -12,7 +12,7 @@ const meta = {
12
12
  tags: ['autodocs'],
13
13
  // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
14
14
  argTypes: {},
15
- } satisfies Meta<typeof ToolTip>;
15
+ } satisfies Meta<typeof Tooltip>;
16
16
 
17
17
  export default meta;
18
18
  type Story = StoryObj<typeof meta>;
@@ -27,84 +27,84 @@ const createToolTipStory = (
27
27
  <div className="h-96 relative">
28
28
  <div className="h-96 relative">
29
29
  <div className="absolute top-0 left-0">
30
- <ToolTip
30
+ <Tooltip
31
31
  position="bottom-right"
32
32
  {...args}
33
33
  text="Cliquez pour plus d'infos"
34
34
  title="Info rapide"
35
35
  >
36
36
  <Button variant={'filledTonal'} label={'Bottom-right'}></Button>
37
- </ToolTip>
37
+ </Tooltip>
38
38
  </div>
39
39
  <div className="absolute top-0 left-1/2 -translate-x-1/2">
40
- <ToolTip
40
+ <Tooltip
41
41
  position="bottom"
42
42
  {...args}
43
43
  text="Cet élément représente les statistiques globales de votre projet."
44
44
  title="Statistiques"
45
45
  >
46
46
  <Button variant={'filledTonal'} label={'Bottom-center'}></Button>
47
- </ToolTip>
47
+ </Tooltip>
48
48
  </div>
49
49
  <div className="absolute top-0 right-0">
50
- <ToolTip
50
+ <Tooltip
51
51
  position="bottom-left"
52
52
  {...args}
53
53
  text="Cliquez ici pour télécharger le fichier associé."
54
54
  title="Téléchargement"
55
55
  >
56
56
  <Button variant={'filledTonal'} label={'Bottom-left'}></Button>
57
- </ToolTip>
57
+ </Tooltip>
58
58
  </div>
59
59
  <div className="absolute top-1/2 left-0 -translate-y-1/2">
60
- <ToolTip
60
+ <Tooltip
61
61
  position="right"
62
62
  {...args}
63
63
  text="Cette action ne peut pas être annulée une fois confirmée."
64
64
  title="Attention"
65
65
  >
66
66
  <Button variant={'filledTonal'} label={'Center-right'}></Button>
67
- </ToolTip>
67
+ </Tooltip>
68
68
  </div>
69
69
  <div className="absolute top-1/2 right-0 -translate-y-1/2">
70
- <ToolTip
70
+ <Tooltip
71
71
  position="left"
72
72
  {...args}
73
73
  text="Modifiez les paramètres dans l'onglet dédié à la personnalisation."
74
74
  title="Personnalisation"
75
75
  >
76
76
  <Button variant={'filledTonal'} label={'Center-left'}></Button>
77
- </ToolTip>
77
+ </Tooltip>
78
78
  </div>
79
79
  <div className="absolute bottom-0 left-0">
80
- <ToolTip
80
+ <Tooltip
81
81
  position="top-right"
82
82
  {...args}
83
83
  text="L'action demandée supprimera toutes les données correspondantes."
84
84
  title="Suppression de données"
85
85
  >
86
86
  <Button variant={'filledTonal'} label={'Top-right'}></Button>
87
- </ToolTip>
87
+ </Tooltip>
88
88
  </div>
89
89
  <div className="absolute bottom-0 left-1/2 -translate-x-1/2">
90
- <ToolTip
90
+ <Tooltip
91
91
  position="top"
92
92
  {...args}
93
93
  text="Double-cliquez pour agrandir l'aperçu de l'élément sélectionné."
94
94
  title="Aperçu"
95
95
  >
96
96
  <Button variant={'filledTonal'} label={'Top-center'}></Button>
97
- </ToolTip>
97
+ </Tooltip>
98
98
  </div>
99
99
  <div className="absolute bottom-0 right-0">
100
- <ToolTip
100
+ <Tooltip
101
101
  position="top-left"
102
102
  {...args}
103
103
  text="Passez la souris sur d'autres icônes pour plus de détails."
104
104
  title="Icones et navigation"
105
105
  >
106
106
  <Button variant={'filledTonal'} label={'Top-left'}></Button>
107
- </ToolTip>
107
+ </Tooltip>
108
108
  </div>
109
109
  </div>
110
110
  </div>
package/tsconfig.json CHANGED
@@ -3,12 +3,6 @@
3
3
  "files": [],
4
4
  "include": [],
5
5
  "references": [
6
- {
7
- "path": "../tailwind"
8
- },
9
- {
10
- "path": "../theme"
11
- },
12
6
  {
13
7
  "path": "./tsconfig.lib.json"
14
8
  },
@@ -1,9 +0,0 @@
1
- import { MotionProps } from '../utils';
2
- import { ToolTipInterface } from '../interfaces';
3
- /**
4
- * Tooltips display brief labels or messages
5
- * @status beta
6
- * @category Communication
7
- */
8
- export declare const ToolTip: ({ variant, buttons, className, children, title, text, position, targetRef, ref, trigger, transition, ...props }: MotionProps<ToolTipInterface>) => import("react/jsx-runtime").JSX.Element;
9
- //# sourceMappingURL=ToolTip.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ToolTip.d.ts","sourceRoot":"","sources":["../../../src/lib/components/ToolTip.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAMjD;;;;GAIG;AACH,eAAO,MAAM,OAAO,GAAI,iHAarB,WAAW,CAAC,gBAAgB,CAAC,4CA8N/B,CAAC"}