Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
40916ee
feat(dropdown): TEDI-Ready Dropdown component #94
airikej Feb 18, 2026
793bc10
chore: fix stories #94
airikej Feb 18, 2026
a6e6ef2
feat(dropdown): add dropdown separator and divided prop + examples #94
airikej Feb 18, 2026
c0d085a
feat(dropdown): tree variant, placement control #94
airikej Feb 20, 2026
dd27964
feat(separator): match stories with Figma #94
airikej Feb 25, 2026
eb19728
Merge branch 'rc' into feat/94-dropdown-tedi-ready-component-development
airikej Feb 26, 2026
3ee3091
fix(dropdown): fix stories #94
airikej Feb 26, 2026
e13e729
feat(date-picker): initial commit #24
airikej Feb 26, 2026
5c763d7
Merge branch 'feat/94-dropdown-tedi-ready-component-development' into…
airikej Feb 26, 2026
ec73cad
feat(dropdown): add className props, add box-shadow, remove unused ty…
airikej Feb 26, 2026
eeb2bdf
Merge branch 'feat/94-dropdown-tedi-ready-component-development' into…
airikej Feb 26, 2026
672d263
feat(date-field): new TEDI-Ready DateField component #24
airikej Mar 3, 2026
d778f1c
feat(date-field): type error fixes #24
airikej Mar 3, 2026
d5ada60
feat(date-field): fix examples, add unit tests #24
airikej Mar 4, 2026
0957015
feat(date-field): states example, css fixes #24
airikej Mar 4, 2026
664a254
fix(dropdown): focused item indicator fix, fix stories #94
airikej Mar 4, 2026
d547f58
fix(dropdown): fix tab targeting on choice items #94
airikej Mar 4, 2026
ca094c4
fix(dropdown): fix focus scrolling bug #94
airikej Mar 4, 2026
dede6b1
Merge branch 'feat/94-dropdown-tedi-ready-component-development' into…
airikej Mar 4, 2026
a9483dc
feat(date-field): fix month and year grid selections when calendarVie…
airikej Mar 4, 2026
8b0f3de
fix(date-field): today button margin issue fix #24
airikej Mar 4, 2026
4c34025
fix(dropdown): design review fixes #94
airikej Mar 5, 2026
9ca8250
fix(dropdown): checkbox/radio tab targeting fix #94
airikej Mar 5, 2026
ad8fb49
fix(dropdown): add deprecated badge to Community component, update Fi…
airikej Mar 5, 2026
a528d7c
fix(dropdown): add more test coverage #94
airikej Mar 5, 2026
31df95d
fix(dropdown): update tests #94
airikej Mar 5, 2026
061ac3e
fix(dropdown): improve test coverage #94
airikej Mar 5, 2026
73e0e2d
Merge branch 'feat/94-dropdown-tedi-ready-component-development' into…
airikej Mar 5, 2026
9f5ee48
feat(date-field, date-calendar): separate components #24
airikej Mar 17, 2026
b49bb74
Merge branch 'rc' into feat/24-datefield-new-tedi-ready-component
airikej Mar 17, 2026
f6cb4c7
feat(date-field): design review fixes #24
airikej Mar 24, 2026
0d04f75
feat(date-field): update stories #24
airikej Mar 24, 2026
21eeca5
fix(date-field): multivalue calendar positioning #24
airikej Mar 24, 2026
273535f
feat(calendar): design review fixes #24
airikej Mar 25, 2026
808e73f
fix(date-field): design review fixes vol 2 #24
airikej Apr 6, 2026
b653378
feat(date-picker): design review fixes on Calendar #24
airikej Apr 6, 2026
2d2b643
fix(date-field): design review fixes #24
airikej Apr 8, 2026
fceb50a
feat(date-field): calendar fixes #24
airikej Apr 8, 2026
86d13ea
feat(date-field): add tests #24
airikej Apr 9, 2026
aaf57b5
Merge branch 'rc' into feat/24-datefield-new-tedi-ready-component
airikej Apr 9, 2026
2a7d22c
fix(date-field, calendar): code review fixes #24
airikej Apr 10, 2026
67fd752
feat(date-field): code review fixes, update test coverage #24
airikej Apr 13, 2026
ae4bed4
feat(date-field): code review fixes #24
airikej Apr 13, 2026
021048e
fix(date-field): code review fixes #24
airikej Apr 14, 2026
8283b08
fix(date-field): code review fixes #24
airikej Apr 15, 2026
3efd978
feat(time-field): new TEDI-ready component #25
airikej Apr 17, 2026
246af09
fix(time-field, time-picker): code review fixes #25
airikej Apr 21, 2026
35adf95
fix(time-picker): code review fixes #25
airikej Apr 21, 2026
cf72695
fix(time-picker): code review fixes, improve test coverage #25
airikej Apr 21, 2026
769e2a3
fix(time-field, time-picker): code review fixes #25
airikej Apr 23, 2026
9723972
fix(time-picker): fix initial value reset #25
airikej Apr 23, 2026
812811a
fix(time-field): radio grid tab selection fix #25
airikej Apr 23, 2026
3050c25
fix(time-field): code review fixes #25
airikej Apr 24, 2026
97d4435
fix(date-field): code review fixes #24
airikej Apr 24, 2026
df2683c
Merge branch 'rc' into feat/24-datefield-new-tedi-ready-component
airikej Apr 24, 2026
ddbd662
fix(calendar): code review fixes #24
airikej Apr 24, 2026
d4cfeee
feat(date-field): fix desc formatting for Storybook to show prop desc…
airikej Apr 28, 2026
1ffe798
feat(date-field): improve prop name #24
airikej Apr 30, 2026
f2070ec
Merge branch 'rc' into feat/24-datefield-new-tedi-ready-component
airikej Apr 30, 2026
304bd61
fix(time-field): improve scrolling, toggle picker button #25
airikej May 4, 2026
2a43344
fix(date-field): code review fixes #24
airikej May 4, 2026
7875880
fix(date-field): code review fixes #24
airikej May 5, 2026
ef05ef4
fix(date-field): multiple months mobile fix #24
airikej May 6, 2026
d289e7d
fix(date-field): locale parsing more universal #24
airikej May 6, 2026
dbcb728
Merge branch 'feat/25-timefield-new-tedi-ready-component-1' into feat…
airikej May 6, 2026
c961b4e
Merge branch 'feat/24-datefield-new-tedi-ready-component' into feat/5…
airikej May 6, 2026
b6c8816
feat(date-time-field): component development #554
airikej May 6, 2026
b89ea98
feat(date-time-field): refactor #554
airikej May 6, 2026
3c9e292
feat(date-time-field): refactor #554
airikej May 6, 2026
9fa8c3b
fix(date-time-field): timewheel fixes #554
airikej May 7, 2026
bcd674b
fix(time-field): code review changes #25
airikej May 11, 2026
639f0ba
fix(time-field): remove duplicate row #25
airikej May 11, 2026
ae15d7b
fix(date-field): code review fixes #24
airikej May 11, 2026
aa2e57d
fix(time-field): allow useNativePicker to be used regardless of break…
airikej May 11, 2026
7c923a4
fix(date-field): add breakpoint props support #24
airikej May 11, 2026
9090caf
fix(date-field): remove duplicate appearance:none; #24
airikej May 11, 2026
fb55cd3
fix(date-field): textfield focus styles, textfield small input height…
airikej May 11, 2026
b9970c3
fix(text-field): align right area content center #24
airikej May 11, 2026
d2502ed
fix(time-field): add formatting when valid time without delimiter is …
airikej May 11, 2026
abc144c
fix(date-field,calendar): cr fixes #24
airikej May 12, 2026
f735235
Merge branch 'rc' into feat/24-datefield-new-tedi-ready-component
airikej May 12, 2026
1c1f698
fix(choice-group): remove comment #25
airikej May 12, 2026
4ab45ea
Merge branch 'feat/24-datefield-new-tedi-ready-component' into feat/5…
airikej May 13, 2026
487a4cc
feat(time-wheel): wrap keyboard navigation at column ends #25
airikej May 13, 2026
765140d
chore: update consumer skills for TimeField and TimePicker #25
airikej May 13, 2026
279dd36
Merge branch 'rc' into feat/25-timefield-new-tedi-ready-component-1
airikej May 14, 2026
b091209
Merge branch 'feat/25-timefield-new-tedi-ready-component-1' into feat…
airikej May 14, 2026
4679416
feat(date-time-field): update consumer skill, update stories #554
airikej May 14, 2026
471ee9e
fix(date-time-field): cr fixes #554
airikej May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion skills/tedi-react/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const [email, setEmail] = useState('');
<Checkbox id="agree" label="I agree" value="agree" onChange={(val, checked) => setAgreed(checked)} />
```

Form controls: `TextField`, `Select`, `TextArea`, `NumberField`, `Checkbox`, `Radio`, `ChoiceGroup`, `Search`, `DateField`, `FileUpload`, `FileDropzone`.
Form controls: `TextField`, `Select`, `TextArea`, `NumberField`, `Checkbox`, `Radio`, `ChoiceGroup`, `Search`, `DateField`, `TimeField`, `DateTimeField`, `FileUpload`, `FileDropzone`.

## Theming

Expand Down
103 changes: 103 additions & 0 deletions skills/tedi-react/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,109 @@ The ref shape mirrors TextField (`{ input, wrapper }`). In `'multiple'` mode the
<DateField id="dob" label="Date of birth" useNativePicker md={{ useNativePicker: false }} />
```

### TimeField
**Props:** `TimeFieldProps` | bp, form
- `id: string` (required), `label: string` (required)
- `value?: string`, `defaultValue?: string` — `"HH:mm"` 24-hour format
- `onChange?: (time: string) => void`
- `placeholder?: string`
- `required?: boolean`, `readOnly?: boolean`
- `stepMinutes?: number = 1` — minute increment for the picker wheel / grid
- `availableTimes?: string[]` — limit selectable times to a fixed list (`["09:00", "09:30", …]`); switches the popover to grid mode
- `inputProps?: Omit<TextFieldProps, 'id' | 'label' | 'value' | 'onChange'>` — pass-through to the underlying input
- `className?: string`
- **Breakpoint-aware:** `useNativePicker?: boolean = false` (swap to `<input type="time">`; ignores `availableTimes`), `showPicker?: boolean = true`, `timePickerTrigger?: 'input' | 'button' = 'button'`, `availableTimesVariant?: 'grid-buttons' | 'grid-radio' | 'dropdown'` — which variant the picker renders when `availableTimes` is set

```tsx
<TimeField id="meeting" label="Meeting time" value={time} onChange={setTime} stepMinutes={15} />

// Constrain to specific slots, render as a radio-button grid
<TimeField
id="slot"
label="Available slot"
availableTimes={['09:00', '09:30', '10:00', '14:00', '15:30']}
availableTimesVariant="grid-radio"
value={slot}
onChange={setSlot}
/>

// Native picker on mobile, custom wheel on desktop
<TimeField id="alarm" label="Alarm" useNativePicker md={{ useNativePicker: false }} />
```

### TimePicker
> **For plain time inputs use `TimeField`.** TimePicker is the lower-level picker primitive — reach for it only when you need a standalone, always-visible time selector (scheduling UI, custom popover, side-by-side with a calendar in a DateTime composite).

**Props:** `TimePickerProps` | form
- `value?: string`, `defaultValue?: string` — `"HH:mm"`
- `onChange?: (time: string) => void`
- `stepMinutes?: number = 1` — minute increment for the wheel
- `availableTimes?: string[]` — switches from scroll-wheel mode to a predefined-slots grid
- `gridVariant?: 'button' | 'radio' = 'button'` — only used with `availableTimes`
- `bordered?: boolean = true` — set `false` when embedding inside a parent that already provides its own surface (e.g. alongside a Calendar)
- `className?: string`

The wheel column supports full keyboard navigation: `ArrowUp` / `ArrowDown` and `PageUp` / `PageDown` cycle through the column (wrap at both ends), `Home` / `End` jump to the bounds, `Enter` / `Space` commit the highlighted value.

```tsx
import { TimePicker } from '@tedi-design-system/react/tedi';

<TimePicker value={time} onChange={setTime} stepMinutes={5} />

// Predefined slots
<TimePicker
availableTimes={['09:00', '10:00', '11:00', '14:00']}
gridVariant="radio"
value={slot}
onChange={setSlot}
/>
```

### DateTimeField
**Props:** `DateTimeFieldProps` | bp, form
- `id: string` (required), `label: string` (required)
- `mode?: 'single' | 'range' = 'single'` — note: no `'multiple'` (combining multi-date with per-row times is ambiguous)
- `value?: Date | DateTimeRange`, `defaultValue?: Date | DateTimeRange` — shape matches `mode` (`Date` for single, `{ from?, to? }` for range)
- `onChange?: (value: Date | DateTimeRange | undefined) => void`
- `placeholder?: string`, `required?: boolean`, `readOnly?: boolean`, `disabled?: boolean`
- `minDate?: Date`, `maxDate?: Date`, `disablePast?: boolean`, `disableFuture?: boolean` — calendar-only; the time wheel still allows every minute
- `stepMinutes?: number = 15` — minute increment for the time wheel (ignored when `availableTimes` is set)
- `initialMonth?: Date`
- `locale?: Locale = et`, `localeCode?: string = 'et-EE'`
- `selectTimeLabel?: string`, `backLabel?: string` — multi-step layout button labels (fall back to localised labels)
- `inputProps?: Omit<TextFieldProps, 'id' | 'label' | 'value' | 'onChange'>` — pass-through to the underlying input (helper, icon, isClearable, size, …)
- **Breakpoint-aware:** `layout?: 'side-by-side' | 'multi-step' = 'side-by-side'`, `availableTimes?: string[]` (switches the time step to a grid of fixed slots), `timeGridVariant?: 'button' | 'radio'` (default `'button'` for side-by-side, `'radio'` for multi-step), `monthYearSelectType?: 'dropdown' | 'grid' = 'dropdown'`, `showOutsideDays?: boolean = true`, `useNativePicker?: boolean = false` (renders `<input type="datetime-local">`; ignored when `mode='range'`), `timeHeading?: ReactNode` (heading above the time picker in side-by-side layout)

In `mode='range'` the popover renders a 2-month calendar plus **two** time pickers stacked underneath — one for `from`, one for `to` — and each `from`/`to` `Date` carries its own time component.

```tsx
import { DateTimeField } from '@tedi-design-system/react/tedi';

// Side-by-side wheel
<DateTimeField id="meeting" label="Meeting" placeholder="pp.kk.aaaa hh:mm" stepMinutes={15} />

// Predefined slots, multi-step layout (pick date → pick slot)
<DateTimeField
id="appointment"
label="Appointment"
layout="multi-step"
availableTimes={['09:30', '10:00', '11:30']}
timeGridVariant="radio"
/>

// Range mode — two calendars + two time pickers
<DateTimeField
id="period"
label="Booking period"
mode="range"
value={range}
onChange={(v) => setRange(v && 'from' in v ? v : undefined)}
/>

// Native fallback (single mode only)
<DateTimeField id="alarm" label="Alarm" useNativePicker md={{ useNativePicker: false }} />
```

### FileUpload
**Props:** `FileUploadProps` | form
- `id: string` (required), `name: string` (required)
Expand Down
99 changes: 99 additions & 0 deletions skills/tedi-react/references/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ TEDI form controls support both **controlled** and **uncontrolled** modes, follo
| ChoiceGroup | `ChoiceGroupValue` | Radio/checkbox groups, segmented variant |
| Search | `string` | Search button, onSearch callback |
| DateField | `Date \| Date[] \| DateRange` | Single/multiple/range, manual input, min/max, native picker, breakpoint-aware |
| TimeField | `string` (`"HH:mm"`) | Wheel / grid picker, native fallback, stepMinutes, availableTimes |
| DateTimeField | `Date \| DateTimeRange` | Calendar + time wheel / slots, side-by-side or multi-step layout, range mode, native fallback |
| FileUpload | `FileUploadFile[]` | Multi-file, validation, loading states |
| FileDropzone | `FileUploadFile[]` | Drag-and-drop |

Expand Down Expand Up @@ -177,6 +179,101 @@ const [date, setDate] = useState<Date>();
/>
```

## TimeField

The value is always a `"HH:mm"` 24-hour string. The popover defaults to a wheel picker; set `availableTimes` to switch to a fixed-slot grid, or `useNativePicker` to drop the custom UI entirely.

```tsx
import { TimeField } from '@tedi-design-system/react/tedi';

// Wheel picker, 15-minute step
<TimeField
id="meeting"
label="Meeting time"
value={time}
onChange={setTime}
stepMinutes={15}
required
/>

// Constrain to predefined slots, render as a radio-button grid
<TimeField
id="slot"
label="Available slot"
availableTimes={['09:00', '09:30', '10:00', '14:00', '15:30']}
availableTimesVariant="grid-radio"
value={slot}
onChange={setSlot}
/>

// Native picker on mobile, custom wheel on desktop
<TimeField
id="alarm"
label="Alarm"
useNativePicker
md={{ useNativePicker: false }}
/>
```

For an always-visible time selector (e.g. side-by-side with a calendar, or inside a custom popover) use the lower-level `TimePicker` directly:

```tsx
import { TimePicker } from '@tedi-design-system/react/tedi';

<TimePicker value={time} onChange={setTime} stepMinutes={5} bordered={false} />
```

## DateTimeField

Combines a `DateField` calendar with a `TimePicker` in one input. The popover shows them either side-by-side (default) or as a two-step flow (`layout='multi-step'`). The committed value is a single `Date` (carrying both date and time) — or a `DateTimeRange` (`{ from, to }`) in `mode='range'`.

```tsx
import { DateTimeField } from '@tedi-design-system/react/tedi';

// Default — side-by-side calendar + time wheel
<DateTimeField
id="meeting"
label="Meeting"
placeholder="pp.kk.aaaa hh:mm"
stepMinutes={15}
value={value}
onChange={(v) => setValue(v instanceof Date ? v : undefined)}
/>

// Predefined time slots rendered as a button grid
<DateTimeField
id="slot"
label="Available slot"
availableTimes={['09:30', '10:00', '11:30', '15:30']}
timeGridVariant="button"
value={slot}
onChange={setSlot}
/>

// Multi-step layout — pick the day first, then a slot
<DateTimeField
id="appointment"
label="Appointment"
layout="multi-step"
availableTimes={['09:30', '10:00', '11:30']}
timeGridVariant="radio"
/>

// Range mode — two-month calendar + `from` / `to` time pickers
<DateTimeField
id="period"
label="Booking period"
mode="range"
value={range}
onChange={(v) => setRange(v && 'from' in v ? v : undefined)}
/>

// Native fallback (`<input type="datetime-local">`) — `mode='single'` only
<DateTimeField id="alarm" label="Alarm" useNativePicker md={{ useNativePicker: false }} />
```

Constraints (`minDate` / `maxDate` / `disablePast` / `disableFuture`) apply to the **calendar only** — the time wheel still allows every minute inside the allowed days.

## Checkbox & Radio

```tsx
Expand Down Expand Up @@ -283,6 +380,8 @@ import { FileUpload, FileDropzone } from '@tedi-design-system/react/tedi';
- **Select:** `onChange?: (value: ISelectOption | ISelectOption[] | null) => void`
- **NumberField:** `onChange?: (value: number) => void`
- **DateField:** `onSelect?: OnSelectHandler<Date | Date[] | DateRange | undefined>` — value shape depends on `mode` (`'single'` → `Date`, `'multiple'` → `Date[]`, `'range'` → `DateRange`)
- **TimeField / TimePicker:** `onChange?: (time: string) => void` — value is always `"HH:mm"` 24-hour format (empty string when cleared)
- **DateTimeField:** `onChange?: (value: Date | DateTimeRange | undefined) => void` — value shape depends on `mode` (`'single'` → `Date`, `'range'` → `{ from, to }`)

## Disabled State

Expand Down
2 changes: 2 additions & 0 deletions src/tedi/components/content/calendar/calendar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@
all: unset;
display: block;
width: 100%;
width: var(--form-calendar-date-width);
height: 100%;
height: var(--form-calendar-date-width);
overflow: hidden;
font-size: var(--body-regular-size);
font-weight: var(--tedi-weight-02);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@
}
}

&:focus-visible {
// The outline fires when the card itself is focused (checkbox variant) or
// when the inner radio input is focused (radio variant — the wrapper has
// tabIndex=-1, so only the input receives focus).
&:focus-visible,
&:has(input:focus-visible) {
z-index: 5;
outline: 2px solid var(--form-checkbox-radio-default-border-selected);
outline-offset: 2px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,19 @@ describe('ChoiceGroupItem', () => {
fireEvent.click(input);
expect(mockInputClick).not.toHaveBeenCalled();
});

it('makes the outer wrapper non-tabbable for radio type (arrow-navigated group)', () => {
const { container } = renderWithContext({ type: 'radio', variant: 'card' });
const card = container.querySelector('.tedi-choice-group-item') as HTMLElement;
expect(card).toHaveAttribute('tabIndex', '-1');
expect(card).not.toHaveAttribute('role');
expect(card).not.toHaveAttribute('aria-checked');
});

it('keeps the outer wrapper tabbable with role=checkbox for checkbox type', () => {
const { container } = renderWithContext({ type: 'checkbox', variant: 'card' });
const card = container.querySelector('.tedi-choice-group-item') as HTMLElement;
expect(card).toHaveAttribute('tabIndex', '0');
expect(card).toHaveAttribute('role', 'checkbox');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,16 @@ export const ChoiceGroupItem = (props: ExtendedChoiceGroupItemProps): React.Reac

document.getElementById(id)?.click();
};

const isRadio = type === 'radio';
return (
<Col {...colProps} className={ColumnBEM}>
<div
className={ChoiceGroupItemBEM}
tabIndex={disabled ? -1 : 0}
tabIndex={isRadio || disabled ? -1 : 0}
onClick={handleClick}
role={type}
aria-checked={isChecked}
role={isRadio ? undefined : type}
aria-checked={isRadio ? undefined : isChecked}
>
{variant === 'default' || showIndicator ? (
<InputComponent
Expand Down
Loading
Loading