@react-aria/calendar 3.0.0-alpha.3 → 3.0.0-alpha.4
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/main.js +282 -220
- package/dist/main.js.map +1 -1
- package/dist/module.js +278 -215
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +78 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +0 -1
- package/src/types.ts +17 -2
- package/src/useCalendar.ts +4 -0
- package/src/useCalendarBase.ts +24 -3
- package/src/useCalendarCell.ts +79 -13
- package/src/useCalendarGrid.ts +37 -9
- package/src/useRangeCalendar.ts +5 -1
- package/src/utils.ts +7 -5
- package/src/useCalendarTableHeader.ts +0 -11
package/src/useCalendarBase.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {DOMProps} from '@react-types/shared';
|
|
|
20
20
|
import intlMessages from '../intl/*.json';
|
|
21
21
|
import {mergeProps, useDescription, useId, useUpdateEffect} from '@react-aria/utils';
|
|
22
22
|
import {useMessageFormatter} from '@react-aria/i18n';
|
|
23
|
+
import {useRef} from 'react';
|
|
23
24
|
|
|
24
25
|
export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: CalendarState | RangeCalendarState): CalendarAria {
|
|
25
26
|
let formatMessage = useMessageFormatter(intlMessages);
|
|
@@ -49,6 +50,21 @@ export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: Cale
|
|
|
49
50
|
// Label the child grid elements by the group element if it is labelled.
|
|
50
51
|
calendarIds.set(state, props['aria-label'] || props['aria-labelledby'] ? calendarId : null);
|
|
51
52
|
|
|
53
|
+
// If the next or previous buttons become disabled while they are focused, move focus to the calendar body.
|
|
54
|
+
let nextFocused = useRef(false);
|
|
55
|
+
let nextDisabled = props.isDisabled || state.isNextVisibleRangeInvalid();
|
|
56
|
+
if (nextDisabled && nextFocused.current) {
|
|
57
|
+
nextFocused.current = false;
|
|
58
|
+
state.setFocused(true);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let previousFocused = useRef(false);
|
|
62
|
+
let previousDisabled = props.isDisabled || state.isPreviousVisibleRangeInvalid();
|
|
63
|
+
if (previousDisabled && previousFocused.current) {
|
|
64
|
+
previousFocused.current = false;
|
|
65
|
+
state.setFocused(true);
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
return {
|
|
53
69
|
calendarProps: mergeProps(descriptionProps, {
|
|
54
70
|
role: 'group',
|
|
@@ -59,12 +75,17 @@ export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: Cale
|
|
|
59
75
|
nextButtonProps: {
|
|
60
76
|
onPress: () => state.focusNextPage(),
|
|
61
77
|
'aria-label': formatMessage('next'),
|
|
62
|
-
isDisabled:
|
|
78
|
+
isDisabled: nextDisabled,
|
|
79
|
+
onFocus: () => nextFocused.current = true,
|
|
80
|
+
onBlur: () => nextFocused.current = false
|
|
63
81
|
},
|
|
64
82
|
prevButtonProps: {
|
|
65
83
|
onPress: () => state.focusPreviousPage(),
|
|
66
84
|
'aria-label': formatMessage('previous'),
|
|
67
|
-
isDisabled:
|
|
68
|
-
|
|
85
|
+
isDisabled: previousDisabled,
|
|
86
|
+
onFocus: () => previousFocused.current = true,
|
|
87
|
+
onBlur: () => previousFocused.current = false
|
|
88
|
+
},
|
|
89
|
+
title: visibleRangeDescription
|
|
69
90
|
};
|
|
70
91
|
}
|
package/src/useCalendarCell.ts
CHANGED
|
@@ -10,27 +10,65 @@
|
|
|
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
15
|
import {focusWithoutScrolling} from '@react-aria/utils';
|
|
16
16
|
import {HTMLAttributes, RefObject, useEffect, useMemo, useRef} from 'react';
|
|
17
17
|
// @ts-ignore
|
|
18
18
|
import intlMessages from '../intl/*.json';
|
|
19
19
|
import {mergeProps} from '@react-aria/utils';
|
|
20
|
-
import {PressProps, usePress} from '@react-aria/interactions';
|
|
21
20
|
import {useDateFormatter, useMessageFormatter} from '@react-aria/i18n';
|
|
21
|
+
import {usePress} from '@react-aria/interactions';
|
|
22
22
|
|
|
23
23
|
export interface AriaCalendarCellProps {
|
|
24
|
+
/** The date that this cell represents. */
|
|
24
25
|
date: CalendarDate,
|
|
26
|
+
/**
|
|
27
|
+
* Whether the cell is disabled. By default, this is determined by the
|
|
28
|
+
* Calendar's `minValue`, `maxValue`, and `isDisabled` props.
|
|
29
|
+
*/
|
|
25
30
|
isDisabled?: boolean
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
interface CalendarCellAria {
|
|
29
|
-
|
|
34
|
+
/** Props for the grid cell element (e.g. `<td>`). */
|
|
35
|
+
cellProps: HTMLAttributes<HTMLElement>,
|
|
36
|
+
/** Props for the button element within the cell. */
|
|
30
37
|
buttonProps: HTMLAttributes<HTMLElement>,
|
|
31
|
-
|
|
38
|
+
/** Whether the cell is currently being pressed. */
|
|
39
|
+
isPressed: boolean,
|
|
40
|
+
/** Whether the cell is selected. */
|
|
41
|
+
isSelected: boolean,
|
|
42
|
+
/** Whether the cell is focused. */
|
|
43
|
+
isFocused: boolean,
|
|
44
|
+
/**
|
|
45
|
+
* Whether the cell is disabled, according to the calendar's `minValue`, `maxValue`, and `isDisabled` props.
|
|
46
|
+
* Disabled dates are not focusable, and cannot be selected by the user. They are typically
|
|
47
|
+
* displayed with a dimmed appearance.
|
|
48
|
+
*/
|
|
49
|
+
isDisabled: boolean,
|
|
50
|
+
/**
|
|
51
|
+
* Whether the cell is unavailable, according to the calendar's `isDateUnavailable` prop. Unavailable dates remain
|
|
52
|
+
* focusable, but cannot be selected by the user. They should be displayed with a visual affordance to indicate they
|
|
53
|
+
* are unavailable, such as a different color or a strikethrough.
|
|
54
|
+
*
|
|
55
|
+
* Note that because they are focusable, unavailable dates must meet a 4.5:1 color contrast ratio,
|
|
56
|
+
* [as defined by WCAG](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html).
|
|
57
|
+
*/
|
|
58
|
+
isUnavailable: boolean,
|
|
59
|
+
/**
|
|
60
|
+
* Whether the cell is outside the visible range of the calendar.
|
|
61
|
+
* For example, dates before the first day of a month in the same week.
|
|
62
|
+
*/
|
|
63
|
+
isOutsideVisibleRange: boolean,
|
|
64
|
+
/** The day number formatted according to the current locale. */
|
|
65
|
+
formattedDate: string
|
|
32
66
|
}
|
|
33
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Provides the behavior and accessibility implementation for a calendar cell component.
|
|
70
|
+
* A calendar cell displays a date cell within a calendar grid which can be selected by the user.
|
|
71
|
+
*/
|
|
34
72
|
export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarState | RangeCalendarState, ref: RefObject<HTMLElement>): CalendarCellAria {
|
|
35
73
|
let {date, isDisabled} = props;
|
|
36
74
|
let formatMessage = useMessageFormatter(intlMessages);
|
|
@@ -45,6 +83,8 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
|
|
|
45
83
|
let isSelected = state.isSelected(date);
|
|
46
84
|
let isFocused = state.isCellFocused(date);
|
|
47
85
|
isDisabled = isDisabled || state.isCellDisabled(date);
|
|
86
|
+
let isUnavailable = state.isCellUnavailable(date);
|
|
87
|
+
let isSelectable = !isDisabled && !isUnavailable;
|
|
48
88
|
|
|
49
89
|
// For performance, reuse the same date object as before if the new date prop is the same.
|
|
50
90
|
// This allows subsequent useMemo results to be reused.
|
|
@@ -77,7 +117,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
|
|
|
77
117
|
|
|
78
118
|
// When a cell is focused and this is a range calendar, add a prompt to help
|
|
79
119
|
// screenreader users know that they are in a range selection mode.
|
|
80
|
-
if ('anchorDate' in state && isFocused && !state.isReadOnly) {
|
|
120
|
+
if ('anchorDate' in state && isFocused && !state.isReadOnly && isSelectable) {
|
|
81
121
|
let rangeSelectionPrompt = '';
|
|
82
122
|
|
|
83
123
|
// If selection has started add "click to finish selecting range"
|
|
@@ -102,8 +142,13 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
|
|
|
102
142
|
// again to trigger onPressStart. Cancel presses immediately when the pointer exits.
|
|
103
143
|
shouldCancelOnPointerExit: 'anchorDate' in state && !!state.anchorDate,
|
|
104
144
|
preventFocusOnPress: true,
|
|
105
|
-
isDisabled,
|
|
145
|
+
isDisabled: !isSelectable,
|
|
106
146
|
onPressStart(e) {
|
|
147
|
+
if (state.isReadOnly) {
|
|
148
|
+
state.setFocusedDate(date);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
107
152
|
if ('highlightedRange' in state && !state.anchorDate && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
|
|
108
153
|
// Allow dragging the start or end date of a range to modify it
|
|
109
154
|
// rather than starting a new selection.
|
|
@@ -149,12 +194,16 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
|
|
|
149
194
|
},
|
|
150
195
|
onPress() {
|
|
151
196
|
// For non-range selection, always select on press up.
|
|
152
|
-
if (!('anchorDate' in state)) {
|
|
197
|
+
if (!('anchorDate' in state) && !state.isReadOnly) {
|
|
153
198
|
state.selectDate(date);
|
|
154
199
|
state.setFocusedDate(date);
|
|
155
200
|
}
|
|
156
201
|
},
|
|
157
202
|
onPressUp(e) {
|
|
203
|
+
if (state.isReadOnly) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
158
207
|
// If the user tapped quickly, the date won't be selected yet and the
|
|
159
208
|
// timer will still be in progress. In this case, select the date on touch up.
|
|
160
209
|
// Timer is cleared in onPressEnd.
|
|
@@ -180,7 +229,10 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
|
|
|
180
229
|
// there will be an announcement to "click to finish selecting range" (above).
|
|
181
230
|
state.selectDate(date);
|
|
182
231
|
let nextDay = date.add({days: 1});
|
|
183
|
-
if (
|
|
232
|
+
if (state.isInvalid(nextDay)) {
|
|
233
|
+
nextDay = date.subtract({days: 1});
|
|
234
|
+
}
|
|
235
|
+
if (!state.isInvalid(nextDay)) {
|
|
184
236
|
state.setFocusedDate(nextDay);
|
|
185
237
|
}
|
|
186
238
|
} else if (e.pointerType === 'virtual') {
|
|
@@ -204,11 +256,19 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
|
|
|
204
256
|
}
|
|
205
257
|
}, [isFocused, ref]);
|
|
206
258
|
|
|
259
|
+
let cellDateFormatter = useDateFormatter({
|
|
260
|
+
day: 'numeric',
|
|
261
|
+
timeZone: state.timeZone,
|
|
262
|
+
calendar: date.calendar.identifier
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
let formattedDate = useMemo(() => cellDateFormatter.format(nativeDate), [cellDateFormatter, nativeDate]);
|
|
266
|
+
|
|
207
267
|
return {
|
|
208
268
|
cellProps: {
|
|
209
269
|
role: 'gridcell',
|
|
210
|
-
'aria-disabled':
|
|
211
|
-
'aria-selected': isSelected
|
|
270
|
+
'aria-disabled': !isSelectable || null,
|
|
271
|
+
'aria-selected': isSelectable ? isSelected : null
|
|
212
272
|
},
|
|
213
273
|
buttonProps: mergeProps(pressProps, {
|
|
214
274
|
onFocus() {
|
|
@@ -218,11 +278,11 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
|
|
|
218
278
|
},
|
|
219
279
|
tabIndex,
|
|
220
280
|
role: 'button',
|
|
221
|
-
'aria-disabled':
|
|
281
|
+
'aria-disabled': !isSelectable || null,
|
|
222
282
|
'aria-label': label,
|
|
223
283
|
onPointerEnter(e) {
|
|
224
284
|
// Highlight the date on hover or drag over a date when selecting a range.
|
|
225
|
-
if ('highlightDate' in state && (e.pointerType !== 'touch' || state.isDragging)) {
|
|
285
|
+
if ('highlightDate' in state && (e.pointerType !== 'touch' || state.isDragging) && isSelectable) {
|
|
226
286
|
state.highlightDate(date);
|
|
227
287
|
}
|
|
228
288
|
},
|
|
@@ -239,6 +299,12 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
|
|
|
239
299
|
e.preventDefault();
|
|
240
300
|
}
|
|
241
301
|
}),
|
|
242
|
-
isPressed
|
|
302
|
+
isPressed,
|
|
303
|
+
isFocused,
|
|
304
|
+
isSelected,
|
|
305
|
+
isDisabled,
|
|
306
|
+
isUnavailable,
|
|
307
|
+
isOutsideVisibleRange: date.compare(state.visibleRange.start) < 0 || date.compare(state.visibleRange.end) > 0,
|
|
308
|
+
formattedDate
|
|
243
309
|
};
|
|
244
310
|
}
|
package/src/useCalendarGrid.ts
CHANGED
|
@@ -10,24 +10,36 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {CalendarDate} from '@internationalized/date';
|
|
13
|
+
import {CalendarDate, startOfWeek} from '@internationalized/date';
|
|
14
14
|
import {CalendarGridAria} from './types';
|
|
15
15
|
import {calendarIds, useSelectedDateDescription, useVisibleRangeDescription} from './utils';
|
|
16
|
-
import {CalendarPropsBase} from '@react-types/calendar';
|
|
17
16
|
import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
|
|
18
17
|
import {KeyboardEvent} from 'react';
|
|
19
18
|
import {mergeProps, useDescription, useLabels} from '@react-aria/utils';
|
|
20
|
-
import {useLocale} from '@react-aria/i18n';
|
|
19
|
+
import {useDateFormatter, useLocale} from '@react-aria/i18n';
|
|
21
20
|
|
|
22
|
-
interface CalendarGridProps
|
|
21
|
+
interface CalendarGridProps {
|
|
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,
|
|
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
|
+
*/
|
|
24
33
|
endDate?: CalendarDate
|
|
25
34
|
}
|
|
26
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Provides the behavior and accessibility implementation for a calendar grid component.
|
|
38
|
+
* A calendar grid displays a single grid of days within a calendar or range calendar which
|
|
39
|
+
* can be keyboard navigated and selected by the user.
|
|
40
|
+
*/
|
|
27
41
|
export function useCalendarGrid(props: CalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria {
|
|
28
42
|
let {
|
|
29
|
-
isReadOnly = false,
|
|
30
|
-
isDisabled = false,
|
|
31
43
|
startDate = state.visibleRange.start,
|
|
32
44
|
endDate = state.visibleRange.end
|
|
33
45
|
} = props;
|
|
@@ -108,15 +120,31 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
|
|
|
108
120
|
'aria-labelledby': calendarIds.get(state)
|
|
109
121
|
});
|
|
110
122
|
|
|
123
|
+
let dayFormatter = useDateFormatter({weekday: 'narrow', timeZone: state.timeZone});
|
|
124
|
+
let dayFormatterLong = useDateFormatter({weekday: 'long', timeZone: state.timeZone});
|
|
125
|
+
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
|
+
});
|
|
137
|
+
|
|
111
138
|
return {
|
|
112
139
|
gridProps: mergeProps(descriptionProps, labelProps, {
|
|
113
140
|
role: 'grid',
|
|
114
|
-
'aria-readonly': isReadOnly || null,
|
|
115
|
-
'aria-disabled': isDisabled || null,
|
|
141
|
+
'aria-readonly': state.isReadOnly || null,
|
|
142
|
+
'aria-disabled': state.isDisabled || null,
|
|
116
143
|
'aria-multiselectable': ('highlightedRange' in state) || undefined,
|
|
117
144
|
onKeyDown,
|
|
118
145
|
onFocus: () => state.setFocused(true),
|
|
119
146
|
onBlur: () => state.setFocused(false)
|
|
120
|
-
})
|
|
147
|
+
}),
|
|
148
|
+
weekDays
|
|
121
149
|
};
|
|
122
150
|
}
|
package/src/useRangeCalendar.ts
CHANGED
|
@@ -17,6 +17,10 @@ import {RefObject, useRef} from 'react';
|
|
|
17
17
|
import {useCalendarBase} from './useCalendarBase';
|
|
18
18
|
import {useEvent, useId} from '@react-aria/utils';
|
|
19
19
|
|
|
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
|
+
*/
|
|
20
24
|
export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<T>, state: RangeCalendarState, ref: RefObject<HTMLElement>): CalendarAria {
|
|
21
25
|
let res = useCalendarBase(props, state);
|
|
22
26
|
res.nextButtonProps.id = useId();
|
|
@@ -49,7 +53,7 @@ export function useRangeCalendar<T extends DateValue>(props: RangeCalendarProps<
|
|
|
49
53
|
let target = e.target as HTMLElement;
|
|
50
54
|
let body = document.getElementById(res.calendarProps.id);
|
|
51
55
|
if (
|
|
52
|
-
(!body.contains(target) || target.
|
|
56
|
+
(!body.contains(target) || !target.closest('[role="button"]')) &&
|
|
53
57
|
!document.getElementById(res.nextButtonProps.id)?.contains(target) &&
|
|
54
58
|
!document.getElementById(res.prevButtonProps.id)?.contains(target)
|
|
55
59
|
) {
|
package/src/utils.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {CalendarDate, endOfMonth, isSameDay, startOfMonth
|
|
13
|
+
import {CalendarDate, endOfMonth, isSameDay, startOfMonth} from '@internationalized/date';
|
|
14
14
|
import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
|
|
15
15
|
// @ts-ignore
|
|
16
16
|
import intlMessages from '../intl/*.json';
|
|
@@ -36,9 +36,9 @@ export function useSelectedDateDescription(state: CalendarState | RangeCalendarS
|
|
|
36
36
|
// Use a single date message if the start and end dates are the same day,
|
|
37
37
|
// otherwise include both dates.
|
|
38
38
|
if (isSameDay(start, end)) {
|
|
39
|
-
return formatMessage('selectedDateDescription', {date: toDate(
|
|
39
|
+
return formatMessage('selectedDateDescription', {date: start.toDate(state.timeZone)});
|
|
40
40
|
} else {
|
|
41
|
-
return formatMessage('selectedRangeDescription', {start: toDate(
|
|
41
|
+
return formatMessage('selectedRangeDescription', {start: start.toDate(state.timeZone), end: end.toDate(state.timeZone)});
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
return '';
|
|
@@ -50,12 +50,14 @@ export function useVisibleRangeDescription(startDate: CalendarDate, endDate: Cal
|
|
|
50
50
|
month: 'long',
|
|
51
51
|
year: 'numeric',
|
|
52
52
|
era: startDate.calendar.identifier !== 'gregory' ? 'long' : undefined,
|
|
53
|
-
calendar: startDate.calendar.identifier
|
|
53
|
+
calendar: startDate.calendar.identifier,
|
|
54
|
+
timeZone
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
let dateFormatter = useDateFormatter({
|
|
57
58
|
dateStyle: 'long',
|
|
58
|
-
calendar: startDate.calendar.identifier
|
|
59
|
+
calendar: startDate.calendar.identifier,
|
|
60
|
+
timeZone
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
return useMemo(() => {
|