850 lines
21 KiB
JavaScript
850 lines
21 KiB
JavaScript
import { useRef, useState } from 'react';
|
||
import { Calendar } from '@/components/ui/calendar';
|
||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { CalendarDays, ChevronLeft, ChevronRight, Clock } from 'lucide-react';
|
||
import { fr } from 'date-fns/locale';
|
||
|
||
const parseDateValue = (value) => {
|
||
if (!value) return undefined;
|
||
|
||
const [
|
||
year,
|
||
month,
|
||
day,
|
||
] = value.split('-').map(Number);
|
||
|
||
if (!year || !month || !day) {
|
||
return undefined;
|
||
}
|
||
|
||
return new Date(
|
||
year,
|
||
month - 1,
|
||
day,
|
||
);
|
||
};
|
||
|
||
|
||
const formatDateValue = (date) => {
|
||
if (!date) return '';
|
||
|
||
const year =
|
||
date.getFullYear();
|
||
|
||
const month = String(
|
||
date.getMonth() + 1,
|
||
).padStart(2, '0');
|
||
|
||
const day = String(
|
||
date.getDate(),
|
||
).padStart(2, '0');
|
||
|
||
return `${year}-${month}-${day}`;
|
||
};
|
||
|
||
|
||
const formatDateLabel = (value) => {
|
||
const date =
|
||
parseDateValue(value);
|
||
|
||
if (!date) {
|
||
return 'Choisir une date';
|
||
}
|
||
|
||
const numericDate =
|
||
date.toLocaleDateString(
|
||
'fr-FR',
|
||
{
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
},
|
||
);
|
||
|
||
const longDate =
|
||
date.toLocaleDateString(
|
||
'fr-FR',
|
||
{
|
||
day: '2-digit',
|
||
month: 'long',
|
||
year: 'numeric',
|
||
},
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<span className="lg:hidden">
|
||
{longDate}
|
||
</span>
|
||
|
||
<span className="hidden lg:inline">
|
||
{numericDate}
|
||
</span>
|
||
</>
|
||
);
|
||
};
|
||
|
||
|
||
const DesktopDatePicker = ({
|
||
value,
|
||
onChange,
|
||
min,
|
||
max,
|
||
placeholder = 'Choisir une date',
|
||
disabled = false,
|
||
className = '',
|
||
}) => {
|
||
const [open, setOpen] =
|
||
useState(false);
|
||
|
||
const selectedDate =
|
||
parseDateValue(value);
|
||
|
||
const minDate =
|
||
parseDateValue(min);
|
||
|
||
const maxDate =
|
||
parseDateValue(max);
|
||
|
||
return (
|
||
<Popover
|
||
open={open}
|
||
onOpenChange={setOpen}
|
||
>
|
||
<PopoverTrigger
|
||
render={
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
disabled={disabled}
|
||
className={`w-full justify-start gap-2 font-normal ${className}`}
|
||
/>
|
||
}
|
||
>
|
||
<CalendarDays className="size-4 text-muted-foreground" />
|
||
|
||
<span
|
||
className={
|
||
selectedDate
|
||
? ''
|
||
: 'text-muted-foreground'
|
||
}
|
||
>
|
||
{selectedDate
|
||
? formatDateLabel(value)
|
||
: placeholder}
|
||
</span>
|
||
</PopoverTrigger>
|
||
|
||
<PopoverContent
|
||
className="w-auto p-0"
|
||
align="start"
|
||
>
|
||
<Calendar
|
||
mode="single"
|
||
locale={fr}
|
||
weekStartsOn={1}
|
||
selected={selectedDate}
|
||
defaultMonth={
|
||
selectedDate ||
|
||
minDate ||
|
||
new Date()
|
||
}
|
||
disabled={
|
||
minDate || maxDate
|
||
? (date) => (
|
||
(minDate && date < minDate) ||
|
||
(maxDate && date > maxDate)
|
||
)
|
||
: undefined
|
||
}
|
||
onSelect={(date) => {
|
||
if (!date) return;
|
||
|
||
onChange(
|
||
formatDateValue(date),
|
||
);
|
||
|
||
setOpen(false);
|
||
}}
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
);
|
||
};
|
||
|
||
|
||
const MONTH_NAMES = [
|
||
'Janvier',
|
||
'Février',
|
||
'Mars',
|
||
'Avril',
|
||
'Mai',
|
||
'Juin',
|
||
'Juillet',
|
||
'Août',
|
||
'Septembre',
|
||
'Octobre',
|
||
'Novembre',
|
||
'Décembre',
|
||
];
|
||
|
||
const parseMonthValue = (value) => {
|
||
if (!value) return null;
|
||
const [year, month] = String(value).split('-').map(Number);
|
||
if (!year || !month || month < 1 || month > 12) return null;
|
||
return { year, month: month - 1 };
|
||
};
|
||
|
||
const formatMonthValue = (year, month) => `${year}-${String(month + 1).padStart(2, '0')}`;
|
||
const monthIndex = (year, month) => year * 12 + month;
|
||
|
||
const yearFromBound = (value, fallback) => {
|
||
const year = Number(String(value || '').slice(0, 4));
|
||
return Number.isFinite(year) && year > 0 ? year : fallback;
|
||
};
|
||
|
||
const DesktopMonthPicker = ({
|
||
value,
|
||
onChange,
|
||
min,
|
||
max,
|
||
placeholder = 'Choisir un mois',
|
||
disabled = false,
|
||
className = '',
|
||
}) => {
|
||
const selected = parseMonthValue(value);
|
||
const current = new Date();
|
||
const [open, setOpen] = useState(false);
|
||
const [displayYear, setDisplayYear] = useState(selected?.year || current.getFullYear());
|
||
const minMonth = parseMonthValue(min);
|
||
const maxMonth = parseMonthValue(max);
|
||
const minIndex = minMonth ? monthIndex(minMonth.year, minMonth.month) : null;
|
||
const maxIndex = maxMonth ? monthIndex(maxMonth.year, maxMonth.month) : null;
|
||
|
||
const isAllowed = (year, month) => {
|
||
const index = monthIndex(year, month);
|
||
return (minIndex === null || index >= minIndex) && (maxIndex === null || index <= maxIndex);
|
||
};
|
||
|
||
const canShowYear = (year) => Array.from({ length: 12 }, (_, month) => isAllowed(year, month)).some(Boolean);
|
||
|
||
return (
|
||
<Popover
|
||
open={open}
|
||
onOpenChange={(nextOpen) => {
|
||
setOpen(nextOpen);
|
||
if (nextOpen) setDisplayYear(selected?.year || current.getFullYear());
|
||
}}
|
||
>
|
||
<PopoverTrigger
|
||
render={
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
disabled={disabled}
|
||
className={`w-full justify-start gap-2 font-normal ${className}`}
|
||
/>
|
||
}
|
||
>
|
||
<CalendarDays className="size-4 text-muted-foreground" />
|
||
<span className={selected ? 'capitalize' : 'text-muted-foreground'}>
|
||
{selected
|
||
? new Date(selected.year, selected.month, 1).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })
|
||
: placeholder}
|
||
</span>
|
||
</PopoverTrigger>
|
||
|
||
<PopoverContent className="w-[300px] p-3" align="start">
|
||
<div className="mb-3 flex items-center justify-between gap-2">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className="size-8"
|
||
disabled={!canShowYear(displayYear - 1)}
|
||
onClick={() => setDisplayYear((year) => year - 1)}
|
||
aria-label="Année précédente"
|
||
>
|
||
<ChevronLeft className="size-4" />
|
||
</Button>
|
||
<span className="text-sm font-semibold tabular-nums">{displayYear}</span>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className="size-8"
|
||
disabled={!canShowYear(displayYear + 1)}
|
||
onClick={() => setDisplayYear((year) => year + 1)}
|
||
aria-label="Année suivante"
|
||
>
|
||
<ChevronRight className="size-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-1.5">
|
||
{MONTH_NAMES.map((name, month) => {
|
||
const active = selected?.year === displayYear && selected?.month === month;
|
||
return (
|
||
<Button
|
||
key={name}
|
||
type="button"
|
||
size="sm"
|
||
variant={active ? 'default' : 'ghost'}
|
||
className="justify-center px-2 text-xs"
|
||
disabled={!isAllowed(displayYear, month)}
|
||
onClick={() => {
|
||
onChange(formatMonthValue(displayYear, month));
|
||
setOpen(false);
|
||
}}
|
||
>
|
||
{name.slice(0, 4)}
|
||
</Button>
|
||
);
|
||
})}
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
);
|
||
};
|
||
|
||
const DesktopYearPicker = ({
|
||
value,
|
||
onChange,
|
||
min,
|
||
max,
|
||
placeholder = 'Choisir une année',
|
||
disabled = false,
|
||
className = '',
|
||
}) => {
|
||
const currentYear = new Date().getFullYear();
|
||
const selectedYear = Number(value) || null;
|
||
const minYear = yearFromBound(min, 1900);
|
||
const maxYear = yearFromBound(max, 2100);
|
||
const baseYear = selectedYear || Math.min(Math.max(currentYear, minYear), maxYear);
|
||
const [open, setOpen] = useState(false);
|
||
const [pageStart, setPageStart] = useState(baseYear - (baseYear % 12));
|
||
const years = Array.from({ length: 12 }, (_, index) => pageStart + index);
|
||
|
||
return (
|
||
<Popover
|
||
open={open}
|
||
onOpenChange={(nextOpen) => {
|
||
setOpen(nextOpen);
|
||
if (nextOpen) {
|
||
const year = selectedYear || Math.min(Math.max(currentYear, minYear), maxYear);
|
||
setPageStart(year - (year % 12));
|
||
}
|
||
}}
|
||
>
|
||
<PopoverTrigger
|
||
render={
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
disabled={disabled}
|
||
className={`w-full justify-start gap-2 font-normal ${className}`}
|
||
/>
|
||
}
|
||
>
|
||
<CalendarDays className="size-4 text-muted-foreground" />
|
||
<span className={selectedYear ? 'tabular-nums' : 'text-muted-foreground'}>
|
||
{selectedYear || placeholder}
|
||
</span>
|
||
</PopoverTrigger>
|
||
|
||
<PopoverContent className="w-[280px] p-3" align="start">
|
||
<div className="mb-3 flex items-center justify-between gap-2">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className="size-8"
|
||
disabled={pageStart <= minYear}
|
||
onClick={() => setPageStart((year) => year - 12)}
|
||
aria-label="Années précédentes"
|
||
>
|
||
<ChevronLeft className="size-4" />
|
||
</Button>
|
||
<span className="text-sm font-semibold tabular-nums">
|
||
{Math.max(pageStart, minYear)} – {Math.min(pageStart + 11, maxYear)}
|
||
</span>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className="size-8"
|
||
disabled={pageStart + 11 >= maxYear}
|
||
onClick={() => setPageStart((year) => year + 12)}
|
||
aria-label="Années suivantes"
|
||
>
|
||
<ChevronRight className="size-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-1.5">
|
||
{years.map((year) => (
|
||
<Button
|
||
key={year}
|
||
type="button"
|
||
size="sm"
|
||
variant={selectedYear === year ? 'default' : 'ghost'}
|
||
disabled={year < minYear || year > maxYear}
|
||
onClick={() => {
|
||
onChange(String(year));
|
||
setOpen(false);
|
||
}}
|
||
>
|
||
{year}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
);
|
||
};
|
||
|
||
|
||
/* =========================================================
|
||
TIME PICKER
|
||
========================================================= */
|
||
|
||
const DesktopTimePicker = ({
|
||
value,
|
||
onChange,
|
||
disabled = false,
|
||
className = '',
|
||
}) => {
|
||
const [open, setOpen] = useState(false);
|
||
const timeRef = useRef(value);
|
||
timeRef.current = value;
|
||
const mouseState = useRef({ active: false, lastY: 0, accumulated: 0 });
|
||
|
||
const touchState = useRef({
|
||
active: false,
|
||
lastY: 0,
|
||
accumulated: 0,
|
||
});
|
||
|
||
const [hours = '', minutes = ''] = value
|
||
? value.split(':')
|
||
: [];
|
||
|
||
const hourOptions = Array.from(
|
||
{ length: 24 },
|
||
(_, i) => String(i).padStart(2, '0'),
|
||
);
|
||
|
||
const minuteOptions = Array.from(
|
||
{ length: 60 },
|
||
(_, i) => String(i).padStart(2, '0'),
|
||
);
|
||
|
||
const updateTime = (
|
||
nextHours,
|
||
nextMinutes,
|
||
) => {
|
||
const nextValue = `${String(nextHours).padStart(2, '0')}:${String(
|
||
nextMinutes,
|
||
).padStart(2, '0')}`;
|
||
timeRef.current = nextValue;
|
||
onChange(nextValue);
|
||
};
|
||
|
||
const changeHour = (direction) => {
|
||
const [currentHour = 0, currentMinute = 0] =
|
||
(timeRef.current || '00:00').split(':').map(Number);
|
||
updateTime((currentHour + direction % 24 + 24) % 24, currentMinute);
|
||
};
|
||
|
||
const changeMinute = (direction) => {
|
||
const [currentHour = 0, currentMinute = 0] =
|
||
(timeRef.current || '00:00').split(':').map(Number);
|
||
const total = ((currentHour * 60 + currentMinute + direction) % 1440 + 1440) % 1440;
|
||
updateTime(Math.floor(total / 60), total % 60);
|
||
};
|
||
|
||
const handleMouseDown = (event) => {
|
||
if (event.pointerType !== 'mouse' || event.button !== 0) return;
|
||
event.preventDefault();
|
||
event.currentTarget.setPointerCapture(event.pointerId);
|
||
mouseState.current = { active: true, lastY: event.clientY, accumulated: 0 };
|
||
};
|
||
|
||
const handleMouseMove = (event, type) => {
|
||
if (!mouseState.current.active || event.pointerType !== 'mouse') return;
|
||
const state = mouseState.current;
|
||
state.accumulated += state.lastY - event.clientY;
|
||
state.lastY = event.clientY;
|
||
const steps = Math.trunc(state.accumulated / 18);
|
||
if (!steps) return;
|
||
state.accumulated -= steps * 18;
|
||
if (type === 'hours') changeHour(steps);
|
||
else changeMinute(steps);
|
||
};
|
||
|
||
const handleMouseUp = () => {
|
||
mouseState.current.active = false;
|
||
mouseState.current.accumulated = 0;
|
||
};
|
||
|
||
const handleWheel = (event, type) => {
|
||
event.stopPropagation();
|
||
|
||
const direction =
|
||
event.deltaY > 0 ? 1 : -1;
|
||
|
||
if (type === 'hours') {
|
||
changeHour(direction);
|
||
} else {
|
||
changeMinute(direction);
|
||
}
|
||
};
|
||
|
||
const handleTouchStart = (event) => {
|
||
const touch = event.touches[0];
|
||
|
||
touchState.current = {
|
||
active: true,
|
||
lastY: touch.clientY,
|
||
accumulated: 0,
|
||
};
|
||
};
|
||
|
||
const handleTouchMove = (event, type) => {
|
||
if (!touchState.current.active) {
|
||
return;
|
||
}
|
||
|
||
const touch = event.touches[0];
|
||
const currentY = touch.clientY;
|
||
|
||
const deltaY =
|
||
touchState.current.lastY - currentY;
|
||
|
||
touchState.current.lastY = currentY;
|
||
|
||
touchState.current.accumulated += deltaY;
|
||
|
||
const pixelsPerUnit = 18;
|
||
|
||
const steps = Math.trunc(
|
||
touchState.current.accumulated /
|
||
pixelsPerUnit,
|
||
);
|
||
|
||
if (steps === 0) {
|
||
return;
|
||
}
|
||
|
||
touchState.current.accumulated -=
|
||
steps * pixelsPerUnit;
|
||
|
||
if (type === 'hours') {
|
||
changeHour(steps);
|
||
} else {
|
||
changeMinute(steps);
|
||
}
|
||
|
||
event.stopPropagation();
|
||
};
|
||
|
||
const handleTouchEnd = () => {
|
||
touchState.current.active = false;
|
||
touchState.current.accumulated = 0;
|
||
};
|
||
|
||
const clearTime = () => {
|
||
onChange('');
|
||
setOpen(false);
|
||
};
|
||
|
||
return (
|
||
<Popover
|
||
open={open}
|
||
onOpenChange={setOpen}
|
||
>
|
||
<PopoverTrigger
|
||
render={
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
disabled={disabled}
|
||
className={`w-full justify-start gap-2 font-normal ${className}`}
|
||
/>
|
||
}
|
||
>
|
||
<Clock className="size-4 text-muted-foreground" />
|
||
|
||
<span
|
||
className={
|
||
value
|
||
? 'tabular-nums'
|
||
: 'text-muted-foreground'
|
||
}
|
||
>
|
||
{value ? (
|
||
value
|
||
) : (
|
||
<>
|
||
<span className="lg:hidden">
|
||
Choisir une heure
|
||
</span>
|
||
|
||
<span className="hidden lg:inline">
|
||
Choisir
|
||
</span>
|
||
</>
|
||
)}
|
||
</span>
|
||
</PopoverTrigger>
|
||
|
||
<PopoverContent
|
||
className="w-[190px] p-3"
|
||
align="start"
|
||
>
|
||
<div className="mb-2 flex items-center justify-between">
|
||
<span className="text-sm font-medium">
|
||
Choisir une heure
|
||
</span>
|
||
|
||
<span className="font-mono text-sm font-semibold tabular-nums">
|
||
{value || '--:--'}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="relative flex h-[144px] gap-2 overflow-hidden">
|
||
<div className="pointer-events-none absolute inset-x-0 top-1/2 z-10 h-9 -translate-y-1/2 rounded-md border bg-muted/50" />
|
||
|
||
<div
|
||
className="flex-1 cursor-grab touch-none select-none active:cursor-grabbing"
|
||
onPointerDown={handleMouseDown}
|
||
onPointerMove={(event) => handleMouseMove(event, 'hours')}
|
||
onPointerUp={handleMouseUp}
|
||
onLostPointerCapture={handleMouseUp}
|
||
onWheel={(event) =>
|
||
handleWheel(
|
||
event,
|
||
'hours',
|
||
)
|
||
}
|
||
onTouchStart={handleTouchStart}
|
||
onTouchMove={(event) =>
|
||
handleTouchMove(
|
||
event,
|
||
'hours',
|
||
)
|
||
}
|
||
onTouchEnd={handleTouchEnd}
|
||
onTouchCancel={handleTouchEnd}
|
||
>
|
||
<div className="flex h-full flex-col items-center justify-center">
|
||
<span className="h-9 text-sm text-muted-foreground/40">
|
||
{
|
||
hourOptions[
|
||
(Number(hours || 0) +
|
||
23) %
|
||
24
|
||
]
|
||
}
|
||
</span>
|
||
|
||
<span className="flex h-9 w-full items-center justify-center rounded-md font-mono text-sm font-semibold tabular-nums">
|
||
{hours || '00'}
|
||
</span>
|
||
|
||
<span className="h-9 text-sm text-muted-foreground/40">
|
||
{
|
||
hourOptions[
|
||
(Number(hours || 0) +
|
||
1) %
|
||
24
|
||
]
|
||
}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-center">
|
||
<span className="font-bold">
|
||
:
|
||
</span>
|
||
</div>
|
||
|
||
<div
|
||
className="flex-1 cursor-grab touch-none select-none active:cursor-grabbing"
|
||
onPointerDown={handleMouseDown}
|
||
onPointerMove={(event) => handleMouseMove(event, 'minutes')}
|
||
onPointerUp={handleMouseUp}
|
||
onLostPointerCapture={handleMouseUp}
|
||
onWheel={(event) =>
|
||
handleWheel(
|
||
event,
|
||
'minutes',
|
||
)
|
||
}
|
||
onTouchStart={handleTouchStart}
|
||
onTouchMove={(event) =>
|
||
handleTouchMove(
|
||
event,
|
||
'minutes',
|
||
)
|
||
}
|
||
onTouchEnd={handleTouchEnd}
|
||
onTouchCancel={handleTouchEnd}
|
||
>
|
||
<div className="flex h-full flex-col items-center justify-center">
|
||
<span className="h-9 text-sm text-muted-foreground/40">
|
||
{
|
||
minuteOptions[
|
||
(Number(minutes || 0) +
|
||
59) %
|
||
60
|
||
]
|
||
}
|
||
</span>
|
||
|
||
<span className="flex h-9 w-full items-center justify-center rounded-md font-mono text-sm font-semibold tabular-nums">
|
||
{minutes || '00'}
|
||
</span>
|
||
|
||
<span className="h-9 text-sm text-muted-foreground/40">
|
||
{
|
||
minuteOptions[
|
||
(Number(minutes || 0) +
|
||
1) %
|
||
60
|
||
]
|
||
}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<p className="mt-2 text-center text-[10px] text-muted-foreground">
|
||
Glissez pour modifier
|
||
</p>
|
||
|
||
<div className="mt-2 border-t pt-2">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-7 w-full text-xs"
|
||
disabled={!value}
|
||
onClick={clearTime}
|
||
>
|
||
Effacer
|
||
</Button>
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
);
|
||
};
|
||
|
||
|
||
// Les champs natifs restent pratiques sur téléphone pour les dates et les heures.
|
||
// Les mois et années utilisent le même DatePicker partout pour garder une UI cohérente.
|
||
export const DatePicker = ({
|
||
value,
|
||
onChange,
|
||
min,
|
||
max,
|
||
placeholder,
|
||
id,
|
||
mode = 'date',
|
||
disabled = false,
|
||
required = false,
|
||
className = '',
|
||
ariaLabel,
|
||
}) => {
|
||
if (mode === 'month') {
|
||
return (
|
||
<DesktopMonthPicker
|
||
value={value}
|
||
onChange={onChange}
|
||
min={min}
|
||
max={max}
|
||
placeholder={placeholder}
|
||
disabled={disabled}
|
||
className={className}
|
||
/>
|
||
);
|
||
}
|
||
|
||
if (mode === 'year') {
|
||
return (
|
||
<DesktopYearPicker
|
||
value={value}
|
||
onChange={onChange}
|
||
min={min}
|
||
max={max}
|
||
placeholder={placeholder}
|
||
disabled={disabled}
|
||
className={className}
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Input
|
||
id={id}
|
||
aria-label={ariaLabel}
|
||
type="date"
|
||
min={min}
|
||
max={max}
|
||
value={value || ''}
|
||
required={required}
|
||
disabled={disabled}
|
||
onChange={(event) => onChange(event.target.value)}
|
||
className={`block w-full min-w-0 md:hidden ${className}`}
|
||
/>
|
||
<div className="hidden md:block">
|
||
<DesktopDatePicker
|
||
value={value}
|
||
onChange={onChange}
|
||
min={min}
|
||
max={max}
|
||
placeholder={placeholder}
|
||
disabled={disabled}
|
||
className={className}
|
||
/>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export const TimePicker = ({
|
||
value,
|
||
onChange,
|
||
id,
|
||
disabled = false,
|
||
required = false,
|
||
className = '',
|
||
ariaLabel,
|
||
}) => (
|
||
<>
|
||
<Input
|
||
id={id}
|
||
aria-label={ariaLabel}
|
||
type="time"
|
||
value={value || ''}
|
||
required={required}
|
||
disabled={disabled}
|
||
onChange={(event) => onChange(event.target.value)}
|
||
className={`block w-full min-w-0 md:hidden ${className}`}
|
||
/>
|
||
<div className="hidden md:block">
|
||
<DesktopTimePicker
|
||
value={value}
|
||
onChange={onChange}
|
||
disabled={disabled}
|
||
className={className}
|
||
/>
|
||
</div>
|
||
</>
|
||
);
|