@react-aria/calendar 3.0.0-nightly.3180 → 3.0.0-rc.0

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.
@@ -11,22 +11,36 @@
11
11
  */
12
12
 
13
13
  import {announce} from '@react-aria/live-announcer';
14
- import {CalendarAria} from './types';
15
- import {calendarIds, useSelectedDateDescription, useVisibleRangeDescription} from './utils';
14
+ import {AriaButtonProps} from '@react-types/button';
16
15
  import {CalendarPropsBase} from '@react-types/calendar';
17
16
  import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
18
17
  import {DOMProps} from '@react-types/shared';
18
+ import {filterDOMProps, mergeProps, useLabels, useSlotId, useUpdateEffect} from '@react-aria/utils';
19
+ import {hookData, useSelectedDateDescription, useVisibleRangeDescription} from './utils';
20
+ import {HTMLAttributes, useRef} from 'react';
19
21
  // @ts-ignore
20
22
  import intlMessages from '../intl/*.json';
21
- import {mergeProps, useDescription, useId, useUpdateEffect} from '@react-aria/utils';
22
23
  import {useMessageFormatter} from '@react-aria/i18n';
23
- import {useRef} from 'react';
24
+
25
+ export interface CalendarAria {
26
+ /** Props for the calendar grouping element. */
27
+ calendarProps: HTMLAttributes<HTMLElement>,
28
+ /** Props for the next button. */
29
+ nextButtonProps: AriaButtonProps,
30
+ /** Props for the previous button. */
31
+ prevButtonProps: AriaButtonProps,
32
+ /** Props for the error message element, if any. */
33
+ errorMessageProps: HTMLAttributes<HTMLElement>,
34
+ /** A description of the visible date range, for use in the calendar title. */
35
+ title: string
36
+ }
24
37
 
25
38
  export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: CalendarState | RangeCalendarState): CalendarAria {
26
39
  let formatMessage = useMessageFormatter(intlMessages);
27
- let calendarId = useId(props.id);
40
+ let domProps = filterDOMProps(props);
28
41
 
29
- let visibleRangeDescription = useVisibleRangeDescription(state.visibleRange.start, state.visibleRange.end, state.timeZone);
42
+ let title = useVisibleRangeDescription(state.visibleRange.start, state.visibleRange.end, state.timeZone, false);
43
+ let visibleRangeDescription = useVisibleRangeDescription(state.visibleRange.start, state.visibleRange.end, state.timeZone, true);
30
44
 
31
45
  // Announce when the visible date range changes
32
46
  useUpdateEffect(() => {
@@ -45,10 +59,15 @@ export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: Cale
45
59
  // handle an update to the caption that describes the currently selected range, to announce the new value
46
60
  }, [selectedDateDescription]);
47
61
 
48
- let descriptionProps = useDescription(visibleRangeDescription);
62
+ let errorMessageId = useSlotId([Boolean(props.errorMessage), props.validationState]);
49
63
 
50
- // Label the child grid elements by the group element if it is labelled.
51
- calendarIds.set(state, props['aria-label'] || props['aria-labelledby'] ? calendarId : null);
64
+ // Pass the label to the child grid elements.
65
+ hookData.set(state, {
66
+ ariaLabel: props['aria-label'],
67
+ ariaLabelledBy: props['aria-labelledby'],
68
+ errorMessageId,
69
+ selectedDateDescription
70
+ });
52
71
 
53
72
  // If the next or previous buttons become disabled while they are focused, move focus to the calendar body.
54
73
  let nextFocused = useRef(false);
@@ -65,12 +84,16 @@ export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: Cale
65
84
  state.setFocused(true);
66
85
  }
67
86
 
87
+ let labelProps = useLabels({
88
+ id: props['id'],
89
+ 'aria-label': [props['aria-label'], visibleRangeDescription].filter(Boolean).join(', '),
90
+ 'aria-labelledby': props['aria-labelledby']
91
+ });
92
+
68
93
  return {
69
- calendarProps: mergeProps(descriptionProps, {
94
+ calendarProps: mergeProps(domProps, labelProps, {
70
95
  role: 'group',
71
- id: calendarId,
72
- 'aria-label': props['aria-label'],
73
- 'aria-labelledby': props['aria-labelledby']
96
+ 'aria-describedby': props['aria-describedby'] || undefined
74
97
  }),
75
98
  nextButtonProps: {
76
99
  onPress: () => state.focusNextPage(),
@@ -86,6 +109,9 @@ export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: Cale
86
109
  onFocus: () => previousFocused.current = true,
87
110
  onBlur: () => previousFocused.current = false
88
111
  },
89
- title: visibleRangeDescription
112
+ errorMessageProps: {
113
+ id: errorMessageId
114
+ },
115
+ title
90
116
  };
91
117
  }
@@ -12,13 +12,14 @@
12
12
 
13
13
  import {CalendarDate, isEqualDay, isSameDay, isToday} from '@internationalized/date';
14
14
  import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
15
- import {focusWithoutScrolling} from '@react-aria/utils';
15
+ import {focusWithoutScrolling, useDescription} from '@react-aria/utils';
16
+ import {getInteractionModality, usePress} from '@react-aria/interactions';
17
+ import {hookData} from './utils';
16
18
  import {HTMLAttributes, RefObject, useEffect, useMemo, useRef} from 'react';
17
19
  // @ts-ignore
18
20
  import intlMessages from '../intl/*.json';
19
21
  import {mergeProps} from '@react-aria/utils';
20
22
  import {useDateFormatter, useMessageFormatter} from '@react-aria/i18n';
21
- import {usePress} from '@react-aria/interactions';
22
23
 
23
24
  export interface AriaCalendarCellProps {
24
25
  /** The date that this cell represents. */
@@ -30,7 +31,7 @@ export interface AriaCalendarCellProps {
30
31
  isDisabled?: boolean
31
32
  }
32
33
 
33
- interface CalendarCellAria {
34
+ export interface CalendarCellAria {
34
35
  /** Props for the grid cell element (e.g. `<td>`). */
35
36
  cellProps: HTMLAttributes<HTMLElement>,
36
37
  /** Props for the button element within the cell. */
@@ -61,6 +62,8 @@ interface CalendarCellAria {
61
62
  * For example, dates before the first day of a month in the same week.
62
63
  */
63
64
  isOutsideVisibleRange: boolean,
65
+ /** Whether the cell is part of an invalid selection. */
66
+ isInvalid: boolean,
64
67
  /** The day number formatted according to the current locale. */
65
68
  formattedDate: string
66
69
  }
@@ -71,6 +74,7 @@ interface CalendarCellAria {
71
74
  */
72
75
  export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarState | RangeCalendarState, ref: RefObject<HTMLElement>): CalendarCellAria {
73
76
  let {date, isDisabled} = props;
77
+ let {errorMessageId, selectedDateDescription} = hookData.get(state);
74
78
  let formatMessage = useMessageFormatter(intlMessages);
75
79
  let dateFormatter = useDateFormatter({
76
80
  weekday: 'long',
@@ -85,6 +89,15 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
85
89
  isDisabled = isDisabled || state.isCellDisabled(date);
86
90
  let isUnavailable = state.isCellUnavailable(date);
87
91
  let isSelectable = !isDisabled && !isUnavailable;
92
+ let isInvalid = state.validationState === 'invalid' && (
93
+ 'highlightedRange' in state
94
+ ? !state.anchorDate && state.highlightedRange && date.compare(state.highlightedRange.start) >= 0 && date.compare(state.highlightedRange.end) <= 0
95
+ : state.value && isSameDay(state.value, date)
96
+ );
97
+
98
+ if (isInvalid) {
99
+ isSelected = true;
100
+ }
88
101
 
89
102
  // For performance, reuse the same date object as before if the new date prop is the same.
90
103
  // This allows subsequent useMemo results to be reused.
@@ -100,26 +113,45 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
100
113
  // aria-label should be localize Day of week, Month, Day and Year without Time.
101
114
  let isDateToday = isToday(date, state.timeZone);
102
115
  let label = useMemo(() => {
116
+ let label = '';
117
+
118
+ // If this is a range calendar, add a description of the full selected range
119
+ // to the first and last selected date.
120
+ if (
121
+ 'highlightedRange' in state &&
122
+ state.value &&
123
+ !state.anchorDate &&
124
+ (isSameDay(date, state.value.start) || isSameDay(date, state.value.end))
125
+ ) {
126
+ label = selectedDateDescription + ', ';
127
+ }
128
+
129
+ label += dateFormatter.format(nativeDate);
103
130
  if (isDateToday) {
104
131
  // If date is today, set appropriate string depending on selected state:
105
- return formatMessage(isSelected ? 'todayDateSelected' : 'todayDate', {
106
- date: nativeDate
132
+ label = formatMessage(isSelected ? 'todayDateSelected' : 'todayDate', {
133
+ date: label
107
134
  });
108
135
  } else if (isSelected) {
109
136
  // If date is selected but not today:
110
- return formatMessage('dateSelected', {
111
- date: nativeDate
137
+ label = formatMessage('dateSelected', {
138
+ date: label
112
139
  });
113
140
  }
114
141
 
115
- return dateFormatter.format(nativeDate);
116
- }, [dateFormatter, nativeDate, formatMessage, isSelected, isDateToday]);
142
+ if (state.minValue && isSameDay(date, state.minValue)) {
143
+ label += ', ' + formatMessage('minimumDate');
144
+ } else if (state.maxValue && isSameDay(date, state.maxValue)) {
145
+ label += ', ' + formatMessage('maximumDate');
146
+ }
147
+
148
+ return label;
149
+ }, [dateFormatter, nativeDate, formatMessage, isSelected, isDateToday, date, state, selectedDateDescription]);
117
150
 
118
151
  // When a cell is focused and this is a range calendar, add a prompt to help
119
152
  // screenreader users know that they are in a range selection mode.
153
+ let rangeSelectionPrompt = '';
120
154
  if ('anchorDate' in state && isFocused && !state.isReadOnly && isSelectable) {
121
- let rangeSelectionPrompt = '';
122
-
123
155
  // If selection has started add "click to finish selecting range"
124
156
  if (state.anchorDate) {
125
157
  rangeSelectionPrompt = formatMessage('finishRangeSelectionPrompt');
@@ -127,13 +159,10 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
127
159
  } else {
128
160
  rangeSelectionPrompt = formatMessage('startRangeSelectionPrompt');
129
161
  }
130
-
131
- // Append to aria-label
132
- if (rangeSelectionPrompt) {
133
- label = `${label} (${rangeSelectionPrompt})`;
134
- }
135
162
  }
136
163
 
164
+ let descriptionProps = useDescription(rangeSelectionPrompt);
165
+
137
166
  let isAnchorPressed = useRef(false);
138
167
  let isRangeBoundaryPressed = useRef(false);
139
168
  let touchDragTimerRef = useRef(null);
@@ -152,7 +181,9 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
152
181
  if ('highlightedRange' in state && !state.anchorDate && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
153
182
  // Allow dragging the start or end date of a range to modify it
154
183
  // rather than starting a new selection.
155
- if (state.highlightedRange) {
184
+ // Don't allow dragging when invalid, or weird jumping behavior may occur as date ranges
185
+ // are constrained to available dates. The user will need to select a new range in this case.
186
+ if (state.highlightedRange && !isInvalid) {
156
187
  if (isSameDay(date, state.highlightedRange.start)) {
157
188
  state.setAnchorDate(state.highlightedRange.end);
158
189
  state.setFocusedDate(date);
@@ -252,7 +283,13 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
252
283
  // Focus the button in the DOM when the state updates.
253
284
  useEffect(() => {
254
285
  if (isFocused && ref.current) {
255
- focusWithoutScrolling(ref.current);
286
+ // Scroll into view if navigating with a keyboard, otherwise
287
+ // 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();
292
+ }
256
293
  }
257
294
  }, [isFocused, ref]);
258
295
 
@@ -268,7 +305,8 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
268
305
  cellProps: {
269
306
  role: 'gridcell',
270
307
  'aria-disabled': !isSelectable || null,
271
- 'aria-selected': isSelectable ? isSelected : null
308
+ 'aria-selected': isSelected || null,
309
+ 'aria-invalid': isInvalid || null
272
310
  },
273
311
  buttonProps: mergeProps(pressProps, {
274
312
  onFocus() {
@@ -280,6 +318,11 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
280
318
  role: 'button',
281
319
  'aria-disabled': !isSelectable || null,
282
320
  'aria-label': label,
321
+ 'aria-invalid': isInvalid || null,
322
+ 'aria-describedby': [
323
+ isInvalid ? errorMessageId : null,
324
+ descriptionProps['aria-describedby']
325
+ ].filter(Boolean).join(' ') || undefined,
283
326
  onPointerEnter(e) {
284
327
  // Highlight the date on hover or drag over a date when selecting a range.
285
328
  if ('highlightDate' in state && (e.pointerType !== 'touch' || state.isDragging) && isSelectable) {
@@ -305,6 +348,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
305
348
  isDisabled,
306
349
  isUnavailable,
307
350
  isOutsideVisibleRange: date.compare(state.visibleRange.start) < 0 || date.compare(state.visibleRange.end) > 0,
351
+ isInvalid,
308
352
  formattedDate
309
353
  };
310
354
  }
@@ -11,14 +11,13 @@
11
11
  */
12
12
 
13
13
  import {CalendarDate, startOfWeek} from '@internationalized/date';
14
- import {CalendarGridAria} from './types';
15
- import {calendarIds, useSelectedDateDescription, useVisibleRangeDescription} from './utils';
16
14
  import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
17
- import {KeyboardEvent} from 'react';
18
- import {mergeProps, useDescription, useLabels} from '@react-aria/utils';
15
+ import {hookData, useVisibleRangeDescription} from './utils';
16
+ import {HTMLAttributes, KeyboardEvent, useMemo} from 'react';
17
+ import {mergeProps, useLabels} from '@react-aria/utils';
19
18
  import {useDateFormatter, useLocale} from '@react-aria/i18n';
20
19
 
21
- interface CalendarGridProps {
20
+ export interface AriaCalendarGridProps {
22
21
  /**
23
22
  * The first date displayed in the calendar grid.
24
23
  * Defaults to the first visible date in the calendar.
@@ -33,12 +32,21 @@ interface CalendarGridProps {
33
32
  endDate?: CalendarDate
34
33
  }
35
34
 
35
+ export interface CalendarGridAria {
36
+ /** Props for the date grid element (e.g. `<table>`). */
37
+ gridProps: HTMLAttributes<HTMLElement>,
38
+ /** Props for the grid header element (e.g. `<thead>`). */
39
+ headerProps: HTMLAttributes<HTMLElement>,
40
+ /** A list of week day abbreviations formatted for the current locale, typically used in column headers. */
41
+ weekDays: string[]
42
+ }
43
+
36
44
  /**
37
45
  * Provides the behavior and accessibility implementation for a calendar grid component.
38
46
  * A calendar grid displays a single grid of days within a calendar or range calendar which
39
47
  * can be keyboard navigated and selected by the user.
40
48
  */
41
- export function useCalendarGrid(props: CalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria {
49
+ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria {
42
50
  let {
43
51
  startDate = state.visibleRange.start,
44
52
  endDate = state.visibleRange.end
@@ -55,27 +63,19 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
55
63
  break;
56
64
  case 'PageUp':
57
65
  e.preventDefault();
58
- if (e.shiftKey) {
59
- state.focusPreviousSection();
60
- } else {
61
- state.focusPreviousPage();
62
- }
66
+ state.focusPreviousSection(e.shiftKey);
63
67
  break;
64
68
  case 'PageDown':
65
69
  e.preventDefault();
66
- if (e.shiftKey) {
67
- state.focusNextSection();
68
- } else {
69
- state.focusNextPage();
70
- }
70
+ state.focusNextSection(e.shiftKey);
71
71
  break;
72
72
  case 'End':
73
73
  e.preventDefault();
74
- state.focusPageEnd();
74
+ state.focusSectionEnd();
75
75
  break;
76
76
  case 'Home':
77
77
  e.preventDefault();
78
- state.focusPageStart();
78
+ state.focusSectionStart();
79
79
  break;
80
80
  case 'ArrowLeft':
81
81
  e.preventDefault();
@@ -111,32 +111,27 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
111
111
  }
112
112
  };
113
113
 
114
- let selectedDateDescription = useSelectedDateDescription(state);
115
- let descriptionProps = useDescription(selectedDateDescription);
116
- let visibleRangeDescription = useVisibleRangeDescription(startDate, endDate, state.timeZone);
114
+ let visibleRangeDescription = useVisibleRangeDescription(startDate, endDate, state.timeZone, true);
117
115
 
116
+ let {ariaLabel, ariaLabelledBy} = hookData.get(state);
118
117
  let labelProps = useLabels({
119
- 'aria-label': visibleRangeDescription,
120
- 'aria-labelledby': calendarIds.get(state)
118
+ 'aria-label': [ariaLabel, visibleRangeDescription].filter(Boolean).join(', '),
119
+ 'aria-labelledby': ariaLabelledBy
121
120
  });
122
121
 
123
122
  let dayFormatter = useDateFormatter({weekday: 'narrow', timeZone: state.timeZone});
124
- let dayFormatterLong = useDateFormatter({weekday: 'long', timeZone: state.timeZone});
125
123
  let {locale} = useLocale();
126
- let weekStart = startOfWeek(state.visibleRange.start, locale);
127
- let weekDays = [...new Array(7).keys()].map((index) => {
128
- let date = weekStart.add({days: index});
129
- let dateDay = date.toDate(state.timeZone);
130
- let narrow = dayFormatter.format(dateDay);
131
- let long = dayFormatterLong.format(dateDay);
132
- return {
133
- narrow,
134
- long
135
- };
136
- });
124
+ let weekDays = useMemo(() => {
125
+ let weekStart = startOfWeek(state.visibleRange.start, locale);
126
+ return [...new Array(7).keys()].map((index) => {
127
+ let date = weekStart.add({days: index});
128
+ let dateDay = date.toDate(state.timeZone);
129
+ return dayFormatter.format(dateDay);
130
+ });
131
+ }, [state.visibleRange.start, locale, state.timeZone, dayFormatter]);
137
132
 
138
133
  return {
139
- gridProps: mergeProps(descriptionProps, labelProps, {
134
+ gridProps: mergeProps(labelProps, {
140
135
  role: 'grid',
141
136
  'aria-readonly': state.isReadOnly || null,
142
137
  'aria-disabled': state.isDisabled || null,
@@ -145,6 +140,11 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
145
140
  onFocus: () => state.setFocused(true),
146
141
  onBlur: () => state.setFocused(false)
147
142
  }),
143
+ headerProps: {
144
+ // Column headers are hidden to screen readers to make navigating with a touch screen reader easier.
145
+ // The day names are already included in the label of each cell, so there's no need to announce them twice.
146
+ 'aria-hidden': true
147
+ },
148
148
  weekDays
149
149
  };
150
150
  }
@@ -10,12 +10,11 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {CalendarAria} from './types';
13
+ import {CalendarAria, useCalendarBase} from './useCalendarBase';
14
14
  import {DateValue, RangeCalendarProps} from '@react-types/calendar';
15
15
  import {RangeCalendarState} from '@react-stately/calendar';
16
16
  import {RefObject, useRef} from 'react';
17
- import {useCalendarBase} from './useCalendarBase';
18
- import {useEvent, useId} from '@react-aria/utils';
17
+ import {useEvent} from '@react-aria/utils';
19
18
 
20
19
  /**
21
20
  * Provides the behavior and accessibility implementation for a range calendar component.
@@ -23,8 +22,6 @@ import {useEvent, useId} from '@react-aria/utils';
23
22
  */
24
23
  export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<T>, state: RangeCalendarState, ref: RefObject<HTMLElement>): CalendarAria {
25
24
  let res = useCalendarBase(props, state);
26
- res.nextButtonProps.id = useId();
27
- res.prevButtonProps.id = useId();
28
25
 
29
26
  // We need to ignore virtual pointer events from VoiceOver due to these bugs.
30
27
  // https://bugs.webkit.org/show_bug.cgi?id=222627
@@ -33,7 +30,8 @@ export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<
33
30
  // We need to match that here otherwise this will fire before the press event in
34
31
  // useCalendarCell, causing range selection to not work properly.
35
32
  let isVirtualClick = useRef(false);
36
- useEvent(useRef(window), 'pointerdown', e => {
33
+ let windowRef = useRef(typeof window !== 'undefined' ? window : null);
34
+ useEvent(windowRef, 'pointerdown', e => {
37
35
  isVirtualClick.current = e.width === 0 && e.height === 0;
38
36
  });
39
37
 
@@ -53,16 +51,23 @@ export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<
53
51
  let target = e.target as HTMLElement;
54
52
  let body = document.getElementById(res.calendarProps.id);
55
53
  if (
56
- (!body.contains(target) || !target.closest('[role="button"]')) &&
57
- !document.getElementById(res.nextButtonProps.id)?.contains(target) &&
58
- !document.getElementById(res.prevButtonProps.id)?.contains(target)
54
+ body &&
55
+ body.contains(document.activeElement) &&
56
+ (!body.contains(target) || !target.closest('button, [role="button"]'))
59
57
  ) {
60
58
  state.selectFocusedDate();
61
59
  }
62
60
  };
63
61
 
64
- useEvent(useRef(window), 'pointerup', endDragging);
65
- useEvent(useRef(window), 'pointercancel', endDragging);
62
+ useEvent(windowRef, 'pointerup', endDragging);
63
+ useEvent(windowRef, 'pointercancel', endDragging);
64
+
65
+ // Also stop range selection on blur, e.g. tabbing away from the calendar.
66
+ res.calendarProps.onBlur = e => {
67
+ if ((!e.relatedTarget || !ref.current.contains(e.relatedTarget)) && state.anchorDate) {
68
+ state.selectFocusedDate();
69
+ }
70
+ };
66
71
 
67
72
  // Prevent touch scrolling while dragging
68
73
  useEvent(ref, 'touchmove', e => {
package/src/utils.ts CHANGED
@@ -10,14 +10,21 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {CalendarDate, endOfMonth, isSameDay, startOfMonth} from '@internationalized/date';
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';
15
16
  // @ts-ignore
16
17
  import intlMessages from '../intl/*.json';
17
- import {useDateFormatter, useMessageFormatter} from '@react-aria/i18n';
18
18
  import {useMemo} from 'react';
19
19
 
20
- export const calendarIds = new WeakMap<CalendarState | RangeCalendarState, string>();
20
+ interface HookData {
21
+ ariaLabel: string,
22
+ ariaLabelledBy: string,
23
+ errorMessageId: string,
24
+ selectedDateDescription: string
25
+ }
26
+
27
+ export const hookData = new WeakMap<CalendarState | RangeCalendarState, HookData>();
21
28
 
22
29
  export function useSelectedDateDescription(state: CalendarState | RangeCalendarState) {
23
30
  let formatMessage = useMessageFormatter(intlMessages);
@@ -29,6 +36,11 @@ export function useSelectedDateDescription(state: CalendarState | RangeCalendarS
29
36
  start = end = state.value;
30
37
  }
31
38
 
39
+ let dateFormatter = useDateFormatter({
40
+ dateStyle: 'full',
41
+ timeZone: state.timeZone
42
+ });
43
+
32
44
  let anchorDate = 'anchorDate' in state ? state.anchorDate : null;
33
45
  return useMemo(() => {
34
46
  // No message if currently selecting a range, or there is nothing highlighted.
@@ -36,16 +48,20 @@ export function useSelectedDateDescription(state: CalendarState | RangeCalendarS
36
48
  // Use a single date message if the start and end dates are the same day,
37
49
  // otherwise include both dates.
38
50
  if (isSameDay(start, end)) {
39
- return formatMessage('selectedDateDescription', {date: start.toDate(state.timeZone)});
51
+ let date = dateFormatter.format(start.toDate(state.timeZone));
52
+ return formatMessage('selectedDateDescription', {date});
40
53
  } else {
41
- return formatMessage('selectedRangeDescription', {start: start.toDate(state.timeZone), end: end.toDate(state.timeZone)});
54
+ let dateRange = formatRange(dateFormatter, formatMessage, start, end, state.timeZone);
55
+
56
+ return formatMessage('selectedRangeDescription', {dateRange});
42
57
  }
43
58
  }
44
59
  return '';
45
- }, [start, end, anchorDate, state.timeZone, formatMessage]);
60
+ }, [start, end, anchorDate, state.timeZone, formatMessage, dateFormatter]);
46
61
  }
47
62
 
48
- export function useVisibleRangeDescription(startDate: CalendarDate, endDate: CalendarDate, timeZone: string) {
63
+ export function useVisibleRangeDescription(startDate: CalendarDate, endDate: CalendarDate, timeZone: string, isAria: boolean) {
64
+ let formatMessage = useMessageFormatter(intlMessages);
49
65
  let monthFormatter = useDateFormatter({
50
66
  month: 'long',
51
67
  year: 'numeric',
@@ -67,10 +83,43 @@ export function useVisibleRangeDescription(startDate: CalendarDate, endDate: Cal
67
83
  if (isSameDay(endDate, endOfMonth(startDate))) {
68
84
  return monthFormatter.format(startDate.toDate(timeZone));
69
85
  } else if (isSameDay(endDate, endOfMonth(endDate))) {
70
- return monthFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
86
+ return isAria
87
+ ? formatRange(monthFormatter, formatMessage, startDate, endDate, timeZone)
88
+ : monthFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
71
89
  }
72
90
  }
73
91
 
74
- return dateFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
75
- }, [startDate, endDate, monthFormatter, dateFormatter, timeZone]);
92
+ return isAria
93
+ ? formatRange(dateFormatter, formatMessage, startDate, endDate, timeZone)
94
+ : dateFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
95
+ }, [startDate, endDate, monthFormatter, dateFormatter, formatMessage, timeZone, isAria]);
96
+ }
97
+
98
+ function formatRange(dateFormatter: DateFormatter, formatMessage: FormatMessage, start: CalendarDate, end: CalendarDate, timeZone: string) {
99
+ let parts = dateFormatter.formatRangeToParts(start.toDate(timeZone), end.toDate(timeZone));
100
+
101
+ // Find the separator between the start and end date. This is determined
102
+ // by finding the last shared literal before the end range.
103
+ let separatorIndex = -1;
104
+ for (let i = 0; i < parts.length; i++) {
105
+ let part = parts[i];
106
+ if (part.source === 'shared' && part.type === 'literal') {
107
+ separatorIndex = i;
108
+ } else if (part.source === 'endRange') {
109
+ break;
110
+ }
111
+ }
112
+
113
+ // Now we can combine the parts into start and end strings.
114
+ let startValue = '';
115
+ let endValue = '';
116
+ for (let i = 0; i < parts.length; i++) {
117
+ if (i < separatorIndex) {
118
+ startValue += parts[i].value;
119
+ } else if (i > separatorIndex) {
120
+ endValue += parts[i].value;
121
+ }
122
+ }
123
+
124
+ return formatMessage('dateRange', {startDate: startValue, endDate: endValue});
76
125
  }
package/src/types.ts DELETED
@@ -1,40 +0,0 @@
1
- /*
2
- * Copyright 2020 Adobe. All rights reserved.
3
- * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License. You may obtain a copy
5
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- *
7
- * Unless required by applicable law or agreed to in writing, software distributed under
8
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
- * OF ANY KIND, either express or implied. See the License for the specific language
10
- * governing permissions and limitations under the License.
11
- */
12
-
13
- import {AriaButtonProps} from '@react-types/button';
14
- import {HTMLAttributes} from 'react';
15
-
16
- export interface CalendarAria {
17
- /** Props for the calendar grouping element. */
18
- calendarProps: HTMLAttributes<HTMLElement>,
19
- /** Props for the next button. */
20
- nextButtonProps: AriaButtonProps,
21
- /** Props for the previous button. */
22
- prevButtonProps: AriaButtonProps,
23
- /** A description of the visible date range, for use in the calendar title. */
24
- title: string
25
- }
26
-
27
-
28
- export interface CalendarGridAria {
29
- /** Props for the date grid element (e.g. `<table>`). */
30
- gridProps: HTMLAttributes<HTMLElement>,
31
- /** A list of week days formatted for the current locale, typically used in column headers. */
32
- weekDays: WeekDay[]
33
- }
34
-
35
- interface WeekDay {
36
- /** A short name (e.g. single letter) for the day. */
37
- narrow: string,
38
- /** The full day name. If not displayed visually, it should be used as the accessiblity name. */
39
- long: string
40
- }