From 8ecbca119baa43d43b0171898a6d78c9b909b1a9 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 13 Nov 2024 10:29:40 -0600 Subject: [PATCH 01/34] add firstDayOfWeek prop --- .../@react-aria/calendar/src/useCalendarGrid.ts | 14 ++++++++++---- .../calendar/src/useCalendarState.ts | 9 ++++++--- packages/@react-types/calendar/src/index.d.ts | 7 ++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/@react-aria/calendar/src/useCalendarGrid.ts b/packages/@react-aria/calendar/src/useCalendarGrid.ts index ec91c10075b..cabc1037ea1 100644 --- a/packages/@react-aria/calendar/src/useCalendarGrid.ts +++ b/packages/@react-aria/calendar/src/useCalendarGrid.ts @@ -12,10 +12,10 @@ import {CalendarDate, startOfWeek, today} from '@internationalized/date'; import {CalendarState, RangeCalendarState} from '@react-stately/calendar'; +import {clamp, mergeProps, useLabels} from '@react-aria/utils'; import {DOMAttributes} from '@react-types/shared'; import {hookData, useVisibleRangeDescription} from './utils'; import {KeyboardEvent, useMemo} from 'react'; -import {mergeProps, useLabels} from '@react-aria/utils'; import {useDateFormatter, useLocale} from '@react-aria/i18n'; export interface AriaCalendarGridProps { @@ -36,7 +36,12 @@ 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, 0-6 (Sunday-Saturday). + * @default 0 + */ + firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 } export interface CalendarGridAria { @@ -137,13 +142,14 @@ 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 firstDayOfWeek = clamp(props.firstDayOfWeek ?? 0, 0, 6); + let weekStart = startOfWeek(today(state.timeZone), locale).add({days: 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, props.firstDayOfWeek]); return { gridProps: mergeProps(labelProps, { diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index 58d6f4e9407..7ce86f7b1f5 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -29,7 +29,7 @@ import { } from '@internationalized/date'; import {CalendarProps, DateValue, MappedDateValue} from '@react-types/calendar'; import {CalendarState} from './types'; -import {useControlledState} from '@react-stately/utils'; +import {clamp, useControlledState} from '@react-stately/utils'; import {useMemo, useState} from 'react'; import {ValidationState} from '@react-types/shared'; @@ -328,11 +328,14 @@ export function useCalendarState(props: Calenda let date = from.add({weeks: weekIndex}); let dates: (CalendarDate | null)[] = []; - date = startOfWeek(date, locale); + let currentDayOfWeek = getDayOfWeek(date, locale); + let firstDayOfWeek = clamp(props.firstDayOfWeek ?? 0, 0, 6); + let daysToSubtract = (currentDayOfWeek - firstDayOfWeek + 7) % 7; + date = date.subtract({days: daysToSubtract}); // startOfWeek will clamp dates within the calendar system's valid range, which may // start in the middle of a week. In this case, add null placeholders. - let dayOfWeek = getDayOfWeek(date, locale); + let dayOfWeek = (getDayOfWeek(date, locale) - firstDayOfWeek + 7) % 7; for (let i = 0; i < dayOfWeek; i++) { dates.push(null); } diff --git a/packages/@react-types/calendar/src/index.d.ts b/packages/@react-types/calendar/src/index.d.ts index 5cdf3791323..6be0b924fc7 100644 --- a/packages/@react-types/calendar/src/index.d.ts +++ b/packages/@react-types/calendar/src/index.d.ts @@ -62,7 +62,12 @@ export interface CalendarPropsBase { * Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. * @default visible */ - pageBehavior?: PageBehavior + pageBehavior?: PageBehavior, + /** + * The day that starts the week, 0-6 (Sunday-Saturday). + * @default 0 + */ + firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 } export type DateRange = RangeValue | null; From 3819daa8f3a08cbfeb448f8429c66136b513b12e Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 13 Nov 2024 10:49:18 -0600 Subject: [PATCH 02/34] pass to DatePicker/DateRangePicker --- packages/@react-spectrum/datepicker/src/DatePicker.tsx | 4 +++- .../@react-spectrum/datepicker/src/DateRangePicker.tsx | 4 +++- packages/@react-types/datepicker/src/index.d.ts | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/@react-spectrum/datepicker/src/DatePicker.tsx b/packages/@react-spectrum/datepicker/src/DatePicker.tsx index b298e62203b..1d36b9e5871 100644 --- a/packages/@react-spectrum/datepicker/src/DatePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DatePicker.tsx @@ -47,7 +47,8 @@ function DatePicker(props: SpectrumDatePickerProps, ref: isDisabled, placeholderValue, maxVisibleMonths = 1, - pageBehavior + pageBehavior, + firstDayOfWeek = 0 } = props; let {hoverProps, isHovered} = useHover({isDisabled}); let targetRef = useRef(undefined); @@ -168,6 +169,7 @@ function DatePicker(props: SpectrumDatePickerProps, ref: {...calendarProps} visibleMonths={visibleMonths} pageBehavior={pageBehavior} + firstDayOfWeek={firstDayOfWeek} UNSAFE_className={classNames(datepickerStyles, 'react-spectrum-Datepicker-calendar', {'is-invalid': isInvalid})} /> {showTimeField &&
diff --git a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx index 11143195d19..a1effbb02b0 100644 --- a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx @@ -47,7 +47,8 @@ function DateRangePicker(props: SpectrumDateRangePickerProp autoFocus, placeholderValue, maxVisibleMonths = 1, - pageBehavior + pageBehavior, + firstDayOfWeek = 0 } = props; let {hoverProps, isHovered} = useHover({isDisabled}); let targetRef = useRef(undefined); @@ -183,6 +184,7 @@ function DateRangePicker(props: SpectrumDateRangePickerProp {...calendarProps} visibleMonths={visibleMonths} pageBehavior={pageBehavior} + firstDayOfWeek={firstDayOfWeek} UNSAFE_className={classNames(datepickerStyles, 'react-spectrum-Datepicker-calendar', {'is-invalid': validationState === 'invalid'})} /> {showTimeField && diff --git a/packages/@react-types/datepicker/src/index.d.ts b/packages/@react-types/datepicker/src/index.d.ts index 1421ca90aba..cd81436f4d9 100644 --- a/packages/@react-types/datepicker/src/index.d.ts +++ b/packages/@react-types/datepicker/src/index.d.ts @@ -71,7 +71,12 @@ interface DatePickerBase extends DateFieldBase, OverlayT * Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. * @default visible */ - pageBehavior?: PageBehavior + pageBehavior?: PageBehavior, + /** + * The day that starts the week, 0-6 (Sunday-Saturday). + * @default 0 + */ + firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 } export interface AriaDatePickerBaseProps extends DatePickerBase, AriaLabelingProps, DOMProps {} From 467590126048a0e7c0c6f4a007e68676c12b0a95 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 13 Nov 2024 10:50:30 -0600 Subject: [PATCH 03/34] add storybook controls --- packages/@react-spectrum/calendar/stories/Calendar.stories.tsx | 3 +++ .../@react-spectrum/calendar/stories/RangeCalendar.stories.tsx | 3 +++ .../@react-spectrum/datepicker/stories/DatePicker.stories.tsx | 3 +++ 3 files changed, 9 insertions(+) diff --git a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx index 1b2ef92a76b..281b170f0b6 100644 --- a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx @@ -80,6 +80,9 @@ export default { control: 'select', options: [null, 'single', 'visible'] }, + firstDayOfWeek: { + control: 'number' + }, isInvalid: { control: 'boolean' }, diff --git a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx index 2e998f502ed..1740817cd45 100644 --- a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx @@ -74,6 +74,9 @@ export default { control: 'select', options: [null, 'single', 'visible'] }, + firstDayOfWeek: { + control: 'number' + }, isInvalid: { control: 'boolean' }, diff --git a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx index c26c70ac3ef..3537c2072c5 100644 --- a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx @@ -182,6 +182,9 @@ export default { }, isOpen: { control: 'boolean' + }, + firstDayOfWeek: { + control: 'number' } } } as ComponentMeta; From b84969e4e53b7fbc133eb28a72eb8bc0031f9962 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 13 Nov 2024 14:03:43 -0600 Subject: [PATCH 04/34] add to RAC --- packages/react-aria-components/src/Calendar.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/src/Calendar.tsx b/packages/react-aria-components/src/Calendar.tsx index 4966efde564..59316749095 100644 --- a/packages/react-aria-components/src/Calendar.tsx +++ b/packages/react-aria-components/src/Calendar.tsx @@ -26,7 +26,7 @@ import { import {ButtonContext} from './Button'; import {CalendarDate, createCalendar, DateDuration, endOfMonth, getWeeksInMonth, isSameDay, isSameMonth} from '@internationalized/date'; import {CalendarState, RangeCalendarState, useCalendarState, useRangeCalendarState} from 'react-stately'; -import {ContextValue, DOMProps, Provider, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps} from './utils'; +import {ContextValue, DOMProps, Provider, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps, useSlottedContext} from './utils'; import {DOMAttributes, FocusableElement, forwardRefType, HoverEvents} from '@react-types/shared'; import {filterDOMProps} from '@react-aria/utils'; import {HeadingContext} from './RSPContexts'; @@ -77,6 +77,8 @@ export const CalendarContext = createContext, HT export const RangeCalendarContext = createContext, HTMLDivElement>>({}); export const CalendarStateContext = createContext(null); export const RangeCalendarStateContext = createContext(null); +const InternalCalendarContext = createContext | null>(null); +const InternalRangeCalendarContext = createContext | null>(null); function Calendar(props: CalendarProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, CalendarContext); @@ -117,6 +119,7 @@ function Calendar(props: CalendarProps, ref: ForwardedRe }], [HeadingContext, {'aria-hidden': true, level: 2, children: title}], [CalendarStateContext, state], + [InternalCalendarContext, props], [TextContext, { slots: { errorMessage: errorMessageProps @@ -196,6 +199,7 @@ function RangeCalendar(props: RangeCalendarProps, ref: F }], [HeadingContext, {'aria-hidden': true, level: 2, children: title}], [RangeCalendarStateContext, state], + [InternalRangeCalendarContext, props], [TextContext, { slots: { errorMessage: errorMessageProps @@ -340,6 +344,8 @@ const InternalCalendarGridContext = createContext) { let calendarState = useContext(CalendarStateContext); let rangeCalendarState = useContext(RangeCalendarStateContext); + let calenderProps = useContext(InternalCalendarContext)!; + let rangeCalenderProps = useContext(InternalRangeCalendarContext)!; let state = calendarState ?? rangeCalendarState!; let startDate = state.visibleRange.start; if (props.offset) { @@ -349,7 +355,8 @@ function CalendarGrid(props: CalendarGridProps, ref: ForwardedRef Date: Wed, 13 Nov 2024 14:14:28 -0600 Subject: [PATCH 05/34] add docs --- packages/@react-aria/calendar/docs/useCalendar.mdx | 10 +++++++++- .../@react-aria/calendar/docs/useRangeCalendar.mdx | 10 +++++++++- .../@react-aria/datepicker/docs/useDatePicker.mdx | 12 ++++++++++-- .../datepicker/docs/useDateRangePicker.mdx | 12 ++++++++++-- packages/@react-spectrum/calendar/docs/Calendar.mdx | 10 ++++++++++ .../@react-spectrum/calendar/docs/RangeCalendar.mdx | 10 ++++++++++ .../@react-spectrum/datepicker/docs/DatePicker.mdx | 8 ++++++++ .../datepicker/docs/DateRangePicker.mdx | 8 ++++++++ packages/react-aria-components/docs/Calendar.mdx | 8 ++++++++ packages/react-aria-components/docs/DatePicker.mdx | 12 ++++++++++-- .../react-aria-components/docs/DateRangePicker.mdx | 12 ++++++++++-- .../react-aria-components/docs/RangeCalendar.mdx | 8 ++++++++ 12 files changed, 110 insertions(+), 10 deletions(-) diff --git a/packages/@react-aria/calendar/docs/useCalendar.mdx b/packages/@react-aria/calendar/docs/useCalendar.mdx index 9c0dafcf919..a45aaffec7b 100644 --- a/packages/@react-aria/calendar/docs/useCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useCalendar.mdx @@ -129,7 +129,7 @@ function Calendar(props) {
- + ); } @@ -458,6 +458,14 @@ The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDi ``` +### Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` + ### 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. diff --git a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx index 3900cdc08a5..e5376e6c11f 100644 --- a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx @@ -129,7 +129,7 @@ function RangeCalendar(props) { - + ); } @@ -477,6 +477,14 @@ The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike ``` +### Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` + ### 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. diff --git a/packages/@react-aria/datepicker/docs/useDatePicker.mdx b/packages/@react-aria/datepicker/docs/useDatePicker.mdx index a138cffafd1..fa5ff0a1518 100644 --- a/packages/@react-aria/datepicker/docs/useDatePicker.mdx +++ b/packages/@react-aria/datepicker/docs/useDatePicker.mdx @@ -106,7 +106,7 @@ function DatePicker(props) { {state.isOpen && - + } @@ -346,7 +346,7 @@ function Calendar(props) { - + ); } @@ -690,3 +690,11 @@ By default, `useDatePicker` displays times in either 12 or 24 hour hour format d granularity="minute" hourCycle={24} /> ``` + +### Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` \ No newline at end of file diff --git a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx index 9ad73b0392d..df1091995e3 100644 --- a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx +++ b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx @@ -113,7 +113,7 @@ function DateRangePicker(props) { {state.isOpen && - + } @@ -359,7 +359,7 @@ function RangeCalendar(props) { - + ); } @@ -755,3 +755,11 @@ By default, `useDateRangePicker` displays times in either 12 or 24 hour hour for granularity="minute" hourCycle={24} /> ``` + +### Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` \ No newline at end of file diff --git a/packages/@react-spectrum/calendar/docs/Calendar.mdx b/packages/@react-spectrum/calendar/docs/Calendar.mdx index 97fc6f8baee..486a39b8bd8 100644 --- a/packages/@react-spectrum/calendar/docs/Calendar.mdx +++ b/packages/@react-spectrum/calendar/docs/Calendar.mdx @@ -220,3 +220,13 @@ By default, when pressing the next or previous buttons, pagination will advance ``` + +### Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example +
+ +
+``` diff --git a/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx b/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx index 2ad4e6243f3..47bbd2d3657 100644 --- a/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx +++ b/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx @@ -252,3 +252,13 @@ By default, when pressing the next or previous buttons, pagination will advance ``` + +### Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example +
+ +
+``` diff --git a/packages/@react-spectrum/datepicker/docs/DatePicker.mdx b/packages/@react-spectrum/datepicker/docs/DatePicker.mdx index ec1ee6ab226..9519a3263fe 100644 --- a/packages/@react-spectrum/datepicker/docs/DatePicker.mdx +++ b/packages/@react-spectrum/datepicker/docs/DatePicker.mdx @@ -441,6 +441,14 @@ By default, `DatePicker` displays times in either 12 or 24 hour hour format depe hourCycle={24} /> ``` +### Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` + ## Testing The DatePicker features an overlay that transitions in and out of the page as it is opened and closed. Depending on diff --git a/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx b/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx index 68be8285a29..6a3a2cd7dc7 100644 --- a/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx +++ b/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx @@ -478,6 +478,14 @@ By default, `DateRangePicker` displays times in either 12 or 24 hour hour format hourCycle={24} /> ``` +### Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` + ## Testing The DateRangePicker features an overlay that transitions in and out of the page as it is opened and closed. Depending on diff --git a/packages/react-aria-components/docs/Calendar.mdx b/packages/react-aria-components/docs/Calendar.mdx index 35e4ea59e94..a6b4dfc919f 100644 --- a/packages/react-aria-components/docs/Calendar.mdx +++ b/packages/react-aria-components/docs/Calendar.mdx @@ -511,6 +511,14 @@ The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDi ``` +## Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` + ## 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. diff --git a/packages/react-aria-components/docs/DatePicker.mdx b/packages/react-aria-components/docs/DatePicker.mdx index 2137e84e6cb..b2278f73fd5 100644 --- a/packages/react-aria-components/docs/DatePicker.mdx +++ b/packages/react-aria-components/docs/DatePicker.mdx @@ -293,7 +293,7 @@ interface MyDatePickerProps extends DatePickerProps { errorMessage?: string | ((validation: ValidationResult) => string) } -function MyDatePicker({label, description, errorMessage, ...props}: MyDatePickerProps) { +function MyDatePicker({label, description, errorMessage, firstDayOfWeek, ...props}: MyDatePickerProps) { return ( @@ -307,7 +307,7 @@ function MyDatePicker({label, description, errorMessage, .. {errorMessage} - +
@@ -711,6 +711,14 @@ By default, `DatePicker` displays times in either 12 or 24 hour hour format depe hourCycle={24} /> ``` +## Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` + ## Props ### DatePicker diff --git a/packages/react-aria-components/docs/DateRangePicker.mdx b/packages/react-aria-components/docs/DateRangePicker.mdx index 7e95226e2f5..0a427933d37 100644 --- a/packages/react-aria-components/docs/DateRangePicker.mdx +++ b/packages/react-aria-components/docs/DateRangePicker.mdx @@ -326,7 +326,7 @@ interface MyDateRangePickerProps extends DateRangePickerPro errorMessage?: string | ((validation: ValidationResult) => string) } -function MyDateRangePicker({label, description, errorMessage, ...props}: MyDateRangePickerProps) { +function MyDateRangePicker({label, description, errorMessage, firstDayOfWeek, ...props}: MyDateRangePickerProps) { return ( @@ -344,7 +344,7 @@ function MyDateRangePicker({label, description, errorMessag {errorMessage} - +
@@ -800,6 +800,14 @@ By default, `DateRangePicker` displays times in either 12 or 24 hour hour format hourCycle={24} /> ``` +### Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` + ## Props ### DateRangePicker diff --git a/packages/react-aria-components/docs/RangeCalendar.mdx b/packages/react-aria-components/docs/RangeCalendar.mdx index 2f0607fb409..7c8cfe293b5 100644 --- a/packages/react-aria-components/docs/RangeCalendar.mdx +++ b/packages/react-aria-components/docs/RangeCalendar.mdx @@ -566,6 +566,14 @@ The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike ``` +## Custom first day of week + +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). + +```tsx example + +``` + ## 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. From 509257ce2bf8a375c372902b024209b96f09b83f Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 13 Nov 2024 14:20:00 -0600 Subject: [PATCH 06/34] add v3 tests --- .../calendar/test/CalendarBase.test.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/@react-spectrum/calendar/test/CalendarBase.test.js b/packages/@react-spectrum/calendar/test/CalendarBase.test.js index c9420ab497b..6d13a8bd362 100644 --- a/packages/@react-spectrum/calendar/test/CalendarBase.test.js +++ b/packages/@react-spectrum/calendar/test/CalendarBase.test.js @@ -789,5 +789,35 @@ describe('CalendarBase', () => { await user.keyboard('{ArrowRight}'); expect(document.activeElement).toBe(selected); }); + + it.each` + Name | Calendar | props + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} + `('$Name should override start of week with firstDayOfWeek={1}', ({Calendar, props}) => { + let {getAllByRole, getByRole} = render(); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['M', 'T', 'W', 'T', 'F', 'S', 'S']); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells[0]).toHaveTextContent('1'); + }); + + it.each` + Name | Calendar | props + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} + `('$Name should override start of week with firstDayOfWeek={6}', ({Calendar, props}) => { + let {getAllByRole, getByRole} = render(); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['S', 'S', 'M', 'T', 'W', 'T', 'F']); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells[2]).toHaveTextContent('1'); + }); }); }); From b03d491533d31ed137a767f0a561db74b6760705 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 13 Nov 2024 14:41:25 -0600 Subject: [PATCH 07/34] add hook tests --- .../@react-aria/calendar/stories/Example.tsx | 46 +++++++++++++++++++ .../calendar/test/useCalendar.test.js | 25 +++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/@react-aria/calendar/stories/Example.tsx b/packages/@react-aria/calendar/stories/Example.tsx index 583a5b847f2..aeff70c0095 100644 --- a/packages/@react-aria/calendar/stories/Example.tsx +++ b/packages/@react-aria/calendar/stories/Example.tsx @@ -97,3 +97,49 @@ function Cell(props) { ); } + +export function ExampleCustomFirstDay(props) { + let {locale} = useLocale(); + const {firstDayOfWeek} = props; + + let state = useCalendarState({ + ...props, + locale, + createCalendar + }); + + let {calendarProps, prevButtonProps, nextButtonProps} = useCalendar(props, state); + + return ( +
+
+ {calendarProps['aria-label']} +
+
+ +
+
+ + +
+
+ ); +} + +function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek: 0 | 1 | 2 | 3 | 4 | 5 | 6}) { + let {locale} = useLocale(); + let {gridProps} = useCalendarGrid({firstDayOfWeek}, state); + let startDate = state.visibleRange.start; + let weeksInMonth = getWeeksInMonth(startDate, locale); + return ( +
+ {[...new Array(weeksInMonth).keys()].map(weekIndex => ( +
+ {state.getDatesInWeek(weekIndex, startDate).map((date, i) => ( + + ))} +
+ ))} +
+ ); +} diff --git a/packages/@react-aria/calendar/test/useCalendar.test.js b/packages/@react-aria/calendar/test/useCalendar.test.js index db34fd92e66..f2ca99e8750 100644 --- a/packages/@react-aria/calendar/test/useCalendar.test.js +++ b/packages/@react-aria/calendar/test/useCalendar.test.js @@ -12,7 +12,7 @@ import {act, pointerMap, render} from '@react-spectrum/test-utils-internal'; import {CalendarDate} from '@internationalized/date'; -import {Example} from '../stories/Example'; +import {Example, ExampleCustomFirstDay} from '../stories/Example'; import React from 'react'; import userEvent from '@testing-library/user-event'; @@ -63,6 +63,13 @@ describe('useCalendar', () => { unmount(); } + async function testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay) { + let {getAllByRole, unmount} = render(); + let cells = getAllByRole('gridcell'); + expect(cells[0].children[0]).toHaveAttribute('aria-label', expectedFirstDay); + unmount(); + } + describe('visibleDuration: 3 days', () => { it('should move the focused date by one day with the left/right arrows', async () => { await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'ArrowLeft', 1, 'Tuesday, June 4, 2019', 'June 4 to 6, 2019', {visibleDuration: {days: 3}}); @@ -227,4 +234,20 @@ describe('useCalendar', () => { await testPagination(defaultValue, rangeBefore, rangeAfter, rel, count, visibleDuration, pageBehavior); }); }); + + describe('firstDayOfWeek', () => { + it.each` + Name | defaultValue | firstDayOfWeek | expectedFirstDay + ${'default'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'Sunday, December 31, 2023'} + ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${0} | ${'Sunday, December 31, 2023'} + ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${1} | ${'Monday, January 1, 2024 selected'} + ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${2} | ${'Tuesday, December 26, 2023'} + ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${3} | ${'Wednesday, December 27, 2023'} + ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${4} | ${'Thursday, December 28, 2023'} + ${'Friday'} | ${new CalendarDate(2024, 1, 1)} | ${5} | ${'Friday, December 29, 2023'} + ${'Saturday'} | ${new CalendarDate(2024, 1, 1)} | ${6} | ${'Saturday, December 30, 2023'} + `('should use firstDayOfWeek $Name', async ({defaultValue, firstDayOfWeek, expectedFirstDay}) => { + await testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay); + }); + }); }); From 68b9f48a824a32ed299cb50e960f30078d7a8d1f Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 13 Nov 2024 15:49:22 -0600 Subject: [PATCH 08/34] lint --- packages/react-aria-components/src/Calendar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/src/Calendar.tsx b/packages/react-aria-components/src/Calendar.tsx index 59316749095..f11cf747734 100644 --- a/packages/react-aria-components/src/Calendar.tsx +++ b/packages/react-aria-components/src/Calendar.tsx @@ -26,7 +26,7 @@ import { import {ButtonContext} from './Button'; import {CalendarDate, createCalendar, DateDuration, endOfMonth, getWeeksInMonth, isSameDay, isSameMonth} from '@internationalized/date'; import {CalendarState, RangeCalendarState, useCalendarState, useRangeCalendarState} from 'react-stately'; -import {ContextValue, DOMProps, Provider, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps, useSlottedContext} from './utils'; +import {ContextValue, DOMProps, Provider, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps} from './utils'; import {DOMAttributes, FocusableElement, forwardRefType, HoverEvents} from '@react-types/shared'; import {filterDOMProps} from '@react-aria/utils'; import {HeadingContext} from './RSPContexts'; From c9f80eb4e1afd542331e153b8b199007ed24a5b0 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 13 Nov 2024 15:53:59 -0600 Subject: [PATCH 09/34] add chromatic stories --- .../@react-spectrum/calendar/chromatic/Calendar.stories.tsx | 2 ++ .../calendar/chromatic/RangeCalendar.stories.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx index f29c9903877..a23023290bf 100644 --- a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx @@ -45,3 +45,5 @@ export const Invalid = () => ; export const ErrorMessage = () => ; export const UnavailableInvalid = () => d.compare(date) === 0} />; export const DisabledInvalid = () => ; +export const CustomWeekStartMonday = () => ; +export const CustomWeekStartSaturday = () => ; diff --git a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx index ea27789d5b0..984a4cc390b 100644 --- a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx @@ -69,3 +69,5 @@ export const NonContiguousInvalid = () => { ); }; +export const CustomWeekStartMonday = () => ; +export const CustomWeekStartSaturday = () => ; From 15a047aa3e215628da62d058e4d1af44118d1884 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 14 Nov 2024 12:11:03 -0600 Subject: [PATCH 10/34] update v3 RangeCalendar styles for first and last day of week --- packages/@react-spectrum/calendar/src/CalendarBase.tsx | 6 ++++-- packages/@react-spectrum/calendar/src/CalendarCell.tsx | 9 +++++---- packages/@react-spectrum/calendar/src/CalendarMonth.tsx | 6 ++++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/@react-spectrum/calendar/src/CalendarBase.tsx b/packages/@react-spectrum/calendar/src/CalendarBase.tsx index 37388db5da3..5fd635dee9a 100644 --- a/packages/@react-spectrum/calendar/src/CalendarBase.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarBase.tsx @@ -45,7 +45,8 @@ export function CalendarBase(props prevButtonProps, errorMessageProps, calendarRef: ref, - visibleMonths = 1 + visibleMonths = 1, + firstDayOfWeek = 0 } = props; let {styleProps} = useStyleProps(props); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/calendar'); @@ -97,7 +98,8 @@ export function CalendarBase(props {...props} key={i} state={state} - startDate={d} /> + startDate={d} + firstDayOfWeek={firstDayOfWeek} /> ); } diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index 24f5552a775..b9fe785e4e7 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -23,10 +23,11 @@ import {useLocale} from '@react-aria/i18n'; interface CalendarCellProps extends AriaCalendarCellProps { state: CalendarState | RangeCalendarState, - currentMonth: CalendarDate + currentMonth: CalendarDate, + firstDayOfWeek: 0 | 1 | 2 | 3 | 4 | 5 | 6 } -export function CalendarCell({state, currentMonth, ...props}: CalendarCellProps) { +export function CalendarCell({state, currentMonth, firstDayOfWeek, ...props}: CalendarCellProps) { let ref = useRef(null); let { cellProps, @@ -49,8 +50,8 @@ export function CalendarCell({state, currentMonth, ...props}: CalendarCellProps) let isSelectionEnd = isSelected && highlightedRange && isSameDay(props.date, highlightedRange.end); let {locale} = useLocale(); let dayOfWeek = getDayOfWeek(props.date, locale); - let isRangeStart = isSelected && (isFirstSelectedAfterDisabled || dayOfWeek === 0 || props.date.day === 1); - let isRangeEnd = isSelected && (isLastSelectedBeforeDisabled || dayOfWeek === 6 || props.date.day === currentMonth.calendar.getDaysInMonth(currentMonth)); + let isRangeStart = isSelected && (isFirstSelectedAfterDisabled || dayOfWeek === firstDayOfWeek || props.date.day === 1); + let isRangeEnd = isSelected && (isLastSelectedBeforeDisabled || ((dayOfWeek - firstDayOfWeek + 7) % 7) === 6 || props.date.day === currentMonth.calendar.getDaysInMonth(currentMonth)); let {focusProps, isFocusVisible} = useFocusRing(); let {hoverProps, isHovered} = useHover({isDisabled: isDisabled || isUnavailable || state.isReadOnly}); diff --git a/packages/@react-spectrum/calendar/src/CalendarMonth.tsx b/packages/@react-spectrum/calendar/src/CalendarMonth.tsx index d768360c81b..c77bff9b6a5 100644 --- a/packages/@react-spectrum/calendar/src/CalendarMonth.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarMonth.tsx @@ -29,7 +29,8 @@ interface CalendarMonthProps extends CalendarPropsBase, DOMProps, StyleProps { export function CalendarMonth(props: CalendarMonthProps) { let { state, - startDate + startDate, + firstDayOfWeek } = props; let { gridProps, @@ -69,7 +70,8 @@ export function CalendarMonth(props: CalendarMonthProps) { key={i} state={state} date={date} - currentMonth={startDate} /> + currentMonth={startDate} + firstDayOfWeek={firstDayOfWeek} /> ) : ))} From 4020fb20cf33a8e57b1734a0d8c367011bc234bc Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 14 Nov 2024 12:22:55 -0600 Subject: [PATCH 11/34] lint --- packages/@react-spectrum/calendar/src/CalendarCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index b9fe785e4e7..1e5e308530b 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -24,7 +24,7 @@ import {useLocale} from '@react-aria/i18n'; interface CalendarCellProps extends AriaCalendarCellProps { state: CalendarState | RangeCalendarState, currentMonth: CalendarDate, - firstDayOfWeek: 0 | 1 | 2 | 3 | 4 | 5 | 6 + firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 } export function CalendarCell({state, currentMonth, firstDayOfWeek, ...props}: CalendarCellProps) { From ab374a80ed9c8eeadc1d8eccc4e64efb51a6236f Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 14 Nov 2024 12:31:37 -0600 Subject: [PATCH 12/34] lint --- packages/@react-spectrum/calendar/src/CalendarCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index 1e5e308530b..df982dfbb3f 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -27,7 +27,7 @@ interface CalendarCellProps extends AriaCalendarCellProps { firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 } -export function CalendarCell({state, currentMonth, firstDayOfWeek, ...props}: CalendarCellProps) { +export function CalendarCell({state, currentMonth, firstDayOfWeek = 0, ...props}: CalendarCellProps) { let ref = useRef(null); let { cellProps, From b554178afef1f3af4578191816ab0957f99b8b86 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 14 Nov 2024 15:58:55 -0600 Subject: [PATCH 13/34] use number instead of enum for type --- packages/@react-aria/calendar/src/useCalendarGrid.ts | 2 +- packages/@react-aria/calendar/stories/Example.tsx | 2 +- packages/@react-spectrum/calendar/src/CalendarCell.tsx | 2 +- packages/@react-types/calendar/src/index.d.ts | 2 +- packages/@react-types/datepicker/src/index.d.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/calendar/src/useCalendarGrid.ts b/packages/@react-aria/calendar/src/useCalendarGrid.ts index cabc1037ea1..852188bdb5d 100644 --- a/packages/@react-aria/calendar/src/useCalendarGrid.ts +++ b/packages/@react-aria/calendar/src/useCalendarGrid.ts @@ -41,7 +41,7 @@ export interface AriaCalendarGridProps { * The day that starts the week, 0-6 (Sunday-Saturday). * @default 0 */ - firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 + firstDayOfWeek?: number } export interface CalendarGridAria { diff --git a/packages/@react-aria/calendar/stories/Example.tsx b/packages/@react-aria/calendar/stories/Example.tsx index aeff70c0095..aea02564871 100644 --- a/packages/@react-aria/calendar/stories/Example.tsx +++ b/packages/@react-aria/calendar/stories/Example.tsx @@ -126,7 +126,7 @@ export function ExampleCustomFirstDay(props) { ); } -function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek: 0 | 1 | 2 | 3 | 4 | 5 | 6}) { +function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek: number}) { let {locale} = useLocale(); let {gridProps} = useCalendarGrid({firstDayOfWeek}, state); let startDate = state.visibleRange.start; diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index df982dfbb3f..8255dc37139 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -24,7 +24,7 @@ import {useLocale} from '@react-aria/i18n'; interface CalendarCellProps extends AriaCalendarCellProps { state: CalendarState | RangeCalendarState, currentMonth: CalendarDate, - firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 + firstDayOfWeek?: number } export function CalendarCell({state, currentMonth, firstDayOfWeek = 0, ...props}: CalendarCellProps) { diff --git a/packages/@react-types/calendar/src/index.d.ts b/packages/@react-types/calendar/src/index.d.ts index 6be0b924fc7..a2a5f1687c5 100644 --- a/packages/@react-types/calendar/src/index.d.ts +++ b/packages/@react-types/calendar/src/index.d.ts @@ -67,7 +67,7 @@ export interface CalendarPropsBase { * The day that starts the week, 0-6 (Sunday-Saturday). * @default 0 */ - firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 + firstDayOfWeek?: number } export type DateRange = RangeValue | null; diff --git a/packages/@react-types/datepicker/src/index.d.ts b/packages/@react-types/datepicker/src/index.d.ts index cd81436f4d9..9562bb9c8ef 100644 --- a/packages/@react-types/datepicker/src/index.d.ts +++ b/packages/@react-types/datepicker/src/index.d.ts @@ -76,7 +76,7 @@ interface DatePickerBase extends DateFieldBase, OverlayT * The day that starts the week, 0-6 (Sunday-Saturday). * @default 0 */ - firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 + firstDayOfWeek?: number } export interface AriaDatePickerBaseProps extends DatePickerBase, AriaLabelingProps, DOMProps {} From 237cdfb06bc7e84200fe5311f6d1b89ba0cf2f0c Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 15 Nov 2024 12:52:52 -0600 Subject: [PATCH 14/34] update type to enum --- .../@react-aria/calendar/docs/useCalendar.mdx | 4 +-- .../calendar/docs/useRangeCalendar.mdx | 4 +-- .../calendar/src/useCalendarGrid.ts | 26 +++++++++++++------ .../@react-aria/calendar/stories/Example.tsx | 2 +- .../datepicker/docs/useDatePicker.mdx | 4 +-- .../datepicker/docs/useDateRangePicker.mdx | 4 +-- .../calendar/chromatic/Calendar.stories.tsx | 2 +- .../chromatic/RangeCalendar.stories.tsx | 2 +- .../calendar/docs/Calendar.mdx | 4 +-- .../calendar/docs/RangeCalendar.mdx | 4 +-- .../calendar/src/CalendarBase.tsx | 2 +- .../calendar/src/CalendarCell.tsx | 4 +-- .../calendar/stories/Calendar.stories.tsx | 3 ++- .../stories/RangeCalendar.stories.tsx | 3 ++- .../calendar/test/CalendarBase.test.js | 4 +-- .../datepicker/docs/DatePicker.mdx | 4 +-- .../datepicker/docs/DateRangePicker.mdx | 4 +-- .../datepicker/src/DatePicker.tsx | 2 +- .../datepicker/src/DateRangePicker.tsx | 2 +- .../datepicker/stories/DatePicker.stories.tsx | 3 ++- .../calendar/src/useCalendarState.ts | 20 ++++++++++---- packages/@react-types/calendar/src/index.d.ts | 6 ++--- .../@react-types/datepicker/src/index.d.ts | 6 ++--- .../react-aria-components/docs/Calendar.mdx | 4 +-- .../react-aria-components/docs/DatePicker.mdx | 4 +-- .../docs/DateRangePicker.mdx | 4 +-- .../docs/RangeCalendar.mdx | 4 +-- 27 files changed, 79 insertions(+), 56 deletions(-) diff --git a/packages/@react-aria/calendar/docs/useCalendar.mdx b/packages/@react-aria/calendar/docs/useCalendar.mdx index a45aaffec7b..b1bc1119ed9 100644 --- a/packages/@react-aria/calendar/docs/useCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useCalendar.mdx @@ -460,10 +460,10 @@ The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDi ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` ### Labeling diff --git a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx index e5376e6c11f..9a627530d80 100644 --- a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx @@ -479,10 +479,10 @@ The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` ### Labeling diff --git a/packages/@react-aria/calendar/src/useCalendarGrid.ts b/packages/@react-aria/calendar/src/useCalendarGrid.ts index 852188bdb5d..5f8a3d9300c 100644 --- a/packages/@react-aria/calendar/src/useCalendarGrid.ts +++ b/packages/@react-aria/calendar/src/useCalendarGrid.ts @@ -12,12 +12,22 @@ import {CalendarDate, startOfWeek, today} from '@internationalized/date'; import {CalendarState, RangeCalendarState} from '@react-stately/calendar'; -import {clamp, mergeProps, useLabels} from '@react-aria/utils'; import {DOMAttributes} from '@react-types/shared'; import {hookData, useVisibleRangeDescription} from './utils'; import {KeyboardEvent, useMemo} from 'react'; +import {mergeProps, useLabels} from '@react-aria/utils'; import {useDateFormatter, useLocale} from '@react-aria/i18n'; +const DAY_MAP = { + Sun: 0, + Mon: 1, + Tue: 2, + Wed: 3, + Thu: 4, + Fri: 5, + Sat: 6 +}; + export interface AriaCalendarGridProps { /** * The first date displayed in the calendar grid. @@ -38,10 +48,10 @@ export interface AriaCalendarGridProps { */ weekdayStyle?: 'narrow' | 'short' | 'long', /** - * The day that starts the week, 0-6 (Sunday-Saturday). - * @default 0 + * The day that starts the week. + * @default 'Sun' */ - firstDayOfWeek?: number + firstDayOfWeek?: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' } export interface CalendarGridAria { @@ -61,7 +71,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 = 'Sun' } = props; let {direction} = useLocale(); @@ -142,14 +153,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 firstDayOfWeek = clamp(props.firstDayOfWeek ?? 0, 0, 6); - let weekStart = startOfWeek(today(state.timeZone), locale).add({days: firstDayOfWeek}); + let weekStart = startOfWeek(today(state.timeZone), locale).add({days: DAY_MAP[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, props.firstDayOfWeek]); + }, [locale, state.timeZone, dayFormatter, firstDayOfWeek]); return { gridProps: mergeProps(labelProps, { diff --git a/packages/@react-aria/calendar/stories/Example.tsx b/packages/@react-aria/calendar/stories/Example.tsx index aea02564871..c64849a1067 100644 --- a/packages/@react-aria/calendar/stories/Example.tsx +++ b/packages/@react-aria/calendar/stories/Example.tsx @@ -126,7 +126,7 @@ export function ExampleCustomFirstDay(props) { ); } -function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek: number}) { +function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat'}) { let {locale} = useLocale(); let {gridProps} = useCalendarGrid({firstDayOfWeek}, state); let startDate = state.visibleRange.start; diff --git a/packages/@react-aria/datepicker/docs/useDatePicker.mdx b/packages/@react-aria/datepicker/docs/useDatePicker.mdx index fa5ff0a1518..efa9f3199b1 100644 --- a/packages/@react-aria/datepicker/docs/useDatePicker.mdx +++ b/packages/@react-aria/datepicker/docs/useDatePicker.mdx @@ -693,8 +693,8 @@ By default, `useDatePicker` displays times in either 12 or 24 hour hour format d ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` \ No newline at end of file diff --git a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx index df1091995e3..ab1e600ec59 100644 --- a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx +++ b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx @@ -758,8 +758,8 @@ By default, `useDateRangePicker` displays times in either 12 or 24 hour hour for ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` \ No newline at end of file diff --git a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx index a23023290bf..0dec3d0f2ec 100644 --- a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx @@ -45,5 +45,5 @@ export const Invalid = () => ; export const ErrorMessage = () => ; export const UnavailableInvalid = () => d.compare(date) === 0} />; export const DisabledInvalid = () => ; -export const CustomWeekStartMonday = () => ; +export const CustomWeekStartMonday = () => ; export const CustomWeekStartSaturday = () => ; diff --git a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx index 984a4cc390b..a6077bb2ba4 100644 --- a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx @@ -69,5 +69,5 @@ export const NonContiguousInvalid = () => { ); }; -export const CustomWeekStartMonday = () => ; +export const CustomWeekStartMonday = () => ; export const CustomWeekStartSaturday = () => ; diff --git a/packages/@react-spectrum/calendar/docs/Calendar.mdx b/packages/@react-spectrum/calendar/docs/Calendar.mdx index 486a39b8bd8..a62df25085c 100644 --- a/packages/@react-spectrum/calendar/docs/Calendar.mdx +++ b/packages/@react-spectrum/calendar/docs/Calendar.mdx @@ -223,10 +223,10 @@ By default, when pressing the next or previous buttons, pagination will advance ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example
- +
``` diff --git a/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx b/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx index 47bbd2d3657..a3d173375ab 100644 --- a/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx +++ b/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx @@ -255,10 +255,10 @@ By default, when pressing the next or previous buttons, pagination will advance ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example
- +
``` diff --git a/packages/@react-spectrum/calendar/src/CalendarBase.tsx b/packages/@react-spectrum/calendar/src/CalendarBase.tsx index 5fd635dee9a..3f61c6e3484 100644 --- a/packages/@react-spectrum/calendar/src/CalendarBase.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarBase.tsx @@ -46,7 +46,7 @@ export function CalendarBase(props errorMessageProps, calendarRef: ref, visibleMonths = 1, - firstDayOfWeek = 0 + firstDayOfWeek = 'Sun' } = props; let {styleProps} = useStyleProps(props); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/calendar'); diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index 8255dc37139..3512a382774 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -24,10 +24,10 @@ import {useLocale} from '@react-aria/i18n'; interface CalendarCellProps extends AriaCalendarCellProps { state: CalendarState | RangeCalendarState, currentMonth: CalendarDate, - firstDayOfWeek?: number + firstDayOfWeek?: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' } -export function CalendarCell({state, currentMonth, firstDayOfWeek = 0, ...props}: CalendarCellProps) { +export function CalendarCell({state, currentMonth, firstDayOfWeek = 'Sun', ...props}: CalendarCellProps) { let ref = useRef(null); let { cellProps, diff --git a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx index 281b170f0b6..3874790bb22 100644 --- a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx @@ -81,7 +81,8 @@ export default { options: [null, 'single', 'visible'] }, firstDayOfWeek: { - control: 'number' + control: 'select', + options: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] }, isInvalid: { control: 'boolean' diff --git a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx index 1740817cd45..7967db82b09 100644 --- a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx @@ -75,7 +75,8 @@ export default { options: [null, 'single', 'visible'] }, firstDayOfWeek: { - control: 'number' + control: 'select', + options: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] }, isInvalid: { control: 'boolean' diff --git a/packages/@react-spectrum/calendar/test/CalendarBase.test.js b/packages/@react-spectrum/calendar/test/CalendarBase.test.js index 6d13a8bd362..7795bf8778c 100644 --- a/packages/@react-spectrum/calendar/test/CalendarBase.test.js +++ b/packages/@react-spectrum/calendar/test/CalendarBase.test.js @@ -794,8 +794,8 @@ describe('CalendarBase', () => { Name | Calendar | props ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} - `('$Name should override start of week with firstDayOfWeek={1}', ({Calendar, props}) => { - let {getAllByRole, getByRole} = render(); + `('$Name should override start of week with firstDayOfWeek="Mon"', ({Calendar, props}) => { + let {getAllByRole, getByRole} = render(); let grid = getByRole('grid'); let headers = getAllByRole('columnheader', {hidden: true}); diff --git a/packages/@react-spectrum/datepicker/docs/DatePicker.mdx b/packages/@react-spectrum/datepicker/docs/DatePicker.mdx index 9519a3263fe..7e5c1c642f7 100644 --- a/packages/@react-spectrum/datepicker/docs/DatePicker.mdx +++ b/packages/@react-spectrum/datepicker/docs/DatePicker.mdx @@ -443,10 +443,10 @@ By default, `DatePicker` displays times in either 12 or 24 hour hour format depe ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` ## Testing diff --git a/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx b/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx index 6a3a2cd7dc7..283cf8c32a5 100644 --- a/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx +++ b/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx @@ -480,10 +480,10 @@ By default, `DateRangePicker` displays times in either 12 or 24 hour hour format ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` ## Testing diff --git a/packages/@react-spectrum/datepicker/src/DatePicker.tsx b/packages/@react-spectrum/datepicker/src/DatePicker.tsx index 1d36b9e5871..0d9510b0247 100644 --- a/packages/@react-spectrum/datepicker/src/DatePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DatePicker.tsx @@ -48,7 +48,7 @@ function DatePicker(props: SpectrumDatePickerProps, ref: placeholderValue, maxVisibleMonths = 1, pageBehavior, - firstDayOfWeek = 0 + firstDayOfWeek = 'Sun' } = props; let {hoverProps, isHovered} = useHover({isDisabled}); let targetRef = useRef(undefined); diff --git a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx index a1effbb02b0..d0f0f637a8a 100644 --- a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx @@ -48,7 +48,7 @@ function DateRangePicker(props: SpectrumDateRangePickerProp placeholderValue, maxVisibleMonths = 1, pageBehavior, - firstDayOfWeek = 0 + firstDayOfWeek = 'Sun' } = props; let {hoverProps, isHovered} = useHover({isDisabled}); let targetRef = useRef(undefined); diff --git a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx index 3537c2072c5..40fb3a6edf4 100644 --- a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx @@ -184,7 +184,8 @@ export default { control: 'boolean' }, firstDayOfWeek: { - control: 'number' + control: 'select', + options: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] } } } as ComponentMeta; diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index 7ce86f7b1f5..8ed3d40f97f 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -29,10 +29,20 @@ import { } from '@internationalized/date'; import {CalendarProps, DateValue, MappedDateValue} from '@react-types/calendar'; import {CalendarState} from './types'; -import {clamp, useControlledState} from '@react-stately/utils'; +import {useControlledState} from '@react-stately/utils'; import {useMemo, useState} from 'react'; import {ValidationState} from '@react-types/shared'; +const DAY_MAP = { + Sun: 0, + Mon: 1, + Tue: 2, + Wed: 3, + Thu: 4, + Fri: 5, + Sat: 6 +}; + export interface CalendarStateOptions extends CalendarProps { /** The locale to display and edit the value according to. */ locale: string, @@ -66,7 +76,8 @@ export function useCalendarState(props: Calenda maxValue, selectionAlignment, isDateUnavailable, - pageBehavior = 'visible' + pageBehavior = 'visible', + firstDayOfWeek = 'Sun' } = props; let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]); @@ -329,13 +340,12 @@ export function useCalendarState(props: Calenda let dates: (CalendarDate | null)[] = []; let currentDayOfWeek = getDayOfWeek(date, locale); - let firstDayOfWeek = clamp(props.firstDayOfWeek ?? 0, 0, 6); - let daysToSubtract = (currentDayOfWeek - firstDayOfWeek + 7) % 7; + let daysToSubtract = (currentDayOfWeek - DAY_MAP[firstDayOfWeek] + 7) % 7; date = date.subtract({days: daysToSubtract}); // startOfWeek will clamp dates within the calendar system's valid range, which may // start in the middle of a week. In this case, add null placeholders. - let dayOfWeek = (getDayOfWeek(date, locale) - firstDayOfWeek + 7) % 7; + let dayOfWeek = (getDayOfWeek(date, locale) - DAY_MAP[firstDayOfWeek] + 7) % 7; for (let i = 0; i < dayOfWeek; i++) { dates.push(null); } diff --git a/packages/@react-types/calendar/src/index.d.ts b/packages/@react-types/calendar/src/index.d.ts index a2a5f1687c5..3c02637d478 100644 --- a/packages/@react-types/calendar/src/index.d.ts +++ b/packages/@react-types/calendar/src/index.d.ts @@ -64,10 +64,10 @@ export interface CalendarPropsBase { */ pageBehavior?: PageBehavior, /** - * The day that starts the week, 0-6 (Sunday-Saturday). - * @default 0 + * The day that starts the week. + * @default 'Sun' */ - firstDayOfWeek?: number + firstDayOfWeek?: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' } export type DateRange = RangeValue | null; diff --git a/packages/@react-types/datepicker/src/index.d.ts b/packages/@react-types/datepicker/src/index.d.ts index 9562bb9c8ef..f6b5d365b86 100644 --- a/packages/@react-types/datepicker/src/index.d.ts +++ b/packages/@react-types/datepicker/src/index.d.ts @@ -73,10 +73,10 @@ interface DatePickerBase extends DateFieldBase, OverlayT */ pageBehavior?: PageBehavior, /** - * The day that starts the week, 0-6 (Sunday-Saturday). - * @default 0 + * The day that starts the week. + * @default 'Sun' */ - firstDayOfWeek?: number + firstDayOfWeek?: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' } export interface AriaDatePickerBaseProps extends DatePickerBase, AriaLabelingProps, DOMProps {} diff --git a/packages/react-aria-components/docs/Calendar.mdx b/packages/react-aria-components/docs/Calendar.mdx index a6b4dfc919f..6b81ea01306 100644 --- a/packages/react-aria-components/docs/Calendar.mdx +++ b/packages/react-aria-components/docs/Calendar.mdx @@ -513,10 +513,10 @@ The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDi ## Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` ## Labeling diff --git a/packages/react-aria-components/docs/DatePicker.mdx b/packages/react-aria-components/docs/DatePicker.mdx index b2278f73fd5..f6a9279b471 100644 --- a/packages/react-aria-components/docs/DatePicker.mdx +++ b/packages/react-aria-components/docs/DatePicker.mdx @@ -713,10 +713,10 @@ By default, `DatePicker` displays times in either 12 or 24 hour hour format depe ## Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` ## Props diff --git a/packages/react-aria-components/docs/DateRangePicker.mdx b/packages/react-aria-components/docs/DateRangePicker.mdx index 0a427933d37..8aec0a49185 100644 --- a/packages/react-aria-components/docs/DateRangePicker.mdx +++ b/packages/react-aria-components/docs/DateRangePicker.mdx @@ -802,10 +802,10 @@ By default, `DateRangePicker` displays times in either 12 or 24 hour hour format ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` ## Props diff --git a/packages/react-aria-components/docs/RangeCalendar.mdx b/packages/react-aria-components/docs/RangeCalendar.mdx index 7c8cfe293b5..ba3bf1bc5a1 100644 --- a/packages/react-aria-components/docs/RangeCalendar.mdx +++ b/packages/react-aria-components/docs/RangeCalendar.mdx @@ -568,10 +568,10 @@ The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike ## Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to a number between 0 (Sunday) and 6 (Saturday). +By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. ```tsx example - + ``` ## Labeling From 11c86e3eea5c0842c8fdacb290a28dea5dd9248e Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 15 Nov 2024 12:57:17 -0600 Subject: [PATCH 15/34] update tests --- .../@react-aria/calendar/test/useCalendar.test.js | 14 +++++++------- .../calendar/chromatic/Calendar.stories.tsx | 2 +- .../calendar/chromatic/RangeCalendar.stories.tsx | 2 +- .../calendar/test/CalendarBase.test.js | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/@react-aria/calendar/test/useCalendar.test.js b/packages/@react-aria/calendar/test/useCalendar.test.js index f2ca99e8750..9c70c48d44e 100644 --- a/packages/@react-aria/calendar/test/useCalendar.test.js +++ b/packages/@react-aria/calendar/test/useCalendar.test.js @@ -239,13 +239,13 @@ describe('useCalendar', () => { it.each` Name | defaultValue | firstDayOfWeek | expectedFirstDay ${'default'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'Sunday, December 31, 2023'} - ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${0} | ${'Sunday, December 31, 2023'} - ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${1} | ${'Monday, January 1, 2024 selected'} - ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${2} | ${'Tuesday, December 26, 2023'} - ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${3} | ${'Wednesday, December 27, 2023'} - ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${4} | ${'Thursday, December 28, 2023'} - ${'Friday'} | ${new CalendarDate(2024, 1, 1)} | ${5} | ${'Friday, December 29, 2023'} - ${'Saturday'} | ${new CalendarDate(2024, 1, 1)} | ${6} | ${'Saturday, December 30, 2023'} + ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${'Sun'} | ${'Sunday, December 31, 2023'} + ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'Mon'} | ${'Monday, January 1, 2024 selected'} + ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${'Tue'} | ${'Tuesday, December 26, 2023'} + ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${'Wed'} | ${'Wednesday, December 27, 2023'} + ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${'Thu'} | ${'Thursday, December 28, 2023'} + ${'Friday'} | ${new CalendarDate(2024, 1, 1)} | ${'Fri'} | ${'Friday, December 29, 2023'} + ${'Saturday'} | ${new CalendarDate(2024, 1, 1)} | ${'Sat'} | ${'Saturday, December 30, 2023'} `('should use firstDayOfWeek $Name', async ({defaultValue, firstDayOfWeek, expectedFirstDay}) => { await testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay); }); diff --git a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx index 0dec3d0f2ec..49a69ba4fe0 100644 --- a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx @@ -46,4 +46,4 @@ export const ErrorMessage = () => d.compare(date) === 0} />; export const DisabledInvalid = () => ; export const CustomWeekStartMonday = () => ; -export const CustomWeekStartSaturday = () => ; +export const CustomWeekStartSaturday = () => ; diff --git a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx index a6077bb2ba4..b58d192cc18 100644 --- a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx @@ -70,4 +70,4 @@ export const NonContiguousInvalid = () => { }; export const CustomWeekStartMonday = () => ; -export const CustomWeekStartSaturday = () => ; +export const CustomWeekStartSaturday = () => ; diff --git a/packages/@react-spectrum/calendar/test/CalendarBase.test.js b/packages/@react-spectrum/calendar/test/CalendarBase.test.js index 7795bf8778c..f8242a7bd2b 100644 --- a/packages/@react-spectrum/calendar/test/CalendarBase.test.js +++ b/packages/@react-spectrum/calendar/test/CalendarBase.test.js @@ -809,8 +809,8 @@ describe('CalendarBase', () => { Name | Calendar | props ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} - `('$Name should override start of week with firstDayOfWeek={6}', ({Calendar, props}) => { - let {getAllByRole, getByRole} = render(); + `('$Name should override start of week with firstDayOfWeek="Sat"', ({Calendar, props}) => { + let {getAllByRole, getByRole} = render(); let grid = getByRole('grid'); let headers = getAllByRole('columnheader', {hidden: true}); From c16d42ec56ea74c0b443160be3ffe156a9d6fd11 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 15 Nov 2024 13:22:39 -0600 Subject: [PATCH 16/34] fix v3 cell style --- .../@react-spectrum/calendar/src/CalendarCell.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index 3512a382774..20b50a3ab60 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -21,6 +21,16 @@ import {useFocusRing} from '@react-aria/focus'; import {useHover} from '@react-aria/interactions'; import {useLocale} from '@react-aria/i18n'; +const DAY_MAP = { + Sun: 0, + Mon: 1, + Tue: 2, + Wed: 3, + Thu: 4, + Fri: 5, + Sat: 6 +}; + interface CalendarCellProps extends AriaCalendarCellProps { state: CalendarState | RangeCalendarState, currentMonth: CalendarDate, @@ -50,8 +60,8 @@ export function CalendarCell({state, currentMonth, firstDayOfWeek = 'Sun', ...pr let isSelectionEnd = isSelected && highlightedRange && isSameDay(props.date, highlightedRange.end); let {locale} = useLocale(); let dayOfWeek = getDayOfWeek(props.date, locale); - let isRangeStart = isSelected && (isFirstSelectedAfterDisabled || dayOfWeek === firstDayOfWeek || props.date.day === 1); - let isRangeEnd = isSelected && (isLastSelectedBeforeDisabled || ((dayOfWeek - firstDayOfWeek + 7) % 7) === 6 || props.date.day === currentMonth.calendar.getDaysInMonth(currentMonth)); + let isRangeStart = isSelected && (isFirstSelectedAfterDisabled || dayOfWeek === DAY_MAP[firstDayOfWeek] || props.date.day === 1); + let isRangeEnd = isSelected && (isLastSelectedBeforeDisabled || ((dayOfWeek - DAY_MAP[firstDayOfWeek] + 7) % 7) === 6 || props.date.day === currentMonth.calendar.getDaysInMonth(currentMonth)); let {focusProps, isFocusVisible} = useFocusRing(); let {hoverProps, isHovered} = useHover({isDisabled: isDisabled || isUnavailable || state.isReadOnly}); From b7f7185eea6e5a083ad38f46df4979d42f818bc7 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Sat, 16 Nov 2024 13:16:06 -0600 Subject: [PATCH 17/34] use lowercase and update docs --- .../@react-aria/calendar/docs/useCalendar.mdx | 4 ++-- .../calendar/docs/useRangeCalendar.mdx | 4 ++-- .../calendar/src/useCalendarGrid.ts | 19 +++++++++---------- .../@react-aria/calendar/stories/Example.tsx | 2 +- .../calendar/test/useCalendar.test.js | 14 +++++++------- .../datepicker/docs/useDatePicker.mdx | 4 ++-- .../datepicker/docs/useDateRangePicker.mdx | 4 ++-- .../calendar/chromatic/Calendar.stories.tsx | 4 ++-- .../chromatic/RangeCalendar.stories.tsx | 4 ++-- .../calendar/docs/Calendar.mdx | 4 ++-- .../calendar/docs/RangeCalendar.mdx | 4 ++-- .../calendar/src/CalendarBase.tsx | 2 +- .../calendar/src/CalendarCell.tsx | 18 +++++++++--------- .../calendar/stories/Calendar.stories.tsx | 2 +- .../stories/RangeCalendar.stories.tsx | 2 +- .../calendar/test/CalendarBase.test.js | 8 ++++---- .../datepicker/docs/DatePicker.mdx | 4 ++-- .../datepicker/docs/DateRangePicker.mdx | 4 ++-- .../datepicker/src/DatePicker.tsx | 2 +- .../datepicker/src/DateRangePicker.tsx | 2 +- .../datepicker/stories/DatePicker.stories.tsx | 2 +- .../calendar/src/useCalendarState.ts | 16 ++++++++-------- packages/@react-types/calendar/src/index.d.ts | 3 +-- .../@react-types/datepicker/src/index.d.ts | 3 +-- .../react-aria-components/docs/Calendar.mdx | 4 ++-- .../react-aria-components/docs/DatePicker.mdx | 4 ++-- .../docs/DateRangePicker.mdx | 4 ++-- .../docs/RangeCalendar.mdx | 4 ++-- 28 files changed, 74 insertions(+), 77 deletions(-) diff --git a/packages/@react-aria/calendar/docs/useCalendar.mdx b/packages/@react-aria/calendar/docs/useCalendar.mdx index b1bc1119ed9..467d397dea4 100644 --- a/packages/@react-aria/calendar/docs/useCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useCalendar.mdx @@ -460,10 +460,10 @@ The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDi ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` ### Labeling diff --git a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx index 9a627530d80..ee5fe7e2c6e 100644 --- a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx @@ -479,10 +479,10 @@ The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` ### Labeling diff --git a/packages/@react-aria/calendar/src/useCalendarGrid.ts b/packages/@react-aria/calendar/src/useCalendarGrid.ts index 5f8a3d9300c..5a17b35ea16 100644 --- a/packages/@react-aria/calendar/src/useCalendarGrid.ts +++ b/packages/@react-aria/calendar/src/useCalendarGrid.ts @@ -19,13 +19,13 @@ import {mergeProps, useLabels} from '@react-aria/utils'; import {useDateFormatter, useLocale} from '@react-aria/i18n'; const DAY_MAP = { - Sun: 0, - Mon: 1, - Tue: 2, - Wed: 3, - Thu: 4, - Fri: 5, - Sat: 6 + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6 }; export interface AriaCalendarGridProps { @@ -49,9 +49,8 @@ export interface AriaCalendarGridProps { weekdayStyle?: 'narrow' | 'short' | 'long', /** * The day that starts the week. - * @default 'Sun' */ - firstDayOfWeek?: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' + firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' } export interface CalendarGridAria { @@ -72,7 +71,7 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta let { startDate = state.visibleRange.start, endDate = state.visibleRange.end, - firstDayOfWeek = 'Sun' + firstDayOfWeek = 'sun' } = props; let {direction} = useLocale(); diff --git a/packages/@react-aria/calendar/stories/Example.tsx b/packages/@react-aria/calendar/stories/Example.tsx index c64849a1067..3ebaf9c3977 100644 --- a/packages/@react-aria/calendar/stories/Example.tsx +++ b/packages/@react-aria/calendar/stories/Example.tsx @@ -126,7 +126,7 @@ export function ExampleCustomFirstDay(props) { ); } -function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat'}) { +function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'}) { let {locale} = useLocale(); let {gridProps} = useCalendarGrid({firstDayOfWeek}, state); let startDate = state.visibleRange.start; diff --git a/packages/@react-aria/calendar/test/useCalendar.test.js b/packages/@react-aria/calendar/test/useCalendar.test.js index 9c70c48d44e..cc821eabf2d 100644 --- a/packages/@react-aria/calendar/test/useCalendar.test.js +++ b/packages/@react-aria/calendar/test/useCalendar.test.js @@ -239,13 +239,13 @@ describe('useCalendar', () => { it.each` Name | defaultValue | firstDayOfWeek | expectedFirstDay ${'default'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'Sunday, December 31, 2023'} - ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${'Sun'} | ${'Sunday, December 31, 2023'} - ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'Mon'} | ${'Monday, January 1, 2024 selected'} - ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${'Tue'} | ${'Tuesday, December 26, 2023'} - ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${'Wed'} | ${'Wednesday, December 27, 2023'} - ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${'Thu'} | ${'Thursday, December 28, 2023'} - ${'Friday'} | ${new CalendarDate(2024, 1, 1)} | ${'Fri'} | ${'Friday, December 29, 2023'} - ${'Saturday'} | ${new CalendarDate(2024, 1, 1)} | ${'Sat'} | ${'Saturday, December 30, 2023'} + ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${'sun'} | ${'Sunday, December 31, 2023'} + ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'Monday, January 1, 2024 selected'} + ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${'tue'} | ${'Tuesday, December 26, 2023'} + ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${'wed'} | ${'Wednesday, December 27, 2023'} + ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${'thu'} | ${'Thursday, December 28, 2023'} + ${'Friday'} | ${new CalendarDate(2024, 1, 1)} | ${'fri'} | ${'Friday, December 29, 2023'} + ${'Saturday'} | ${new CalendarDate(2024, 1, 1)} | ${'sat'} | ${'Saturday, December 30, 2023'} `('should use firstDayOfWeek $Name', async ({defaultValue, firstDayOfWeek, expectedFirstDay}) => { await testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay); }); diff --git a/packages/@react-aria/datepicker/docs/useDatePicker.mdx b/packages/@react-aria/datepicker/docs/useDatePicker.mdx index efa9f3199b1..723557bf57b 100644 --- a/packages/@react-aria/datepicker/docs/useDatePicker.mdx +++ b/packages/@react-aria/datepicker/docs/useDatePicker.mdx @@ -693,8 +693,8 @@ By default, `useDatePicker` displays times in either 12 or 24 hour hour format d ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` \ No newline at end of file diff --git a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx index ab1e600ec59..31114e9d17b 100644 --- a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx +++ b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx @@ -758,8 +758,8 @@ By default, `useDateRangePicker` displays times in either 12 or 24 hour hour for ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` \ No newline at end of file diff --git a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx index 49a69ba4fe0..e7b84703c2d 100644 --- a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx @@ -45,5 +45,5 @@ export const Invalid = () => ; export const ErrorMessage = () => ; export const UnavailableInvalid = () => d.compare(date) === 0} />; export const DisabledInvalid = () => ; -export const CustomWeekStartMonday = () => ; -export const CustomWeekStartSaturday = () => ; +export const CustomWeekStartMonday = () => ; +export const CustomWeekStartSaturday = () => ; diff --git a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx index b58d192cc18..4e71d2b573f 100644 --- a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx @@ -69,5 +69,5 @@ export const NonContiguousInvalid = () => { ); }; -export const CustomWeekStartMonday = () => ; -export const CustomWeekStartSaturday = () => ; +export const CustomWeekStartMonday = () => ; +export const CustomWeekStartSaturday = () => ; diff --git a/packages/@react-spectrum/calendar/docs/Calendar.mdx b/packages/@react-spectrum/calendar/docs/Calendar.mdx index a62df25085c..cdbb0f56eaf 100644 --- a/packages/@react-spectrum/calendar/docs/Calendar.mdx +++ b/packages/@react-spectrum/calendar/docs/Calendar.mdx @@ -223,10 +223,10 @@ By default, when pressing the next or previous buttons, pagination will advance ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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
- +
``` diff --git a/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx b/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx index a3d173375ab..13f034ed9de 100644 --- a/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx +++ b/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx @@ -255,10 +255,10 @@ By default, when pressing the next or previous buttons, pagination will advance ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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
- +
``` diff --git a/packages/@react-spectrum/calendar/src/CalendarBase.tsx b/packages/@react-spectrum/calendar/src/CalendarBase.tsx index 3f61c6e3484..91e43b174a9 100644 --- a/packages/@react-spectrum/calendar/src/CalendarBase.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarBase.tsx @@ -46,7 +46,7 @@ export function CalendarBase(props errorMessageProps, calendarRef: ref, visibleMonths = 1, - firstDayOfWeek = 'Sun' + firstDayOfWeek = 'sun' } = props; let {styleProps} = useStyleProps(props); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/calendar'); diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index 20b50a3ab60..56b84e4b4ac 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -22,22 +22,22 @@ import {useHover} from '@react-aria/interactions'; import {useLocale} from '@react-aria/i18n'; const DAY_MAP = { - Sun: 0, - Mon: 1, - Tue: 2, - Wed: 3, - Thu: 4, - Fri: 5, - Sat: 6 + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6 }; interface CalendarCellProps extends AriaCalendarCellProps { state: CalendarState | RangeCalendarState, currentMonth: CalendarDate, - firstDayOfWeek?: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' + firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' } -export function CalendarCell({state, currentMonth, firstDayOfWeek = 'Sun', ...props}: CalendarCellProps) { +export function CalendarCell({state, currentMonth, firstDayOfWeek = 'sun', ...props}: CalendarCellProps) { let ref = useRef(null); let { cellProps, diff --git a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx index 3874790bb22..25bb9a77d7f 100644 --- a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx @@ -82,7 +82,7 @@ export default { }, firstDayOfWeek: { control: 'select', - options: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + options: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] }, isInvalid: { control: 'boolean' diff --git a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx index 7967db82b09..d43ded1aeb6 100644 --- a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx @@ -76,7 +76,7 @@ export default { }, firstDayOfWeek: { control: 'select', - options: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + options: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] }, isInvalid: { control: 'boolean' diff --git a/packages/@react-spectrum/calendar/test/CalendarBase.test.js b/packages/@react-spectrum/calendar/test/CalendarBase.test.js index f8242a7bd2b..fd013c51448 100644 --- a/packages/@react-spectrum/calendar/test/CalendarBase.test.js +++ b/packages/@react-spectrum/calendar/test/CalendarBase.test.js @@ -794,8 +794,8 @@ describe('CalendarBase', () => { Name | Calendar | props ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} - `('$Name should override start of week with firstDayOfWeek="Mon"', ({Calendar, props}) => { - let {getAllByRole, getByRole} = render(); + `('$Name should override start of week with firstDayOfWeek="mon"', ({Calendar, props}) => { + let {getAllByRole, getByRole} = render(); let grid = getByRole('grid'); let headers = getAllByRole('columnheader', {hidden: true}); @@ -809,8 +809,8 @@ describe('CalendarBase', () => { Name | Calendar | props ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} - `('$Name should override start of week with firstDayOfWeek="Sat"', ({Calendar, props}) => { - let {getAllByRole, getByRole} = render(); + `('$Name should override start of week with firstDayOfWeek="sat"', ({Calendar, props}) => { + let {getAllByRole, getByRole} = render(); let grid = getByRole('grid'); let headers = getAllByRole('columnheader', {hidden: true}); diff --git a/packages/@react-spectrum/datepicker/docs/DatePicker.mdx b/packages/@react-spectrum/datepicker/docs/DatePicker.mdx index 7e5c1c642f7..508a63dd091 100644 --- a/packages/@react-spectrum/datepicker/docs/DatePicker.mdx +++ b/packages/@react-spectrum/datepicker/docs/DatePicker.mdx @@ -443,10 +443,10 @@ By default, `DatePicker` displays times in either 12 or 24 hour hour format depe ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` ## Testing diff --git a/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx b/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx index 283cf8c32a5..c8fb2b73024 100644 --- a/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx +++ b/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx @@ -480,10 +480,10 @@ By default, `DateRangePicker` displays times in either 12 or 24 hour hour format ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` ## Testing diff --git a/packages/@react-spectrum/datepicker/src/DatePicker.tsx b/packages/@react-spectrum/datepicker/src/DatePicker.tsx index 0d9510b0247..0c7b5ef008a 100644 --- a/packages/@react-spectrum/datepicker/src/DatePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DatePicker.tsx @@ -48,7 +48,7 @@ function DatePicker(props: SpectrumDatePickerProps, ref: placeholderValue, maxVisibleMonths = 1, pageBehavior, - firstDayOfWeek = 'Sun' + firstDayOfWeek = 'sun' } = props; let {hoverProps, isHovered} = useHover({isDisabled}); let targetRef = useRef(undefined); diff --git a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx index d0f0f637a8a..154415c745a 100644 --- a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx @@ -48,7 +48,7 @@ function DateRangePicker(props: SpectrumDateRangePickerProp placeholderValue, maxVisibleMonths = 1, pageBehavior, - firstDayOfWeek = 'Sun' + firstDayOfWeek = 'sun' } = props; let {hoverProps, isHovered} = useHover({isDisabled}); let targetRef = useRef(undefined); diff --git a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx index 40fb3a6edf4..821a88a1af4 100644 --- a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx @@ -185,7 +185,7 @@ export default { }, firstDayOfWeek: { control: 'select', - options: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + options: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] } } } as ComponentMeta; diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index 8ed3d40f97f..579a98d9afc 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -34,13 +34,13 @@ import {useMemo, useState} from 'react'; import {ValidationState} from '@react-types/shared'; const DAY_MAP = { - Sun: 0, - Mon: 1, - Tue: 2, - Wed: 3, - Thu: 4, - Fri: 5, - Sat: 6 + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6 }; export interface CalendarStateOptions extends CalendarProps { @@ -77,7 +77,7 @@ export function useCalendarState(props: Calenda selectionAlignment, isDateUnavailable, pageBehavior = 'visible', - firstDayOfWeek = 'Sun' + firstDayOfWeek = 'sun' } = props; let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]); diff --git a/packages/@react-types/calendar/src/index.d.ts b/packages/@react-types/calendar/src/index.d.ts index 3c02637d478..a39ffa6dab6 100644 --- a/packages/@react-types/calendar/src/index.d.ts +++ b/packages/@react-types/calendar/src/index.d.ts @@ -65,9 +65,8 @@ export interface CalendarPropsBase { pageBehavior?: PageBehavior, /** * The day that starts the week. - * @default 'Sun' */ - firstDayOfWeek?: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' + firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' } export type DateRange = RangeValue | null; diff --git a/packages/@react-types/datepicker/src/index.d.ts b/packages/@react-types/datepicker/src/index.d.ts index f6b5d365b86..8ecfc3ad30e 100644 --- a/packages/@react-types/datepicker/src/index.d.ts +++ b/packages/@react-types/datepicker/src/index.d.ts @@ -74,9 +74,8 @@ interface DatePickerBase extends DateFieldBase, OverlayT pageBehavior?: PageBehavior, /** * The day that starts the week. - * @default 'Sun' */ - firstDayOfWeek?: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' + firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' } export interface AriaDatePickerBaseProps extends DatePickerBase, AriaLabelingProps, DOMProps {} diff --git a/packages/react-aria-components/docs/Calendar.mdx b/packages/react-aria-components/docs/Calendar.mdx index 6b81ea01306..628f28f7794 100644 --- a/packages/react-aria-components/docs/Calendar.mdx +++ b/packages/react-aria-components/docs/Calendar.mdx @@ -513,10 +513,10 @@ The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDi ## Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` ## Labeling diff --git a/packages/react-aria-components/docs/DatePicker.mdx b/packages/react-aria-components/docs/DatePicker.mdx index f6a9279b471..0230d60cded 100644 --- a/packages/react-aria-components/docs/DatePicker.mdx +++ b/packages/react-aria-components/docs/DatePicker.mdx @@ -713,10 +713,10 @@ By default, `DatePicker` displays times in either 12 or 24 hour hour format depe ## Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` ## Props diff --git a/packages/react-aria-components/docs/DateRangePicker.mdx b/packages/react-aria-components/docs/DateRangePicker.mdx index 8aec0a49185..39e1564f4cd 100644 --- a/packages/react-aria-components/docs/DateRangePicker.mdx +++ b/packages/react-aria-components/docs/DateRangePicker.mdx @@ -802,10 +802,10 @@ By default, `DateRangePicker` displays times in either 12 or 24 hour hour format ### Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` ## Props diff --git a/packages/react-aria-components/docs/RangeCalendar.mdx b/packages/react-aria-components/docs/RangeCalendar.mdx index ba3bf1bc5a1..66d3863a9ef 100644 --- a/packages/react-aria-components/docs/RangeCalendar.mdx +++ b/packages/react-aria-components/docs/RangeCalendar.mdx @@ -568,10 +568,10 @@ The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike ## Custom first day of week -By default, the first day of the week is Sunday. This can be changed by setting the `firstDayOfWeek` prop to `'Sun'`, `'Mon'`, `'Tue'`, `'Wed'`, `'Thu'`, `'Fri'`, or `'Sat'`. +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 - + ``` ## Labeling From 158341eaa4bbb254662fa488622fd7531dc8c2e9 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 27 Nov 2024 11:28:58 -0600 Subject: [PATCH 18/34] update to use firstDayOfWeek day regardless of locale --- packages/@internationalized/date/src/index.ts | 3 +- .../@internationalized/date/src/queries.ts | 2 +- .../calendar/src/useCalendarGrid.ts | 7 ++-- .../@react-aria/calendar/stories/Example.tsx | 2 +- .../calendar/test/useCalendar.test.js | 39 ++++++++++++------- .../calendar/src/CalendarBase.tsx | 2 +- .../calendar/stories/Calendar.stories.tsx | 2 +- .../stories/RangeCalendar.stories.tsx | 2 +- .../datepicker/src/DatePicker.tsx | 2 +- .../datepicker/src/DateRangePicker.tsx | 2 +- .../datepicker/stories/DatePicker.stories.tsx | 2 +- .../calendar/src/useCalendarState.ts | 19 +++------ 12 files changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/@internationalized/date/src/index.ts b/packages/@internationalized/date/src/index.ts index 9fed618bc02..c2d91cc04f8 100644 --- a/packages/@internationalized/date/src/index.ts +++ b/packages/@internationalized/date/src/index.ts @@ -74,7 +74,8 @@ export { minDate, maxDate, isWeekend, - isWeekday + isWeekday, + getWeekStart } from './queries'; export { parseDate, diff --git a/packages/@internationalized/date/src/queries.ts b/packages/@internationalized/date/src/queries.ts index 1a50aab51e1..0ca43ababb0 100644 --- a/packages/@internationalized/date/src/queries.ts +++ b/packages/@internationalized/date/src/queries.ts @@ -225,7 +225,7 @@ function getRegion(locale: string): string | undefined { return part === 'u' ? undefined : part; } -function getWeekStart(locale: string): number { +export function getWeekStart(locale: string): number { // TODO: use Intl.Locale for this once browsers support the weekInfo property // https://github.com/tc39/proposal-intl-locale-info let region = getRegion(locale); diff --git a/packages/@react-aria/calendar/src/useCalendarGrid.ts b/packages/@react-aria/calendar/src/useCalendarGrid.ts index 5a17b35ea16..66fe9bd6b3f 100644 --- a/packages/@react-aria/calendar/src/useCalendarGrid.ts +++ b/packages/@react-aria/calendar/src/useCalendarGrid.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {CalendarDate, startOfWeek, today} from '@internationalized/date'; +import {CalendarDate, getWeekStart, startOfWeek, today} from '@internationalized/date'; import {CalendarState, RangeCalendarState} from '@react-stately/calendar'; import {DOMAttributes} from '@react-types/shared'; import {hookData, useVisibleRangeDescription} from './utils'; @@ -71,7 +71,7 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta let { startDate = state.visibleRange.start, endDate = state.visibleRange.end, - firstDayOfWeek = 'sun' + firstDayOfWeek } = props; let {direction} = useLocale(); @@ -152,7 +152,8 @@ 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).add({days: DAY_MAP[firstDayOfWeek]}); + let offset = firstDayOfWeek ? (DAY_MAP[firstDayOfWeek] - getWeekStart(locale) + 7) % 7 : 0; + let weekStart = startOfWeek(today(state.timeZone), locale).add({days: offset}); return [...new Array(7).keys()].map((index) => { let date = weekStart.add({days: index}); let dateDay = date.toDate(state.timeZone); diff --git a/packages/@react-aria/calendar/stories/Example.tsx b/packages/@react-aria/calendar/stories/Example.tsx index 3ebaf9c3977..88ca10532e1 100644 --- a/packages/@react-aria/calendar/stories/Example.tsx +++ b/packages/@react-aria/calendar/stories/Example.tsx @@ -126,7 +126,7 @@ export function ExampleCustomFirstDay(props) { ); } -function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'}) { +function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'}) { let {locale} = useLocale(); let {gridProps} = useCalendarGrid({firstDayOfWeek}, state); let startDate = state.visibleRange.start; diff --git a/packages/@react-aria/calendar/test/useCalendar.test.js b/packages/@react-aria/calendar/test/useCalendar.test.js index cc821eabf2d..cf51a46c2b6 100644 --- a/packages/@react-aria/calendar/test/useCalendar.test.js +++ b/packages/@react-aria/calendar/test/useCalendar.test.js @@ -13,6 +13,7 @@ import {act, pointerMap, render} from '@react-spectrum/test-utils-internal'; import {CalendarDate} from '@internationalized/date'; import {Example, ExampleCustomFirstDay} from '../stories/Example'; +import {I18nProvider} from '@react-aria/i18n'; import React from 'react'; import userEvent from '@testing-library/user-event'; @@ -63,8 +64,12 @@ describe('useCalendar', () => { unmount(); } - async function testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay) { - let {getAllByRole, unmount} = render(); + async function testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay, locale = 'en-US') { + let {getAllByRole, unmount} = render( + + + + ); let cells = getAllByRole('gridcell'); expect(cells[0].children[0]).toHaveAttribute('aria-label', expectedFirstDay); unmount(); @@ -237,17 +242,25 @@ describe('useCalendar', () => { describe('firstDayOfWeek', () => { it.each` - Name | defaultValue | firstDayOfWeek | expectedFirstDay - ${'default'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'Sunday, December 31, 2023'} - ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${'sun'} | ${'Sunday, December 31, 2023'} - ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'Monday, January 1, 2024 selected'} - ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${'tue'} | ${'Tuesday, December 26, 2023'} - ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${'wed'} | ${'Wednesday, December 27, 2023'} - ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${'thu'} | ${'Thursday, December 28, 2023'} - ${'Friday'} | ${new CalendarDate(2024, 1, 1)} | ${'fri'} | ${'Friday, December 29, 2023'} - ${'Saturday'} | ${new CalendarDate(2024, 1, 1)} | ${'sat'} | ${'Saturday, December 30, 2023'} - `('should use firstDayOfWeek $Name', async ({defaultValue, firstDayOfWeek, expectedFirstDay}) => { - await testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay); + Name | defaultValue | firstDayOfWeek | expectedFirstDay | locale + ${'default'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'Sunday, December 31, 2023'} | ${'en-US'} + ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${'sun'} | ${'Sunday, December 31, 2023'} | ${'en-US'} + ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'Monday, December 25, 2023'} | ${'en-US'} + ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${'tue'} | ${'Tuesday, December 26, 2023'} | ${'en-US'} + ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${'wed'} | ${'Wednesday, December 27, 2023'} | ${'en-US'} + ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${'thu'} | ${'Thursday, December 28, 2023'} | ${'en-US'} + ${'Friday'} | ${new CalendarDate(2024, 1, 1)} | ${'fri'} | ${'Friday, December 29, 2023'} | ${'en-US'} + ${'Saturday'} | ${new CalendarDate(2024, 1, 1)} | ${'sat'} | ${'Saturday, December 30, 2023'} | ${'en-US'} + ${'default (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'lundi 1 janvier 2024 sélectionné'} | ${'fr-FR'} + ${'Sunday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'sun'} | ${'dimanche 31 décembre 2023'} | ${'fr-FR'} + ${'Monday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'lundi 1 janvier 2024 sélectionné'} | ${'fr-FR'} + ${'Tuesday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'tue'} | ${'mardi 26 décembre 2023'} | ${'fr-FR'} + ${'Wednesday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'wed'} | ${'mercredi 27 décembre 2023'} | ${'fr-FR'} + ${'Thursday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'thu'} | ${'jeudi 28 décembre 2023'} | ${'fr-FR'} + ${'Friday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'fri'} | ${'vendredi 29 décembre 2023'} | ${'fr-FR'} + ${'Saturday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'sat'} | ${'samedi 30 décembre 2023'} | ${'fr-FR'} + `('should use firstDayOfWeek $Name', async ({defaultValue, firstDayOfWeek, expectedFirstDay, locale}) => { + await testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay, locale); }); }); }); diff --git a/packages/@react-spectrum/calendar/src/CalendarBase.tsx b/packages/@react-spectrum/calendar/src/CalendarBase.tsx index 91e43b174a9..82e965fe82a 100644 --- a/packages/@react-spectrum/calendar/src/CalendarBase.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarBase.tsx @@ -46,7 +46,7 @@ export function CalendarBase(props errorMessageProps, calendarRef: ref, visibleMonths = 1, - firstDayOfWeek = 'sun' + firstDayOfWeek } = props; let {styleProps} = useStyleProps(props); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/calendar'); diff --git a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx index 25bb9a77d7f..03b3abbea0d 100644 --- a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx @@ -82,7 +82,7 @@ export default { }, firstDayOfWeek: { control: 'select', - options: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + options: [undefined, 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] }, isInvalid: { control: 'boolean' diff --git a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx index d43ded1aeb6..d4195943524 100644 --- a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx @@ -76,7 +76,7 @@ export default { }, firstDayOfWeek: { control: 'select', - options: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + options: [undefined, 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] }, isInvalid: { control: 'boolean' diff --git a/packages/@react-spectrum/datepicker/src/DatePicker.tsx b/packages/@react-spectrum/datepicker/src/DatePicker.tsx index 0c7b5ef008a..eef7e321c22 100644 --- a/packages/@react-spectrum/datepicker/src/DatePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DatePicker.tsx @@ -48,7 +48,7 @@ function DatePicker(props: SpectrumDatePickerProps, ref: placeholderValue, maxVisibleMonths = 1, pageBehavior, - firstDayOfWeek = 'sun' + firstDayOfWeek } = props; let {hoverProps, isHovered} = useHover({isDisabled}); let targetRef = useRef(undefined); diff --git a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx index 154415c745a..5aa5db9e753 100644 --- a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx @@ -48,7 +48,7 @@ function DateRangePicker(props: SpectrumDateRangePickerProp placeholderValue, maxVisibleMonths = 1, pageBehavior, - firstDayOfWeek = 'sun' + firstDayOfWeek } = props; let {hoverProps, isHovered} = useHover({isDisabled}); let targetRef = useRef(undefined); diff --git a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx index 821a88a1af4..ae6982c621a 100644 --- a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx @@ -185,7 +185,7 @@ export default { }, firstDayOfWeek: { control: 'select', - options: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + options: [undefined, 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] } } } as ComponentMeta; diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index 579a98d9afc..1b111733c57 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -19,6 +19,7 @@ import { endOfMonth, endOfWeek, getDayOfWeek, + getWeekStart, GregorianCalendar, isSameDay, startOfMonth, @@ -77,7 +78,7 @@ export function useCalendarState(props: Calenda selectionAlignment, isDateUnavailable, pageBehavior = 'visible', - firstDayOfWeek = 'sun' + firstDayOfWeek } = props; let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]); @@ -335,21 +336,13 @@ export function useCalendarState(props: Calenda return isSameDay(next, endDate) || this.isInvalid(next); }, getDatesInWeek(weekIndex, from = startDate) { - // let date = startOfWeek(from, locale); + let weekStartDay = firstDayOfWeek !== undefined ? DAY_MAP[firstDayOfWeek] : getWeekStart(locale); let date = from.add({weeks: weekIndex}); + let offset = (getWeekStart(locale) - weekStartDay + 7) % 7; + date = startOfWeek(date, locale); + date = date.subtract({days: offset}); let dates: (CalendarDate | null)[] = []; - let currentDayOfWeek = getDayOfWeek(date, locale); - let daysToSubtract = (currentDayOfWeek - DAY_MAP[firstDayOfWeek] + 7) % 7; - date = date.subtract({days: daysToSubtract}); - - // startOfWeek will clamp dates within the calendar system's valid range, which may - // start in the middle of a week. In this case, add null placeholders. - let dayOfWeek = (getDayOfWeek(date, locale) - DAY_MAP[firstDayOfWeek] + 7) % 7; - for (let i = 0; i < dayOfWeek; i++) { - dates.push(null); - } - while (dates.length < 7) { dates.push(date); let nextDate = date.add({days: 1}); From 363b9be621008563e1fe8203c1caa62198c85db7 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 27 Nov 2024 11:41:36 -0600 Subject: [PATCH 19/34] lint --- packages/@react-stately/calendar/src/useCalendarState.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index 1b111733c57..7bfbf3aa9c9 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -18,7 +18,6 @@ import { DateFormatter, endOfMonth, endOfWeek, - getDayOfWeek, getWeekStart, GregorianCalendar, isSameDay, From 94241a4296940db0b88e975ac19ff297593ee6f9 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 27 Nov 2024 13:43:23 -0600 Subject: [PATCH 20/34] fix cell style --- .../@react-spectrum/calendar/src/CalendarCell.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index 56b84e4b4ac..75fbbfba035 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -11,7 +11,7 @@ */ import {AriaCalendarCellProps, useCalendarCell} from '@react-aria/calendar'; -import {CalendarDate, getDayOfWeek, isSameDay, isSameMonth, isToday} from '@internationalized/date'; +import {CalendarDate, getDayOfWeek, getWeekStart, isSameDay, isSameMonth, isToday} from '@internationalized/date'; import {CalendarState, RangeCalendarState} from '@react-stately/calendar'; import {classNames} from '@react-spectrum/utils'; import {mergeProps} from '@react-aria/utils'; @@ -37,7 +37,7 @@ interface CalendarCellProps extends AriaCalendarCellProps { firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' } -export function CalendarCell({state, currentMonth, firstDayOfWeek = 'sun', ...props}: CalendarCellProps) { +export function CalendarCell({state, currentMonth, firstDayOfWeek, ...props}: CalendarCellProps) { let ref = useRef(null); let { cellProps, @@ -60,8 +60,11 @@ export function CalendarCell({state, currentMonth, firstDayOfWeek = 'sun', ...pr let isSelectionEnd = isSelected && highlightedRange && isSameDay(props.date, highlightedRange.end); let {locale} = useLocale(); let dayOfWeek = getDayOfWeek(props.date, locale); - let isRangeStart = isSelected && (isFirstSelectedAfterDisabled || dayOfWeek === DAY_MAP[firstDayOfWeek] || props.date.day === 1); - let isRangeEnd = isSelected && (isLastSelectedBeforeDisabled || ((dayOfWeek - DAY_MAP[firstDayOfWeek] + 7) % 7) === 6 || props.date.day === currentMonth.calendar.getDaysInMonth(currentMonth)); + let weekStart = firstDayOfWeek !== undefined ? DAY_MAP[firstDayOfWeek] : getWeekStart(locale); + let isFirstDayOfWeek = dayOfWeek + getWeekStart(locale) === weekStart; + let isLastDayOfWeek = ((dayOfWeek + getWeekStart(locale) - weekStart + 7) % 7) === 6; + let isRangeStart = isSelected && (isFirstSelectedAfterDisabled || isFirstDayOfWeek || props.date.day === 1); + let isRangeEnd = isSelected && (isLastSelectedBeforeDisabled || isLastDayOfWeek || props.date.day === currentMonth.calendar.getDaysInMonth(currentMonth)); let {focusProps, isFocusVisible} = useFocusRing(); let {hoverProps, isHovered} = useHover({isDisabled: isDisabled || isUnavailable || state.isReadOnly}); From 37b329aeab783be51278925a9ede03f36b5ff79e Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 27 Nov 2024 14:50:21 -0600 Subject: [PATCH 21/34] more tests --- .../calendar/test/CalendarBase.test.js | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/packages/@react-spectrum/calendar/test/CalendarBase.test.js b/packages/@react-spectrum/calendar/test/CalendarBase.test.js index fd013c51448..190a572c2d5 100644 --- a/packages/@react-spectrum/calendar/test/CalendarBase.test.js +++ b/packages/@react-spectrum/calendar/test/CalendarBase.test.js @@ -791,11 +791,15 @@ describe('CalendarBase', () => { }); it.each` - Name | Calendar | props - ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} - ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} - `('$Name should override start of week with firstDayOfWeek="mon"', ({Calendar, props}) => { - let {getAllByRole, getByRole} = render(); + Name | Calendar | props | locale + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} | ${'en-US'}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} | ${'en-US'}} + `('$Name should override start of week with firstDayOfWeek="mon" (en-US)', ({Calendar, props, locale = 'en-US'}) => { + let {getAllByRole, getByRole} = render( + + + + ); let grid = getByRole('grid'); let headers = getAllByRole('columnheader', {hidden: true}); @@ -806,11 +810,15 @@ describe('CalendarBase', () => { }); it.each` - Name | Calendar | props - ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} - ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} - `('$Name should override start of week with firstDayOfWeek="sat"', ({Calendar, props}) => { - let {getAllByRole, getByRole} = render(); + Name | Calendar | props | locale + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} | ${'en-US'}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} | ${'en-US'}} + `('$Name should override start of week with firstDayOfWeek="sat" (en-US)', ({Calendar, props, locale}) => { + let {getAllByRole, getByRole} = render( + + + + ); let grid = getByRole('grid'); let headers = getAllByRole('columnheader', {hidden: true}); @@ -819,5 +827,43 @@ describe('CalendarBase', () => { let cells = within(grid).getAllByRole('gridcell'); expect(cells[2]).toHaveTextContent('1'); }); + + it.each` + Name | Calendar | props | locale + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} | ${'fr-FR'}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} | ${'fr-FR'}} + `('$Name should override start of week with firstDayOfWeek="mon" (fr-FR)', ({Calendar, props, locale}) => { + let {getAllByRole, getByRole} = render( + + + + ); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['L', 'M', 'M', 'J', 'V', 'S', 'D']); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells[0]).toHaveTextContent('1'); + }); + + it.each` + Name | Calendar | props | locale + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} | ${'fr-FR'}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} | ${'fr-FR'}} + `('$Name should override start of week with firstDayOfWeek="sat" (fr-FR)', ({Calendar, props, locale}) => { + let {getAllByRole, getByRole} = render( + + + + ); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['S', 'D', 'L', 'M', 'M', 'J', 'V']); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells[2]).toHaveTextContent('1'); + }); }); }); From 0f481096772ecda2dea0b718178f20478962754b Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 6 Dec 2024 11:27:40 -0600 Subject: [PATCH 22/34] update test --- packages/@react-aria/calendar/test/useCalendar.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/calendar/test/useCalendar.test.js b/packages/@react-aria/calendar/test/useCalendar.test.js index cf51a46c2b6..c2ca17a19ca 100644 --- a/packages/@react-aria/calendar/test/useCalendar.test.js +++ b/packages/@react-aria/calendar/test/useCalendar.test.js @@ -245,7 +245,7 @@ describe('useCalendar', () => { Name | defaultValue | firstDayOfWeek | expectedFirstDay | locale ${'default'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'Sunday, December 31, 2023'} | ${'en-US'} ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${'sun'} | ${'Sunday, December 31, 2023'} | ${'en-US'} - ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'Monday, December 25, 2023'} | ${'en-US'} + ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'January 1, 2024'} | ${'en-US'} ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${'tue'} | ${'Tuesday, December 26, 2023'} | ${'en-US'} ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${'wed'} | ${'Wednesday, December 27, 2023'} | ${'en-US'} ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${'thu'} | ${'Thursday, December 28, 2023'} | ${'en-US'} From 60b00a3c819f36c125c7d02d8a3d9e68face9924 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 6 Dec 2024 11:32:50 -0600 Subject: [PATCH 23/34] update getDatesInWeek logic --- .../calendar/src/useCalendarState.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index 7bfbf3aa9c9..3ee45bb72b1 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -18,6 +18,7 @@ import { DateFormatter, endOfMonth, endOfWeek, + getDayOfWeek, getWeekStart, GregorianCalendar, isSameDay, @@ -335,26 +336,25 @@ export function useCalendarState(props: Calenda return isSameDay(next, endDate) || this.isInvalid(next); }, getDatesInWeek(weekIndex, from = startDate) { - let weekStartDay = firstDayOfWeek !== undefined ? DAY_MAP[firstDayOfWeek] : getWeekStart(locale); let date = from.add({weeks: weekIndex}); - let offset = (getWeekStart(locale) - weekStartDay + 7) % 7; - date = startOfWeek(date, locale); - date = date.subtract({days: offset}); + if (firstDayOfWeek) { + let day = getDayOfWeek(date, locale); + let offset = (DAY_MAP[firstDayOfWeek] - getWeekStart(locale) + 7) % 7; + let diff = (7 + day - offset) % 7; + date = date.subtract({days: diff}); + } else { + date = startOfWeek(date, locale); + } + let dates: (CalendarDate | null)[] = []; - while (dates.length < 7) { - dates.push(date); - let nextDate = date.add({days: 1}); - if (isSameDay(date, nextDate)) { - // If the next day is the same, we have hit the end of the calendar system. - break; + for (let i = 0; i < 7; i++) { + let current = date.add({days: i}); + if (current.compare(startDate) < 0 || current.compare(endDate) > 0) { + dates.push(null); + } else { + dates.push(current); } - date = nextDate; - } - - // Add null placeholders if at the end of the calendar system. - while (dates.length < 7) { - dates.push(null); } return dates; From 932fac8d82afc8e645fe0d5ac1f09e893af5bacb Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Dec 2024 17:02:09 -0600 Subject: [PATCH 24/34] fix test --- packages/@react-aria/calendar/test/useCalendar.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/calendar/test/useCalendar.test.js b/packages/@react-aria/calendar/test/useCalendar.test.js index c2ca17a19ca..415219f2389 100644 --- a/packages/@react-aria/calendar/test/useCalendar.test.js +++ b/packages/@react-aria/calendar/test/useCalendar.test.js @@ -245,7 +245,7 @@ describe('useCalendar', () => { Name | defaultValue | firstDayOfWeek | expectedFirstDay | locale ${'default'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'Sunday, December 31, 2023'} | ${'en-US'} ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${'sun'} | ${'Sunday, December 31, 2023'} | ${'en-US'} - ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'January 1, 2024'} | ${'en-US'} + ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'Monday, January 1, 2024 selected'} | ${'en-US'} ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${'tue'} | ${'Tuesday, December 26, 2023'} | ${'en-US'} ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${'wed'} | ${'Wednesday, December 27, 2023'} | ${'en-US'} ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${'thu'} | ${'Thursday, December 28, 2023'} | ${'en-US'} From cdfc45dc2fb4783c677dd197499e8c889d9a6ec7 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Dec 2024 17:03:46 -0600 Subject: [PATCH 25/34] fix null dates --- .../calendar/src/useCalendarState.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index dae574b45b4..07ac44981e5 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -339,6 +339,8 @@ export function useCalendarState(props: Calenda }, getDatesInWeek(weekIndex, from = startDate) { let date = from.add({weeks: weekIndex}); + let dates: (CalendarDate | null)[] = []; + if (firstDayOfWeek) { let day = getDayOfWeek(date, locale); let offset = (DAY_MAP[firstDayOfWeek] - getWeekStart(locale) + 7) % 7; @@ -348,15 +350,19 @@ export function useCalendarState(props: Calenda date = startOfWeek(date, locale); } - let dates: (CalendarDate | null)[] = []; - - for (let i = 0; i < 7; i++) { - let current = date.add({days: i}); - if (current.compare(startDate) < 0 || current.compare(endDate) > 0) { - dates.push(null); - } else { - dates.push(current); + while (dates.length < 7) { + dates.push(date); + let nextDate = date.add({days: 1}); + if (isSameDay(date, nextDate)) { + // If the next day is the same, we have hit the end of the calendar system. + break; } + date = nextDate; + } + + // Add null placeholders if at the end of the calendar system. + while (dates.length < 7) { + dates.push(null); } return dates; From 48c3dc00a659e2535ca2ca2abf769cf6d162c2f3 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Dec 2024 17:43:33 -0600 Subject: [PATCH 26/34] fix minimum dates case --- packages/@react-stately/calendar/src/useCalendarState.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index 07ac44981e5..fe721077c88 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -348,6 +348,11 @@ export function useCalendarState(props: Calenda date = date.subtract({days: diff}); } else { date = startOfWeek(date, locale); + + let dayOfWeek = getDayOfWeek(date, locale); + for (let i = 0; i < dayOfWeek; i++) { + dates.push(null); + } } while (dates.length < 7) { From 80409f7d3851e3341f653241f49e2414d91eced0 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Dec 2024 13:06:26 -0600 Subject: [PATCH 27/34] add offset logic to startOfWeek --- .../@internationalized/date/src/queries.ts | 27 +++++++++++++++---- .../calendar/src/useCalendarGrid.ts | 3 +-- .../calendar/src/useCalendarState.ts | 13 ++++----- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/@internationalized/date/src/queries.ts b/packages/@internationalized/date/src/queries.ts index 0ca43ababb0..71accb58185 100644 --- a/packages/@internationalized/date/src/queries.ts +++ b/packages/@internationalized/date/src/queries.ts @@ -180,13 +180,30 @@ export function getMinimumDayInMonth(date: AnyCalendarDate) { return 1; } +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 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 { +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); + if (firstDayOfWeek) { + let offset = (DAY_MAP[firstDayOfWeek] - getWeekStart(locale) + 7) % 7; + let diff = (7 + dayOfWeek - offset) % 7; + return date.subtract({days: diff}); + } return date.subtract({days: dayOfWeek}); } diff --git a/packages/@react-aria/calendar/src/useCalendarGrid.ts b/packages/@react-aria/calendar/src/useCalendarGrid.ts index 66fe9bd6b3f..5c304aa0222 100644 --- a/packages/@react-aria/calendar/src/useCalendarGrid.ts +++ b/packages/@react-aria/calendar/src/useCalendarGrid.ts @@ -152,8 +152,7 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta let dayFormatter = useDateFormatter({weekday: props.weekdayStyle || 'narrow', timeZone: state.timeZone}); let {locale} = useLocale(); let weekDays = useMemo(() => { - let offset = firstDayOfWeek ? (DAY_MAP[firstDayOfWeek] - getWeekStart(locale) + 7) % 7 : 0; - let weekStart = startOfWeek(today(state.timeZone), locale).add({days: offset}); + 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); diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index fe721077c88..fc595ea833d 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -340,15 +340,12 @@ export function useCalendarState(props: Calenda getDatesInWeek(weekIndex, from = startDate) { let date = from.add({weeks: weekIndex}); let dates: (CalendarDate | null)[] = []; - - if (firstDayOfWeek) { - let day = getDayOfWeek(date, locale); - let offset = (DAY_MAP[firstDayOfWeek] - getWeekStart(locale) + 7) % 7; - let diff = (7 + day - offset) % 7; - date = date.subtract({days: diff}); - } else { - date = startOfWeek(date, locale); + date = startOfWeek(date, locale, firstDayOfWeek); + + if (!firstDayOfWeek) { + // startOfWeek will clamp dates within the calendar system's valid range, which may + // start in the middle of a week. In this case, add null placeholders. let dayOfWeek = getDayOfWeek(date, locale); for (let i = 0; i < dayOfWeek; i++) { dates.push(null); From 38c0ed23dc6e8be30e5f50be36b1902017747109 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Dec 2024 13:21:10 -0600 Subject: [PATCH 28/34] lint --- packages/@react-aria/calendar/src/useCalendarGrid.ts | 12 +----------- .../@react-stately/calendar/src/useCalendarState.ts | 11 ----------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/@react-aria/calendar/src/useCalendarGrid.ts b/packages/@react-aria/calendar/src/useCalendarGrid.ts index 5c304aa0222..0da6e54feb8 100644 --- a/packages/@react-aria/calendar/src/useCalendarGrid.ts +++ b/packages/@react-aria/calendar/src/useCalendarGrid.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {CalendarDate, getWeekStart, startOfWeek, today} from '@internationalized/date'; +import {CalendarDate, startOfWeek, today} from '@internationalized/date'; import {CalendarState, RangeCalendarState} from '@react-stately/calendar'; import {DOMAttributes} from '@react-types/shared'; import {hookData, useVisibleRangeDescription} from './utils'; @@ -18,16 +18,6 @@ import {KeyboardEvent, useMemo} from 'react'; import {mergeProps, useLabels} from '@react-aria/utils'; import {useDateFormatter, useLocale} from '@react-aria/i18n'; -const DAY_MAP = { - sun: 0, - mon: 1, - tue: 2, - wed: 3, - thu: 4, - fri: 5, - sat: 6 -}; - export interface AriaCalendarGridProps { /** * The first date displayed in the calendar grid. diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index fc595ea833d..6fa4efc81a0 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -19,7 +19,6 @@ import { endOfMonth, endOfWeek, getDayOfWeek, - getWeekStart, GregorianCalendar, isSameDay, startOfMonth, @@ -34,16 +33,6 @@ import {useControlledState} from '@react-stately/utils'; import {useMemo, useState} from 'react'; import {ValidationState} from '@react-types/shared'; -const DAY_MAP = { - sun: 0, - mon: 1, - tue: 2, - wed: 3, - thu: 4, - fri: 5, - sat: 6 -}; - export interface CalendarStateOptions extends CalendarProps { /** The locale to display and edit the value according to. */ locale: string, From a6bf5a190e217d8841bbc174c094aeaaab83c708 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 6 Jan 2025 13:22:37 -0600 Subject: [PATCH 29/34] pass firstDayOfWeek into getWeeksInMonth --- .../@internationalized/date/src/queries.ts | 10 ++++--- .../@react-aria/calendar/docs/useCalendar.mdx | 2 +- .../calendar/docs/useRangeCalendar.mdx | 2 +- .../@react-aria/calendar/stories/Example.tsx | 2 +- .../datepicker/docs/useDateRangePicker.mdx | 2 +- .../calendar/src/CalendarMonth.tsx | 2 +- .../calendar/test/CalendarBase.test.js | 27 +++++++++++++++++++ .../react-aria-components/src/Calendar.tsx | 13 +++++---- 8 files changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/@internationalized/date/src/queries.ts b/packages/@internationalized/date/src/queries.ts index 71accb58185..d732d7804c3 100644 --- a/packages/@internationalized/date/src/queries.ts +++ b/packages/@internationalized/date/src/queries.ts @@ -250,9 +250,13 @@ export function getWeekStart(locale: string): number { } /** Returns the number of weeks in the given month and locale. */ -export function getWeeksInMonth(date: DateValue, locale: string): number { - let days = date.calendar.getDaysInMonth(date); - return Math.ceil((getDayOfWeek(startOfMonth(date), locale) + days) / 7); +export function getWeeksInMonth(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number { + let monthStart = startOfMonth(date); + let firstVisibleDay = startOfWeek(monthStart, locale, firstDayOfWeek); + let monthEnd = endOfMonth(date); + let lastVisibleDay = startOfWeek(monthEnd, locale, firstDayOfWeek).add({days: 6}); + let totalDays = lastVisibleDay.calendar.toJulianDay(lastVisibleDay) - firstVisibleDay.calendar.toJulianDay(firstVisibleDay) + 1; + return Math.ceil(totalDays / 7); } /** Returns the lesser of the two provider dates. */ diff --git a/packages/@react-aria/calendar/docs/useCalendar.mdx b/packages/@react-aria/calendar/docs/useCalendar.mdx index 467d397dea4..65d64177ebe 100644 --- a/packages/@react-aria/calendar/docs/useCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useCalendar.mdx @@ -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 ( diff --git a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx index ee5fe7e2c6e..f34e455c1af 100644 --- a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx @@ -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 (
diff --git a/packages/@react-aria/calendar/stories/Example.tsx b/packages/@react-aria/calendar/stories/Example.tsx index 88ca10532e1..0f93b75f6d1 100644 --- a/packages/@react-aria/calendar/stories/Example.tsx +++ b/packages/@react-aria/calendar/stories/Example.tsx @@ -130,7 +130,7 @@ function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarSt let {locale} = useLocale(); let {gridProps} = useCalendarGrid({firstDayOfWeek}, state); let startDate = state.visibleRange.start; - let weeksInMonth = getWeeksInMonth(startDate, locale); + let weeksInMonth = getWeeksInMonth(startDate, locale, firstDayOfWeek); return (
{[...new Array(weeksInMonth).keys()].map(weekIndex => ( diff --git a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx index bf38182eeb7..50d20d7b25d 100644 --- a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx +++ b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx @@ -369,7 +369,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 (
diff --git a/packages/@react-spectrum/calendar/src/CalendarMonth.tsx b/packages/@react-spectrum/calendar/src/CalendarMonth.tsx index c77bff9b6a5..c81a8bf42a9 100644 --- a/packages/@react-spectrum/calendar/src/CalendarMonth.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarMonth.tsx @@ -42,7 +42,7 @@ export function CalendarMonth(props: CalendarMonthProps) { }, state); let {locale} = useLocale(); - let weeksInMonth = getWeeksInMonth(startDate, locale); + let weeksInMonth = getWeeksInMonth(startDate, locale, firstDayOfWeek); return (
{ let cells = within(grid).getAllByRole('gridcell'); expect(cells[2]).toHaveTextContent('1'); }); + + it.each` + Name | Calendar | props + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2025, 1, 1)}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2025, 1, 1), end: new CalendarDate(2025, 1, 1)}}} + `('should render enough weeks to display all days in month for firstDayOfWeek', ({Calendar, props}) => { + let {getAllByRole, getByRole} = render( + + + + ); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['T', 'F', 'S', 'S', 'M', 'T', 'W']); + + let rows = within(grid).getAllByRole('row'); + expect(rows).toHaveLength(6); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells).toHaveLength(42); + + expect(cells[35]).toHaveTextContent('30'); + expect(cells[36]).toHaveTextContent('31'); + expect(cells[cells.length - 1]).toHaveAttribute('aria-disabled', 'true'); + expect(cells[cells.length - 1]).toHaveTextContent('5'); + }); }); }); diff --git a/packages/react-aria-components/src/Calendar.tsx b/packages/react-aria-components/src/Calendar.tsx index 88f39a1a022..4f288e2d204 100644 --- a/packages/react-aria-components/src/Calendar.tsx +++ b/packages/react-aria-components/src/Calendar.tsx @@ -330,7 +330,8 @@ export interface CalendarGridProps extends StyleProps { interface InternalCalendarGridContextValue { headerProps: DOMAttributes, weekDays: string[], - startDate: CalendarDate + startDate: CalendarDate, + firstDayOfWeek: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | undefined } const InternalCalendarGridContext = createContext(null); @@ -350,15 +351,17 @@ export const CalendarGrid = /*#__PURE__*/ (forwardRef as forwardRefType)(functio startDate = startDate.add(props.offset); } + let firstDayOfWeek = calenderProps?.firstDayOfWeek ?? rangeCalenderProps?.firstDayOfWeek; + let {gridProps, headerProps, weekDays} = useCalendarGrid({ startDate, endDate: endOfMonth(startDate), weekdayStyle: props.weekdayStyle, - firstDayOfWeek: calenderProps?.firstDayOfWeek ?? rangeCalenderProps?.firstDayOfWeek + firstDayOfWeek }, state); return ( - +
Date: Wed, 8 Jan 2025 10:00:22 -0600 Subject: [PATCH 30/34] add firstDayOfWeek param to endOfWeek --- packages/@internationalized/date/src/queries.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@internationalized/date/src/queries.ts b/packages/@internationalized/date/src/queries.ts index d732d7804c3..e5da035ad16 100644 --- a/packages/@internationalized/date/src/queries.ts +++ b/packages/@internationalized/date/src/queries.ts @@ -208,12 +208,12 @@ export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: Da } /** 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(); @@ -254,7 +254,7 @@ export function getWeeksInMonth(date: DateValue, locale: string, firstDayOfWeek? let monthStart = startOfMonth(date); let firstVisibleDay = startOfWeek(monthStart, locale, firstDayOfWeek); let monthEnd = endOfMonth(date); - let lastVisibleDay = startOfWeek(monthEnd, locale, firstDayOfWeek).add({days: 6}); + let lastVisibleDay = endOfWeek(monthEnd, locale, firstDayOfWeek); let totalDays = lastVisibleDay.calendar.toJulianDay(lastVisibleDay) - firstVisibleDay.calendar.toJulianDay(firstVisibleDay) + 1; return Math.ceil(totalDays / 7); } From 6d061df28fb7067b8fef8d863d443a05045a5653 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 8 Jan 2025 14:06:19 -0600 Subject: [PATCH 31/34] add firstDayOfWeek to getDayOfWeek --- packages/@internationalized/date/src/index.ts | 3 +-- .../@internationalized/date/src/queries.ts | 17 ++++++++------- .../calendar/src/CalendarCell.tsx | 21 ++++--------------- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/packages/@internationalized/date/src/index.ts b/packages/@internationalized/date/src/index.ts index c2d91cc04f8..9fed618bc02 100644 --- a/packages/@internationalized/date/src/index.ts +++ b/packages/@internationalized/date/src/index.ts @@ -74,8 +74,7 @@ export { minDate, maxDate, isWeekend, - isWeekday, - getWeekStart + isWeekday } from './queries'; export { parseDate, diff --git a/packages/@internationalized/date/src/queries.ts b/packages/@internationalized/date/src/queries.ts index e5da035ad16..931ce37a19d 100644 --- a/packages/@internationalized/date/src/queries.ts +++ b/packages/@internationalized/date/src/queries.ts @@ -69,7 +69,7 @@ export function isToday(date: DateValue, timeZone: string): boolean { * 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 @@ -79,6 +79,12 @@ export function getDayOfWeek(date: DateValue, locale: string): number { dayOfWeek += 7; } + if (firstDayOfWeek) { + let offset = (DAY_MAP[firstDayOfWeek] - getWeekStart(locale) + 7) % 7; + let diff = (7 + dayOfWeek - offset) % 7; + dayOfWeek = diff; + } + return dayOfWeek; } @@ -198,12 +204,7 @@ export function startOfWeek(date: CalendarDateTime, locale: string, firstDayOfWe 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); - if (firstDayOfWeek) { - let offset = (DAY_MAP[firstDayOfWeek] - getWeekStart(locale) + 7) % 7; - let diff = (7 + dayOfWeek - offset) % 7; - return date.subtract({days: diff}); - } + let dayOfWeek = getDayOfWeek(date, locale, firstDayOfWeek); return date.subtract({days: dayOfWeek}); } @@ -242,7 +243,7 @@ function getRegion(locale: string): string | undefined { return part === 'u' ? undefined : part; } -export function getWeekStart(locale: string): number { +function getWeekStart(locale: string): number { // TODO: use Intl.Locale for this once browsers support the weekInfo property // https://github.com/tc39/proposal-intl-locale-info let region = getRegion(locale); diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index 75fbbfba035..4f391a7489d 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -11,7 +11,7 @@ */ import {AriaCalendarCellProps, useCalendarCell} from '@react-aria/calendar'; -import {CalendarDate, getDayOfWeek, getWeekStart, isSameDay, isSameMonth, isToday} from '@internationalized/date'; +import {CalendarDate, getDayOfWeek, isSameDay, isSameMonth, isToday} from '@internationalized/date'; import {CalendarState, RangeCalendarState} from '@react-stately/calendar'; import {classNames} from '@react-spectrum/utils'; import {mergeProps} from '@react-aria/utils'; @@ -21,16 +21,6 @@ import {useFocusRing} from '@react-aria/focus'; import {useHover} from '@react-aria/interactions'; import {useLocale} from '@react-aria/i18n'; -const DAY_MAP = { - sun: 0, - mon: 1, - tue: 2, - wed: 3, - thu: 4, - fri: 5, - sat: 6 -}; - interface CalendarCellProps extends AriaCalendarCellProps { state: CalendarState | RangeCalendarState, currentMonth: CalendarDate, @@ -59,12 +49,9 @@ export function CalendarCell({state, currentMonth, firstDayOfWeek, ...props}: Ca let isSelectionStart = isSelected && highlightedRange && isSameDay(props.date, highlightedRange.start); let isSelectionEnd = isSelected && highlightedRange && isSameDay(props.date, highlightedRange.end); let {locale} = useLocale(); - let dayOfWeek = getDayOfWeek(props.date, locale); - let weekStart = firstDayOfWeek !== undefined ? DAY_MAP[firstDayOfWeek] : getWeekStart(locale); - let isFirstDayOfWeek = dayOfWeek + getWeekStart(locale) === weekStart; - let isLastDayOfWeek = ((dayOfWeek + getWeekStart(locale) - weekStart + 7) % 7) === 6; - let isRangeStart = isSelected && (isFirstSelectedAfterDisabled || isFirstDayOfWeek || props.date.day === 1); - let isRangeEnd = isSelected && (isLastSelectedBeforeDisabled || isLastDayOfWeek || props.date.day === currentMonth.calendar.getDaysInMonth(currentMonth)); + let dayOfWeek = getDayOfWeek(props.date, locale, firstDayOfWeek); + let isRangeStart = isSelected && (isFirstSelectedAfterDisabled || dayOfWeek === 0 || props.date.day === 1); + let isRangeEnd = isSelected && (isLastSelectedBeforeDisabled || dayOfWeek === 6 || props.date.day === currentMonth.calendar.getDaysInMonth(currentMonth)); let {focusProps, isFocusVisible} = useFocusRing(); let {hoverProps, isHovered} = useHover({isDisabled: isDisabled || isUnavailable || state.isReadOnly}); From 34a34421e24501de4201023d66a0fdc5b6592541 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 8 Jan 2025 15:26:02 -0600 Subject: [PATCH 32/34] pass firstDayOfWeek to getDayOfWeek --- .../@react-stately/calendar/src/useCalendarState.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index 6fa4efc81a0..4b7b8163caa 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -332,13 +332,11 @@ export function useCalendarState(props: Calenda date = startOfWeek(date, locale, firstDayOfWeek); - if (!firstDayOfWeek) { - // startOfWeek will clamp dates within the calendar system's valid range, which may - // start in the middle of a week. In this case, add null placeholders. - let dayOfWeek = getDayOfWeek(date, locale); - for (let i = 0; i < dayOfWeek; i++) { - dates.push(null); - } + // startOfWeek will clamp dates within the calendar system's valid range, which may + // start in the middle of a week. In this case, add null placeholders. + let dayOfWeek = getDayOfWeek(date, locale, firstDayOfWeek); + for (let i = 0; i < dayOfWeek; i++) { + dates.push(null); } while (dates.length < 7) { From e631c510f17a62a53311353f08df0a73e850ff28 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 9 Jan 2025 13:26:40 -0500 Subject: [PATCH 33/34] slight simplification and add tests --- .../@internationalized/date/src/queries.ts | 41 ++++++++----------- .../date/tests/queries.test.js | 30 +++++++++++++- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/packages/@internationalized/date/src/queries.ts b/packages/@internationalized/date/src/queries.ts index 931ce37a19d..dcf3e8ad0af 100644 --- a/packages/@internationalized/date/src/queries.ts +++ b/packages/@internationalized/date/src/queries.ts @@ -64,6 +64,18 @@ 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, @@ -74,17 +86,12 @@ export function getDayOfWeek(date: DateValue, locale: string, firstDayOfWeek?: D // 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; } - if (firstDayOfWeek) { - let offset = (DAY_MAP[firstDayOfWeek] - getWeekStart(locale) + 7) % 7; - let diff = (7 + dayOfWeek - offset) % 7; - dayOfWeek = diff; - } - return dayOfWeek; } @@ -186,18 +193,6 @@ export function getMinimumDayInMonth(date: AnyCalendarDate) { return 1; } -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 first date of the week for the given date and locale. */ export function startOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime; export function startOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime; @@ -252,12 +247,8 @@ function getWeekStart(locale: string): number { /** Returns the number of weeks in the given month and locale. */ export function getWeeksInMonth(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number { - let monthStart = startOfMonth(date); - let firstVisibleDay = startOfWeek(monthStart, locale, firstDayOfWeek); - let monthEnd = endOfMonth(date); - let lastVisibleDay = endOfWeek(monthEnd, locale, firstDayOfWeek); - let totalDays = lastVisibleDay.calendar.toJulianDay(lastVisibleDay) - firstVisibleDay.calendar.toJulianDay(firstVisibleDay) + 1; - return Math.ceil(totalDays / 7); + let days = date.calendar.getDaysInMonth(date); + return Math.ceil((getDayOfWeek(startOfMonth(date), locale, firstDayOfWeek) + days) / 7); } /** Returns the lesser of the two provider dates. */ diff --git a/packages/@internationalized/date/tests/queries.test.js b/packages/@internationalized/date/tests/queries.test.js index 8db66b89e58..e13b0e03127 100644 --- a/packages/@internationalized/date/tests/queries.test.js +++ b/packages/@internationalized/date/tests/queries.test.js @@ -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 () { @@ -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 () { @@ -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 () { From 32f9e252a5105ec0022b8f70777973b1b6c52345 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 9 Jan 2025 13:37:54 -0500 Subject: [PATCH 34/34] add docs --- .../date/docs/CalendarDate.mdx | 18 ++++++++++++++++++ .../date/docs/CalendarDateTime.mdx | 18 ++++++++++++++++++ .../date/docs/ZonedDateTime.mdx | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/packages/@internationalized/date/docs/CalendarDate.mdx b/packages/@internationalized/date/docs/CalendarDate.mdx index dea35217c58..ab51b533a97 100644 --- a/packages/@internationalized/date/docs/CalendarDate.mdx +++ b/packages/@internationalized/date/docs/CalendarDate.mdx @@ -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 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 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 and 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); 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 +``` diff --git a/packages/@internationalized/date/docs/CalendarDateTime.mdx b/packages/@internationalized/date/docs/CalendarDateTime.mdx index 946e126cef6..3796777ed49 100644 --- a/packages/@internationalized/date/docs/CalendarDateTime.mdx +++ b/packages/@internationalized/date/docs/CalendarDateTime.mdx @@ -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 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 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 and 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); 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 +``` diff --git a/packages/@internationalized/date/docs/ZonedDateTime.mdx b/packages/@internationalized/date/docs/ZonedDateTime.mdx index 1bb2c0ef398..543f89db6dd 100644 --- a/packages/@internationalized/date/docs/ZonedDateTime.mdx +++ b/packages/@internationalized/date/docs/ZonedDateTime.mdx @@ -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 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 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 and 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]'); 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 +```