Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 59 additions & 0 deletions skills/tedi-angular/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,65 @@ Form-control wrapper around the Calendar. Exposes a typed text input paired with
<tedi-date-field inputId="multi-mo" [numberOfMonths]="2" />
```

### DateTimeField
**Selector:** `tedi-date-time-field` | ControlValueAccessor

Form-control wrapper that pairs a typed text input with a popover holding the Calendar and a TimePicker. `side-by-side` shows both together; `multi-step` picks the date first, then advances to a separate time step. Setting `availableTimes` swaps the scroll-wheel for a grid of predefined slots. Supports `single` and `range` modes, a native `datetime-local` fallback, and a mobile modal. Like DateField it owns no label — compose with `tedi-form-field` + `tedi-label`. Inherits the same calendar options as DateField (`minDate`, `maxDate`, `disablePast`/`disableFuture`, `disabledMatchers`, `availableDays`/`unavailableDays`, `selectionLevel`, `monthYearSelectType`, `initialMonth`, `showOutsideDays`, `showWeekNumbers`).

**Model:** `value: Date | DateRange | null` (range carries a time on each end)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the actual DateTimeFieldValue contract.

The component and stories use the exported DateTimeFieldValue type, but the documentation advertises DateRange, which can imply the calendar-only range shape. Use the public alias and explicitly describe the from/to values as date-times.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/tedi-angular/references/components.md` at line 1041, Update the model
documentation to use the exported DateTimeFieldValue alias instead of Date |
DateRange | null, and explicitly state that range values contain date-time
values in both from and to.

**Outputs:**
- `openChange: boolean` — emitted when the picker (popover/modal) open state changes

**Inputs:**
- `inputId: string` (required) — unique ID for label association
- `mode: DateTimeFieldMode = "single"` — "single" (one Date) or "range" ({ from, to })
- `layout: DateTimeFieldLayout = "side-by-side"` — "side-by-side" or "multi-step"; `range` always renders side-by-side
- `availableTimes: string[] | ((date: Date) => string[]) | undefined` — predefined `HH:mm` slots (static or per-date); renders a slot grid instead of the wheel
- `timeGridVariant: "button" | "radio" | undefined` — slot grid style; defaults to "button" for side-by-side, "radio" for multi-step
- `minuteStep: number = 15` — minute interval for the scroll-wheel; ignored when `availableTimes` is set
- `slotColumns: number = 3` — columns in the slot grid
- `timeHeading / selectTimeLabel / backLabel: string | undefined` — label overrides (default to translated strings)
- `placeholder: string = ""` — placeholder when no value
- `localeCode: string = "et-EE"` — BCP-47 locale for the calendar and date/time formatting
- `inputDisabled / readOnly / required: boolean = false`
- `size: "default" | "small" = "default"` — should match the surrounding `tedi-form-field`
- `numberOfMonths: BreakpointInput<number> = { xs: 1, md: 2 }` — months side by side in `range` mode (single is always 1)
- `useNativePicker: boolean | "sm" | "md" | "lg" | "xl" = false` — OS-native `datetime-local` picker (single mode only); `true` always, breakpoint name uses native below it
- `modal: boolean | "sm" | "md" | "lg" | "xl" = false` — open in a centered modal (with Cancel/Confirm) instead of the popover; breakpoint name means modal below it
- `fullscreen: boolean | "sm" | "md" | "lg" | "xl" = false` — render the modal fullscreen; only applies when it opens as a modal
- `formatDate: ((value: DateTimeFieldValue) => string) | undefined` — custom display formatter

```html
<!-- Default: calendar + scroll-wheel time -->
<tedi-form-field>
<label tedi-label for="dt">Date and time</label>
<tedi-date-time-field inputId="dt" [formControl]="control" placeholder="pp.kk.aaaa tt:mm" />
</tedi-form-field>

<!-- Predefined time slots (button grid) -->
<tedi-date-time-field
inputId="dt-slots"
[formControl]="control"
[availableTimes]="['09:30', '10:00', '11:30', '15:30']"
timeGridVariant="button"
/>

<!-- Multi-step: pick the date, then the time (radio slots) -->
<tedi-date-time-field
inputId="dt-steps"
[formControl]="control"
layout="multi-step"
[availableTimes]="slotsForDate"
/>

<!-- Range with a time picker per end -->
<tedi-date-time-field inputId="dt-range" mode="range" [formControl]="rangeControl" />

<!-- Native datetime-local on phones, modal below md, popover above -->
<tedi-date-time-field inputId="dt-native" [formControl]="control" useNativePicker="sm" />
<tedi-date-time-field inputId="dt-modal" [formControl]="control" modal="md" />
```

### TimeField
**Selector:** `tedi-time-field`
**Model:** `value: string | null` — `HH:mm`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,14 @@
min-width: 0;
}

// When useNativePicker is active the input swaps to type=date. WebKit/Blink
// (Chrome, Safari, Edge) render their own calendar-picker indicator on the
// right, plus inner spin/clear buttons — we replace them with our own icon
// button, so suppress the browser-default ones. Firefox does not render a
// native indicator on <input type="date"> so no Firefox-specific rule is
// needed.
&__input[type="date"] {
// When useNativePicker is active the input swaps to type=date (date-field)
// or type=datetime-local (date-time-field). WebKit/Blink (Chrome, Safari,
// Edge) render their own calendar-picker indicator on the right, plus inner
// spin/clear buttons — we replace them with our own icon button, so suppress
// the browser-default ones. Firefox does not render a native indicator on
// these inputs so no Firefox-specific rule is needed.
&__input[type="date"],
&__input[type="datetime-local"] {
&::-webkit-calendar-picker-indicator {
display: none;
appearance: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export class DateInputComponent implements AfterViewChecked {
readonly iconActive = input<boolean>(false);
readonly iconDisabled = input<boolean>(false);
readonly useNativePicker = input<boolean>(false);
/** Native input type used when `useNativePicker` is `true`. `date` for date-only fields, `datetime-local` for date-and-time fields. */
readonly nativeInputType = input<"date" | "datetime-local">("date");
readonly nativeIsoValue = input<string>("");
readonly clearable = input<boolean>(false);

Expand Down Expand Up @@ -150,7 +152,9 @@ export class DateInputComponent implements AfterViewChecked {
return total - visible;
});

readonly inputType = computed(() => (this.useNativePicker() ? "date" : "text"));
readonly inputType = computed(() =>
this.useNativePicker() ? this.nativeInputType() : "text",
);

readonly inputValue = computed(() =>
this.useNativePicker() ? this.nativeIsoValue() : this.value(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<tedi-modal class="tedi-date-time-field-modal">
<tedi-modal-header>
<h2>{{ "date-time-field.modal-title" | tediTranslate }}</h2>
</tedi-modal-header>
<tedi-modal-content
class="tedi-date-time-field-modal__content"
[class.tedi-date-time-field-modal__content--range]="isRange"
>
<tedi-calendar
#calendar
class="tedi-date-time-field-modal__calendar"
[bordered]="false"
[value]="draft()"
[currentMonth]="month()"
[mode]="data.mode"
[selectionLevel]="data.selectionLevel"
[localeCode]="data.localeCode"
[showOutsideDays]="data.showOutsideDays"
[showWeekNumbers]="data.showWeekNumbers"
[numberOfMonths]="data.numberOfMonths"
[monthYearSelectType]="data.monthYearSelectType"
[required]="data.required"
[disabledMatchers]="data.disabledMatchers"
[availableDays]="data.availableDays"
[unavailableDays]="data.unavailableDays"
[shouldDisableMonth]="data.shouldDisableMonth"
[shouldDisableYear]="data.shouldDisableYear"
(currentMonthChange)="month.set($event)"
(select)="handleCalendarSelect()"
/>

<div class="tedi-date-time-field-modal__separator" aria-hidden="true">
<span class="tedi-date-time-field-modal__separator-line"></span>
</div>

@if (isRange) {
<div class="tedi-date-time-field-modal__times">
<ng-container
[ngTemplateOutlet]="rangeTimeTpl"
[ngTemplateOutletContext]="{
kind: 'from',
time: fromTime(),
slots: fromSlots(),
heading: data.timeHeading ?? ('date-time-field.time-heading-from' | tediTranslate),
dateLabel: fromDateLabel(),
}"
/>
<ng-container
[ngTemplateOutlet]="rangeTimeTpl"
[ngTemplateOutletContext]="{
kind: 'to',
time: toTime(),
slots: toSlots(),
heading: data.timeHeading ?? ('date-time-field.time-heading-to' | tediTranslate),
dateLabel: toDateLabel(),
}"
/>
</div>
} @else {
<div class="tedi-date-time-field-modal__time">
<span class="tedi-date-time-field-modal__heading">
{{ data.timeHeading ?? ("date-time-field.time-heading" | tediTranslate) }}
</span>
@if (!singleSlots()?.length && useNativeTimeInput()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Inconsistent native time input condition between single and range modes.

Single mode (line 64) gates native time input on both !singleSlots()?.length && useNativeTimeInput() — i.e., no slots and below md breakpoint. Range mode (line 116) only checks !slots?.length — no slots regardless of breakpoint. On desktop with no predefined slots, single mode renders the scroll-wheel tedi-time-picker while range mode renders native <input type="time"> inputs. If this is intentional (e.g., two scroll-wheels are too wide), a brief comment explaining the asymmetry would help future maintainers.

Also applies to: 116-116

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tedi/components/form/date-time-field/date-time-field-modal/date-time-field-modal.component.html`
at line 64, Align the no-slot native time input conditions in the single-mode
and range-mode templates, or add a brief comment documenting the intentional
breakpoint asymmetry; update the checks near the single-mode condition and
range-mode condition so their behavior is explicit and consistent with the
intended desktop layout.

<tedi-form-field
class="tedi-date-time-field-modal__native-time"
[size]="data.size"
>
<tedi-time-field
inputId="dtf-modal-time"
[useNativePicker]="true"
[clearable]="false"
[value]="singleTime()"
(valueChange)="handleTimeSelect($event)"
/>
</tedi-form-field>
} @else {
<tedi-time-picker
[value]="singleTime()"
[variant]="singleSlots()?.length ? 'slots' : 'scroll'"
[timeSlots]="singleSlots() ?? []"
[showSlotIndicator]="data.gridVariant === 'radio'"
[columns]="data.slotColumns"
[minuteStep]="data.minuteStep"
(valueChange)="handleTimeSelect($event)"
/>
}
</div>
}
</tedi-modal-content>
<tedi-modal-footer>
<button tedi-button type="button" variant="secondary" (click)="cancel()">
{{ "date-field.cancel" | tediTranslate }}
</button>
<button tedi-button type="button" (click)="confirm()">
{{ "date-field.confirm" | tediTranslate }}
</button>
</tedi-modal-footer>
</tedi-modal>

<ng-template
#rangeTimeTpl
let-kind="kind"
let-time="time"
let-slots="slots"
let-heading="heading"
let-dateLabel="dateLabel"
>
<div class="tedi-date-time-field-modal__time">
<div class="tedi-date-time-field-modal__time-header">
<span class="tedi-date-time-field-modal__heading">{{ heading }}</span>
@if (dateLabel) {
<span class="tedi-date-time-field-modal__date">{{ dateLabel }}</span>
}
</div>
@if (!slots?.length) {
<tedi-form-field
class="tedi-date-time-field-modal__native-time"
[size]="data.size"
>
<tedi-time-field
[inputId]="'dtf-modal-time-' + kind"
[useNativePicker]="true"
[clearable]="false"
[value]="time"
(valueChange)="handleRangeTimeSelect(kind, $event)"
/>
</tedi-form-field>
} @else {
<tedi-time-picker
[value]="time"
variant="slots"
[timeSlots]="slots"
[showSlotIndicator]="data.gridVariant === 'radio'"
[columns]="data.slotColumns"
[minuteStep]="data.minuteStep"
(valueChange)="handleRangeTimeSelect(kind, $event)"
/>
}
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
@use "@tedi-design-system/core/bootstrap-utility/breakpoints";

.tedi-date-time-field-modal {
--_tedi-modal-body-padding: 0;

// Calendar and time picker sit side-by-side on wider screens and stack (time
// below calendar) once the row no longer fits — the modal itself shrink-wraps
// this content (see `width: "fit-content"` in date-time-field.component.ts).
//
// `.tedi-modal-dialog .tedi-modal-content` (modal.component.scss) forces a
// column layout with a large gap; the selector below (ancestor + compound
// classes) outweighs it so the row layout and the tight, separator-driven
// spacing win.
.tedi-date-time-field-modal__content.tedi-modal-content {
flex-direction: row;
gap: 0;
align-items: stretch;

@include breakpoints.media-breakpoint-down(md) {
flex-direction: column;
align-items: center;
}
}

// Range: stack the calendar over the two ends. Side-mounting the two time
// pickers left the calendar with a tall empty gap and overflowed into a
// horizontal scroll on tablet, and the calendar↔time link was lost. Stacking
// keeps the ends grouped as a set beneath the shared calendar.
.tedi-date-time-field-modal__content--range.tedi-modal-content {
flex-direction: column;
align-items: center;
}

&__calendar {
flex-shrink: 0;
}

// The separator only makes sense while the calendar and time picker sit
// side-by-side. Once they stack, hide it — a horizontal line plus its padding
// just adds dead space between the two.
&__separator {
display: flex;
flex-shrink: 0;
align-items: stretch;
padding: var(--card-padding-md-default) var(--separator-spacing-x-01);

@include breakpoints.media-breakpoint-down(md) {
display: none;
}
}

&__separator-line {
width: var(--tedi-borders-01);
background: var(--general-separator-primary);
}

// The vertical separator belongs to the single side-by-side layout; range
// stacks vertically, so the two ends carry their own top divider instead.
&__content--range &__separator {
display: none;
}

&__times {
display: flex;
gap: var(--layout-grid-gutters-04);
justify-content: center;

@include breakpoints.media-breakpoint-down(md) {
flex-direction: column;
align-items: center;
width: 100%;
}
}

&__content--range &__times {
align-self: stretch;
padding-top: var(--card-padding-md-default);
border-top: var(--tedi-borders-01) solid var(--general-separator-primary);
}

// Groups the heading with its start/end date so they read as one label, and
// owns the gap down to the picker (same spacing whether or not a date shows).
&__time-header {
display: flex;
flex-direction: column;
align-self: stretch;
padding-top: var(--card-padding-md-default);
padding-bottom: var(--card-padding-xs);
}

&__time-header &__heading {
padding: 0;
}

// Echoes the start/end date under each heading so it stays clear which
// calendar end the picker below drives.
&__date {
font-size: var(--body-small-regular-size);
color: var(--general-text-secondary);
text-align: center;
}

&__time {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 var(--card-padding-md-default) var(--card-padding-md-default);
}

&__native-time {
width: 100%;
max-width: var(--tedi-containers-01);
}

&__heading {
align-self: stretch;
padding-top: var(--card-padding-md-default);
padding-bottom: var(--card-padding-xs);
font-size: var(--body-bold-size);
font-weight: var(--body-bold-weight);
line-height: var(--body-bold-line-height);
color: var(--general-text-primary);
text-align: center;
}

// See date-time-field.component.scss: re-assert the slot grid's `display: grid` (which
// tedi-radio-card-group's flex can override by injection order) and drop its own
// padding since `&__time` already provides it.
.tedi-time-picker__grid {
display: grid;
padding: 0;
}
}
Loading
Loading