Skip to content

Commit 82a4de5

Browse files
feat(Calendar): Add firstDayOfWeek prop (#7363)
* add firstDayOfWeek prop * pass to DatePicker/DateRangePicker * add storybook controls * add to RAC * add docs * add v3 tests * add hook tests * lint * add chromatic stories * update v3 RangeCalendar styles for first and last day of week * lint * lint * use number instead of enum for type * update type to enum * update tests * fix v3 cell style * use lowercase and update docs * update to use firstDayOfWeek day regardless of locale * lint * fix cell style * more tests * update test * update getDatesInWeek logic * fix test * fix null dates * fix minimum dates case * add offset logic to startOfWeek * lint * pass firstDayOfWeek into getWeeksInMonth * add firstDayOfWeek param to endOfWeek * add firstDayOfWeek to getDayOfWeek * pass firstDayOfWeek to getDayOfWeek * slight simplification and add tests * add docs --------- Co-authored-by: Devon Govett <[email protected]>
1 parent d87cc44 commit 82a4de5

35 files changed

+484
-58
lines changed

packages/@internationalized/date/docs/CalendarDate.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,12 @@ startOfWeek(date, 'en-US'); // 2022-01-30
315315
startOfWeek(date, 'fr-FR'); // 2022-01-31
316316
```
317317

318+
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.
319+
320+
```tsx
321+
startOfWeek(date, 'en-US', 'mon'); // 2022-01-31
322+
```
323+
318324
### Day of week
319325

320326
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.
@@ -328,6 +334,12 @@ getDayOfWeek(date, 'en-US'); // 0
328334
getDayOfWeek(date, 'fr-FR'); // 6
329335
```
330336

337+
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.
338+
339+
```tsx
340+
getDayOfWeek(date, 'en-US', 'mon'); // 6
341+
```
342+
331343
### Weekdays and weekends
332344

333345
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.
@@ -356,3 +368,9 @@ let date = new CalendarDate(2021, 1, 1);
356368
getWeeksInMonth(date, 'en-US'); // 6
357369
getWeeksInMonth(date, 'fr-FR'); // 5
358370
```
371+
372+
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.
373+
374+
```tsx
375+
getWeeksInMonth(date, 'en-US', 'mon'); // 5
376+
```

packages/@internationalized/date/docs/CalendarDateTime.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,12 @@ startOfWeek(date, 'en-US'); // 2022-01-30T09:45
367367
startOfWeek(date, 'fr-FR'); // 2022-01-31T09:45
368368
```
369369

370+
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.
371+
372+
```tsx
373+
startOfWeek(date, 'en-US', 'mon'); // 2022-01-31T09:45
374+
```
375+
370376
### Day of week
371377

372378
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.
@@ -380,6 +386,12 @@ getDayOfWeek(date, 'en-US'); // 0
380386
getDayOfWeek(date, 'fr-FR'); // 6
381387
```
382388

389+
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.
390+
391+
```tsx
392+
getDayOfWeek(date, 'en-US', 'mon'); // 6
393+
```
394+
383395
### Weekdays and weekends
384396

385397
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.
@@ -408,3 +420,9 @@ let date = new CalendarDateTime(2021, 1, 1, 8, 30);
408420
getWeeksInMonth(date, 'en-US'); // 6
409421
getWeeksInMonth(date, 'fr-FR'); // 5
410422
```
423+
424+
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.
425+
426+
```tsx
427+
getWeeksInMonth(date, 'en-US', 'mon'); // 5
428+
```

packages/@internationalized/date/docs/ZonedDateTime.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,12 @@ startOfWeek(date, 'en-US'); // 2022-01-30T09:45[America/Los_Angeles]
475475
startOfWeek(date, 'fr-FR'); // 2022-01-31T09:45[America/Los_Angeles]
476476
```
477477

478+
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.
479+
480+
```tsx
481+
startOfWeek(date, 'en-US', 'mon'); // 2022-01-31T09:45[America/Los_Angeles]
482+
```
483+
478484
### Day of week
479485

480486
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.
@@ -488,6 +494,12 @@ getDayOfWeek(date, 'en-US'); // 0
488494
getDayOfWeek(locale, 'fr-FR'); // 6
489495
```
490496

497+
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.
498+
499+
```tsx
500+
getDayOfWeek(date, 'en-US', 'mon'); // 6
501+
```
502+
491503
### Weekdays and weekends
492504

493505
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.
@@ -516,3 +528,9 @@ let date = parseZonedDateTime('2023-01-01T08:30[America/Los_Angeles]');
516528
getWeeksInMonth(date, 'en-US'); // 5
517529
getWeeksInMonth(date, 'fr-FR'); // 6
518530
```
531+
532+
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.
533+
534+
```tsx
535+
getWeeksInMonth(date, 'en-US', 'mon'); // 6
536+
```

packages/@internationalized/date/src/queries.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,30 @@ export function isToday(date: DateValue, timeZone: string): boolean {
6464
return isSameDay(date, today(timeZone));
6565
}
6666

67+
const DAY_MAP = {
68+
sun: 0,
69+
mon: 1,
70+
tue: 2,
71+
wed: 3,
72+
thu: 4,
73+
fri: 5,
74+
sat: 6
75+
};
76+
77+
type DayOfWeek = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat';
78+
6779
/**
6880
* Returns the day of week for the given date and locale. Days are numbered from zero to six,
6981
* where zero is the first day of the week in the given locale. For example, in the United States,
7082
* the first day of the week is Sunday, but in France it is Monday.
7183
*/
72-
export function getDayOfWeek(date: DateValue, locale: string): number {
84+
export function getDayOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number {
7385
let julian = date.calendar.toJulianDay(date);
7486

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

183196
/** Returns the first date of the week for the given date and locale. */
184-
export function startOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime;
185-
export function startOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime;
186-
export function startOfWeek(date: CalendarDate, locale: string): CalendarDate;
187-
export function startOfWeek(date: DateValue, locale: string): DateValue;
188-
export function startOfWeek(date: DateValue, locale: string): DateValue {
189-
let dayOfWeek = getDayOfWeek(date, locale);
197+
export function startOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime;
198+
export function startOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime;
199+
export function startOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate;
200+
export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue;
201+
export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue {
202+
let dayOfWeek = getDayOfWeek(date, locale, firstDayOfWeek);
190203
return date.subtract({days: dayOfWeek});
191204
}
192205

193206
/** Returns the last date of the week for the given date and locale. */
194-
export function endOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime;
195-
export function endOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime;
196-
export function endOfWeek(date: CalendarDate, locale: string): CalendarDate;
197-
export function endOfWeek(date: DateValue, locale: string): DateValue;
198-
export function endOfWeek(date: DateValue, locale: string): DateValue {
199-
return startOfWeek(date, locale).add({days: 6});
207+
export function endOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime;
208+
export function endOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime;
209+
export function endOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate;
210+
export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue;
211+
export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue {
212+
return startOfWeek(date, locale, firstDayOfWeek).add({days: 6});
200213
}
201214

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

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

241254
/** Returns the lesser of the two provider dates. */

packages/@internationalized/date/tests/queries.test.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,13 @@ describe('queries', function () {
246246
it('should return the day of week in fr', function () {
247247
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'fr')).toBe(2);
248248
});
249+
250+
it('should return the day of the week with a custom firstDayOfWeek', function () {
251+
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toBe(2);
252+
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'tue')).toBe(1);
253+
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'mon')).toBe(2);
254+
expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'tue')).toBe(1);
255+
});
249256
});
250257

251258
describe('startOfWeek', function () {
@@ -256,16 +263,28 @@ describe('queries', function () {
256263
it('should return the start of week in fr-FR', function () {
257264
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR')).toEqual(new CalendarDate(2021, 8, 2));
258265
});
266+
267+
it('should return the start of the week with a custom firstDayOfWeek', function () {
268+
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toEqual(new CalendarDate(2021, 8, 2));
269+
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'tue')).toEqual(new CalendarDate(2021, 8, 3));
270+
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'sun')).toEqual(new CalendarDate(2021, 8, 1));
271+
});
259272
});
260273

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

266-
it('should return the start of week in fr-FR', function () {
279+
it('should return the end of week in fr-FR', function () {
267280
expect(endOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR')).toEqual(new CalendarDate(2021, 8, 8));
268281
});
282+
283+
it('should return the end of the week with a custom firstDayOfWeek', function () {
284+
expect(endOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toEqual(new CalendarDate(2021, 8, 8));
285+
expect(endOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'tue')).toEqual(new CalendarDate(2021, 8, 9));
286+
expect(endOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'sun')).toEqual(new CalendarDate(2021, 8, 7));
287+
});
269288
});
270289

271290
describe('getWeeksInMonth', function () {
@@ -280,6 +299,13 @@ describe('queries', function () {
280299
it('should work for other calendars', function () {
281300
expect(getWeeksInMonth(new CalendarDate(new EthiopicCalendar(), 2013, 13, 4), 'en-US')).toBe(1);
282301
});
302+
303+
it('should support custom firstDayOfWeek', function () {
304+
expect(getWeeksInMonth(new CalendarDate(2021, 8, 4), 'en-US', 'sun')).toBe(5);
305+
expect(getWeeksInMonth(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toBe(6);
306+
expect(getWeeksInMonth(new CalendarDate(2021, 10, 4), 'en-US', 'sun')).toBe(6);
307+
expect(getWeeksInMonth(new CalendarDate(2021, 10, 4), 'en-US', 'mon')).toBe(5);
308+
});
283309
});
284310

285311
describe('getMinimumMonthInYear', function () {

packages/@react-aria/calendar/docs/useCalendar.mdx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ function Calendar(props) {
129129
<Button {...prevButtonProps}>&lt;</Button>
130130
<Button {...nextButtonProps}>&gt;</Button>
131131
</div>
132-
<CalendarGrid state={state} />
132+
<CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} />
133133
</div>
134134
);
135135
}
@@ -152,7 +152,7 @@ function CalendarGrid({state, ...props}) {
152152
let {gridProps, headerProps, weekDays} = useCalendarGrid(props, state);
153153

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

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

461+
### Custom first day of week
462+
463+
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'`.
464+
465+
```tsx example
466+
<Calendar aria-label="Event date" value={today(getLocalTimeZone())} firstDayOfWeek="mon" />
467+
```
468+
461469
### Labeling
462470

463471
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.

packages/@react-aria/calendar/docs/useRangeCalendar.mdx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ function RangeCalendar(props) {
129129
<Button {...prevButtonProps}>&lt;</Button>
130130
<Button {...nextButtonProps}>&gt;</Button>
131131
</div>
132-
<CalendarGrid state={state} />
132+
<CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} />
133133
</div>
134134
);
135135
}
@@ -152,7 +152,7 @@ function CalendarGrid({state, ...props}) {
152152
let {gridProps, headerProps, weekDays} = useCalendarGrid(props, state);
153153

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

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

480+
### Custom first day of week
481+
482+
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'`.
483+
484+
```tsx example
485+
<RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon" />
486+
```
487+
480488
### Labeling
481489

482490
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.

packages/@react-aria/calendar/src/useCalendarGrid.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ export interface AriaCalendarGridProps {
3636
* e.g. single letter, abbreviation, or full day name.
3737
* @default "narrow"
3838
*/
39-
weekdayStyle?: 'narrow' | 'short' | 'long'
39+
weekdayStyle?: 'narrow' | 'short' | 'long',
40+
/**
41+
* The day that starts the week.
42+
*/
43+
firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
4044
}
4145

4246
export interface CalendarGridAria {
@@ -56,7 +60,8 @@ export interface CalendarGridAria {
5660
export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria {
5761
let {
5862
startDate = state.visibleRange.start,
59-
endDate = state.visibleRange.end
63+
endDate = state.visibleRange.end,
64+
firstDayOfWeek
6065
} = props;
6166

6267
let {direction} = useLocale();
@@ -137,13 +142,13 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta
137142
let dayFormatter = useDateFormatter({weekday: props.weekdayStyle || 'narrow', timeZone: state.timeZone});
138143
let {locale} = useLocale();
139144
let weekDays = useMemo(() => {
140-
let weekStart = startOfWeek(today(state.timeZone), locale);
145+
let weekStart = startOfWeek(today(state.timeZone), locale, firstDayOfWeek);
141146
return [...new Array(7).keys()].map((index) => {
142147
let date = weekStart.add({days: index});
143148
let dateDay = date.toDate(state.timeZone);
144149
return dayFormatter.format(dateDay);
145150
});
146-
}, [locale, state.timeZone, dayFormatter]);
151+
}, [locale, state.timeZone, dayFormatter, firstDayOfWeek]);
147152

148153
return {
149154
gridProps: mergeProps(labelProps, {

0 commit comments

Comments
 (0)