@udixio/ui-react 2.9.13 → 2.9.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/index.cjs +3 -3
  3. package/dist/index.js +2259 -1814
  4. package/dist/lib/components/Button.d.ts.map +1 -1
  5. package/dist/lib/components/Card.d.ts +2 -2
  6. package/dist/lib/components/Card.d.ts.map +1 -1
  7. package/dist/lib/components/Checkbox.d.ts +15 -0
  8. package/dist/lib/components/Checkbox.d.ts.map +1 -0
  9. package/dist/lib/components/DatePicker.d.ts +9 -0
  10. package/dist/lib/components/DatePicker.d.ts.map +1 -0
  11. package/dist/lib/components/FabMenu.d.ts.map +1 -1
  12. package/dist/lib/components/IconButton.d.ts.map +1 -1
  13. package/dist/lib/components/TabGroup.d.ts +1 -0
  14. package/dist/lib/components/TabGroup.d.ts.map +1 -1
  15. package/dist/lib/components/TabGroupContext.d.ts +1 -0
  16. package/dist/lib/components/TabGroupContext.d.ts.map +1 -1
  17. package/dist/lib/components/TabPanel.d.ts +1 -0
  18. package/dist/lib/components/TabPanel.d.ts.map +1 -1
  19. package/dist/lib/components/TabPanels.d.ts +1 -0
  20. package/dist/lib/components/TabPanels.d.ts.map +1 -1
  21. package/dist/lib/components/TextField.d.ts +4 -4
  22. package/dist/lib/components/TextField.d.ts.map +1 -1
  23. package/dist/lib/components/index.d.ts +2 -0
  24. package/dist/lib/components/index.d.ts.map +1 -1
  25. package/dist/lib/effects/State.d.ts +3 -1
  26. package/dist/lib/effects/State.d.ts.map +1 -1
  27. package/dist/lib/interfaces/card.interface.d.ts +1 -1
  28. package/dist/lib/interfaces/card.interface.d.ts.map +1 -1
  29. package/dist/lib/interfaces/checkbox.interface.d.ts +38 -0
  30. package/dist/lib/interfaces/checkbox.interface.d.ts.map +1 -0
  31. package/dist/lib/interfaces/date-picker.interface.d.ts +67 -0
  32. package/dist/lib/interfaces/date-picker.interface.d.ts.map +1 -0
  33. package/dist/lib/interfaces/icon-button.interface.d.ts +2 -1
  34. package/dist/lib/interfaces/icon-button.interface.d.ts.map +1 -1
  35. package/dist/lib/interfaces/index.d.ts +1 -0
  36. package/dist/lib/interfaces/index.d.ts.map +1 -1
  37. package/dist/lib/interfaces/text-field.interface.d.ts +7 -4
  38. package/dist/lib/interfaces/text-field.interface.d.ts.map +1 -1
  39. package/dist/lib/styles/card.style.d.ts +3 -3
  40. package/dist/lib/styles/checkbox.style.d.ts +45 -0
  41. package/dist/lib/styles/checkbox.style.d.ts.map +1 -0
  42. package/dist/lib/styles/date-picker.style.d.ts +45 -0
  43. package/dist/lib/styles/date-picker.style.d.ts.map +1 -0
  44. package/dist/lib/styles/icon-button.style.d.ts +10 -4
  45. package/dist/lib/styles/icon-button.style.d.ts.map +1 -1
  46. package/dist/lib/styles/index.d.ts +1 -0
  47. package/dist/lib/styles/index.d.ts.map +1 -1
  48. package/dist/lib/styles/text-field.style.d.ts +18 -9
  49. package/dist/lib/styles/text-field.style.d.ts.map +1 -1
  50. package/package.json +3 -3
  51. package/src/lib/components/Button.tsx +1 -0
  52. package/src/lib/components/Card.tsx +9 -4
  53. package/src/lib/components/Checkbox.tsx +120 -0
  54. package/src/lib/components/DatePicker.tsx +432 -0
  55. package/src/lib/components/FabMenu.tsx +4 -5
  56. package/src/lib/components/IconButton.tsx +9 -7
  57. package/src/lib/components/TabGroup.tsx +8 -6
  58. package/src/lib/components/TabGroupContext.tsx +1 -1
  59. package/src/lib/components/TabPanel.tsx +1 -0
  60. package/src/lib/components/TabPanels.tsx +1 -0
  61. package/src/lib/components/TextField.tsx +95 -108
  62. package/src/lib/components/index.ts +2 -0
  63. package/src/lib/effects/State.tsx +4 -1
  64. package/src/lib/interfaces/card.interface.ts +1 -1
  65. package/src/lib/interfaces/checkbox.interface.ts +39 -0
  66. package/src/lib/interfaces/date-picker.interface.ts +79 -0
  67. package/src/lib/interfaces/icon-button.interface.ts +2 -1
  68. package/src/lib/interfaces/index.ts +1 -0
  69. package/src/lib/interfaces/text-field.interface.ts +7 -4
  70. package/src/lib/styles/checkbox.style.ts +64 -0
  71. package/src/lib/styles/date-picker.style.ts +43 -0
  72. package/src/lib/styles/index.ts +1 -0
  73. package/src/lib/styles/text-field.style.ts +2 -2
  74. package/src/stories/containment/card.stories.tsx +1 -1
@@ -0,0 +1,432 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useDatePickerStyle } from '../styles/date-picker.style';
3
+ import { classNames, ReactProps } from '../utils';
4
+ import {
5
+ DatePickerInterface,
6
+ DateRange,
7
+ } from '../interfaces/date-picker.interface';
8
+ import {
9
+ faChevronDown,
10
+ faChevronLeft,
11
+ faChevronRight,
12
+ } from '@fortawesome/free-solid-svg-icons';
13
+ import { Button } from './Button';
14
+ import { IconButton } from './IconButton';
15
+ import { AnimatePresence, motion } from 'motion/react';
16
+ import { Icon } from '../icon';
17
+
18
+ /**
19
+ * DatePickers let users select a date, or a range of dates.
20
+ * @status beta
21
+ * @category Selection
22
+ */
23
+ export const DatePicker = ({
24
+ value: valueProp,
25
+ defaultValue,
26
+ onChange,
27
+ minDate,
28
+ maxDate,
29
+ shouldDisableDate,
30
+ locale = 'default',
31
+ weekStartDay = 0,
32
+ className,
33
+ style,
34
+ mode = 'single',
35
+ ...restProps
36
+ }: ReactProps<DatePickerInterface>) => {
37
+ // State for the currently displayed month (always set to the 1st of the month)
38
+ const [viewDate, setViewDate] = useState(() => {
39
+ // Try to find a valid start date from value to focus
40
+ const extractDate = (v: any): Date | null => {
41
+ if (v instanceof Date) return v;
42
+ if (Array.isArray(v) && v[0]) return v[0];
43
+ return null;
44
+ };
45
+ const start =
46
+ extractDate(valueProp) || extractDate(defaultValue) || new Date();
47
+ return new Date(start.getFullYear(), start.getMonth(), 1);
48
+ });
49
+
50
+ const [direction, setDirection] = useState(0);
51
+ const [viewMode, setViewMode] = useState<'day' | 'year'>('day');
52
+
53
+ // State for selected date
54
+ const isControlled = valueProp !== undefined;
55
+ const [internalValue, setInternalValue] = useState<Date | DateRange | null>(
56
+ defaultValue || null,
57
+ );
58
+ const selectedValue = isControlled ? valueProp || null : internalValue;
59
+
60
+ // Calendar generation logic
61
+ const daysInMonth = (year: number, month: number) =>
62
+ new Date(year, month + 1, 0).getDate();
63
+
64
+ const calendarDays = useMemo(() => {
65
+ const year = viewDate.getFullYear();
66
+ const month = viewDate.getMonth();
67
+ const daysCount = daysInMonth(year, month);
68
+ const startDay = new Date(year, month, 1).getDay(); // 0=Sun (Fixed JS getDay)
69
+
70
+ // Adjust start index based on weekStartDay
71
+ // shift: logic to map standard JS Day (0=Sun) to our week start
72
+ // If weekStart=1 (Mon): Sun(0) -> 6, Mon(1) -> 0, Tue(2) -> 1
73
+ const startIndex = (startDay - weekStartDay + 7) % 7;
74
+
75
+ const days: Array<{ date: Date; isCurrentMonth: boolean }> = [];
76
+
77
+ // Prev month
78
+ const prevMonthDaysCount = daysInMonth(year, month - 1);
79
+ for (let i = startIndex - 1; i >= 0; i--) {
80
+ days.push({
81
+ date: new Date(year, month - 1, prevMonthDaysCount - i),
82
+ isCurrentMonth: false,
83
+ });
84
+ }
85
+
86
+ // Current month
87
+ for (let i = 1; i <= daysCount; i++) {
88
+ days.push({ date: new Date(year, month, i), isCurrentMonth: true });
89
+ }
90
+
91
+ // Next month padding - Ensure always 42 days (6 rows) for fixed height animation
92
+ const currentLen = days.length;
93
+ const remaining = 42 - currentLen;
94
+ for (let i = 1; i <= remaining; i++) {
95
+ days.push({
96
+ date: new Date(year, month + 1, i),
97
+ isCurrentMonth: false,
98
+ });
99
+ }
100
+
101
+ return days;
102
+ }, [viewDate, weekStartDay]);
103
+
104
+ const years = useMemo(() => {
105
+ const currentYear = new Date().getFullYear();
106
+ const start = currentYear - 100;
107
+ const end = currentYear + 100;
108
+ const list = [];
109
+ for (let i = start; i <= end; i++) {
110
+ list.push(i);
111
+ }
112
+ return list;
113
+ }, []);
114
+
115
+ const yearsContainerRef = useRef<HTMLDivElement>(null);
116
+
117
+ // Scroll to selected year when opening year view
118
+ useEffect(() => {
119
+ if (viewMode === 'year' && yearsContainerRef.current) {
120
+ const selectedYearBtn = yearsContainerRef.current.querySelector(
121
+ '[data-selected="true"]',
122
+ );
123
+ if (selectedYearBtn) {
124
+ selectedYearBtn.scrollIntoView({ block: 'center' });
125
+ }
126
+ }
127
+ }, [viewMode]);
128
+
129
+ // Formatters
130
+ const monthFormatter = useMemo(
131
+ () => new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }),
132
+ [locale],
133
+ );
134
+ const weekDayFormatter = useMemo(
135
+ () => new Intl.DateTimeFormat(locale, { weekday: 'narrow' }),
136
+ [locale],
137
+ );
138
+
139
+ const weekDays = useMemo(() => {
140
+ const baseDate = new Date(2023, 0, 1 + weekStartDay); // Jan 1 2023 was Sun. Jan (1+1)=2 is Mon.
141
+ return Array.from({ length: 7 }).map((_, i) => {
142
+ const d = new Date(baseDate);
143
+ d.setDate(baseDate.getDate() + i);
144
+ return weekDayFormatter.format(d).charAt(0).toUpperCase();
145
+ });
146
+ }, [weekDayFormatter, weekStartDay]);
147
+
148
+ // Handlers
149
+ const handlePrevMonth = () => {
150
+ setDirection(-1);
151
+ setViewDate((d) => new Date(d.getFullYear(), d.getMonth() - 1, 1));
152
+ };
153
+ const handleNextMonth = () => {
154
+ setDirection(1);
155
+ setViewDate((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1));
156
+ };
157
+
158
+ const handleYearSelect = (year: number) => {
159
+ setViewDate((d) => new Date(year, d.getMonth(), 1));
160
+ setViewMode('day');
161
+ };
162
+
163
+ const isSameDay = (d1: Date | null | undefined, d2: Date) => {
164
+ if (!d1) return false;
165
+ return (
166
+ d1.getDate() === d2.getDate() &&
167
+ d1.getMonth() === d2.getMonth() &&
168
+ d1.getFullYear() === d2.getFullYear()
169
+ );
170
+ };
171
+
172
+ const isToday = (date: Date) => isSameDay(new Date(), date);
173
+
174
+ const handleDateClick = (date: Date) => {
175
+ let newValue: Date | DateRange | null = date;
176
+
177
+ if (mode === 'single') {
178
+ newValue = date;
179
+ // Single mode always sets date
180
+ } else {
181
+ // Range mode
182
+ const current = selectedValue as DateRange | null;
183
+ const [start, end] = Array.isArray(current) ? current : [null, null];
184
+
185
+ if (!start || (start && end)) {
186
+ // Start new range (if was simple date or full range)
187
+ newValue = [date, null];
188
+ } else {
189
+ // Complete range
190
+ if (date < start) {
191
+ newValue = [date, start];
192
+ } else {
193
+ newValue = [start, date];
194
+ }
195
+ }
196
+ }
197
+
198
+ if (!isControlled) {
199
+ setInternalValue(newValue);
200
+ }
201
+ if (onChange) {
202
+ onChange(newValue);
203
+ }
204
+ };
205
+
206
+ const checkSelection = (date: Date) => {
207
+ if (mode === 'single') {
208
+ return {
209
+ isSelected: isSameDay(selectedValue as Date, date),
210
+ isStart: false,
211
+ isEnd: false,
212
+ isInRange: false,
213
+ };
214
+ }
215
+ const safeRange = Array.isArray(selectedValue)
216
+ ? selectedValue
217
+ : [selectedValue, null];
218
+ const [start, end] = safeRange as DateRange;
219
+ const isStart = isSameDay(start, date);
220
+ const isEnd = isSameDay(end, date);
221
+
222
+ // Check range
223
+ let isInRange = false;
224
+ if (start && end) {
225
+ // Simple range check (ignore time components for safety)
226
+ // Normalize to midnight for strict day comparison
227
+ const s = new Date(
228
+ start.getFullYear(),
229
+ start.getMonth(),
230
+ start.getDate(),
231
+ ).getTime();
232
+ const e = new Date(
233
+ end.getFullYear(),
234
+ end.getMonth(),
235
+ end.getDate(),
236
+ ).getTime();
237
+ const d = new Date(
238
+ date.getFullYear(),
239
+ date.getMonth(),
240
+ date.getDate(),
241
+ ).getTime();
242
+ isInRange = d > s && d < e;
243
+ }
244
+
245
+ return { isSelected: isStart || isEnd, isStart, isEnd, isInRange };
246
+ };
247
+
248
+ const styles = useDatePickerStyle({
249
+ hasSelected: !!selectedValue,
250
+ });
251
+
252
+ const variants = {
253
+ enter: (direction: number) => ({
254
+ x: direction > 0 ? '100%' : '-100%',
255
+ opacity: 0,
256
+ }),
257
+ center: {
258
+ x: 0,
259
+ opacity: 1,
260
+ },
261
+ exit: (direction: number) => ({
262
+ x: direction < 0 ? '100%' : '-100%',
263
+ opacity: 0,
264
+ }),
265
+ };
266
+
267
+ return (
268
+ <div
269
+ className={classNames(styles.datePicker, className)}
270
+ style={style}
271
+ {...(restProps as any)}
272
+ >
273
+ {/* Header */}
274
+ <div className={styles.header}>
275
+ <Button
276
+ variant="text"
277
+ disableTextMargins
278
+ size="small"
279
+ onClick={() => setViewMode((m) => (m === 'day' ? 'year' : 'day'))}
280
+ className="text-label-large font-bold capitalize text-on-surface hover:bg-surface-container-highest"
281
+ >
282
+ <span className="mr-2">
283
+ {viewMode === 'day'
284
+ ? monthFormatter.format(viewDate)
285
+ : viewDate.getFullYear()}
286
+ </span>
287
+ <Icon
288
+ icon={faChevronDown}
289
+ className={classNames(
290
+ 'w-3 h-3 transition-transform duration-200 inline',
291
+ viewMode === 'year' && 'rotate-180',
292
+ )}
293
+ />
294
+ </Button>
295
+
296
+ {viewMode === 'day' && (
297
+ <div className="flex items-center">
298
+ <IconButton
299
+ size={'xSmall'}
300
+ allowShapeTransformation={false}
301
+ onClick={handlePrevMonth}
302
+ icon={faChevronLeft}
303
+ label="Previous month"
304
+ title={null}
305
+ />
306
+ <IconButton
307
+ size={'xSmall'}
308
+ allowShapeTransformation={false}
309
+ onClick={handleNextMonth}
310
+ icon={faChevronRight}
311
+ label="Next month"
312
+ title={null}
313
+ />
314
+ </div>
315
+ )}
316
+ </div>
317
+
318
+ {viewMode === 'year' ? (
319
+ <div
320
+ className="h-[280px] overflow-y-auto grid grid-cols-3 gap-2 p-2 scrollbar-hide"
321
+ ref={yearsContainerRef}
322
+ >
323
+ {years.map((year) => (
324
+ <Button
325
+ size={'small'}
326
+ key={year}
327
+ variant={year === viewDate.getFullYear() ? 'filled' : 'text'}
328
+ onClick={() => handleYearSelect(year)}
329
+ data-selected={year === viewDate.getFullYear()}
330
+ className={classNames('w-full', {
331
+ 'text-on-surface': year !== viewDate.getFullYear(),
332
+ })}
333
+ label={year.toString()}
334
+ >
335
+ {year}
336
+ </Button>
337
+ ))}
338
+ </div>
339
+ ) : (
340
+ <>
341
+ {/* Week Days */}
342
+ <div className={styles.weekDays}>
343
+ {weekDays.map((day, i) => (
344
+ <div key={i} className={styles.weekDay}>
345
+ {day}
346
+ </div>
347
+ ))}
348
+ </div>
349
+
350
+ {/* Days Grid */}
351
+ <div className="overflow-hidden relative min-h-[240px]">
352
+ <AnimatePresence
353
+ mode="popLayout"
354
+ initial={false}
355
+ custom={direction}
356
+ >
357
+ <motion.div
358
+ key={viewDate.toISOString()}
359
+ custom={direction}
360
+ variants={variants}
361
+ initial="enter"
362
+ animate="center"
363
+ exit="exit"
364
+ transition={{ type: 'spring', bounce: 0, duration: 0.3 }}
365
+ className={styles.daysGrid}
366
+ >
367
+ {calendarDays.map((item, index) => {
368
+ if (!item.isCurrentMonth) {
369
+ return <div key={index} className={styles.dayCell} />;
370
+ }
371
+
372
+ const { isSelected, isStart, isEnd, isInRange } =
373
+ checkSelection(item.date);
374
+ const isTodayDate = isToday(item.date);
375
+ const isDisabled =
376
+ (minDate && item.date < minDate) ||
377
+ (maxDate && item.date > maxDate) ||
378
+ shouldDisableDate?.(item.date);
379
+
380
+ return (
381
+ <div
382
+ key={index}
383
+ className={classNames(
384
+ styles.dayCell,
385
+ // Range background styles applied to the cell wrapper
386
+ isInRange && 'bg-primary/20',
387
+ isStart &&
388
+ (selectedValue as DateRange)?.[1] &&
389
+ 'bg-gradient-to-r from-transparent to-primary/20',
390
+ isEnd &&
391
+ (selectedValue as DateRange)?.[0] &&
392
+ 'bg-gradient-to-l from-transparent to-primary/20',
393
+ )}
394
+ >
395
+ <Button
396
+ className={() => ({
397
+ button: classNames('aspect-square h-[40px] p-0', {
398
+ 'text-on-surface': !isSelected && !isTodayDate,
399
+ 'opacity-50': isDisabled,
400
+ }),
401
+ stateLayer: classNames({
402
+ '!bg-transparent': isDisabled,
403
+ }),
404
+ })}
405
+ size="small"
406
+ allowShapeTransformation={false}
407
+ variant={
408
+ classNames({
409
+ filled: isSelected,
410
+ outlined: isTodayDate,
411
+ text: !isSelected && !isTodayDate,
412
+ }) as any
413
+ }
414
+ label={item.date.getDate().toString()}
415
+ onClick={() => handleDateClick(item.date)}
416
+ disabled={isDisabled}
417
+ >
418
+ {item.date.getDate().toString()}
419
+ </Button>
420
+ </div>
421
+ );
422
+ })}
423
+ </motion.div>
424
+ </AnimatePresence>
425
+ </div>
426
+ </>
427
+ )}
428
+ </div>
429
+ );
430
+ };
431
+
432
+ // Helper for generic check - removed unused
@@ -1,4 +1,4 @@
1
- import React, { useRef, useState } from 'react';
1
+ import React, { useId, useRef, useState } from 'react';
2
2
  import { FabMenuInterface } from '../interfaces/fab-menu.interface';
3
3
  import { useFabMenuStyle } from '../styles/fab-menu.style';
4
4
  import { ReactProps } from '../utils/component';
@@ -9,7 +9,6 @@ import { classNames } from '../utils';
9
9
  import { IconButton } from './IconButton';
10
10
  import { faClose } from '@fortawesome/free-solid-svg-icons';
11
11
  import { AnimatePresence, motion } from 'motion/react';
12
- import { v4 } from 'uuid';
13
12
 
14
13
  /**
15
14
  * Floating action buttons (FABs) help people take primary actions
@@ -94,7 +93,7 @@ export const FabMenu = ({
94
93
  />
95
94
  );
96
95
 
97
- const [uuid] = useState(v4());
96
+ const id = useId();
98
97
 
99
98
  return (
100
99
  <div className={styles.fabMenu} ref={resolvedRef} {...restProps}>
@@ -187,13 +186,13 @@ export const FabMenu = ({
187
186
  renderFab({
188
187
  className: '',
189
188
  layout: true,
190
- layoutId: 'fab-menu' + uuid,
189
+ layoutId: 'fab-menu' + id,
191
190
  })}
192
191
  {open && (
193
192
  <>
194
193
  <MotionIconButton
195
194
  layout
196
- layoutId={'fab-menu' + uuid}
195
+ layoutId={'fab-menu' + id}
197
196
  variant={'filled'}
198
197
  className={() => ({
199
198
  iconButton: classNames('', {
@@ -46,7 +46,7 @@ export const IconButton = ({
46
46
  'IconButton component requires either a label prop or children content to provide an accessible aria-label',
47
47
  );
48
48
  }
49
- if (!title) {
49
+ if (!title && title !== null) {
50
50
  title = label;
51
51
  }
52
52
 
@@ -108,11 +108,13 @@ export const IconButton = ({
108
108
  onClick={handleClick}
109
109
  ref={resolvedRef}
110
110
  >
111
- <Tooltip
112
- targetRef={resolvedRef}
113
- trigger={disabled ? null : undefined}
114
- text={title}
115
- ></Tooltip>
111
+ {title !== null && (
112
+ <Tooltip
113
+ targetRef={resolvedRef}
114
+ trigger={disabled ? null : undefined}
115
+ text={title}
116
+ ></Tooltip>
117
+ )}
116
118
 
117
119
  <div className={styles.touchTarget} />
118
120
  <State
@@ -138,7 +140,7 @@ export const IconButton = ({
138
140
  )}
139
141
  stateClassName={'state-ripple-group-[icon-button]'}
140
142
  />
141
- {icon && <Icon icon={icon} className={styles.icon} />}
143
+ {icon ? <Icon icon={icon} className={styles.icon} /> : children}
142
144
  </ElementType>
143
145
  );
144
146
  };
@@ -1,5 +1,4 @@
1
- import React, { useMemo, useRef, useState } from 'react';
2
- import { v4 as uuidv4 } from 'uuid';
1
+ import React, { useId, useMemo, useRef, useState } from 'react';
3
2
  import { TabGroupContext, TabGroupContextValue } from './TabGroupContext';
4
3
  import { TabGroupInterface } from '../interfaces/tab-group.interface';
5
4
  import { ReactProps } from '../utils/component';
@@ -7,6 +6,7 @@ import { ReactProps } from '../utils/component';
7
6
  /**
8
7
  * TabGroup provides shared state for Tabs and TabPanels
9
8
  * @status beta
9
+ * @parent Tabs
10
10
  * @category Navigation
11
11
  * @devx
12
12
  * - Provides selection + slide direction for Tabs/TabPanels.
@@ -26,7 +26,9 @@ export const TabGroup = ({
26
26
 
27
27
  // Priorité : props externes > état interne
28
28
  const selectedTab =
29
- externalSelectedTab !== undefined ? externalSelectedTab : internalSelectedTab;
29
+ externalSelectedTab !== undefined
30
+ ? externalSelectedTab
31
+ : internalSelectedTab;
30
32
 
31
33
  const setSelectedTab = externalSetSelectedTab ?? internalSetSelectedTab;
32
34
 
@@ -43,7 +45,7 @@ export const TabGroup = ({
43
45
  previousTabRef.current = selectedTab;
44
46
  }
45
47
 
46
- const tabsId = useMemo(() => uuidv4(), []);
48
+ const id = useId();
47
49
 
48
50
  const contextValue: TabGroupContextValue = useMemo(
49
51
  () => ({
@@ -51,9 +53,9 @@ export const TabGroup = ({
51
53
  setSelectedTab,
52
54
  previousTab: previousTabRef.current,
53
55
  direction,
54
- tabsId,
56
+ tabsId: id,
55
57
  }),
56
- [selectedTab, setSelectedTab, direction, tabsId],
58
+ [selectedTab, setSelectedTab, direction, id],
57
59
  );
58
60
 
59
61
  return (
@@ -2,9 +2,9 @@ import { createContext, Dispatch, SetStateAction } from 'react';
2
2
 
3
3
  /**
4
4
  * Shared state container for Tabs and TabPanels.
5
+ * @parent Tabs
5
6
  * @internal
6
7
  */
7
-
8
8
  export interface TabGroupContextValue {
9
9
  selectedTab: number | null;
10
10
  setSelectedTab: Dispatch<SetStateAction<number | null>>;
@@ -7,6 +7,7 @@ import { useTabPanelStyle } from '../styles/tab-panels.style';
7
7
  * TabPanel contains the content for a single tab
8
8
  * Must be used within TabPanels
9
9
  * @status beta
10
+ * @parent Tabs
10
11
  * @category Navigation
11
12
  * @devx
12
13
  * - Should be rendered inside `TabPanels` for animations and aria wiring.
@@ -10,6 +10,7 @@ import { TabPanel } from './TabPanel';
10
10
  * TabPanels renders the content panels with slide animation
11
11
  * Must be used within a TabGroup
12
12
  * @status beta
13
+ * @parent Tabs
13
14
  * @category Navigation
14
15
  * @devx
15
16
  * - Requires `TabGroup` context; otherwise it renders nothing.