@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.
- package/CHANGELOG.md +26 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +2259 -1814
- package/dist/lib/components/Button.d.ts.map +1 -1
- package/dist/lib/components/Card.d.ts +2 -2
- package/dist/lib/components/Card.d.ts.map +1 -1
- package/dist/lib/components/Checkbox.d.ts +15 -0
- package/dist/lib/components/Checkbox.d.ts.map +1 -0
- package/dist/lib/components/DatePicker.d.ts +9 -0
- package/dist/lib/components/DatePicker.d.ts.map +1 -0
- package/dist/lib/components/FabMenu.d.ts.map +1 -1
- package/dist/lib/components/IconButton.d.ts.map +1 -1
- package/dist/lib/components/TabGroup.d.ts +1 -0
- package/dist/lib/components/TabGroup.d.ts.map +1 -1
- package/dist/lib/components/TabGroupContext.d.ts +1 -0
- package/dist/lib/components/TabGroupContext.d.ts.map +1 -1
- package/dist/lib/components/TabPanel.d.ts +1 -0
- package/dist/lib/components/TabPanel.d.ts.map +1 -1
- package/dist/lib/components/TabPanels.d.ts +1 -0
- package/dist/lib/components/TabPanels.d.ts.map +1 -1
- package/dist/lib/components/TextField.d.ts +4 -4
- package/dist/lib/components/TextField.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/State.d.ts +3 -1
- package/dist/lib/effects/State.d.ts.map +1 -1
- package/dist/lib/interfaces/card.interface.d.ts +1 -1
- package/dist/lib/interfaces/card.interface.d.ts.map +1 -1
- package/dist/lib/interfaces/checkbox.interface.d.ts +38 -0
- package/dist/lib/interfaces/checkbox.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/date-picker.interface.d.ts +67 -0
- package/dist/lib/interfaces/date-picker.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/icon-button.interface.d.ts +2 -1
- package/dist/lib/interfaces/icon-button.interface.d.ts.map +1 -1
- package/dist/lib/interfaces/index.d.ts +1 -0
- package/dist/lib/interfaces/index.d.ts.map +1 -1
- package/dist/lib/interfaces/text-field.interface.d.ts +7 -4
- package/dist/lib/interfaces/text-field.interface.d.ts.map +1 -1
- package/dist/lib/styles/card.style.d.ts +3 -3
- package/dist/lib/styles/checkbox.style.d.ts +45 -0
- package/dist/lib/styles/checkbox.style.d.ts.map +1 -0
- package/dist/lib/styles/date-picker.style.d.ts +45 -0
- package/dist/lib/styles/date-picker.style.d.ts.map +1 -0
- package/dist/lib/styles/icon-button.style.d.ts +10 -4
- package/dist/lib/styles/icon-button.style.d.ts.map +1 -1
- package/dist/lib/styles/index.d.ts +1 -0
- package/dist/lib/styles/index.d.ts.map +1 -1
- package/dist/lib/styles/text-field.style.d.ts +18 -9
- package/dist/lib/styles/text-field.style.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/lib/components/Button.tsx +1 -0
- package/src/lib/components/Card.tsx +9 -4
- package/src/lib/components/Checkbox.tsx +120 -0
- package/src/lib/components/DatePicker.tsx +432 -0
- package/src/lib/components/FabMenu.tsx +4 -5
- package/src/lib/components/IconButton.tsx +9 -7
- package/src/lib/components/TabGroup.tsx +8 -6
- package/src/lib/components/TabGroupContext.tsx +1 -1
- package/src/lib/components/TabPanel.tsx +1 -0
- package/src/lib/components/TabPanels.tsx +1 -0
- package/src/lib/components/TextField.tsx +95 -108
- package/src/lib/components/index.ts +2 -0
- package/src/lib/effects/State.tsx +4 -1
- package/src/lib/interfaces/card.interface.ts +1 -1
- package/src/lib/interfaces/checkbox.interface.ts +39 -0
- package/src/lib/interfaces/date-picker.interface.ts +79 -0
- package/src/lib/interfaces/icon-button.interface.ts +2 -1
- package/src/lib/interfaces/index.ts +1 -0
- package/src/lib/interfaces/text-field.interface.ts +7 -4
- package/src/lib/styles/checkbox.style.ts +64 -0
- package/src/lib/styles/date-picker.style.ts +43 -0
- package/src/lib/styles/index.ts +1 -0
- package/src/lib/styles/text-field.style.ts +2 -2
- 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
|
|
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' +
|
|
189
|
+
layoutId: 'fab-menu' + id,
|
|
191
190
|
})}
|
|
192
191
|
{open && (
|
|
193
192
|
<>
|
|
194
193
|
<MotionIconButton
|
|
195
194
|
layout
|
|
196
|
-
layoutId={'fab-menu' +
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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.
|