Skip to content
Merged
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
20 changes: 20 additions & 0 deletions packages/frontile/src/components/collections/calendar/calendar.gts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import type { CalendarSlots, CalendarVariants } from '@frontile/theme';
import type {
CalendarDay,
CalendarLabels,
CalendarMode,
CalendarMonthData,
CalendarValue,
Expand Down Expand Up @@ -195,8 +196,21 @@ export interface CalendarArgs<M extends CalendarMode = 'single'> {
* @defaultValue 'label'
*/
captionLayout?: 'label' | 'dropdown';

/**
* Replaces any of the texts the calendar renders, e.g. to translate them.
* Anything left out keeps its English default; see `CalendarLabels`.
*/
labels?: Partial<CalendarLabels>;
}

const DEFAULT_LABELS: CalendarLabels = {
previousMonth: 'Previous month',
nextMonth: 'Next month',
month: 'Month',
year: 'Year'
};

export interface CalendarSignature<M extends CalendarMode = 'single'> {
Args: CalendarArgs<M>;
Blocks: {
Expand Down Expand Up @@ -234,6 +248,10 @@ class Calendar<M extends CalendarMode = 'single'> extends Component<
return this.args.captionLayout ?? 'label';
}

get labels(): CalendarLabels {
return { ...DEFAULT_LABELS, ...this.args.labels };
}

// The autofocus below is deferred a frame; `deferredWork` holds the
// test-waiter token so `settled()` does not race it, and cancels cleanly on
// teardown.
Expand Down Expand Up @@ -1097,6 +1115,7 @@ class Calendar<M extends CalendarMode = 'single'> extends Component<
@months={{this.monthOptions}}
@isYearGridOpen={{this.isYearGridOpen}}
@onToggleYearGrid={{this.toggleYearGrid}}
@labels={{this.labels}}
@classes={{this.headerClasses}}
/>
{{/if}}
Expand All @@ -1113,6 +1132,7 @@ class Calendar<M extends CalendarMode = 'single'> extends Component<
@currentYear={{this.visibleYear}}
@onSelect={{this.pickYear}}
@onDismiss={{this.dismissYearGrid}}
@label={{this.labels.year}}
@classes={{this.yearGridClasses}}
/>
{{else}}
Expand Down
25 changes: 25 additions & 0 deletions packages/frontile/src/components/collections/calendar/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,31 @@ import { Calendar } from 'frontile/collections';
<template><Calendar @locale='nl-NL' @weekStartsOn={{1}} /></template>
```

## Labels

`@locale` covers everything `Intl` can format, but the accessible names of the
previous/next buttons, and of the month `<select>` and year grid under
`@captionLayout="dropdown"`, are English by default. `@labels` replaces any of
them, e.g. to translate them; anything you leave out keeps its default.

```gts preview
import { hash } from '@ember/helper';
import { Calendar } from 'frontile/collections';

<template>
<Calendar
@locale='cs-CZ'
@captionLayout='dropdown'
@labels={{hash
previousMonth='Předchozí měsíc'
nextMonth='Další měsíc'
month='Měsíc'
year='Rok'
}}
/>
</template>
```

## Disabled and read-only

`@isDisabled` blocks navigation and selection entirely. `@isReadOnly` still allows paging
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ChevronLeftIcon,
ChevronRightIcon
} from '../../../-private/icons';
import type { CalendarLabels } from './types';

export interface CalendarHeaderContext {
month: Date;
Expand All @@ -26,6 +27,7 @@ export interface CalendarHeaderSignature {
months: { value: number; label: string; isDisabled: boolean }[];
isYearGridOpen: boolean;
onToggleYearGrid: () => void;
labels: Pick<CalendarLabels, 'previousMonth' | 'nextMonth' | 'month'>;
classes: {
header: string;
title: string;
Expand Down Expand Up @@ -81,7 +83,7 @@ export default class CalendarHeader extends Component<CalendarHeaderSignature> {
class={{@classes.navButton}}
disabled={{if @context.canGoPrevious false true}}
data-disabled={{if @context.canGoPrevious "false" "true"}}
aria-label="Previous month"
aria-label={{@labels.previousMonth}}
{{on "click" @context.goToPrevious}}
><ChevronLeftIcon /></button>

Expand All @@ -107,7 +109,7 @@ export default class CalendarHeader extends Component<CalendarHeaderSignature> {

<select
data-part="month-select"
aria-label="Month"
aria-label={{@labels.month}}
class={{@classes.monthSelect}}
{{on "change" this.onMonthSelect}}
>
Expand Down Expand Up @@ -141,7 +143,7 @@ export default class CalendarHeader extends Component<CalendarHeaderSignature> {
class={{@classes.navButton}}
disabled={{if @context.canGoNext false true}}
data-disabled={{if @context.canGoNext "false" "true"}}
aria-label="Next month"
aria-label={{@labels.nextMonth}}
{{on "click" @context.goToNext}}
><ChevronRightIcon /></button>
</div>
Expand Down
16 changes: 16 additions & 0 deletions packages/frontile/src/components/collections/calendar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,19 @@ export interface DayCellClasses {
dayContent: string;
indicator: string;
}

/**
* Every text the calendar renders itself, beyond what `Intl` produces from
* `@locale`. Override any of them with `@labels`, e.g. to translate them; the
* rest keep these English defaults.
*/
export interface CalendarLabels {
/** Accessible name of the previous-month button. */
previousMonth: string;
/** Accessible name of the next-month button. */
nextMonth: string;
/** Accessible name of the month `<select>` (`@captionLayout="dropdown"`). */
month: string;
/** Accessible name of the year grid (`@captionLayout="dropdown"`). */
year: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface YearGridSignature {
currentYear: number;
onSelect: (year: number) => void;
onDismiss: () => void;
/** The grid's accessible name. */
label: string;
classes: { yearGrid: string; yearCell: string };
};
Element: HTMLDivElement;
Expand Down Expand Up @@ -167,7 +169,7 @@ export default class YearGrid extends Component<YearGridSignature> {
plain, non-focusable listbox role. }}
<div
role="listbox"
aria-label="Year"
aria-label={{@label}}
data-part="year-grid"
class={{@classes.yearGrid}}
{{this.setup}}
Expand Down
24 changes: 24 additions & 0 deletions packages/frontile/src/components/forms/date-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,30 @@ const value = new Date(2026, 0, 20);
</template>
```

## Labels

The segments' accessible names are `@segmentLabels`. The only other text the
field renders, the clear button's accessible name, is `@labels.clear`; it
defaults to "Clear".

```gts preview
import { hash } from '@ember/helper';
import { DateInput } from 'frontile';

const value = new Date(2026, 0, 20);

<template>
<DateInput
@label='Začátek'
@locale='cs-CZ'
@defaultValue={{value}}
@isClearable={{true}}
@segmentLabels={{hash year='rok' month='měsíc' day='den'}}
@labels={{hash clear='Vymazat'}}
/>
</template>
```

## Sizes

```gts preview
Expand Down
11 changes: 10 additions & 1 deletion packages/frontile/src/components/forms/date-input/date-input.gts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ import type {
Part,
DateInputArgs,
DateInputClasses,
DateInputLabels,
TimeOptions
} from './types';

const DEFAULT_LABELS: DateInputLabels = {
clear: 'Clear'
};

interface DateInputSignature {
Args: DateInputArgs;
Element: HTMLDivElement;
Expand Down Expand Up @@ -213,6 +218,10 @@ class DateInput extends Component<DateInputSignature> {
this.args.onBlur?.();
};

get labels(): DateInputLabels {
return { ...DEFAULT_LABELS, ...this.args.labels };
}

/**
* A disabled or read-only field never offers it: it carries no disabled
* state of its own and would be a live button clearing a field the user may
Expand Down Expand Up @@ -301,7 +310,7 @@ class DateInput extends Component<DateInputSignature> {
class={{this.classes.endContent class=@classes.endContent}}
>
<CloseButton
@title="Clear"
@title={{this.labels.clear}}
@variant="soft"
@size="xs"
@class={{this.classes.clearButton class=@classes.clearButton}}
Expand Down
17 changes: 17 additions & 0 deletions packages/frontile/src/components/forms/date-input/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ interface LiteralPart {

type Part = Segment | LiteralPart;

/**
* Every text the field renders itself. Override any of them with `@labels`,
* e.g. to translate them; the rest keep these English defaults. The
* segments' names are `@segmentLabels`.
*/
interface DateInputLabels {
/** Accessible name of the clear button (`@isClearable`). */
clear: string;
}

interface DateInputArgs extends FormControlSharedArgs {
/** The unique identifier for the control. */
id?: string;
Expand Down Expand Up @@ -201,11 +211,18 @@ interface DateInputArgs extends FormControlSharedArgs {
*/
segmentLabels?: Partial<Record<SegmentType, string>>;

/**
* Replaces any of the texts the field renders, e.g. to translate them.
* Anything left out keeps its English default; see `DateInputLabels`.
*/
labels?: Partial<DateInputLabels>;

/** Per-slot class overrides. */
classes?: SlotsToClasses<DateInputSlots>;
}

export type {
DateInputLabels,
SegmentType,
Granularity,
HourCycle,
Expand Down
50 changes: 48 additions & 2 deletions packages/frontile/src/components/forms/date-picker.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,52 @@ const defaultValue = new Date(2026, 0, 20);
</template>
```

## Labels

Every text the picker renders itself is English by default: the clear and
calendar buttons' accessible names, and those of its calendar's controls.
`@labels` replaces any of them, e.g. to translate them; anything you leave out
keeps its default. The calendar's are handed on to it, including to a calendar
you render from the `:calendar` block. `chooseDate` is a function of the
field's `@label`, since some languages put it elsewhere in the sentence. The
segments' names are `@segmentLabels`.

```gts preview collapsible
import { hash } from '@ember/helper';
import { DatePicker } from 'frontile';

const defaultValue = new Date(2026, 0, 20, 9, 30);
const chooseDate = (label?: string) =>
label ? `Vybrat datum, ${label}` : 'Vybrat datum';

<template>
<DatePicker
@label='Začátek'
@locale='cs-CZ'
@granularity='minute'
@hourCycle={{24}}
@captionLayout='dropdown'
@defaultValue={{defaultValue}}
@isClearable={{true}}
@segmentLabels={{hash
year='rok'
month='měsíc'
day='den'
hour='hodina'
minute='minuta'
}}
@labels={{hash
clear='Vymazat'
chooseDate=chooseDate
previousMonth='Předchozí měsíc'
nextMonth='Další měsíc'
month='Měsíc'
year='Rok'
}}
/>
</template>
```

## Forms

Inside a `<Form>`, a `DatePicker` given `@name` submits one field: the wire value is the same
Expand Down Expand Up @@ -535,11 +581,11 @@ and `calendarButton`.
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Segment | `role="spinbutton"` with `aria-label` (`month`, `day`, `year`, overridable through `@segmentLabels`), `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and `aria-valuetext` — which is what makes a month announce as "January" rather than "1". Carries the field's `aria-invalid` and `aria-readonly`. |
| Segment group | `role="group"`, named by `@label` and described by `@description` and any feedback. In range mode there are two, named `"<label> start"` and `"<label> end"`. |
| Calendar button | A `<button>` labelled `"Choose date, <label>"`, carrying the `aria-haspopup`, `aria-expanded` and `aria-controls` the popover trigger applies. |
| Calendar button | A `<button>` labelled `"Choose date, <label>"` (`@labels.chooseDate`), carrying the `aria-haspopup`, `aria-expanded` and `aria-controls` the popover trigger applies. |
| Trigger | Under `@isEditable={{false}}`, a `<button>` carrying the same popover trigger attributes. When a `:value` block is supplied, it also carries an explicit `aria-label` composed from `@label` and the formatted value, since the block's content may not be readable text on its own. |
| Popover content | `role="dialog"`, labeled by `@label`. |
| Calendar grid | Labeled by the field's own id, so the default calendar is always labelled. A consumer rendering their own calendar from the `:calendar` block must pass `@id` to `DatePicker` for the grid to be labelled — the block's `labelledBy` reflects `@id`, not the id `FormControl` would otherwise generate. |
| Clear button | Announced as "Clear". |
| Clear button | Announced as "Clear" (`@labels.clear`). |

The segments take the same keys as [DateInput](./date-input#accessibility), and every one
of them is a tab stop — three for a single field, six for a range.
Expand Down
Loading
Loading