@react-aria/calendar 3.0.0-nightly.3113 → 3.0.0-nightly.3114

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.
Files changed (221) hide show
  1. package/dist/ar-AE.main.js +17 -0
  2. package/dist/ar-AE.main.js.map +1 -0
  3. package/dist/ar-AE.mjs +19 -0
  4. package/dist/ar-AE.module.js +19 -0
  5. package/dist/ar-AE.module.js.map +1 -0
  6. package/dist/bg-BG.main.js +17 -0
  7. package/dist/bg-BG.main.js.map +1 -0
  8. package/dist/bg-BG.mjs +19 -0
  9. package/dist/bg-BG.module.js +19 -0
  10. package/dist/bg-BG.module.js.map +1 -0
  11. package/dist/cs-CZ.main.js +17 -0
  12. package/dist/cs-CZ.main.js.map +1 -0
  13. package/dist/cs-CZ.mjs +19 -0
  14. package/dist/cs-CZ.module.js +19 -0
  15. package/dist/cs-CZ.module.js.map +1 -0
  16. package/dist/da-DK.main.js +17 -0
  17. package/dist/da-DK.main.js.map +1 -0
  18. package/dist/da-DK.mjs +19 -0
  19. package/dist/da-DK.module.js +19 -0
  20. package/dist/da-DK.module.js.map +1 -0
  21. package/dist/de-DE.main.js +17 -0
  22. package/dist/de-DE.main.js.map +1 -0
  23. package/dist/de-DE.mjs +19 -0
  24. package/dist/de-DE.module.js +19 -0
  25. package/dist/de-DE.module.js.map +1 -0
  26. package/dist/el-GR.main.js +17 -0
  27. package/dist/el-GR.main.js.map +1 -0
  28. package/dist/el-GR.mjs +19 -0
  29. package/dist/el-GR.module.js +19 -0
  30. package/dist/el-GR.module.js.map +1 -0
  31. package/dist/en-US.main.js +17 -0
  32. package/dist/en-US.main.js.map +1 -0
  33. package/dist/en-US.mjs +19 -0
  34. package/dist/en-US.module.js +19 -0
  35. package/dist/en-US.module.js.map +1 -0
  36. package/dist/es-ES.main.js +17 -0
  37. package/dist/es-ES.main.js.map +1 -0
  38. package/dist/es-ES.mjs +19 -0
  39. package/dist/es-ES.module.js +19 -0
  40. package/dist/es-ES.module.js.map +1 -0
  41. package/dist/et-EE.main.js +17 -0
  42. package/dist/et-EE.main.js.map +1 -0
  43. package/dist/et-EE.mjs +19 -0
  44. package/dist/et-EE.module.js +19 -0
  45. package/dist/et-EE.module.js.map +1 -0
  46. package/dist/fi-FI.main.js +17 -0
  47. package/dist/fi-FI.main.js.map +1 -0
  48. package/dist/fi-FI.mjs +19 -0
  49. package/dist/fi-FI.module.js +19 -0
  50. package/dist/fi-FI.module.js.map +1 -0
  51. package/dist/fr-FR.main.js +17 -0
  52. package/dist/fr-FR.main.js.map +1 -0
  53. package/dist/fr-FR.mjs +19 -0
  54. package/dist/fr-FR.module.js +19 -0
  55. package/dist/fr-FR.module.js.map +1 -0
  56. package/dist/he-IL.main.js +17 -0
  57. package/dist/he-IL.main.js.map +1 -0
  58. package/dist/he-IL.mjs +19 -0
  59. package/dist/he-IL.module.js +19 -0
  60. package/dist/he-IL.module.js.map +1 -0
  61. package/dist/hr-HR.main.js +17 -0
  62. package/dist/hr-HR.main.js.map +1 -0
  63. package/dist/hr-HR.mjs +19 -0
  64. package/dist/hr-HR.module.js +19 -0
  65. package/dist/hr-HR.module.js.map +1 -0
  66. package/dist/hu-HU.main.js +17 -0
  67. package/dist/hu-HU.main.js.map +1 -0
  68. package/dist/hu-HU.mjs +19 -0
  69. package/dist/hu-HU.module.js +19 -0
  70. package/dist/hu-HU.module.js.map +1 -0
  71. package/dist/import.mjs +23 -0
  72. package/dist/intlStrings.main.js +108 -0
  73. package/dist/intlStrings.main.js.map +1 -0
  74. package/dist/intlStrings.mjs +110 -0
  75. package/dist/intlStrings.module.js +110 -0
  76. package/dist/intlStrings.module.js.map +1 -0
  77. package/dist/it-IT.main.js +17 -0
  78. package/dist/it-IT.main.js.map +1 -0
  79. package/dist/it-IT.mjs +19 -0
  80. package/dist/it-IT.module.js +19 -0
  81. package/dist/it-IT.module.js.map +1 -0
  82. package/dist/ja-JP.main.js +17 -0
  83. package/dist/ja-JP.main.js.map +1 -0
  84. package/dist/ja-JP.mjs +19 -0
  85. package/dist/ja-JP.module.js +19 -0
  86. package/dist/ja-JP.module.js.map +1 -0
  87. package/dist/ko-KR.main.js +17 -0
  88. package/dist/ko-KR.main.js.map +1 -0
  89. package/dist/ko-KR.mjs +19 -0
  90. package/dist/ko-KR.module.js +19 -0
  91. package/dist/ko-KR.module.js.map +1 -0
  92. package/dist/lt-LT.main.js +17 -0
  93. package/dist/lt-LT.main.js.map +1 -0
  94. package/dist/lt-LT.mjs +19 -0
  95. package/dist/lt-LT.module.js +19 -0
  96. package/dist/lt-LT.module.js.map +1 -0
  97. package/dist/lv-LV.main.js +17 -0
  98. package/dist/lv-LV.main.js.map +1 -0
  99. package/dist/lv-LV.mjs +19 -0
  100. package/dist/lv-LV.module.js +19 -0
  101. package/dist/lv-LV.module.js.map +1 -0
  102. package/dist/main.js +19 -660
  103. package/dist/main.js.map +1 -1
  104. package/dist/module.js +16 -644
  105. package/dist/module.js.map +1 -1
  106. package/dist/nb-NO.main.js +17 -0
  107. package/dist/nb-NO.main.js.map +1 -0
  108. package/dist/nb-NO.mjs +19 -0
  109. package/dist/nb-NO.module.js +19 -0
  110. package/dist/nb-NO.module.js.map +1 -0
  111. package/dist/nl-NL.main.js +17 -0
  112. package/dist/nl-NL.main.js.map +1 -0
  113. package/dist/nl-NL.mjs +19 -0
  114. package/dist/nl-NL.module.js +19 -0
  115. package/dist/nl-NL.module.js.map +1 -0
  116. package/dist/pl-PL.main.js +17 -0
  117. package/dist/pl-PL.main.js.map +1 -0
  118. package/dist/pl-PL.mjs +19 -0
  119. package/dist/pl-PL.module.js +19 -0
  120. package/dist/pl-PL.module.js.map +1 -0
  121. package/dist/pt-BR.main.js +17 -0
  122. package/dist/pt-BR.main.js.map +1 -0
  123. package/dist/pt-BR.mjs +19 -0
  124. package/dist/pt-BR.module.js +19 -0
  125. package/dist/pt-BR.module.js.map +1 -0
  126. package/dist/pt-PT.main.js +17 -0
  127. package/dist/pt-PT.main.js.map +1 -0
  128. package/dist/pt-PT.mjs +19 -0
  129. package/dist/pt-PT.module.js +19 -0
  130. package/dist/pt-PT.module.js.map +1 -0
  131. package/dist/ro-RO.main.js +17 -0
  132. package/dist/ro-RO.main.js.map +1 -0
  133. package/dist/ro-RO.mjs +19 -0
  134. package/dist/ro-RO.module.js +19 -0
  135. package/dist/ro-RO.module.js.map +1 -0
  136. package/dist/ru-RU.main.js +17 -0
  137. package/dist/ru-RU.main.js.map +1 -0
  138. package/dist/ru-RU.mjs +19 -0
  139. package/dist/ru-RU.module.js +19 -0
  140. package/dist/ru-RU.module.js.map +1 -0
  141. package/dist/sk-SK.main.js +17 -0
  142. package/dist/sk-SK.main.js.map +1 -0
  143. package/dist/sk-SK.mjs +19 -0
  144. package/dist/sk-SK.module.js +19 -0
  145. package/dist/sk-SK.module.js.map +1 -0
  146. package/dist/sl-SI.main.js +17 -0
  147. package/dist/sl-SI.main.js.map +1 -0
  148. package/dist/sl-SI.mjs +19 -0
  149. package/dist/sl-SI.module.js +19 -0
  150. package/dist/sl-SI.module.js.map +1 -0
  151. package/dist/sr-SP.main.js +17 -0
  152. package/dist/sr-SP.main.js.map +1 -0
  153. package/dist/sr-SP.mjs +19 -0
  154. package/dist/sr-SP.module.js +19 -0
  155. package/dist/sr-SP.module.js.map +1 -0
  156. package/dist/sv-SE.main.js +17 -0
  157. package/dist/sv-SE.main.js.map +1 -0
  158. package/dist/sv-SE.mjs +19 -0
  159. package/dist/sv-SE.module.js +19 -0
  160. package/dist/sv-SE.module.js.map +1 -0
  161. package/dist/tr-TR.main.js +17 -0
  162. package/dist/tr-TR.main.js.map +1 -0
  163. package/dist/tr-TR.mjs +19 -0
  164. package/dist/tr-TR.module.js +19 -0
  165. package/dist/tr-TR.module.js.map +1 -0
  166. package/dist/types.d.ts +96 -21
  167. package/dist/types.d.ts.map +1 -1
  168. package/dist/uk-UA.main.js +17 -0
  169. package/dist/uk-UA.main.js.map +1 -0
  170. package/dist/uk-UA.mjs +19 -0
  171. package/dist/uk-UA.module.js +19 -0
  172. package/dist/uk-UA.module.js.map +1 -0
  173. package/dist/useCalendar.main.js +25 -0
  174. package/dist/useCalendar.main.js.map +1 -0
  175. package/{src/types.ts → dist/useCalendar.mjs} +7 -12
  176. package/dist/useCalendar.module.js +20 -0
  177. package/dist/useCalendar.module.js.map +1 -0
  178. package/dist/useCalendarBase.main.js +112 -0
  179. package/dist/useCalendarBase.main.js.map +1 -0
  180. package/dist/useCalendarBase.mjs +107 -0
  181. package/dist/useCalendarBase.module.js +107 -0
  182. package/dist/useCalendarBase.module.js.map +1 -0
  183. package/dist/useCalendarCell.main.js +276 -0
  184. package/dist/useCalendarCell.main.js.map +1 -0
  185. package/dist/useCalendarCell.mjs +271 -0
  186. package/dist/useCalendarCell.module.js +271 -0
  187. package/dist/useCalendarCell.module.js.map +1 -0
  188. package/dist/useCalendarGrid.main.js +139 -0
  189. package/dist/useCalendarGrid.main.js.map +1 -0
  190. package/dist/useCalendarGrid.mjs +134 -0
  191. package/dist/useCalendarGrid.module.js +134 -0
  192. package/dist/useCalendarGrid.module.js.map +1 -0
  193. package/dist/useRangeCalendar.main.js +66 -0
  194. package/dist/useRangeCalendar.main.js.map +1 -0
  195. package/dist/useRangeCalendar.mjs +61 -0
  196. package/dist/useRangeCalendar.module.js +61 -0
  197. package/dist/useRangeCalendar.module.js.map +1 -0
  198. package/dist/utils.main.js +138 -0
  199. package/dist/utils.main.js.map +1 -0
  200. package/dist/utils.mjs +130 -0
  201. package/dist/utils.module.js +130 -0
  202. package/dist/utils.module.js.map +1 -0
  203. package/dist/zh-CN.main.js +17 -0
  204. package/dist/zh-CN.main.js.map +1 -0
  205. package/dist/zh-CN.mjs +19 -0
  206. package/dist/zh-CN.module.js +19 -0
  207. package/dist/zh-CN.module.js.map +1 -0
  208. package/dist/zh-TW.main.js +17 -0
  209. package/dist/zh-TW.main.js.map +1 -0
  210. package/dist/zh-TW.mjs +19 -0
  211. package/dist/zh-TW.module.js +19 -0
  212. package/dist/zh-TW.module.js.map +1 -0
  213. package/package.json +19 -15
  214. package/src/index.ts +9 -6
  215. package/src/useCalendar.ts +7 -4
  216. package/src/useCalendarBase.ts +67 -22
  217. package/src/useCalendarCell.ts +154 -45
  218. package/src/useCalendarGrid.ts +76 -33
  219. package/src/useRangeCalendar.ts +28 -17
  220. package/src/utils.ts +79 -15
  221. package/src/useCalendarTableHeader.ts +0 -11
@@ -10,90 +10,153 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {CalendarDate, isEqualDay, isSameDay, isSameMonth, isToday} from '@internationalized/date';
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';
16
- import {HTMLAttributes, RefObject, useEffect, useMemo, useRef} from 'react';
15
+ import {DOMAttributes, RefObject} from '@react-types/shared';
16
+ import {focusWithoutScrolling, getScrollParent, mergeProps, scrollIntoViewport, useDeepMemo, useDescription} from '@react-aria/utils';
17
+ import {getEraFormat, hookData} from './utils';
18
+ import {getInteractionModality, usePress} from '@react-aria/interactions';
17
19
  // @ts-ignore
18
20
  import intlMessages from '../intl/*.json';
19
- import {mergeProps} from '@react-aria/utils';
20
- import {PressProps, usePress} from '@react-aria/interactions';
21
- import {useDateFormatter, useMessageFormatter} from '@react-aria/i18n';
21
+ import {useDateFormatter, useLocalizedStringFormatter} from '@react-aria/i18n';
22
+ import {useEffect, useMemo, useRef} from 'react';
22
23
 
23
24
  export interface AriaCalendarCellProps {
25
+ /** The date that this cell represents. */
24
26
  date: CalendarDate,
27
+ /**
28
+ * Whether the cell is disabled. By default, this is determined by the
29
+ * Calendar's `minValue`, `maxValue`, and `isDisabled` props.
30
+ */
25
31
  isDisabled?: boolean
26
32
  }
27
33
 
28
- interface CalendarCellAria {
29
- cellProps: PressProps & HTMLAttributes<HTMLElement>,
30
- buttonProps: HTMLAttributes<HTMLElement>,
31
- isPressed: boolean
34
+ export interface CalendarCellAria {
35
+ /** Props for the grid cell element (e.g. `<td>`). */
36
+ cellProps: DOMAttributes,
37
+ /** Props for the button element within the cell. */
38
+ buttonProps: DOMAttributes,
39
+ /** Whether the cell is currently being pressed. */
40
+ isPressed: boolean,
41
+ /** Whether the cell is selected. */
42
+ isSelected: boolean,
43
+ /** Whether the cell is focused. */
44
+ isFocused: boolean,
45
+ /**
46
+ * Whether the cell is disabled, according to the calendar's `minValue`, `maxValue`, and `isDisabled` props.
47
+ * Disabled dates are not focusable, and cannot be selected by the user. They are typically
48
+ * displayed with a dimmed appearance.
49
+ */
50
+ isDisabled: boolean,
51
+ /**
52
+ * Whether the cell is unavailable, according to the calendar's `isDateUnavailable` prop. Unavailable dates remain
53
+ * focusable, but cannot be selected by the user. They should be displayed with a visual affordance to indicate they
54
+ * are unavailable, such as a different color or a strikethrough.
55
+ *
56
+ * Note that because they are focusable, unavailable dates must meet a 4.5:1 color contrast ratio,
57
+ * [as defined by WCAG](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html).
58
+ */
59
+ isUnavailable: boolean,
60
+ /**
61
+ * Whether the cell is outside the visible range of the calendar.
62
+ * For example, dates before the first day of a month in the same week.
63
+ */
64
+ isOutsideVisibleRange: boolean,
65
+ /** Whether the cell is part of an invalid selection. */
66
+ isInvalid: boolean,
67
+ /** The day number formatted according to the current locale. */
68
+ formattedDate: string
32
69
  }
33
70
 
34
- export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarState | RangeCalendarState, ref: RefObject<HTMLElement>): CalendarCellAria {
71
+ /**
72
+ * Provides the behavior and accessibility implementation for a calendar cell component.
73
+ * A calendar cell displays a date cell within a calendar grid which can be selected by the user.
74
+ */
75
+ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarState | RangeCalendarState, ref: RefObject<HTMLElement | null>): CalendarCellAria {
35
76
  let {date, isDisabled} = props;
36
- let formatMessage = useMessageFormatter(intlMessages);
77
+ let {errorMessageId, selectedDateDescription} = hookData.get(state);
78
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/calendar');
37
79
  let dateFormatter = useDateFormatter({
38
80
  weekday: 'long',
39
81
  day: 'numeric',
40
82
  month: 'long',
41
83
  year: 'numeric',
42
- era: date.calendar.identifier !== 'gregory' ? 'long' : undefined,
84
+ era: getEraFormat(date),
43
85
  timeZone: state.timeZone
44
86
  });
45
87
  let isSelected = state.isSelected(date);
46
88
  let isFocused = state.isCellFocused(date);
47
89
  isDisabled = isDisabled || state.isCellDisabled(date);
90
+ let isUnavailable = state.isCellUnavailable(date);
91
+ let isSelectable = !isDisabled && !isUnavailable;
92
+ let isInvalid = state.isValueInvalid && (
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
+ );
48
97
 
49
- // For performance, reuse the same date object as before if the new date prop is the same.
50
- // This allows subsequent useMemo results to be reused.
51
- let lastDate = useRef(null);
52
- if (lastDate.current && isEqualDay(date, lastDate.current)) {
53
- date = lastDate.current;
98
+ if (isInvalid) {
99
+ isSelected = true;
54
100
  }
55
101
 
56
- lastDate.current = date;
57
-
102
+ // For performance, reuse the same date object as before if the new date prop is the same.
103
+ // This allows subsequent useMemo results to be reused.
104
+ date = useDeepMemo<CalendarDate>(date, isEqualDay);
58
105
  let nativeDate = useMemo(() => date.toDate(state.timeZone), [date, state.timeZone]);
59
106
 
60
107
  // aria-label should be localize Day of week, Month, Day and Year without Time.
61
108
  let isDateToday = isToday(date, state.timeZone);
62
109
  let label = useMemo(() => {
110
+ let label = '';
111
+
112
+ // If this is a range calendar, add a description of the full selected range
113
+ // to the first and last selected date.
114
+ if (
115
+ 'highlightedRange' in state &&
116
+ state.value &&
117
+ !state.anchorDate &&
118
+ (isSameDay(date, state.value.start) || isSameDay(date, state.value.end))
119
+ ) {
120
+ label = selectedDateDescription + ', ';
121
+ }
122
+
123
+ label += dateFormatter.format(nativeDate);
63
124
  if (isDateToday) {
64
125
  // If date is today, set appropriate string depending on selected state:
65
- return formatMessage(isSelected ? 'todayDateSelected' : 'todayDate', {
66
- date: nativeDate
126
+ label = stringFormatter.format(isSelected ? 'todayDateSelected' : 'todayDate', {
127
+ date: label
67
128
  });
68
129
  } else if (isSelected) {
69
130
  // If date is selected but not today:
70
- return formatMessage('dateSelected', {
71
- date: nativeDate
131
+ label = stringFormatter.format('dateSelected', {
132
+ date: label
72
133
  });
73
134
  }
74
135
 
75
- return dateFormatter.format(nativeDate);
76
- }, [dateFormatter, nativeDate, formatMessage, isSelected, isDateToday]);
136
+ if (state.minValue && isSameDay(date, state.minValue)) {
137
+ label += ', ' + stringFormatter.format('minimumDate');
138
+ } else if (state.maxValue && isSameDay(date, state.maxValue)) {
139
+ label += ', ' + stringFormatter.format('maximumDate');
140
+ }
141
+
142
+ return label;
143
+ }, [dateFormatter, nativeDate, stringFormatter, isSelected, isDateToday, date, state, selectedDateDescription]);
77
144
 
78
145
  // When a cell is focused and this is a range calendar, add a prompt to help
79
146
  // screenreader users know that they are in a range selection mode.
80
- if ('anchorDate' in state && isFocused && !state.isReadOnly) {
81
- let rangeSelectionPrompt = '';
82
-
147
+ let rangeSelectionPrompt = '';
148
+ if ('anchorDate' in state && isFocused && !state.isReadOnly && isSelectable) {
83
149
  // If selection has started add "click to finish selecting range"
84
150
  if (state.anchorDate) {
85
- rangeSelectionPrompt = formatMessage('finishRangeSelectionPrompt');
151
+ rangeSelectionPrompt = stringFormatter.format('finishRangeSelectionPrompt');
86
152
  // Otherwise, add "click to start selecting range" prompt
87
153
  } else {
88
- rangeSelectionPrompt = formatMessage('startRangeSelectionPrompt');
89
- }
90
-
91
- // Append to aria-label
92
- if (rangeSelectionPrompt) {
93
- label = `${label} (${rangeSelectionPrompt})`;
154
+ rangeSelectionPrompt = stringFormatter.format('startRangeSelectionPrompt');
94
155
  }
95
156
  }
96
157
 
158
+ let descriptionProps = useDescription(rangeSelectionPrompt);
159
+
97
160
  let isAnchorPressed = useRef(false);
98
161
  let isRangeBoundaryPressed = useRef(false);
99
162
  let touchDragTimerRef = useRef(null);
@@ -102,12 +165,19 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
102
165
  // again to trigger onPressStart. Cancel presses immediately when the pointer exits.
103
166
  shouldCancelOnPointerExit: 'anchorDate' in state && !!state.anchorDate,
104
167
  preventFocusOnPress: true,
105
- isDisabled,
168
+ isDisabled: !isSelectable || state.isReadOnly,
106
169
  onPressStart(e) {
170
+ if (state.isReadOnly) {
171
+ state.setFocusedDate(date);
172
+ return;
173
+ }
174
+
107
175
  if ('highlightedRange' in state && !state.anchorDate && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
108
176
  // Allow dragging the start or end date of a range to modify it
109
177
  // rather than starting a new selection.
110
- if (state.highlightedRange) {
178
+ // Don't allow dragging when invalid, or weird jumping behavior may occur as date ranges
179
+ // are constrained to available dates. The user will need to select a new range in this case.
180
+ if (state.highlightedRange && !isInvalid) {
111
181
  if (isSameDay(date, state.highlightedRange.start)) {
112
182
  state.setAnchorDate(state.highlightedRange.end);
113
183
  state.setFocusedDate(date);
@@ -149,12 +219,16 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
149
219
  },
150
220
  onPress() {
151
221
  // For non-range selection, always select on press up.
152
- if (!('anchorDate' in state)) {
222
+ if (!('anchorDate' in state) && !state.isReadOnly) {
153
223
  state.selectDate(date);
154
224
  state.setFocusedDate(date);
155
225
  }
156
226
  },
157
227
  onPressUp(e) {
228
+ if (state.isReadOnly) {
229
+ return;
230
+ }
231
+
158
232
  // If the user tapped quickly, the date won't be selected yet and the
159
233
  // timer will still be in progress. In this case, select the date on touch up.
160
234
  // Timer is cleared in onPressEnd.
@@ -180,7 +254,10 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
180
254
  // there will be an announcement to "click to finish selecting range" (above).
181
255
  state.selectDate(date);
182
256
  let nextDay = date.add({days: 1});
183
- if (isSameMonth(date, nextDay)) {
257
+ if (state.isInvalid(nextDay)) {
258
+ nextDay = date.subtract({days: 1});
259
+ }
260
+ if (!state.isInvalid(nextDay)) {
184
261
  state.setFocusedDate(nextDay);
185
262
  }
186
263
  } else if (e.pointerType === 'virtual') {
@@ -201,14 +278,34 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
201
278
  useEffect(() => {
202
279
  if (isFocused && ref.current) {
203
280
  focusWithoutScrolling(ref.current);
281
+
282
+ // Scroll into view if navigating with a keyboard, otherwise
283
+ // try not to shift the view under the user's mouse/finger.
284
+ // If in a overlay, scrollIntoViewport will only cause scrolling
285
+ // up to the overlay scroll body to prevent overlay shifting.
286
+ // Also only scroll into view if the cell actually got focused.
287
+ // There are some cases where the cell might be disabled or inside,
288
+ // an inert container and we don't want to scroll then.
289
+ if (getInteractionModality() !== 'pointer' && document.activeElement === ref.current) {
290
+ scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
291
+ }
204
292
  }
205
293
  }, [isFocused, ref]);
206
294
 
295
+ let cellDateFormatter = useDateFormatter({
296
+ day: 'numeric',
297
+ timeZone: state.timeZone,
298
+ calendar: date.calendar.identifier
299
+ });
300
+
301
+ let formattedDate = useMemo(() => cellDateFormatter.formatToParts(nativeDate).find(part => part.type === 'day').value, [cellDateFormatter, nativeDate]);
302
+
207
303
  return {
208
304
  cellProps: {
209
305
  role: 'gridcell',
210
- 'aria-disabled': isDisabled || null,
211
- 'aria-selected': isSelected
306
+ 'aria-disabled': !isSelectable || null,
307
+ 'aria-selected': isSelected || null,
308
+ 'aria-invalid': isInvalid || null
212
309
  },
213
310
  buttonProps: mergeProps(pressProps, {
214
311
  onFocus() {
@@ -218,11 +315,16 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
218
315
  },
219
316
  tabIndex,
220
317
  role: 'button',
221
- 'aria-disabled': isDisabled || null,
318
+ 'aria-disabled': !isSelectable || null,
222
319
  'aria-label': label,
320
+ 'aria-invalid': isInvalid || null,
321
+ 'aria-describedby': [
322
+ isInvalid ? errorMessageId : null,
323
+ descriptionProps['aria-describedby']
324
+ ].filter(Boolean).join(' ') || undefined,
223
325
  onPointerEnter(e) {
224
326
  // Highlight the date on hover or drag over a date when selecting a range.
225
- if ('highlightDate' in state && (e.pointerType !== 'touch' || state.isDragging)) {
327
+ if ('highlightDate' in state && (e.pointerType !== 'touch' || state.isDragging) && isSelectable) {
226
328
  state.highlightDate(date);
227
329
  }
228
330
  },
@@ -239,6 +341,13 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
239
341
  e.preventDefault();
240
342
  }
241
343
  }),
242
- isPressed
344
+ isPressed,
345
+ isFocused,
346
+ isSelected,
347
+ isDisabled,
348
+ isUnavailable,
349
+ isOutsideVisibleRange: date.compare(state.visibleRange.start) < 0 || date.compare(state.visibleRange.end) > 0,
350
+ isInvalid,
351
+ formattedDate
243
352
  };
244
353
  }
@@ -10,24 +10,51 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {CalendarDate} from '@internationalized/date';
14
- import {CalendarGridAria} from './types';
15
- import {calendarIds, useSelectedDateDescription, useVisibleRangeDescription} from './utils';
16
- import {CalendarPropsBase} from '@react-types/calendar';
13
+ import {CalendarDate, startOfWeek, today} from '@internationalized/date';
17
14
  import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
18
- import {KeyboardEvent} from 'react';
19
- import {mergeProps, useDescription, useLabels} from '@react-aria/utils';
20
- import {useLocale} from '@react-aria/i18n';
15
+ import {DOMAttributes} from '@react-types/shared';
16
+ import {hookData, useVisibleRangeDescription} from './utils';
17
+ import {KeyboardEvent, useMemo} from 'react';
18
+ import {mergeProps, useLabels} from '@react-aria/utils';
19
+ import {useDateFormatter, useLocale} from '@react-aria/i18n';
21
20
 
22
- interface CalendarGridProps extends CalendarPropsBase {
21
+ export interface AriaCalendarGridProps {
22
+ /**
23
+ * The first date displayed in the calendar grid.
24
+ * Defaults to the first visible date in the calendar.
25
+ * Override this to display multiple date grids in a calendar.
26
+ */
23
27
  startDate?: CalendarDate,
24
- endDate?: CalendarDate
28
+ /**
29
+ * The last date displayed in the calendar grid.
30
+ * Defaults to the last visible date in the calendar.
31
+ * Override this to display multiple date grids in a calendar.
32
+ */
33
+ endDate?: CalendarDate,
34
+ /**
35
+ * The style of weekday names to display in the calendar grid header,
36
+ * e.g. single letter, abbreviation, or full day name.
37
+ * @default "narrow"
38
+ */
39
+ weekdayStyle?: 'narrow' | 'short' | 'long'
25
40
  }
26
41
 
27
- export function useCalendarGrid(props: CalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria {
42
+ export interface CalendarGridAria {
43
+ /** Props for the date grid element (e.g. `<table>`). */
44
+ gridProps: DOMAttributes,
45
+ /** Props for the grid header element (e.g. `<thead>`). */
46
+ headerProps: DOMAttributes,
47
+ /** A list of week day abbreviations formatted for the current locale, typically used in column headers. */
48
+ weekDays: string[]
49
+ }
50
+
51
+ /**
52
+ * Provides the behavior and accessibility implementation for a calendar grid component.
53
+ * A calendar grid displays a single grid of days within a calendar or range calendar which
54
+ * can be keyboard navigated and selected by the user.
55
+ */
56
+ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria {
28
57
  let {
29
- isReadOnly = false,
30
- isDisabled = false,
31
58
  startDate = state.visibleRange.start,
32
59
  endDate = state.visibleRange.end
33
60
  } = props;
@@ -43,30 +70,27 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
43
70
  break;
44
71
  case 'PageUp':
45
72
  e.preventDefault();
46
- if (e.shiftKey) {
47
- state.focusPreviousSection();
48
- } else {
49
- state.focusPreviousPage();
50
- }
73
+ e.stopPropagation();
74
+ state.focusPreviousSection(e.shiftKey);
51
75
  break;
52
76
  case 'PageDown':
53
77
  e.preventDefault();
54
- if (e.shiftKey) {
55
- state.focusNextSection();
56
- } else {
57
- state.focusNextPage();
58
- }
78
+ e.stopPropagation();
79
+ state.focusNextSection(e.shiftKey);
59
80
  break;
60
81
  case 'End':
61
82
  e.preventDefault();
62
- state.focusPageEnd();
83
+ e.stopPropagation();
84
+ state.focusSectionEnd();
63
85
  break;
64
86
  case 'Home':
65
87
  e.preventDefault();
66
- state.focusPageStart();
88
+ e.stopPropagation();
89
+ state.focusSectionStart();
67
90
  break;
68
91
  case 'ArrowLeft':
69
92
  e.preventDefault();
93
+ e.stopPropagation();
70
94
  if (direction === 'rtl') {
71
95
  state.focusNextDay();
72
96
  } else {
@@ -75,10 +99,12 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
75
99
  break;
76
100
  case 'ArrowUp':
77
101
  e.preventDefault();
102
+ e.stopPropagation();
78
103
  state.focusPreviousRow();
79
104
  break;
80
105
  case 'ArrowRight':
81
106
  e.preventDefault();
107
+ e.stopPropagation();
82
108
  if (direction === 'rtl') {
83
109
  state.focusPreviousDay();
84
110
  } else {
@@ -87,6 +113,7 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
87
113
  break;
88
114
  case 'ArrowDown':
89
115
  e.preventDefault();
116
+ e.stopPropagation();
90
117
  state.focusNextRow();
91
118
  break;
92
119
  case 'Escape':
@@ -99,24 +126,40 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
99
126
  }
100
127
  };
101
128
 
102
- let selectedDateDescription = useSelectedDateDescription(state);
103
- let descriptionProps = useDescription(selectedDateDescription);
104
- let visibleRangeDescription = useVisibleRangeDescription(startDate, endDate, state.timeZone);
129
+ let visibleRangeDescription = useVisibleRangeDescription(startDate, endDate, state.timeZone, true);
105
130
 
131
+ let {ariaLabel, ariaLabelledBy} = hookData.get(state);
106
132
  let labelProps = useLabels({
107
- 'aria-label': visibleRangeDescription,
108
- 'aria-labelledby': calendarIds.get(state)
133
+ 'aria-label': [ariaLabel, visibleRangeDescription].filter(Boolean).join(', '),
134
+ 'aria-labelledby': ariaLabelledBy
109
135
  });
110
136
 
137
+ let dayFormatter = useDateFormatter({weekday: props.weekdayStyle || 'narrow', timeZone: state.timeZone});
138
+ let {locale} = useLocale();
139
+ let weekDays = useMemo(() => {
140
+ let weekStart = startOfWeek(today(state.timeZone), locale);
141
+ return [...new Array(7).keys()].map((index) => {
142
+ let date = weekStart.add({days: index});
143
+ let dateDay = date.toDate(state.timeZone);
144
+ return dayFormatter.format(dateDay);
145
+ });
146
+ }, [locale, state.timeZone, dayFormatter]);
147
+
111
148
  return {
112
- gridProps: mergeProps(descriptionProps, labelProps, {
149
+ gridProps: mergeProps(labelProps, {
113
150
  role: 'grid',
114
- 'aria-readonly': isReadOnly || null,
115
- 'aria-disabled': isDisabled || null,
151
+ 'aria-readonly': state.isReadOnly || null,
152
+ 'aria-disabled': state.isDisabled || null,
116
153
  'aria-multiselectable': ('highlightedRange' in state) || undefined,
117
154
  onKeyDown,
118
155
  onFocus: () => state.setFocused(true),
119
156
  onBlur: () => state.setFocused(false)
120
- })
157
+ }),
158
+ headerProps: {
159
+ // Column headers are hidden to screen readers to make navigating with a touch screen reader easier.
160
+ // The day names are already included in the label of each cell, so there's no need to announce them twice.
161
+ 'aria-hidden': true
162
+ },
163
+ weekDays
121
164
  };
122
165
  }
@@ -10,17 +10,19 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {CalendarAria} from './types';
14
- import {DateValue, RangeCalendarProps} from '@react-types/calendar';
13
+ import {AriaRangeCalendarProps, DateValue} from '@react-types/calendar';
14
+ import {CalendarAria, useCalendarBase} from './useCalendarBase';
15
+ import {FocusableElement, RefObject} from '@react-types/shared';
15
16
  import {RangeCalendarState} from '@react-stately/calendar';
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';
18
+ import {useRef} from 'react';
19
19
 
20
- export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<T>, state: RangeCalendarState, ref: RefObject<HTMLElement>): CalendarAria {
20
+ /**
21
+ * Provides the behavior and accessibility implementation for a range calendar component.
22
+ * A range calendar displays one or more date grids and allows users to select a contiguous range of dates.
23
+ */
24
+ export function useRangeCalendar<T extends DateValue>(props: AriaRangeCalendarProps<T>, state: RangeCalendarState, ref: RefObject<FocusableElement | null>): CalendarAria {
21
25
  let res = useCalendarBase(props, state);
22
- res.nextButtonProps.id = useId();
23
- res.prevButtonProps.id = useId();
24
26
 
25
27
  // We need to ignore virtual pointer events from VoiceOver due to these bugs.
26
28
  // https://bugs.webkit.org/show_bug.cgi?id=222627
@@ -29,13 +31,14 @@ export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<
29
31
  // We need to match that here otherwise this will fire before the press event in
30
32
  // useCalendarCell, causing range selection to not work properly.
31
33
  let isVirtualClick = useRef(false);
32
- useEvent(useRef(window), 'pointerdown', e => {
34
+ let windowRef = useRef(typeof window !== 'undefined' ? window : null);
35
+ useEvent(windowRef, 'pointerdown', e => {
33
36
  isVirtualClick.current = e.width === 0 && e.height === 0;
34
37
  });
35
38
 
36
39
  // Stop range selection when pressing or releasing a pointer outside the calendar body,
37
40
  // except when pressing the next or previous buttons to switch months.
38
- let endDragging = e => {
41
+ let endDragging = (e: PointerEvent) => {
39
42
  if (isVirtualClick.current) {
40
43
  isVirtualClick.current = false;
41
44
  return;
@@ -46,19 +49,27 @@ export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<
46
49
  return;
47
50
  }
48
51
 
49
- let target = e.target as HTMLElement;
50
- let body = document.getElementById(res.calendarProps.id);
52
+ let target = e.target as Element;
51
53
  if (
52
- (!body.contains(target) || target.getAttribute('role') !== 'button') &&
53
- !document.getElementById(res.nextButtonProps.id)?.contains(target) &&
54
- !document.getElementById(res.prevButtonProps.id)?.contains(target)
54
+ ref.current &&
55
+ ref.current.contains(document.activeElement) &&
56
+ (!ref.current.contains(target) || !target.closest('button, [role="button"]'))
55
57
  ) {
56
58
  state.selectFocusedDate();
57
59
  }
58
60
  };
59
61
 
60
- useEvent(useRef(window), 'pointerup', endDragging);
61
- useEvent(useRef(window), 'pointercancel', endDragging);
62
+ useEvent(windowRef, 'pointerup', endDragging);
63
+
64
+ // Also stop range selection on blur, e.g. tabbing away from the calendar.
65
+ res.calendarProps.onBlur = e => {
66
+ if (!ref.current) {
67
+ return;
68
+ }
69
+ if ((!e.relatedTarget || !ref.current.contains(e.relatedTarget)) && state.anchorDate) {
70
+ state.selectFocusedDate();
71
+ }
72
+ };
62
73
 
63
74
  // Prevent touch scrolling while dragging
64
75
  useEvent(ref, 'touchmove', e => {