Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions packages/@internationalized/date/src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayO
}

const cachedRegions = new Map<string, string>();
const cachedWeekInfo = new Map<string, {firstDay: number}>();

function getRegion(locale: string): string | undefined {
// If the Intl.Locale API is available, use it to get the region for the locale.
Expand Down Expand Up @@ -251,8 +252,47 @@ function getRegion(locale: string): string | undefined {
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);
return region ? weekStartData[region] || 0 : 0;
let weekInfo = cachedWeekInfo.get(locale);
if (!weekInfo) {
if (Intl.Locale) {
// @ts-ignore
let localeInst = new Intl.Locale(locale);
if ('getWeekInfo' in localeInst) {
// @ts-expect-error
weekInfo = localeInst.getWeekInfo();
if (weekInfo) {
cachedWeekInfo.set(locale, weekInfo);
return weekInfo.firstDay;
}
}
}
let region = getRegion(locale);
if (locale.includes('-fw-')) {
let day = locale.split('-fw-')[1];
if (day === 'mon') {
weekInfo = {firstDay: 1};
} else if (day === 'tue') {
weekInfo = {firstDay: 2};
} else if (day === 'wed') {
weekInfo = {firstDay: 3};
} else if (day === 'thu') {
weekInfo = {firstDay: 4};
} else if (day === 'fri') {
weekInfo = {firstDay: 5};
} else if (day === 'sat') {
weekInfo = {firstDay: 6};
} else {
weekInfo = {firstDay: 0};
}
} else if (locale.includes('u-ca-iso8601')) {
weekInfo = {firstDay: 1};
} else {
weekInfo = {firstDay: region ? weekStartData[region] || 0 : 0};
}
cachedWeekInfo.set(locale, weekInfo);
}

return weekInfo.firstDay;
}

/** Returns the number of weeks in the given month and locale. */
Expand Down
9 changes: 9 additions & 0 deletions packages/@internationalized/date/tests/queries.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ describe('queries', function () {
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'sun')).toEqual(new CalendarDate(2021, 8, 1));
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'thu')).toEqual(new CalendarDate(2021, 7, 29));
});

it('should return the start of the week in en-US-u-ca-iso8601', function () {
// start of week is monday
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US-u-ca-iso8601')).toEqual(new CalendarDate(2021, 8, 2));
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR-u-ca-iso8601')).toEqual(new CalendarDate(2021, 8, 2));

// override first day of week
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US-u-ca-iso8601-fw-tue')).toEqual(new CalendarDate(2021, 8, 3));
});
});

describe('endOfWeek', function () {
Expand Down
121 changes: 48 additions & 73 deletions packages/react-aria-components/stories/Calendar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@
* governing permissions and limitations under the License.
*/

import {Button, Calendar, CalendarCell, CalendarGrid, CalendarStateContext, Heading, RangeCalendar} from 'react-aria-components';
import {CalendarDate, parseDate} from '@internationalized/date';
import {Button, Calendar, CalendarCell, CalendarGrid, CalendarStateContext, DateValue, Heading, I18nProvider, RangeCalendar} from 'react-aria-components';
import {Meta, StoryObj} from '@storybook/react';
import React, {useContext} from 'react';
import './styles.css';
import {CalendarProps} from 'react-aria';

export default {
title: 'React Aria Components/Calendar',
component: Calendar
} as Meta<typeof Calendar>;

export type CalendarStory = StoryObj<typeof Calendar>;
export type RangeCalendarStory = StoryObj<typeof RangeCalendar>;


function Footer() {
const state = useContext(CalendarStateContext);
Expand Down Expand Up @@ -74,53 +74,61 @@ export const CalendarResetValue: CalendarStory = {
)
};

function CalendarMultiMonthExample(args) {
let defaultDate = new CalendarDate(2021, 7, 1);
let [focusedDate, setFocusedDate] = React.useState(defaultDate);

return (
<>
<button
style={{marginBottom: 20}}
onClick={() => setFocusedDate(defaultDate)}>
Reset focused date
</button>
<Calendar style={{width: 500}} visibleDuration={{months: 3}} focusedValue={focusedDate} onFocusChange={setFocusedDate} defaultValue={defaultDate} {...args}>
<div style={{display: 'flex', alignItems: 'center'}}>
<Button slot="previous">&lt;</Button>
<Heading style={{flex: 1, textAlign: 'center'}} />
<Button slot="next">&gt;</Button>
</div>
<div style={{display: 'flex', gap: 20}}>
<CalendarGrid style={{flex: 1}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({opacity: isOutsideMonth ? '0.5' : '', textAlign: 'center', cursor: 'default', background: isSelected && !isOutsideMonth ? 'blue' : ''})} />}
</CalendarGrid>
<CalendarGrid style={{flex: 1}} offset={{months: 1}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({opacity: isOutsideMonth ? '0.5' : '', textAlign: 'center', cursor: 'default', background: isSelected && !isOutsideMonth ? 'blue' : ''})} />}
</CalendarGrid>
<CalendarGrid style={{flex: 1}} offset={{months: 2}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({opacity: isOutsideMonth ? '0.5' : '', textAlign: 'center', cursor: 'default', background: isSelected && !isOutsideMonth ? 'blue' : ''})} />}
</CalendarGrid>
</div>
</Calendar>
</>
);
export const CalendarMultiMonth: CalendarStory = {
render: () => (
<Calendar style={{width: 500}} visibleDuration={{months: 2}}>
<div style={{display: 'flex', alignItems: 'center'}}>
<Button slot="previous">&lt;</Button>
<Heading style={{flex: 1, textAlign: 'center'}} />
<Button slot="next">&gt;</Button>
</div>
<div style={{display: 'flex', gap: 20}}>
<CalendarGrid style={{flex: 1}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({opacity: isOutsideMonth ? '0.5' : '', textAlign: 'center', cursor: 'default', background: isSelected && !isOutsideMonth ? 'blue' : ''})} />}
</CalendarGrid>
<CalendarGrid style={{flex: 1}} offset={{months: 1}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({opacity: isOutsideMonth ? '0.5' : '', textAlign: 'center', cursor: 'default', background: isSelected && !isOutsideMonth ? 'blue' : ''})} />}
</CalendarGrid>
</div>
</Calendar>
)
};

export const CalendarMultiMonth: CalendarStory = {
render: (args) => <CalendarMultiMonthExample {...args} />,
interface CalendarFirstDayOfWeekExampleProps extends CalendarProps<DateValue> {
locale: string
}

export const CalendarFirstDayOfWeekExample: StoryObj<CalendarFirstDayOfWeekExampleProps> = {
render: function Example(args) {
return (
<div>
<I18nProvider locale={args.locale}>
<Calendar style={{width: 220}}>
<div style={{display: 'flex', alignItems: 'center'}}>
<Button slot="previous">&lt;</Button>
<Heading style={{flex: 1, textAlign: 'center'}} />
<Button slot="next">&gt;</Button>
</div>
<CalendarGrid style={{width: '100%'}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({display: isOutsideMonth ? 'none' : '', textAlign: 'center', cursor: 'default', background: isSelected ? 'blue' : ''})} />}
</CalendarGrid>
</Calendar>
</I18nProvider>
</div>
);
},
args: {
selectionAlignment: 'center'
locale: 'en-US-u-ca-iso8601-fw-tue'
},
argTypes: {
selectionAlignment: {
locale: {
control: 'select',
options: ['start', 'center', 'end']
options: ['en-US-u-ca-iso8601-fw-tue', 'en-US-u-ca-iso8601', 'en-US', 'fr-FR-u-ca-iso8601-fw-tue', 'fr-FR-u-ca-iso8601', 'fr-FR']
}
}
};

export const RangeCalendarExample: RangeCalendarStory = {
export const RangeCalendarExample: CalendarStory = {
render: () => (
<RangeCalendar style={{width: 220}}>
<div style={{display: 'flex', alignItems: 'center'}}>
Expand All @@ -134,36 +142,3 @@ export const RangeCalendarExample: RangeCalendarStory = {
</RangeCalendar>
)
};


export const RangeCalendarMultiMonthExample: RangeCalendarStory = {
render: (args) => (
<RangeCalendar style={{width: 500}} visibleDuration={{months: 3}} defaultValue={{start: parseDate('2025-08-04'), end: parseDate('2025-08-10')}} {...args} >
<div style={{display: 'flex', alignItems: 'center'}}>
<Button slot="previous">&lt;</Button>
<Heading style={{flex: 1, textAlign: 'center'}} />
<Button slot="next">&gt;</Button>
</div>
<div style={{display: 'flex', gap: 20}}>
<CalendarGrid style={{flex: 1}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({display: isOutsideMonth ? 'none' : '', textAlign: 'center', cursor: 'default', background: isSelected ? 'blue' : ''})} />}
</CalendarGrid>
<CalendarGrid style={{flex: 1}} offset={{months: 1}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({display: isOutsideMonth ? 'none' : '', textAlign: 'center', cursor: 'default', background: isSelected ? 'blue' : ''})} />}
</CalendarGrid>
<CalendarGrid style={{flex: 1}} offset={{months: 2}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({display: isOutsideMonth ? 'none' : '', textAlign: 'center', cursor: 'default', background: isSelected ? 'blue' : ''})} />}
</CalendarGrid>
</div>
</RangeCalendar>
),
args: {
selectionAlignment: 'center'
},
argTypes: {
selectionAlignment: {
control: 'select',
options: ['start', 'center', 'end']
}
}
};