@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.
- package/dist/ar-AE.main.js +17 -0
- package/dist/ar-AE.main.js.map +1 -0
- package/dist/ar-AE.mjs +19 -0
- package/dist/ar-AE.module.js +19 -0
- package/dist/ar-AE.module.js.map +1 -0
- package/dist/bg-BG.main.js +17 -0
- package/dist/bg-BG.main.js.map +1 -0
- package/dist/bg-BG.mjs +19 -0
- package/dist/bg-BG.module.js +19 -0
- package/dist/bg-BG.module.js.map +1 -0
- package/dist/cs-CZ.main.js +17 -0
- package/dist/cs-CZ.main.js.map +1 -0
- package/dist/cs-CZ.mjs +19 -0
- package/dist/cs-CZ.module.js +19 -0
- package/dist/cs-CZ.module.js.map +1 -0
- package/dist/da-DK.main.js +17 -0
- package/dist/da-DK.main.js.map +1 -0
- package/dist/da-DK.mjs +19 -0
- package/dist/da-DK.module.js +19 -0
- package/dist/da-DK.module.js.map +1 -0
- package/dist/de-DE.main.js +17 -0
- package/dist/de-DE.main.js.map +1 -0
- package/dist/de-DE.mjs +19 -0
- package/dist/de-DE.module.js +19 -0
- package/dist/de-DE.module.js.map +1 -0
- package/dist/el-GR.main.js +17 -0
- package/dist/el-GR.main.js.map +1 -0
- package/dist/el-GR.mjs +19 -0
- package/dist/el-GR.module.js +19 -0
- package/dist/el-GR.module.js.map +1 -0
- package/dist/en-US.main.js +17 -0
- package/dist/en-US.main.js.map +1 -0
- package/dist/en-US.mjs +19 -0
- package/dist/en-US.module.js +19 -0
- package/dist/en-US.module.js.map +1 -0
- package/dist/es-ES.main.js +17 -0
- package/dist/es-ES.main.js.map +1 -0
- package/dist/es-ES.mjs +19 -0
- package/dist/es-ES.module.js +19 -0
- package/dist/es-ES.module.js.map +1 -0
- package/dist/et-EE.main.js +17 -0
- package/dist/et-EE.main.js.map +1 -0
- package/dist/et-EE.mjs +19 -0
- package/dist/et-EE.module.js +19 -0
- package/dist/et-EE.module.js.map +1 -0
- package/dist/fi-FI.main.js +17 -0
- package/dist/fi-FI.main.js.map +1 -0
- package/dist/fi-FI.mjs +19 -0
- package/dist/fi-FI.module.js +19 -0
- package/dist/fi-FI.module.js.map +1 -0
- package/dist/fr-FR.main.js +17 -0
- package/dist/fr-FR.main.js.map +1 -0
- package/dist/fr-FR.mjs +19 -0
- package/dist/fr-FR.module.js +19 -0
- package/dist/fr-FR.module.js.map +1 -0
- package/dist/he-IL.main.js +17 -0
- package/dist/he-IL.main.js.map +1 -0
- package/dist/he-IL.mjs +19 -0
- package/dist/he-IL.module.js +19 -0
- package/dist/he-IL.module.js.map +1 -0
- package/dist/hr-HR.main.js +17 -0
- package/dist/hr-HR.main.js.map +1 -0
- package/dist/hr-HR.mjs +19 -0
- package/dist/hr-HR.module.js +19 -0
- package/dist/hr-HR.module.js.map +1 -0
- package/dist/hu-HU.main.js +17 -0
- package/dist/hu-HU.main.js.map +1 -0
- package/dist/hu-HU.mjs +19 -0
- package/dist/hu-HU.module.js +19 -0
- package/dist/hu-HU.module.js.map +1 -0
- package/dist/import.mjs +23 -0
- package/dist/intlStrings.main.js +108 -0
- package/dist/intlStrings.main.js.map +1 -0
- package/dist/intlStrings.mjs +110 -0
- package/dist/intlStrings.module.js +110 -0
- package/dist/intlStrings.module.js.map +1 -0
- package/dist/it-IT.main.js +17 -0
- package/dist/it-IT.main.js.map +1 -0
- package/dist/it-IT.mjs +19 -0
- package/dist/it-IT.module.js +19 -0
- package/dist/it-IT.module.js.map +1 -0
- package/dist/ja-JP.main.js +17 -0
- package/dist/ja-JP.main.js.map +1 -0
- package/dist/ja-JP.mjs +19 -0
- package/dist/ja-JP.module.js +19 -0
- package/dist/ja-JP.module.js.map +1 -0
- package/dist/ko-KR.main.js +17 -0
- package/dist/ko-KR.main.js.map +1 -0
- package/dist/ko-KR.mjs +19 -0
- package/dist/ko-KR.module.js +19 -0
- package/dist/ko-KR.module.js.map +1 -0
- package/dist/lt-LT.main.js +17 -0
- package/dist/lt-LT.main.js.map +1 -0
- package/dist/lt-LT.mjs +19 -0
- package/dist/lt-LT.module.js +19 -0
- package/dist/lt-LT.module.js.map +1 -0
- package/dist/lv-LV.main.js +17 -0
- package/dist/lv-LV.main.js.map +1 -0
- package/dist/lv-LV.mjs +19 -0
- package/dist/lv-LV.module.js +19 -0
- package/dist/lv-LV.module.js.map +1 -0
- package/dist/main.js +19 -660
- package/dist/main.js.map +1 -1
- package/dist/module.js +16 -644
- package/dist/module.js.map +1 -1
- package/dist/nb-NO.main.js +17 -0
- package/dist/nb-NO.main.js.map +1 -0
- package/dist/nb-NO.mjs +19 -0
- package/dist/nb-NO.module.js +19 -0
- package/dist/nb-NO.module.js.map +1 -0
- package/dist/nl-NL.main.js +17 -0
- package/dist/nl-NL.main.js.map +1 -0
- package/dist/nl-NL.mjs +19 -0
- package/dist/nl-NL.module.js +19 -0
- package/dist/nl-NL.module.js.map +1 -0
- package/dist/pl-PL.main.js +17 -0
- package/dist/pl-PL.main.js.map +1 -0
- package/dist/pl-PL.mjs +19 -0
- package/dist/pl-PL.module.js +19 -0
- package/dist/pl-PL.module.js.map +1 -0
- package/dist/pt-BR.main.js +17 -0
- package/dist/pt-BR.main.js.map +1 -0
- package/dist/pt-BR.mjs +19 -0
- package/dist/pt-BR.module.js +19 -0
- package/dist/pt-BR.module.js.map +1 -0
- package/dist/pt-PT.main.js +17 -0
- package/dist/pt-PT.main.js.map +1 -0
- package/dist/pt-PT.mjs +19 -0
- package/dist/pt-PT.module.js +19 -0
- package/dist/pt-PT.module.js.map +1 -0
- package/dist/ro-RO.main.js +17 -0
- package/dist/ro-RO.main.js.map +1 -0
- package/dist/ro-RO.mjs +19 -0
- package/dist/ro-RO.module.js +19 -0
- package/dist/ro-RO.module.js.map +1 -0
- package/dist/ru-RU.main.js +17 -0
- package/dist/ru-RU.main.js.map +1 -0
- package/dist/ru-RU.mjs +19 -0
- package/dist/ru-RU.module.js +19 -0
- package/dist/ru-RU.module.js.map +1 -0
- package/dist/sk-SK.main.js +17 -0
- package/dist/sk-SK.main.js.map +1 -0
- package/dist/sk-SK.mjs +19 -0
- package/dist/sk-SK.module.js +19 -0
- package/dist/sk-SK.module.js.map +1 -0
- package/dist/sl-SI.main.js +17 -0
- package/dist/sl-SI.main.js.map +1 -0
- package/dist/sl-SI.mjs +19 -0
- package/dist/sl-SI.module.js +19 -0
- package/dist/sl-SI.module.js.map +1 -0
- package/dist/sr-SP.main.js +17 -0
- package/dist/sr-SP.main.js.map +1 -0
- package/dist/sr-SP.mjs +19 -0
- package/dist/sr-SP.module.js +19 -0
- package/dist/sr-SP.module.js.map +1 -0
- package/dist/sv-SE.main.js +17 -0
- package/dist/sv-SE.main.js.map +1 -0
- package/dist/sv-SE.mjs +19 -0
- package/dist/sv-SE.module.js +19 -0
- package/dist/sv-SE.module.js.map +1 -0
- package/dist/tr-TR.main.js +17 -0
- package/dist/tr-TR.main.js.map +1 -0
- package/dist/tr-TR.mjs +19 -0
- package/dist/tr-TR.module.js +19 -0
- package/dist/tr-TR.module.js.map +1 -0
- package/dist/types.d.ts +96 -21
- package/dist/types.d.ts.map +1 -1
- package/dist/uk-UA.main.js +17 -0
- package/dist/uk-UA.main.js.map +1 -0
- package/dist/uk-UA.mjs +19 -0
- package/dist/uk-UA.module.js +19 -0
- package/dist/uk-UA.module.js.map +1 -0
- package/dist/useCalendar.main.js +25 -0
- package/dist/useCalendar.main.js.map +1 -0
- package/{src/types.ts → dist/useCalendar.mjs} +7 -12
- package/dist/useCalendar.module.js +20 -0
- package/dist/useCalendar.module.js.map +1 -0
- package/dist/useCalendarBase.main.js +112 -0
- package/dist/useCalendarBase.main.js.map +1 -0
- package/dist/useCalendarBase.mjs +107 -0
- package/dist/useCalendarBase.module.js +107 -0
- package/dist/useCalendarBase.module.js.map +1 -0
- package/dist/useCalendarCell.main.js +276 -0
- package/dist/useCalendarCell.main.js.map +1 -0
- package/dist/useCalendarCell.mjs +271 -0
- package/dist/useCalendarCell.module.js +271 -0
- package/dist/useCalendarCell.module.js.map +1 -0
- package/dist/useCalendarGrid.main.js +139 -0
- package/dist/useCalendarGrid.main.js.map +1 -0
- package/dist/useCalendarGrid.mjs +134 -0
- package/dist/useCalendarGrid.module.js +134 -0
- package/dist/useCalendarGrid.module.js.map +1 -0
- package/dist/useRangeCalendar.main.js +66 -0
- package/dist/useRangeCalendar.main.js.map +1 -0
- package/dist/useRangeCalendar.mjs +61 -0
- package/dist/useRangeCalendar.module.js +61 -0
- package/dist/useRangeCalendar.module.js.map +1 -0
- package/dist/utils.main.js +138 -0
- package/dist/utils.main.js.map +1 -0
- package/dist/utils.mjs +130 -0
- package/dist/utils.module.js +130 -0
- package/dist/utils.module.js.map +1 -0
- package/dist/zh-CN.main.js +17 -0
- package/dist/zh-CN.main.js.map +1 -0
- package/dist/zh-CN.mjs +19 -0
- package/dist/zh-CN.module.js +19 -0
- package/dist/zh-CN.module.js.map +1 -0
- package/dist/zh-TW.main.js +17 -0
- package/dist/zh-TW.main.js.map +1 -0
- package/dist/zh-TW.mjs +19 -0
- package/dist/zh-TW.module.js +19 -0
- package/dist/zh-TW.module.js.map +1 -0
- package/package.json +19 -15
- package/src/index.ts +9 -6
- package/src/useCalendar.ts +7 -4
- package/src/useCalendarBase.ts +67 -22
- package/src/useCalendarCell.ts +154 -45
- package/src/useCalendarGrid.ts +76 -33
- package/src/useRangeCalendar.ts +28 -17
- package/src/utils.ts +79 -15
- package/src/useCalendarTableHeader.ts +0 -11
package/src/useCalendarCell.ts
CHANGED
|
@@ -10,90 +10,153 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {CalendarDate, isEqualDay, isSameDay,
|
|
13
|
+
import {CalendarDate, isEqualDay, isSameDay, isToday} from '@internationalized/date';
|
|
14
14
|
import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
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 {
|
|
20
|
-
import {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
date:
|
|
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
|
-
|
|
71
|
-
date:
|
|
131
|
+
label = stringFormatter.format('dateSelected', {
|
|
132
|
+
date: label
|
|
72
133
|
});
|
|
73
134
|
}
|
|
74
135
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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 =
|
|
151
|
+
rangeSelectionPrompt = stringFormatter.format('finishRangeSelectionPrompt');
|
|
86
152
|
// Otherwise, add "click to start selecting range" prompt
|
|
87
153
|
} else {
|
|
88
|
-
rangeSelectionPrompt =
|
|
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
|
-
|
|
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 (
|
|
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':
|
|
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':
|
|
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
|
}
|
package/src/useCalendarGrid.ts
CHANGED
|
@@ -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 {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
state.focusSectionEnd();
|
|
63
85
|
break;
|
|
64
86
|
case 'Home':
|
|
65
87
|
e.preventDefault();
|
|
66
|
-
|
|
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
|
|
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':
|
|
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(
|
|
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
|
}
|
package/src/useRangeCalendar.ts
CHANGED
|
@@ -10,17 +10,19 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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 {
|
|
17
|
-
import {
|
|
18
|
-
import {useEvent, useId} from '@react-aria/utils';
|
|
17
|
+
import {useEvent} from '@react-aria/utils';
|
|
18
|
+
import {useRef} from 'react';
|
|
19
19
|
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
50
|
-
let body = document.getElementById(res.calendarProps.id);
|
|
52
|
+
let target = e.target as Element;
|
|
51
53
|
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
!
|
|
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(
|
|
61
|
-
|
|
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 => {
|