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
27 changes: 27 additions & 0 deletions apps/v4/app/components/docs/examples/calendar-locale.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { nl } from 'date-fns/locale';

import { Calendar } from '@/components/ui/calendar';

import type { DateRange } from '@/components/ui/calendar';

export default class CalendarLocale extends Component {
locale = nl;

@tracked date: Date | undefined = new Date();

handleSelect = (date: Date | DateRange | undefined) => {
this.date = date as Date | undefined;
};

<template>
<Calendar
@class="rounded-md border shadow-sm"
@locale={{this.locale}}
@mode="single"
@onSelect={{this.handleSelect}}
@selected={{this.date}}
/>
</template>
}
1 change: 1 addition & 0 deletions apps/v4/app/components/docs/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export { default as CalendarBookedDates } from './calendar-booked-dates';
export { default as CalendarCaption } from './calendar-caption';
export { default as CalendarCustomDays } from './calendar-custom-days';
export { default as CalendarDemo } from './calendar-demo';
export { default as CalendarLocale } from './calendar-locale';
export { default as CalendarPresets } from './calendar-presets';
export { default as CalendarRange } from './calendar-range';
export { default as CalendarTime } from './calendar-time';
Expand Down
22 changes: 22 additions & 0 deletions apps/v4/app/content/docs/components/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ Use `@showWeekNumber` to show week numbers.

<ComponentPreview name="calendar-week-numbers" />

### Locale

Use `@locale` to localize the calendar. Pass a [date-fns locale](https://date-fns.org/docs/Locale) to translate month names, weekday labels, and set the first day of the week. Use `@weekStartsOn` to override the week start day.

```gts showLineNumbers
import { nl } from 'date-fns/locale';
```

```hbs showLineNumbers
<Calendar
@mode="single"
@locale={{this.nlLocale}}
@selected={{this.date}}
@onSelect={{this.setDate}}
@class="rounded-lg border"
/>
```

<ComponentPreview name="calendar-locale" />

## API Reference

| Argument | Type | Default | Description |
Expand All @@ -141,6 +161,8 @@ Use `@showWeekNumber` to show week numbers.
| `@showWeekNumber` | `boolean` | `false` | Show week numbers. |
| `@fixedWeeks` | `boolean` | `false` | Always show 6 weeks. |
| `@buttonVariant` | `'default' \| 'ghost' \| 'outline'` | `'ghost'` | Variant for nav buttons. |
| `@locale` | `Locale` | — | date-fns locale for i18n. |
| `@weekStartsOn` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6` | `0` | First day of the week (0=Sun). |
| `@startMonth` | `Date` | — | Earliest month navigable. |
| `@endMonth` | `Date` | — | Latest month navigable. |
| `@class` | `string` | — | Additional CSS classes. |
65 changes: 53 additions & 12 deletions apps/v4/registry/new-york-v4/ui/calendar.gts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';

import type Owner from '@ember/owner';
import type { Locale } from 'date-fns/locale';

import ChevronDown from '~icons/lucide/chevron-down';
import ChevronLeft from '~icons/lucide/chevron-left';
Expand Down Expand Up @@ -51,6 +52,8 @@ interface CalendarSignature {
fixedWeeks?: boolean;
class?: string;
buttonVariant?: 'default' | 'ghost' | 'outline';
locale?: Locale;
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
timeZone?: string;
startMonth?: Date;
endMonth?: Date;
Expand Down Expand Up @@ -88,8 +91,8 @@ interface MonthData {
weeks: WeekInfo[];
}

const WEEKDAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
const MONTH_NAMES_SHORT = [
const DEFAULT_WEEKDAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
const DEFAULT_MONTH_NAMES_SHORT = [
'Jan',
'Feb',
'Mar',
Expand Down Expand Up @@ -122,8 +125,8 @@ function selectedSingle(day: DayInfo): boolean {
);
}

function monthNameShort(index: number): string {
return MONTH_NAMES_SHORT[index] ?? '';
function monthNameShort(index: number, monthNames: string[]): string {
return monthNames[index] ?? '';
}

class Calendar extends Component<CalendarSignature> {
Expand Down Expand Up @@ -153,6 +156,42 @@ class Calendar extends Component<CalendarSignature> {
return this.args.buttonVariant ?? 'ghost';
}

get weekStartsOn(): 0 | 1 | 2 | 3 | 4 | 5 | 6 {
return (
this.args.weekStartsOn ?? this.args.locale?.options?.weekStartsOn ?? 0
);
}

get weekOptions(): { weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6 } {
return { weekStartsOn: this.weekStartsOn };
}

get weekdayLabels(): string[] {
if (this.args.locale) {
const refDate = startOfWeek(new Date(), this.weekOptions);
return Array.from({ length: 7 }, (_, i) =>
format(addDays(refDate, i), 'EEEEEE', {
locale: this.args.locale,
})
);
}
const wso = this.weekStartsOn;
if (wso === 0) return DEFAULT_WEEKDAY_LABELS;
return [
...DEFAULT_WEEKDAY_LABELS.slice(wso),
...DEFAULT_WEEKDAY_LABELS.slice(0, wso),
];
}

get monthNamesShort(): string[] {
if (this.args.locale) {
return Array.from({ length: 12 }, (_, i) =>
format(new Date(2024, i, 1), 'MMM', { locale: this.args.locale })
);
}
return DEFAULT_MONTH_NAMES_SHORT;
}

get displayMonth(): Date {
if (this.args.month) {
return startOfMonth(this.args.month);
Expand Down Expand Up @@ -190,7 +229,7 @@ class Calendar extends Component<CalendarSignature> {
}

get monthOptions(): { value: number; label: string }[] {
return MONTH_NAMES_SHORT.map((label, i) => ({ value: i, label }));
return this.monthNamesShort.map((label, i) => ({ value: i, label }));
}

get months(): MonthData[] {
Expand All @@ -217,8 +256,8 @@ class Calendar extends Component<CalendarSignature> {
buildMonthData(monthDate: Date): MonthData {
const monthStart = startOfMonth(monthDate);
const monthEnd = endOfMonth(monthDate);
const calendarStart = startOfWeek(monthStart);
const calendarEnd = endOfWeek(monthEnd);
const calendarStart = startOfWeek(monthStart, this.weekOptions);
const calendarEnd = endOfWeek(monthEnd, this.weekOptions);

const allDays = eachDayOfInterval({
start: calendarStart,
Expand Down Expand Up @@ -262,16 +301,18 @@ class Calendar extends Component<CalendarSignature> {
});

weeks.push({
weekNumber: getWeek(weekDays[0]!),
weekNumber: getWeek(weekDays[0]!, this.weekOptions),
days,
});
}

return {
month: monthDate.getMonth(),
year: monthDate.getFullYear(),
label: format(monthDate, 'MMMM yyyy'),
labelShort: format(monthDate, 'MMM'),
label: format(monthDate, 'MMMM yyyy', {
locale: this.args.locale,
}),
labelShort: format(monthDate, 'MMM', { locale: this.args.locale }),
weeks,
};
}
Expand Down Expand Up @@ -471,7 +512,7 @@ class Calendar extends Component<CalendarSignature> {
<span
class="flex items-center gap-1 rounded-(--cell-radius) text-sm"
>
{{monthNameShort monthData.month}}
{{monthNameShort monthData.month this.monthNamesShort}}
<ChevronDown class="size-3.5 text-muted-foreground" />
</span>
<select
Expand Down Expand Up @@ -527,7 +568,7 @@ class Calendar extends Component<CalendarSignature> {
<span class="sr-only">Week number</span>
</th>
{{/if}}
{{#each WEEKDAY_LABELS as |label|}}
{{#each this.weekdayLabels as |label|}}
<th
class="flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none"
scope="col"
Expand Down
Loading