laif-ds
Version:
Design System di Laif con componenti React basati su principi di Atomic Design
307 lines (242 loc) • 12.8 kB
Markdown
# AppTimePicker
## Overview
Standalone time picker component for selecting hours, minutes, and optionally seconds. Displays a trigger with a clock icon and opens a popover with scrollable columns for each time unit. Supports controlled/uncontrolled usage, UTC mode, clearable selection, min/max time constraints, multiple sizes, and visual variants.
---
## Props
| Prop | Type | Default | Description |
| ---------------- | ----------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------ |
| `value` | `Date` | `undefined` | Controlled selected time as a Date object. |
| `onChange` | `(date: Date \| undefined) => void` | `undefined` | Callback fired when the selected time changes. |
| `onClear` | `() => void` | `undefined` | Callback fired when the value is cleared. |
| `label` | `string \| React.ReactNode` | `undefined` | Label displayed above the trigger. |
| `placeholder` | `string` | `"Seleziona orario"` | Placeholder text shown when no time is selected. |
| `disabled` | `boolean` | `false` | Disables the time picker. |
| `clearable` | `boolean` | `false` | Show a clear (X) button in the trigger to reset the selection. |
| `className` | `string` | `""` | Additional CSS classes applied to the trigger element. |
| `labelClassName` | `string` | `""` | CSS classes for the label element. |
| `wrpClassName` | `string` | `""` | CSS classes for the outer wrapper `<div>`. |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` | Size variant of the trigger. |
| `variant` | `"default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| "link"` | `"default"` | Visual variant of the trigger. |
| `useUtc` | `boolean` | `false` | Use UTC time instead of local time. Shows a "UTC" badge. |
| `withSeconds` | `boolean` | `false` | Show a seconds column in the time picker. |
| `minuteStep` | `number` | `1` | Step interval for minute options (`1`-`59`). |
| `secondStep` | `number` | `1` | Step interval for second options (`1`-`59`). |
| `minTime` | `Date` | `undefined` | Minimum selectable time. Hours/minutes/seconds before this are disabled. |
| `maxTime` | `Date` | `undefined` | Maximum selectable time. Hours/minutes/seconds after this are disabled. |
| `id` | `string` | `undefined` | HTML `id` for the trigger element. |
| `data-testid` | `string` | `undefined` | `data-testid` attribute for E2E testing (e.g. Playwright). |
---
## Behavior
### Time Selection
- **Columns**: The popover displays scrollable columns for hours (00–23) and minutes. Minute values follow `minuteStep` and an optional seconds column follows `secondStep` when `withSeconds` is `true`.
- **Scroll-to-selected**: When the popover opens, the selected value is automatically scrolled into view.
- **Display format**: The trigger shows time in `HH:MM` format (or `HH:MM:SS` with `withSeconds`).
### Controlled vs Uncontrolled
- **Controlled**: Pass `value` and `onChange` to manage state externally. The component detects controlled mode by checking if the `value` prop is provided (even if `undefined`).
- **Uncontrolled**: Omit the `value` prop entirely. The component manages its own internal state.
### Clearable
- **Trigger clear button**: When `clearable` is `true` and a value is selected, an X button appears in the trigger.
- **Popover clear button**: A "Clear" button always appears at the bottom of the popover when a value is selected.
- **Callbacks**: Both `onChange(undefined)` and `onClear()` are called when clearing.
### Min/Max Time Constraints
- **Disabled options**: When `minTime` or `maxTime` is set, out-of-range hours are visually disabled and not selectable.
- **Cascading constraints**: Minutes are constrained only when the selected hour matches the boundary hour. Seconds are constrained only when both hour and minute match.
- **UTC-aware**: Constraints respect the `useUtc` setting.
### UTC Mode
- When `useUtc` is `true`, all time reading/writing uses UTC methods (`getUTCHours`, `setUTCHours`, etc.).
- A small "UTC" badge is displayed in the trigger.
---
## Exported Types
```ts
type AppTimePickerSize = "sm" | "default" | "lg";
type AppTimePickerVariant =
| "default"
| "destructive"
| "outline"
| "secondary"
| "ghost"
| "link";
interface AppTimePickerProps {
/* see Props table above */
}
```
---
## Examples
### Basic
```tsx
import { AppTimePicker } from "laif-ds";
import { useState } from "react";
export function BasicTimePicker() {
const [time, setTime] = useState<Date | undefined>();
return (
<AppTimePicker
value={time}
onChange={setTime}
label="Select Time"
placeholder="Pick a time"
/>
);
}
```
### With Seconds
```tsx
import { AppTimePicker } from "laif-ds";
import { useState } from "react";
export function TimePickerWithSeconds() {
const [time, setTime] = useState<Date | undefined>();
return (
<AppTimePicker
value={time}
onChange={setTime}
withSeconds
label="Time with Seconds"
/>
);
}
```
### Clearable
```tsx
import { AppTimePicker } from "laif-ds";
import { useState } from "react";
export function ClearableTimePicker() {
const [time, setTime] = useState<Date | undefined>(new Date());
return (
<AppTimePicker
value={time}
onChange={setTime}
clearable
label="Clearable Picker"
/>
);
}
```
### UTC Mode
```tsx
import { AppTimePicker } from "laif-ds";
import { useState } from "react";
export function UtcTimePicker() {
const [time, setTime] = useState<Date | undefined>(new Date());
return (
<AppTimePicker
value={time}
onChange={setTime}
useUtc
label="UTC Time Picker"
/>
);
}
```
### Min/Max Time Constraints
```tsx
import { AppTimePicker } from "laif-ds";
import { useState } from "react";
export function BusinessHoursTimePicker() {
const [time, setTime] = useState<Date | undefined>();
const minTime = new Date();
minTime.setHours(9, 0, 0, 0);
const maxTime = new Date();
maxTime.setHours(17, 30, 0, 0);
return (
<AppTimePicker
value={time}
onChange={setTime}
minTime={minTime}
maxTime={maxTime}
label="Business Hours Only"
/>
);
}
```
### Sizes and Variants
```tsx
import { AppTimePicker } from "laif-ds";
export function TimePickerVariants() {
return (
<div className="flex flex-col gap-4">
<AppTimePicker size="sm" label="Small" />
<AppTimePicker size="default" label="Default" />
<AppTimePicker size="lg" label="Large" />
<AppTimePicker variant="outline" label="Outline Variant" />
<AppTimePicker variant="ghost" label="Ghost Variant" />
</div>
);
}
```
### Disabled
```tsx
import { AppTimePicker } from "laif-ds";
export function DisabledTimePicker() {
return <AppTimePicker value={new Date()} disabled label="Disabled Picker" />;
}
```
### In Dialog
```tsx
import { AppTimePicker } from "laif-ds";
import { AppDialog } from "laif-ds";
import { Button } from "laif-ds";
import { useState } from "react";
export function TimePickerInDialog() {
const [time, setTime] = useState<Date | undefined>();
const [open, setOpen] = useState(false);
return (
<AppDialog
open={open}
onOpenChange={setOpen}
title="Seleziona Orario"
description="Scegli l'orario per la tua prenotazione."
trigger={<Button>Apri Time Picker Dialog</Button>}
footer={
<>
<Button variant="outline" onClick={() => setOpen(false)}>
Annulla
</Button>
<Button onClick={() => setOpen(false)}>Conferma</Button>
</>
}
asChild
>
<div className="space-y-4">
<AppTimePicker
value={time}
onChange={setTime}
label="Orario di inizio"
placeholder="Seleziona un'orario"
clearable
/>
{time && (
<div className="bg-d-muted/50 flex items-center gap-2 rounded border p-3 text-sm">
<span className="text-d-muted-foreground">Orario selezionato:</span>
<span className="font-medium tabular-nums">
{time.toLocaleTimeString("it-IT", {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
)}
</div>
</AppDialog>
);
}
```
---
## Helper Component: TimePickerColumn
The `TimePickerColumn` component is exported and can be used independently (e.g., inside the `DatePicker` component for datetime selection).
### TimePickerColumn Props
| Prop | Type | Default | Description |
| ---------------- | -------------------------------- | ----------- | ------------------------------------------------------- |
| `options` | `number[]` | — | Array of numeric options to display. |
| `value` | `number \| undefined` | `undefined` | Currently selected value. |
| `onChange` | `(value: number) => void` | — | Callback fired when an option is selected. |
| `type` | `"hour" \| "minute" \| "second"` | — | Column type — determines the header label (HH, MM, SS). |
| `className` | `string` | `""` | Additional CSS classes for the column wrapper. |
| `disabledValues` | `Set<number>` | `undefined` | Set of values that should be disabled (not selectable). |
---
## Notes
- **Sizing**: Use `size` to control the trigger height (sm: h-8, default: h-9, lg: h-10).
- **Variants**: Use `variant` to change the trigger's visual style (background, border, text color).
- **UTC badge**: When `useUtc` is `true`, a small "UTC" label appears in the trigger for clarity.
- **Controlled detection**: The component uses `props.hasOwnProperty("value")` to detect controlled mode, so passing `value={undefined}` keeps it controlled.
- **Performance**: Option arrays (0–23 for hours, 0–59 for minutes/seconds) are memoized to avoid re-creation on every render.
- **Accessibility**: The trigger has proper ARIA attributes. Disabled options in the columns are not focusable.
- **Integration with DatePicker**: The `TimePickerColumn` sub-component is reused by `DatePicker` when `showTime` is enabled.