Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Calendar): Add firstDayOfWeek prop #7363

Merged
merged 41 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8ecbca1
add firstDayOfWeek prop
reidbarber Nov 13, 2024
3819daa
pass to DatePicker/DateRangePicker
reidbarber Nov 13, 2024
4675901
add storybook controls
reidbarber Nov 13, 2024
b84969e
add to RAC
reidbarber Nov 13, 2024
afa1c0f
add docs
reidbarber Nov 13, 2024
509257c
add v3 tests
reidbarber Nov 13, 2024
b03d491
add hook tests
reidbarber Nov 13, 2024
68b9f48
lint
reidbarber Nov 13, 2024
c9f80eb
add chromatic stories
reidbarber Nov 13, 2024
15a047a
update v3 RangeCalendar styles for first and last day of week
reidbarber Nov 14, 2024
c4f7e5f
Merge remote-tracking branch 'origin/main' into calendar-firstDayOfWeek
reidbarber Nov 14, 2024
4020fb2
lint
reidbarber Nov 14, 2024
ab374a8
lint
reidbarber Nov 14, 2024
b554178
use number instead of enum for type
reidbarber Nov 14, 2024
237cdfb
update type to enum
reidbarber Nov 15, 2024
11c86e3
update tests
reidbarber Nov 15, 2024
c16d42e
fix v3 cell style
reidbarber Nov 15, 2024
b7f7185
use lowercase and update docs
reidbarber Nov 16, 2024
e4a342b
Merge remote-tracking branch 'origin/main' into calendar-firstDayOfWeek
reidbarber Nov 16, 2024
158341e
update to use firstDayOfWeek day regardless of locale
reidbarber Nov 27, 2024
363b9be
lint
reidbarber Nov 27, 2024
94241a4
fix cell style
reidbarber Nov 27, 2024
37b329a
more tests
reidbarber Nov 27, 2024
0f48109
update test
reidbarber Dec 6, 2024
60b00a3
update getDatesInWeek logic
reidbarber Dec 6, 2024
4e373d7
Merge remote-tracking branch 'origin/main' into calendar-firstDayOfWeek
reidbarber Dec 6, 2024
6d255c0
Merge remote-tracking branch 'origin/main' into calendar-firstDayOfWeek
reidbarber Dec 18, 2024
932fac8
fix test
reidbarber Dec 20, 2024
cdfc45d
fix null dates
reidbarber Dec 20, 2024
48c3dc0
fix minimum dates case
reidbarber Dec 20, 2024
d9f6bd1
Merge remote-tracking branch 'origin/main' into calendar-firstDayOfWeek
reidbarber Dec 20, 2024
80409f7
add offset logic to startOfWeek
reidbarber Dec 23, 2024
38c0ed2
lint
reidbarber Dec 23, 2024
a6bf5a1
pass firstDayOfWeek into getWeeksInMonth
reidbarber Jan 6, 2025
ad1a352
Merge remote-tracking branch 'origin/main' into calendar-firstDayOfWeek
reidbarber Jan 6, 2025
22d398f
add firstDayOfWeek param to endOfWeek
reidbarber Jan 8, 2025
779505b
Merge remote-tracking branch 'origin/main' into calendar-firstDayOfWeek
reidbarber Jan 8, 2025
6d061df
add firstDayOfWeek to getDayOfWeek
reidbarber Jan 8, 2025
34a3442
pass firstDayOfWeek to getDayOfWeek
reidbarber Jan 8, 2025
e631c51
slight simplification and add tests
devongovett Jan 9, 2025
32f9e25
add docs
devongovett Jan 9, 2025
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
18 changes: 18 additions & 0 deletions packages/@internationalized/date/docs/CalendarDate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ startOfWeek(date, 'en-US'); // 2022-01-30
startOfWeek(date, 'fr-FR'); // 2022-01-31
```

You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc.

```tsx
startOfWeek(date, 'en-US', 'mon'); // 2022-01-31
```

### Day of week

The <TypeLink links={docs.links} type={docs.exports.getDayOfWeek} /> function returns the day of the week for the given date and locale. Days are numbered from zero to six, where zero is the first day of the week in the given locale. For example, in the United States, the first day of the week is Sunday, but in France it is Monday.
Expand All @@ -328,6 +334,12 @@ getDayOfWeek(date, 'en-US'); // 0
getDayOfWeek(date, 'fr-FR'); // 6
```

You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc.

```tsx
getDayOfWeek(date, 'en-US', 'mon'); // 6
```

### Weekdays and weekends

The <TypeLink links={docs.links} type={docs.exports.isWeekday} /> and <TypeLink links={docs.links} type={docs.exports.isWeekend} /> functions can be used to determine if a date is weekday or weekend respectively. This depends on the locale. For example, in the United States, weekends are Saturday and Sunday, but in Israel they are Friday and Saturday.
Expand Down Expand Up @@ -356,3 +368,9 @@ let date = new CalendarDate(2021, 1, 1);
getWeeksInMonth(date, 'en-US'); // 6
getWeeksInMonth(date, 'fr-FR'); // 5
```

You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc.

```tsx
getWeeksInMonth(date, 'en-US', 'mon'); // 5
```
18 changes: 18 additions & 0 deletions packages/@internationalized/date/docs/CalendarDateTime.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@ startOfWeek(date, 'en-US'); // 2022-01-30T09:45
startOfWeek(date, 'fr-FR'); // 2022-01-31T09:45
```

You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc.

```tsx
startOfWeek(date, 'en-US', 'mon'); // 2022-01-31T09:45
```

### Day of week

The <TypeLink links={docs.links} type={docs.exports.getDayOfWeek} /> function returns the day of the week for the given date and locale. Days are numbered from zero to six, where zero is the first day of the week in the given locale. For example, in the United States, the first day of the week is Sunday, but in France it is Monday.
Expand All @@ -380,6 +386,12 @@ getDayOfWeek(date, 'en-US'); // 0
getDayOfWeek(date, 'fr-FR'); // 6
```

You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc.

```tsx
getDayOfWeek(date, 'en-US', 'mon'); // 6
```

### Weekdays and weekends

The <TypeLink links={docs.links} type={docs.exports.isWeekday} /> and <TypeLink links={docs.links} type={docs.exports.isWeekend} /> functions can be used to determine if a date is weekday or weekend respectively. This depends on the locale. For example, in the United States, weekends are Saturday and Sunday, but in Israel they are Friday and Saturday.
Expand Down Expand Up @@ -408,3 +420,9 @@ let date = new CalendarDateTime(2021, 1, 1, 8, 30);
getWeeksInMonth(date, 'en-US'); // 6
getWeeksInMonth(date, 'fr-FR'); // 5
```

You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc.

```tsx
getWeeksInMonth(date, 'en-US', 'mon'); // 5
```
18 changes: 18 additions & 0 deletions packages/@internationalized/date/docs/ZonedDateTime.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,12 @@ startOfWeek(date, 'en-US'); // 2022-01-30T09:45[America/Los_Angeles]
startOfWeek(date, 'fr-FR'); // 2022-01-31T09:45[America/Los_Angeles]
```

You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc.

```tsx
startOfWeek(date, 'en-US', 'mon'); // 2022-01-31T09:45[America/Los_Angeles]
```

### Day of week

The <TypeLink links={docs.links} type={docs.exports.getDayOfWeek} /> function returns the day of the week for the given date and locale. Days are numbered from zero to six, where zero is the first day of the week in the given locale. For example, in the United States, the first day of the week is Sunday, but in France it is Monday.
Expand All @@ -488,6 +494,12 @@ getDayOfWeek(date, 'en-US'); // 0
getDayOfWeek(locale, 'fr-FR'); // 6
```

You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc.

```tsx
getDayOfWeek(date, 'en-US', 'mon'); // 6
```

### Weekdays and weekends

The <TypeLink links={docs.links} type={docs.exports.isWeekday} /> and <TypeLink links={docs.links} type={docs.exports.isWeekend} /> functions can be used to determine if a date is weekday or weekend respectively. This depends on the locale. For example, in the United States, weekends are Saturday and Sunday, but in Israel they are Friday and Saturday.
Expand Down Expand Up @@ -516,3 +528,9 @@ let date = parseZonedDateTime('2023-01-01T08:30[America/Los_Angeles]');
getWeeksInMonth(date, 'en-US'); // 5
getWeeksInMonth(date, 'fr-FR'); // 6
```

You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc.

```tsx
getWeeksInMonth(date, 'en-US', 'mon'); // 6
```
45 changes: 29 additions & 16 deletions packages/@internationalized/date/src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,30 @@ export function isToday(date: DateValue, timeZone: string): boolean {
return isSameDay(date, today(timeZone));
}

const DAY_MAP = {
sun: 0,
mon: 1,
tue: 2,
wed: 3,
thu: 4,
fri: 5,
sat: 6
};

type DayOfWeek = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat';

/**
* Returns the day of week for the given date and locale. Days are numbered from zero to six,
* where zero is the first day of the week in the given locale. For example, in the United States,
* the first day of the week is Sunday, but in France it is Monday.
*/
export function getDayOfWeek(date: DateValue, locale: string): number {
export function getDayOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number {
let julian = date.calendar.toJulianDay(date);

// If julian is negative, then julian % 7 will be negative, so we adjust
// accordingly. Julian day 0 is Monday.
let dayOfWeek = Math.ceil(julian + 1 - getWeekStart(locale)) % 7;
let weekStart = firstDayOfWeek ? DAY_MAP[firstDayOfWeek] : getWeekStart(locale);
let dayOfWeek = Math.ceil(julian + 1 - weekStart) % 7;
if (dayOfWeek < 0) {
dayOfWeek += 7;
}
Expand Down Expand Up @@ -181,22 +194,22 @@ export function getMinimumDayInMonth(date: AnyCalendarDate) {
}

/** Returns the first date of the week for the given date and locale. */
export function startOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime;
export function startOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime;
export function startOfWeek(date: CalendarDate, locale: string): CalendarDate;
export function startOfWeek(date: DateValue, locale: string): DateValue;
export function startOfWeek(date: DateValue, locale: string): DateValue {
let dayOfWeek = getDayOfWeek(date, locale);
export function startOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime;
export function startOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime;
export function startOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate;
export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue;
export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue {
let dayOfWeek = getDayOfWeek(date, locale, firstDayOfWeek);
return date.subtract({days: dayOfWeek});
}

/** Returns the last date of the week for the given date and locale. */
export function endOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime;
export function endOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime;
export function endOfWeek(date: CalendarDate, locale: string): CalendarDate;
export function endOfWeek(date: DateValue, locale: string): DateValue;
export function endOfWeek(date: DateValue, locale: string): DateValue {
return startOfWeek(date, locale).add({days: 6});
export function endOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime;
export function endOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime;
export function endOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate;
export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue;
export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue {
return startOfWeek(date, locale, firstDayOfWeek).add({days: 6});
}

const cachedRegions = new Map<string, string>();
Expand Down Expand Up @@ -233,9 +246,9 @@ function getWeekStart(locale: string): number {
}

/** Returns the number of weeks in the given month and locale. */
export function getWeeksInMonth(date: DateValue, locale: string): number {
export function getWeeksInMonth(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number {
let days = date.calendar.getDaysInMonth(date);
return Math.ceil((getDayOfWeek(startOfMonth(date), locale) + days) / 7);
return Math.ceil((getDayOfWeek(startOfMonth(date), locale, firstDayOfWeek) + days) / 7);
}

/** Returns the lesser of the two provider dates. */
Expand Down
30 changes: 28 additions & 2 deletions packages/@internationalized/date/tests/queries.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ describe('queries', function () {
it('should return the day of week in fr', function () {
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'fr')).toBe(2);
});

it('should return the day of the week with a custom firstDayOfWeek', function () {
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toBe(2);
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'tue')).toBe(1);
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'mon')).toBe(2);
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'tue')).toBe(1);
});
});

describe('startOfWeek', function () {
Expand All @@ -256,16 +263,28 @@ describe('queries', function () {
it('should return the start of week in fr-FR', function () {
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR')).toEqual(new CalendarDate(2021, 8, 2));
});

it('should return the start of the week with a custom firstDayOfWeek', function () {
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toEqual(new CalendarDate(2021, 8, 2));
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'tue')).toEqual(new CalendarDate(2021, 8, 3));
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'sun')).toEqual(new CalendarDate(2021, 8, 1));
});
});

describe('endOfWeek', function () {
it('should return the start of week in en-US', function () {
it('should return the end of week in en-US', function () {
expect(endOfWeek(new CalendarDate(2021, 8, 4), 'en-US')).toEqual(new CalendarDate(2021, 8, 7));
});

it('should return the start of week in fr-FR', function () {
it('should return the end of week in fr-FR', function () {
expect(endOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR')).toEqual(new CalendarDate(2021, 8, 8));
});

it('should return the end of the week with a custom firstDayOfWeek', function () {
expect(endOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toEqual(new CalendarDate(2021, 8, 8));
expect(endOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'tue')).toEqual(new CalendarDate(2021, 8, 9));
expect(endOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'sun')).toEqual(new CalendarDate(2021, 8, 7));
});
});

describe('getWeeksInMonth', function () {
Expand All @@ -280,6 +299,13 @@ describe('queries', function () {
it('should work for other calendars', function () {
expect(getWeeksInMonth(new CalendarDate(new EthiopicCalendar(), 2013, 13, 4), 'en-US')).toBe(1);
});

it('should support custom firstDayOfWeek', function () {
expect(getWeeksInMonth(new CalendarDate(2021, 8, 4), 'en-US', 'sun')).toBe(5);
expect(getWeeksInMonth(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toBe(6);
expect(getWeeksInMonth(new CalendarDate(2021, 10, 4), 'en-US', 'sun')).toBe(6);
expect(getWeeksInMonth(new CalendarDate(2021, 10, 4), 'en-US', 'mon')).toBe(5);
});
});

describe('getMinimumMonthInYear', function () {
Expand Down
12 changes: 10 additions & 2 deletions packages/@react-aria/calendar/docs/useCalendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function Calendar(props) {
<Button {...prevButtonProps}>&lt;</Button>
<Button {...nextButtonProps}>&gt;</Button>
</div>
<CalendarGrid state={state} />
<CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} />
</div>
);
}
Expand All @@ -152,7 +152,7 @@ function CalendarGrid({state, ...props}) {
let {gridProps, headerProps, weekDays} = useCalendarGrid(props, state);

// Get the number of weeks in the month so we can render the proper number of rows.
let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale);
let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale, props.firstDayOfWeek);

return (
<table {...gridProps}>
Expand Down Expand Up @@ -458,6 +458,14 @@ The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDi
<Calendar aria-label="Event date" value={today(getLocalTimeZone())} isReadOnly />
```

### Custom first day of week

By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`.

```tsx example
<Calendar aria-label="Event date" value={today(getLocalTimeZone())} firstDayOfWeek="mon" />
```

### Labeling

An aria-label must be provided to the `Calendar` for accessibility. If it is labeled by a separate element, an `aria-labelledby` prop must be provided using the `id` of the labeling element instead.
Expand Down
12 changes: 10 additions & 2 deletions packages/@react-aria/calendar/docs/useRangeCalendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function RangeCalendar(props) {
<Button {...prevButtonProps}>&lt;</Button>
<Button {...nextButtonProps}>&gt;</Button>
</div>
<CalendarGrid state={state} />
<CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} />
</div>
);
}
Expand All @@ -152,7 +152,7 @@ function CalendarGrid({state, ...props}) {
let {gridProps, headerProps, weekDays} = useCalendarGrid(props, state);

// Get the number of weeks in the month so we can render the proper number of rows.
let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale);
let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale, props.firstDayOfWeek);

return (
<table {...gridProps}>
Expand Down Expand Up @@ -477,6 +477,14 @@ The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike
<RangeCalendar aria-label="Trip dates" value={{start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1 })}} isReadOnly />
```

### Custom first day of week

By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`.

```tsx example
<RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon" />
```

### Labeling

An aria-label must be provided to the `RangeCalendar` for accessibility. If it is labeled by a separate element, an `aria-labelledby` prop must be provided using the `id` of the labeling element instead.
Expand Down
13 changes: 9 additions & 4 deletions packages/@react-aria/calendar/src/useCalendarGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ export interface AriaCalendarGridProps {
* e.g. single letter, abbreviation, or full day name.
* @default "narrow"
*/
weekdayStyle?: 'narrow' | 'short' | 'long'
weekdayStyle?: 'narrow' | 'short' | 'long',
/**
* The day that starts the week.
*/
firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
}

export interface CalendarGridAria {
Expand All @@ -56,7 +60,8 @@ export interface CalendarGridAria {
export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria {
let {
startDate = state.visibleRange.start,
endDate = state.visibleRange.end
endDate = state.visibleRange.end,
firstDayOfWeek
} = props;

let {direction} = useLocale();
Expand Down Expand Up @@ -137,13 +142,13 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta
let dayFormatter = useDateFormatter({weekday: props.weekdayStyle || 'narrow', timeZone: state.timeZone});
let {locale} = useLocale();
let weekDays = useMemo(() => {
let weekStart = startOfWeek(today(state.timeZone), locale);
let weekStart = startOfWeek(today(state.timeZone), locale, firstDayOfWeek);
return [...new Array(7).keys()].map((index) => {
let date = weekStart.add({days: index});
let dateDay = date.toDate(state.timeZone);
return dayFormatter.format(dateDay);
});
}, [locale, state.timeZone, dayFormatter]);
}, [locale, state.timeZone, dayFormatter, firstDayOfWeek]);

return {
gridProps: mergeProps(labelProps, {
Expand Down
Loading
Loading