Skip to content

Commit 83cd95d

Browse files
authored
Bug fixes for DatePicker and Calendar (#2964)
1 parent 1a00eaa commit 83cd95d

File tree

26 files changed

+358
-282
lines changed

26 files changed

+358
-282
lines changed

packages/@adobe/spectrum-css-temp/components/calendar/index.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ governing permissions and limitations under the License.
9090
justify-content: flex-end;
9191
height: 100%;
9292

93-
width: var(--spectrum-calendar-day-width);
93+
width: 100%;
9494

9595
border-bottom: none !important; /* override abbr styling from normalize.css */
9696

@@ -207,6 +207,7 @@ governing permissions and limitations under the License.
207207
height: 2px;
208208
transform: rotate(-16deg);
209209
border-radius: 1px;
210+
background: currentColor;
210211
}
211212
}
212213
}

packages/@adobe/spectrum-css-temp/components/calendar/skin.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,5 @@ governing permissions and limitations under the License.
120120
&.is-today {
121121
--background: transparent;
122122
}
123-
124-
& .spectrum-Calendar-dateText span:after {
125-
background: var(--spectrum-global-color-gray-600);
126-
}
127123
}
128124
}

packages/@internationalized/date/src/calendars/BuddhistCalendar.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,22 @@ export class BuddhistCalendar extends GregorianCalendar {
3535
}
3636

3737
toJulianDay(date: AnyCalendarDate) {
38-
return super.toJulianDay(
39-
new CalendarDate(
40-
date.year + BUDDHIST_ERA_START,
41-
date.month,
42-
date.day
43-
)
44-
);
38+
return super.toJulianDay(toGregorian(date));
4539
}
4640

4741
getEras() {
4842
return ['BE'];
4943
}
44+
45+
getDaysInMonth(date: AnyCalendarDate): number {
46+
return super.getDaysInMonth(toGregorian(date));
47+
}
48+
}
49+
50+
function toGregorian(date: AnyCalendarDate) {
51+
return new CalendarDate(
52+
date.year + BUDDHIST_ERA_START,
53+
date.month,
54+
date.day
55+
);
5056
}

packages/@internationalized/date/src/calendars/JapaneseCalendar.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ export class JapaneseCalendar extends GregorianCalendar {
146146
return years;
147147
}
148148

149+
getDaysInMonth(date: AnyCalendarDate): number {
150+
return super.getDaysInMonth(toGregorian(date));
151+
}
152+
149153
getMinimumMonthInYear(date: AnyCalendarDate): number {
150154
let start = getMinimums(date);
151155
return start ? start[1] : 1;

packages/@internationalized/date/src/calendars/TaiwanCalendar.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,7 @@ export class TaiwanCalendar extends GregorianCalendar {
5252
}
5353

5454
toJulianDay(date: AnyCalendarDate) {
55-
return super.toJulianDay(
56-
new CalendarDate(
57-
gregorianYear(date),
58-
date.month,
59-
date.day
60-
)
61-
);
55+
return super.toJulianDay(toGregorian(date));
6256
}
6357

6458
getEras() {
@@ -72,4 +66,16 @@ export class TaiwanCalendar extends GregorianCalendar {
7266
getYearsToAdd(date: Mutable<AnyCalendarDate>, years: number) {
7367
return date.era === 'before_minguo' ? -years : years;
7468
}
69+
70+
getDaysInMonth(date: AnyCalendarDate): number {
71+
return super.getDaysInMonth(toGregorian(date));
72+
}
73+
}
74+
75+
function toGregorian(date: AnyCalendarDate) {
76+
return new CalendarDate(
77+
gregorianYear(date),
78+
date.month,
79+
date.day
80+
);
7581
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ describe('CalendarDate conversion', function () {
165165
date = new CalendarDate(2020, 4, 30);
166166
expect(toCalendar(date, new JapaneseCalendar())).toEqual(new CalendarDate(new JapaneseCalendar(), 'reiwa', 2, 4, 30));
167167
});
168+
169+
it('returns the correct number of days for leap and non-leap years', function () {
170+
let date = new CalendarDate(new JapaneseCalendar(), 'reiwa', 4, 2, 5);
171+
expect(date.calendar.getDaysInMonth(date)).toBe(28);
172+
173+
date = new CalendarDate(new JapaneseCalendar(), 'reiwa', 2, 2, 5);
174+
expect(date.calendar.getDaysInMonth(date)).toBe(29);
175+
});
168176
});
169177

170178
describe('taiwan', function () {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
120120
'aria-labelledby': calendarIds.get(state)
121121
});
122122

123-
let dayFormatter = useDateFormatter({weekday: 'narrow'});
124-
let dayFormatterLong = useDateFormatter({weekday: 'long'});
123+
let dayFormatter = useDateFormatter({weekday: 'narrow', timeZone: state.timeZone});
124+
let dayFormatterLong = useDateFormatter({weekday: 'long', timeZone: state.timeZone});
125125
let {locale} = useLocale();
126126
let weekStart = startOfWeek(state.visibleRange.start, locale);
127127
let weekDays = [...new Array(7).keys()].map((index) => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<
5353
let target = e.target as HTMLElement;
5454
let body = document.getElementById(res.calendarProps.id);
5555
if (
56-
(!body.contains(target) || target.getAttribute('role') !== 'button') &&
56+
(!body.contains(target) || !target.closest('[role="button"]')) &&
5757
!document.getElementById(res.nextButtonProps.id)?.contains(target) &&
5858
!document.getElementById(res.prevButtonProps.id)?.contains(target)
5959
) {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ export function useVisibleRangeDescription(startDate: CalendarDate, endDate: Cal
5050
month: 'long',
5151
year: 'numeric',
5252
era: startDate.calendar.identifier !== 'gregory' ? 'long' : undefined,
53-
calendar: startDate.calendar.identifier
53+
calendar: startDate.calendar.identifier,
54+
timeZone
5455
});
5556

5657
let dateFormatter = useDateFormatter({
5758
dateStyle: 'long',
58-
calendar: startDate.calendar.identifier
59+
calendar: startDate.calendar.identifier,
60+
timeZone
5961
});
6062

6163
return useMemo(() => {

packages/@react-aria/datepicker/src/useDateField.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import {AriaDatePickerProps, AriaTimeFieldProps, DateValue, TimeValue} from '@react-types/datepicker';
1414
import {createFocusManager, FocusManager} from '@react-aria/focus';
1515
import {DateFieldState} from '@react-stately/datepicker';
16-
import {focusManagerSymbol} from './useDateRangePicker';
1716
import {HTMLAttributes, RefObject, useEffect, useMemo, useRef} from 'react';
1817
import {mergeProps, useDescription} from '@react-aria/utils';
1918
import {useDatePickerGroup} from './useDatePickerGroup';
@@ -36,13 +35,19 @@ interface DateFieldAria {
3635

3736
// Data that is passed between useDateField and useDateSegment.
3837
interface HookData {
38+
ariaLabel: string,
3939
ariaLabelledBy: string,
4040
ariaDescribedBy: string,
4141
focusManager: FocusManager
4242
}
4343

4444
export const hookData = new WeakMap<DateFieldState, HookData>();
4545

46+
// Private props that we pass from useDatePicker/useDateRangePicker.
47+
// Ideally we'd use a Symbol for this, but React doesn't support them: https://github.com/facebook/react/issues/7552
48+
export const roleSymbol = '__role_' + Date.now();
49+
export const focusManagerSymbol = '__focusManager_' + Date.now();
50+
4651
/**
4752
* Provides the behavior and accessibility implementation for a date field component.
4853
* A date field allows users to enter and edit date and time values using a keyboard.
@@ -64,19 +69,41 @@ export function useDateField<T extends DateValue>(props: DateFieldProps<T>, stat
6469

6570
let descProps = useDescription(state.formatValue({month: 'long'}));
6671

67-
let segmentLabelledBy = fieldProps['aria-labelledby'] || fieldProps.id;
68-
let describedBy = [descProps['aria-describedby'], fieldProps['aria-describedby']].filter(Boolean).join(' ') || undefined;
72+
// If within a date picker or date range picker, the date field will have role="presentation" and an aria-describedby
73+
// will be passed in that references the value (e.g. entire range). Otherwise, add the field's value description.
74+
let describedBy = props[roleSymbol] === 'presentation'
75+
? fieldProps['aria-describedby']
76+
: [descProps['aria-describedby'], fieldProps['aria-describedby']].filter(Boolean).join(' ') || undefined;
6977
let propsFocusManager = props[focusManagerSymbol];
7078
let focusManager = useMemo(() => propsFocusManager || createFocusManager(ref), [propsFocusManager, ref]);
7179

80+
// Pass labels and other information to segments.
7281
hookData.set(state, {
73-
ariaLabelledBy: segmentLabelledBy,
82+
ariaLabel: props['aria-label'],
83+
ariaLabelledBy: [props['aria-labelledby'], labelProps.id].filter(Boolean).join(' ') || undefined,
7484
ariaDescribedBy: describedBy,
7585
focusManager
7686
});
7787

7888
let autoFocusRef = useRef(props.autoFocus);
7989

90+
// When used within a date picker or date range picker, the field gets role="presentation"
91+
// rather than role="group". Since the date picker/date range picker already has a role="group"
92+
// with a label and description, and the segments are already labeled by this as well, this
93+
// avoids very verbose duplicate announcements.
94+
let fieldDOMProps: HTMLAttributes<HTMLElement>;
95+
if (props[roleSymbol] === 'presentation') {
96+
fieldDOMProps = {
97+
role: 'presentation'
98+
};
99+
} else {
100+
fieldDOMProps = mergeProps(fieldProps, {
101+
role: 'group',
102+
'aria-disabled': props.isDisabled || undefined,
103+
'aria-describedby': describedBy
104+
});
105+
}
106+
80107
useEffect(() => {
81108
if (autoFocusRef.current) {
82109
focusManager.focusFirst();
@@ -91,11 +118,7 @@ export function useDateField<T extends DateValue>(props: DateFieldProps<T>, stat
91118
focusManager.focusFirst();
92119
}
93120
},
94-
fieldProps: mergeProps(fieldProps, descProps, groupProps, focusWithinProps, {
95-
role: 'group',
96-
'aria-disabled': props.isDisabled || undefined,
97-
'aria-describedby': describedBy
98-
}),
121+
fieldProps: mergeProps(fieldDOMProps, groupProps, focusWithinProps),
99122
descriptionProps,
100123
errorMessageProps
101124
};

0 commit comments

Comments
 (0)