@react-aria/calendar 3.0.0-rc.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-aria/calendar",
3
- "version": "3.0.0-rc.0",
3
+ "version": "3.0.1",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
@@ -18,22 +18,22 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@babel/runtime": "^7.6.2",
21
- "@internationalized/date": "3.0.0-rc.0",
22
- "@react-aria/i18n": "^3.3.9",
23
- "@react-aria/interactions": "^3.8.4",
24
- "@react-aria/live-announcer": "^3.0.6",
25
- "@react-aria/utils": "^3.12.0",
26
- "@react-stately/calendar": "3.0.0-rc.0",
27
- "@react-types/button": "^3.4.5",
28
- "@react-types/calendar": "3.0.0-rc.0",
29
- "@react-types/shared": "^3.12.0"
21
+ "@internationalized/date": "^3.0.1",
22
+ "@react-aria/i18n": "^3.5.0",
23
+ "@react-aria/interactions": "^3.10.0",
24
+ "@react-aria/live-announcer": "^3.1.1",
25
+ "@react-aria/utils": "^3.13.2",
26
+ "@react-stately/calendar": "^3.0.1",
27
+ "@react-types/button": "^3.6.0",
28
+ "@react-types/calendar": "^3.0.1",
29
+ "@react-types/shared": "^3.14.0"
30
30
  },
31
31
  "peerDependencies": {
32
- "react": "^16.8.0 || ^17.0.0-rc.1",
33
- "react-dom": "^16.8.0 || ^17.0.0-rc.1"
32
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
33
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
- "gitHead": "6a503b715e0dbbf92038cd7f08b1bcdde4c78e82"
38
+ "gitHead": "cd7c0ec917122c7612f653c22f8ed558f8b66ecd"
39
39
  }
@@ -14,29 +14,30 @@ import {announce} from '@react-aria/live-announcer';
14
14
  import {AriaButtonProps} from '@react-types/button';
15
15
  import {CalendarPropsBase} from '@react-types/calendar';
16
16
  import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
17
+ import {DOMAttributes} from '@react-types/shared';
17
18
  import {DOMProps} from '@react-types/shared';
18
19
  import {filterDOMProps, mergeProps, useLabels, useSlotId, useUpdateEffect} from '@react-aria/utils';
19
20
  import {hookData, useSelectedDateDescription, useVisibleRangeDescription} from './utils';
20
- import {HTMLAttributes, useRef} from 'react';
21
21
  // @ts-ignore
22
22
  import intlMessages from '../intl/*.json';
23
- import {useMessageFormatter} from '@react-aria/i18n';
23
+ import {useLocalizedStringFormatter} from '@react-aria/i18n';
24
+ import {useRef} from 'react';
24
25
 
25
26
  export interface CalendarAria {
26
27
  /** Props for the calendar grouping element. */
27
- calendarProps: HTMLAttributes<HTMLElement>,
28
+ calendarProps: DOMAttributes,
28
29
  /** Props for the next button. */
29
30
  nextButtonProps: AriaButtonProps,
30
31
  /** Props for the previous button. */
31
32
  prevButtonProps: AriaButtonProps,
32
33
  /** Props for the error message element, if any. */
33
- errorMessageProps: HTMLAttributes<HTMLElement>,
34
+ errorMessageProps: DOMAttributes,
34
35
  /** A description of the visible date range, for use in the calendar title. */
35
36
  title: string
36
37
  }
37
38
 
38
39
  export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: CalendarState | RangeCalendarState): CalendarAria {
39
- let formatMessage = useMessageFormatter(intlMessages);
40
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
40
41
  let domProps = filterDOMProps(props);
41
42
 
42
43
  let title = useVisibleRangeDescription(state.visibleRange.start, state.visibleRange.end, state.timeZone, false);
@@ -97,14 +98,14 @@ export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: Cale
97
98
  }),
98
99
  nextButtonProps: {
99
100
  onPress: () => state.focusNextPage(),
100
- 'aria-label': formatMessage('next'),
101
+ 'aria-label': stringFormatter.format('next'),
101
102
  isDisabled: nextDisabled,
102
103
  onFocus: () => nextFocused.current = true,
103
104
  onBlur: () => nextFocused.current = false
104
105
  },
105
106
  prevButtonProps: {
106
107
  onPress: () => state.focusPreviousPage(),
107
- 'aria-label': formatMessage('previous'),
108
+ 'aria-label': stringFormatter.format('previous'),
108
109
  isDisabled: previousDisabled,
109
110
  onFocus: () => previousFocused.current = true,
110
111
  onBlur: () => previousFocused.current = false
@@ -12,14 +12,15 @@
12
12
 
13
13
  import {CalendarDate, isEqualDay, isSameDay, isToday} from '@internationalized/date';
14
14
  import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
15
- import {focusWithoutScrolling, useDescription} from '@react-aria/utils';
15
+ import {DOMAttributes} from '@react-types/shared';
16
+ import {focusWithoutScrolling, getScrollParent, scrollIntoView, useDescription} from '@react-aria/utils';
17
+ import {getEraFormat, hookData} from './utils';
16
18
  import {getInteractionModality, usePress} from '@react-aria/interactions';
17
- import {hookData} from './utils';
18
- import {HTMLAttributes, RefObject, useEffect, useMemo, useRef} from 'react';
19
19
  // @ts-ignore
20
20
  import intlMessages from '../intl/*.json';
21
21
  import {mergeProps} from '@react-aria/utils';
22
- import {useDateFormatter, useMessageFormatter} from '@react-aria/i18n';
22
+ import {RefObject, useEffect, useMemo, useRef} from 'react';
23
+ import {useDateFormatter, useLocalizedStringFormatter} from '@react-aria/i18n';
23
24
 
24
25
  export interface AriaCalendarCellProps {
25
26
  /** The date that this cell represents. */
@@ -33,9 +34,9 @@ export interface AriaCalendarCellProps {
33
34
 
34
35
  export interface CalendarCellAria {
35
36
  /** Props for the grid cell element (e.g. `<td>`). */
36
- cellProps: HTMLAttributes<HTMLElement>,
37
+ cellProps: DOMAttributes,
37
38
  /** Props for the button element within the cell. */
38
- buttonProps: HTMLAttributes<HTMLElement>,
39
+ buttonProps: DOMAttributes,
39
40
  /** Whether the cell is currently being pressed. */
40
41
  isPressed: boolean,
41
42
  /** Whether the cell is selected. */
@@ -75,13 +76,13 @@ export interface CalendarCellAria {
75
76
  export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarState | RangeCalendarState, ref: RefObject<HTMLElement>): CalendarCellAria {
76
77
  let {date, isDisabled} = props;
77
78
  let {errorMessageId, selectedDateDescription} = hookData.get(state);
78
- let formatMessage = useMessageFormatter(intlMessages);
79
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
79
80
  let dateFormatter = useDateFormatter({
80
81
  weekday: 'long',
81
82
  day: 'numeric',
82
83
  month: 'long',
83
84
  year: 'numeric',
84
- era: date.calendar.identifier !== 'gregory' ? 'long' : undefined,
85
+ era: getEraFormat(date),
85
86
  timeZone: state.timeZone
86
87
  });
87
88
  let isSelected = state.isSelected(date);
@@ -129,24 +130,24 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
129
130
  label += dateFormatter.format(nativeDate);
130
131
  if (isDateToday) {
131
132
  // If date is today, set appropriate string depending on selected state:
132
- label = formatMessage(isSelected ? 'todayDateSelected' : 'todayDate', {
133
+ label = stringFormatter.format(isSelected ? 'todayDateSelected' : 'todayDate', {
133
134
  date: label
134
135
  });
135
136
  } else if (isSelected) {
136
137
  // If date is selected but not today:
137
- label = formatMessage('dateSelected', {
138
+ label = stringFormatter.format('dateSelected', {
138
139
  date: label
139
140
  });
140
141
  }
141
142
 
142
143
  if (state.minValue && isSameDay(date, state.minValue)) {
143
- label += ', ' + formatMessage('minimumDate');
144
+ label += ', ' + stringFormatter.format('minimumDate');
144
145
  } else if (state.maxValue && isSameDay(date, state.maxValue)) {
145
- label += ', ' + formatMessage('maximumDate');
146
+ label += ', ' + stringFormatter.format('maximumDate');
146
147
  }
147
148
 
148
149
  return label;
149
- }, [dateFormatter, nativeDate, formatMessage, isSelected, isDateToday, date, state, selectedDateDescription]);
150
+ }, [dateFormatter, nativeDate, stringFormatter, isSelected, isDateToday, date, state, selectedDateDescription]);
150
151
 
151
152
  // When a cell is focused and this is a range calendar, add a prompt to help
152
153
  // screenreader users know that they are in a range selection mode.
@@ -154,10 +155,10 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
154
155
  if ('anchorDate' in state && isFocused && !state.isReadOnly && isSelectable) {
155
156
  // If selection has started add "click to finish selecting range"
156
157
  if (state.anchorDate) {
157
- rangeSelectionPrompt = formatMessage('finishRangeSelectionPrompt');
158
+ rangeSelectionPrompt = stringFormatter.format('finishRangeSelectionPrompt');
158
159
  // Otherwise, add "click to start selecting range" prompt
159
160
  } else {
160
- rangeSelectionPrompt = formatMessage('startRangeSelectionPrompt');
161
+ rangeSelectionPrompt = stringFormatter.format('startRangeSelectionPrompt');
161
162
  }
162
163
  }
163
164
 
@@ -283,12 +284,14 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
283
284
  // Focus the button in the DOM when the state updates.
284
285
  useEffect(() => {
285
286
  if (isFocused && ref.current) {
287
+ focusWithoutScrolling(ref.current);
288
+
286
289
  // Scroll into view if navigating with a keyboard, otherwise
287
290
  // try not to shift the view under the user's mouse/finger.
288
- if (getInteractionModality() === 'pointer') {
289
- focusWithoutScrolling(ref.current);
290
- } else {
291
- ref.current.focus();
291
+ // Only scroll the direct scroll parent, not the whole page, so
292
+ // we don't scroll to the bottom when opening date picker popover.
293
+ if (getInteractionModality() !== 'pointer') {
294
+ scrollIntoView(getScrollParent(ref.current) as HTMLElement, ref.current);
292
295
  }
293
296
  }
294
297
  }, [isFocused, ref]);
@@ -10,10 +10,11 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {CalendarDate, startOfWeek} from '@internationalized/date';
13
+ import {CalendarDate, startOfWeek, today} from '@internationalized/date';
14
14
  import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
15
+ import {DOMAttributes} from '@react-types/shared';
15
16
  import {hookData, useVisibleRangeDescription} from './utils';
16
- import {HTMLAttributes, KeyboardEvent, useMemo} from 'react';
17
+ import {KeyboardEvent, useMemo} from 'react';
17
18
  import {mergeProps, useLabels} from '@react-aria/utils';
18
19
  import {useDateFormatter, useLocale} from '@react-aria/i18n';
19
20
 
@@ -34,9 +35,9 @@ export interface AriaCalendarGridProps {
34
35
 
35
36
  export interface CalendarGridAria {
36
37
  /** Props for the date grid element (e.g. `<table>`). */
37
- gridProps: HTMLAttributes<HTMLElement>,
38
+ gridProps: DOMAttributes,
38
39
  /** Props for the grid header element (e.g. `<thead>`). */
39
- headerProps: HTMLAttributes<HTMLElement>,
40
+ headerProps: DOMAttributes,
40
41
  /** A list of week day abbreviations formatted for the current locale, typically used in column headers. */
41
42
  weekDays: string[]
42
43
  }
@@ -63,22 +64,27 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta
63
64
  break;
64
65
  case 'PageUp':
65
66
  e.preventDefault();
67
+ e.stopPropagation();
66
68
  state.focusPreviousSection(e.shiftKey);
67
69
  break;
68
70
  case 'PageDown':
69
71
  e.preventDefault();
72
+ e.stopPropagation();
70
73
  state.focusNextSection(e.shiftKey);
71
74
  break;
72
75
  case 'End':
73
76
  e.preventDefault();
77
+ e.stopPropagation();
74
78
  state.focusSectionEnd();
75
79
  break;
76
80
  case 'Home':
77
81
  e.preventDefault();
82
+ e.stopPropagation();
78
83
  state.focusSectionStart();
79
84
  break;
80
85
  case 'ArrowLeft':
81
86
  e.preventDefault();
87
+ e.stopPropagation();
82
88
  if (direction === 'rtl') {
83
89
  state.focusNextDay();
84
90
  } else {
@@ -87,10 +93,12 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta
87
93
  break;
88
94
  case 'ArrowUp':
89
95
  e.preventDefault();
96
+ e.stopPropagation();
90
97
  state.focusPreviousRow();
91
98
  break;
92
99
  case 'ArrowRight':
93
100
  e.preventDefault();
101
+ e.stopPropagation();
94
102
  if (direction === 'rtl') {
95
103
  state.focusPreviousDay();
96
104
  } else {
@@ -99,6 +107,7 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta
99
107
  break;
100
108
  case 'ArrowDown':
101
109
  e.preventDefault();
110
+ e.stopPropagation();
102
111
  state.focusNextRow();
103
112
  break;
104
113
  case 'Escape':
@@ -122,13 +131,13 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta
122
131
  let dayFormatter = useDateFormatter({weekday: 'narrow', timeZone: state.timeZone});
123
132
  let {locale} = useLocale();
124
133
  let weekDays = useMemo(() => {
125
- let weekStart = startOfWeek(state.visibleRange.start, locale);
134
+ let weekStart = startOfWeek(today(state.timeZone), locale);
126
135
  return [...new Array(7).keys()].map((index) => {
127
136
  let date = weekStart.add({days: index});
128
137
  let dateDay = date.toDate(state.timeZone);
129
138
  return dayFormatter.format(dateDay);
130
139
  });
131
- }, [state.visibleRange.start, locale, state.timeZone, dayFormatter]);
140
+ }, [locale, state.timeZone, dayFormatter]);
132
141
 
133
142
  return {
134
143
  gridProps: mergeProps(labelProps, {
@@ -12,6 +12,7 @@
12
12
 
13
13
  import {CalendarAria, useCalendarBase} from './useCalendarBase';
14
14
  import {DateValue, RangeCalendarProps} from '@react-types/calendar';
15
+ import {FocusableElement} from '@react-types/shared';
15
16
  import {RangeCalendarState} from '@react-stately/calendar';
16
17
  import {RefObject, useRef} from 'react';
17
18
  import {useEvent} from '@react-aria/utils';
@@ -20,7 +21,7 @@ import {useEvent} from '@react-aria/utils';
20
21
  * Provides the behavior and accessibility implementation for a range calendar component.
21
22
  * A range calendar displays one or more date grids and allows users to select a contiguous range of dates.
22
23
  */
23
- export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<T>, state: RangeCalendarState, ref: RefObject<HTMLElement>): CalendarAria {
24
+ export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<T>, state: RangeCalendarState, ref: RefObject<FocusableElement>): CalendarAria {
24
25
  let res = useCalendarBase(props, state);
25
26
 
26
27
  // We need to ignore virtual pointer events from VoiceOver due to these bugs.
@@ -37,7 +38,7 @@ export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<
37
38
 
38
39
  // Stop range selection when pressing or releasing a pointer outside the calendar body,
39
40
  // except when pressing the next or previous buttons to switch months.
40
- let endDragging = e => {
41
+ let endDragging = (e: PointerEvent) => {
41
42
  if (isVirtualClick.current) {
42
43
  isVirtualClick.current = false;
43
44
  return;
@@ -48,7 +49,7 @@ export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<
48
49
  return;
49
50
  }
50
51
 
51
- let target = e.target as HTMLElement;
52
+ let target = e.target as Element;
52
53
  let body = document.getElementById(res.calendarProps.id);
53
54
  if (
54
55
  body &&
package/src/utils.ts CHANGED
@@ -12,9 +12,10 @@
12
12
 
13
13
  import {CalendarDate, DateFormatter, endOfMonth, isSameDay, startOfMonth} from '@internationalized/date';
14
14
  import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
15
- import {FormatMessage, useDateFormatter, useMessageFormatter} from '@react-aria/i18n';
16
15
  // @ts-ignore
17
16
  import intlMessages from '../intl/*.json';
17
+ import type {LocalizedStringFormatter} from '@internationalized/string';
18
+ import {useDateFormatter, useLocalizedStringFormatter} from '@react-aria/i18n';
18
19
  import {useMemo} from 'react';
19
20
 
20
21
  interface HookData {
@@ -26,8 +27,12 @@ interface HookData {
26
27
 
27
28
  export const hookData = new WeakMap<CalendarState | RangeCalendarState, HookData>();
28
29
 
30
+ export function getEraFormat(date: CalendarDate): 'short' | undefined {
31
+ return date?.calendar.identifier === 'gregory' && date.era === 'BC' ? 'short' : undefined;
32
+ }
33
+
29
34
  export function useSelectedDateDescription(state: CalendarState | RangeCalendarState) {
30
- let formatMessage = useMessageFormatter(intlMessages);
35
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
31
36
 
32
37
  let start: CalendarDate, end: CalendarDate;
33
38
  if ('highlightedRange' in state) {
@@ -37,7 +42,11 @@ export function useSelectedDateDescription(state: CalendarState | RangeCalendarS
37
42
  }
38
43
 
39
44
  let dateFormatter = useDateFormatter({
40
- dateStyle: 'full',
45
+ weekday: 'long',
46
+ month: 'long',
47
+ year: 'numeric',
48
+ day: 'numeric',
49
+ era: getEraFormat(start) || getEraFormat(end),
41
50
  timeZone: state.timeZone
42
51
  });
43
52
 
@@ -49,29 +58,33 @@ export function useSelectedDateDescription(state: CalendarState | RangeCalendarS
49
58
  // otherwise include both dates.
50
59
  if (isSameDay(start, end)) {
51
60
  let date = dateFormatter.format(start.toDate(state.timeZone));
52
- return formatMessage('selectedDateDescription', {date});
61
+ return stringFormatter.format('selectedDateDescription', {date});
53
62
  } else {
54
- let dateRange = formatRange(dateFormatter, formatMessage, start, end, state.timeZone);
63
+ let dateRange = formatRange(dateFormatter, stringFormatter, start, end, state.timeZone);
55
64
 
56
- return formatMessage('selectedRangeDescription', {dateRange});
65
+ return stringFormatter.format('selectedRangeDescription', {dateRange});
57
66
  }
58
67
  }
59
68
  return '';
60
- }, [start, end, anchorDate, state.timeZone, formatMessage, dateFormatter]);
69
+ }, [start, end, anchorDate, state.timeZone, stringFormatter, dateFormatter]);
61
70
  }
62
71
 
63
72
  export function useVisibleRangeDescription(startDate: CalendarDate, endDate: CalendarDate, timeZone: string, isAria: boolean) {
64
- let formatMessage = useMessageFormatter(intlMessages);
73
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
74
+ let era: any = getEraFormat(startDate) || getEraFormat(endDate);
65
75
  let monthFormatter = useDateFormatter({
66
76
  month: 'long',
67
77
  year: 'numeric',
68
- era: startDate.calendar.identifier !== 'gregory' ? 'long' : undefined,
78
+ era,
69
79
  calendar: startDate.calendar.identifier,
70
80
  timeZone
71
81
  });
72
82
 
73
83
  let dateFormatter = useDateFormatter({
74
- dateStyle: 'long',
84
+ month: 'long',
85
+ year: 'numeric',
86
+ day: 'numeric',
87
+ era,
75
88
  calendar: startDate.calendar.identifier,
76
89
  timeZone
77
90
  });
@@ -84,18 +97,18 @@ export function useVisibleRangeDescription(startDate: CalendarDate, endDate: Cal
84
97
  return monthFormatter.format(startDate.toDate(timeZone));
85
98
  } else if (isSameDay(endDate, endOfMonth(endDate))) {
86
99
  return isAria
87
- ? formatRange(monthFormatter, formatMessage, startDate, endDate, timeZone)
100
+ ? formatRange(monthFormatter, stringFormatter, startDate, endDate, timeZone)
88
101
  : monthFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
89
102
  }
90
103
  }
91
104
 
92
105
  return isAria
93
- ? formatRange(dateFormatter, formatMessage, startDate, endDate, timeZone)
106
+ ? formatRange(dateFormatter, stringFormatter, startDate, endDate, timeZone)
94
107
  : dateFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
95
- }, [startDate, endDate, monthFormatter, dateFormatter, formatMessage, timeZone, isAria]);
108
+ }, [startDate, endDate, monthFormatter, dateFormatter, stringFormatter, timeZone, isAria]);
96
109
  }
97
110
 
98
- function formatRange(dateFormatter: DateFormatter, formatMessage: FormatMessage, start: CalendarDate, end: CalendarDate, timeZone: string) {
111
+ function formatRange(dateFormatter: DateFormatter, stringFormatter: LocalizedStringFormatter, start: CalendarDate, end: CalendarDate, timeZone: string) {
99
112
  let parts = dateFormatter.formatRangeToParts(start.toDate(timeZone), end.toDate(timeZone));
100
113
 
101
114
  // Find the separator between the start and end date. This is determined
@@ -121,5 +134,5 @@ function formatRange(dateFormatter: DateFormatter, formatMessage: FormatMessage,
121
134
  }
122
135
  }
123
136
 
124
- return formatMessage('dateRange', {startDate: startValue, endDate: endValue});
137
+ return stringFormatter.format('dateRange', {startDate: startValue, endDate: endValue});
125
138
  }