@react-stately/calendar 3.0.0-alpha.1 → 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.
@@ -14,7 +14,8 @@ import {alignCenter, alignEnd, alignStart, constrainStart, constrainValue, isInv
14
14
  import {
15
15
  Calendar,
16
16
  CalendarDate,
17
- Duration,
17
+ DateDuration,
18
+ DateFormatter,
18
19
  GregorianCalendar,
19
20
  isSameDay,
20
21
  toCalendar,
@@ -24,23 +25,37 @@ import {
24
25
  import {CalendarProps, DateValue} from '@react-types/calendar';
25
26
  import {CalendarState} from './types';
26
27
  import {useControlledState} from '@react-stately/utils';
27
- import {useDateFormatter} from '@react-aria/i18n';
28
- import {useEffect, useMemo, useRef, useState} from 'react';
28
+ import {useMemo, useRef, useState} from 'react';
29
29
 
30
- interface CalendarStateOptions<T extends DateValue> extends CalendarProps<T> {
30
+ interface CalendarStateOptions extends CalendarProps<DateValue> {
31
+ /** The locale to display and edit the value according to. */
31
32
  locale: string,
33
+ /**
34
+ * A function that creates a [Calendar](../internationalized/date/Calendar.html)
35
+ * object for a given calendar identifier. Such a function may be imported from the
36
+ * `@internationalized/date` package, or manually implemented to include support for
37
+ * only certain calendars.
38
+ */
32
39
  createCalendar: (name: string) => Calendar,
33
- visibleDuration?: Duration,
40
+ /**
41
+ * The amount of days that will be displayed at once. This affects how pagination works.
42
+ * @default {months: 1}
43
+ */
44
+ visibleDuration?: DateDuration,
45
+ /** Determines how to align the initial selection relative to the visible date range. */
34
46
  selectionAlignment?: 'start' | 'center' | 'end'
35
47
  }
36
48
 
37
- export function useCalendarState<T extends DateValue>(props: CalendarStateOptions<T>): CalendarState {
38
- let defaultFormatter = useDateFormatter();
49
+ /**
50
+ * Provides state management for a calendar component.
51
+ * A calendar displays one or more date grids and allows users to select a single date.
52
+ */
53
+ export function useCalendarState(props: CalendarStateOptions): CalendarState {
54
+ let defaultFormatter = useMemo(() => new DateFormatter(props.locale), [props.locale]);
39
55
  let resolvedOptions = useMemo(() => defaultFormatter.resolvedOptions(), [defaultFormatter]);
40
56
  let {
41
57
  locale,
42
58
  createCalendar,
43
- timeZone = resolvedOptions.timeZone,
44
59
  visibleDuration = {months: 1},
45
60
  minValue,
46
61
  maxValue,
@@ -51,46 +66,58 @@ export function useCalendarState<T extends DateValue>(props: CalendarStateOption
51
66
 
52
67
  let [value, setControlledValue] = useControlledState<DateValue>(props.value, props.defaultValue, props.onChange);
53
68
  let calendarDateValue = useMemo(() => value ? toCalendar(toCalendarDate(value), calendar) : null, [value, calendar]);
54
- let defaultDate = useMemo(() => calendarDateValue || constrainValue(toCalendar(today(timeZone), calendar), minValue, maxValue), [calendarDateValue, timeZone, calendar, minValue, maxValue]);
69
+ let timeZone = useMemo(() => value && 'timeZone' in value ? value.timeZone : resolvedOptions.timeZone, [value, resolvedOptions.timeZone]);
70
+ let focusedCalendarDate = useMemo(() => (
71
+ props.focusedValue
72
+ ? constrainValue(toCalendar(toCalendarDate(props.focusedValue), calendar), minValue, maxValue)
73
+ : undefined
74
+ ), [props.focusedValue, calendar, minValue, maxValue]);
75
+ let defaultFocusedCalendarDate = useMemo(() => (
76
+ constrainValue(
77
+ props.defaultFocusedValue
78
+ ? toCalendar(toCalendarDate(props.defaultFocusedValue), calendar)
79
+ : calendarDateValue || toCalendar(today(timeZone), calendar),
80
+ minValue,
81
+ maxValue
82
+ )
83
+ ), [props.defaultFocusedValue, calendarDateValue, timeZone, calendar, minValue, maxValue]);
84
+ let [focusedDate, setFocusedDate] = useControlledState(focusedCalendarDate, defaultFocusedCalendarDate, props.onFocusChange);
55
85
  let [startDate, setStartDate] = useState(() => {
56
86
  switch (selectionAlignment) {
57
87
  case 'start':
58
- return alignStart(defaultDate, visibleDuration, locale, minValue, maxValue);
88
+ return alignStart(focusedDate, visibleDuration, locale, minValue, maxValue);
59
89
  case 'end':
60
- return alignEnd(defaultDate, visibleDuration, locale, minValue, maxValue);
90
+ return alignEnd(focusedDate, visibleDuration, locale, minValue, maxValue);
61
91
  case 'center':
62
92
  default:
63
- return alignCenter(defaultDate, visibleDuration, locale, minValue, maxValue);
93
+ return alignCenter(focusedDate, visibleDuration, locale, minValue, maxValue);
64
94
  }
65
95
  });
66
- let [focusedDate, setFocusedDate] = useState(defaultDate);
67
96
  let [isFocused, setFocused] = useState(props.autoFocus || false);
68
97
 
69
98
  let endDate = useMemo(() => startDate.add(visibleDuration).subtract({days: 1}), [startDate, visibleDuration]);
70
99
 
71
100
  // Reset focused date and visible range when calendar changes.
72
101
  let lastCalendarIdentifier = useRef(calendar.identifier);
73
- useEffect(() => {
74
- if (calendar.identifier !== lastCalendarIdentifier.current) {
75
- let newFocusedDate = toCalendar(focusedDate, calendar);
76
- setStartDate(alignCenter(newFocusedDate, visibleDuration, locale, minValue, maxValue));
77
- setFocusedDate(newFocusedDate);
78
- lastCalendarIdentifier.current = calendar.identifier;
79
- }
80
- }, [calendar, focusedDate, visibleDuration, locale, minValue, maxValue]);
102
+ if (calendar.identifier !== lastCalendarIdentifier.current) {
103
+ let newFocusedDate = toCalendar(focusedDate, calendar);
104
+ setStartDate(alignCenter(newFocusedDate, visibleDuration, locale, minValue, maxValue));
105
+ setFocusedDate(newFocusedDate);
106
+ lastCalendarIdentifier.current = calendar.identifier;
107
+ }
108
+
109
+ if (isInvalid(focusedDate, minValue, maxValue)) {
110
+ // If the focused date was moved to an invalid value, it can't be focused, so constrain it.
111
+ setFocusedDate(constrainValue(focusedDate, minValue, maxValue));
112
+ } else if (focusedDate.compare(startDate) < 0) {
113
+ setStartDate(alignEnd(focusedDate, visibleDuration, locale, minValue, maxValue));
114
+ } else if (focusedDate.compare(startDate.add(visibleDuration)) >= 0) {
115
+ setStartDate(alignStart(focusedDate, visibleDuration, locale, minValue, maxValue));
116
+ }
81
117
 
82
118
  // Sets focus to a specific cell date
83
119
  function focusCell(date: CalendarDate) {
84
- // date = constrain(focusedDate, date, visibleDuration, locale, minValue, maxValue);
85
120
  date = constrainValue(date, minValue, maxValue);
86
-
87
- let next = startDate.add(visibleDuration);
88
- if (date.compare(startDate) < 0) {
89
- setStartDate(alignEnd(date, visibleDuration, locale, minValue, maxValue));
90
- } else if (date.compare(next) >= 0) {
91
- setStartDate(alignStart(date, visibleDuration, locale, minValue, maxValue));
92
- }
93
-
94
121
  setFocusedDate(date);
95
122
  }
96
123
 
@@ -121,7 +148,7 @@ export function useCalendarState<T extends DateValue>(props: CalendarStateOption
121
148
  focusedDate,
122
149
  timeZone,
123
150
  setFocusedDate(date) {
124
- setFocusedDate(date);
151
+ focusCell(date);
125
152
  setFocused(true);
126
153
  },
127
154
  focusNextDay() {
@@ -146,13 +173,13 @@ export function useCalendarState<T extends DateValue>(props: CalendarStateOption
146
173
  },
147
174
  focusNextPage() {
148
175
  let start = startDate.add(visibleDuration);
149
- setStartDate(constrainStart(focusedDate, start, visibleDuration, locale, minValue, maxValue));
150
176
  setFocusedDate(constrainValue(focusedDate.add(visibleDuration), minValue, maxValue));
177
+ setStartDate(constrainStart(focusedDate, start, visibleDuration, locale, minValue, maxValue));
151
178
  },
152
179
  focusPreviousPage() {
153
180
  let start = startDate.subtract(visibleDuration);
154
- setStartDate(constrainStart(focusedDate, start, visibleDuration, locale, minValue, maxValue));
155
181
  setFocusedDate(constrainValue(focusedDate.subtract(visibleDuration), minValue, maxValue));
182
+ setStartDate(constrainStart(focusedDate, start, visibleDuration, locale, minValue, maxValue));
156
183
  },
157
184
  focusPageStart() {
158
185
  focusCell(startDate);
@@ -190,19 +217,22 @@ export function useCalendarState<T extends DateValue>(props: CalendarStateOption
190
217
  return isInvalid(date, minValue, maxValue);
191
218
  },
192
219
  isSelected(date) {
193
- return calendarDateValue != null && isSameDay(date, calendarDateValue);
220
+ return calendarDateValue != null && isSameDay(date, calendarDateValue) && !this.isCellDisabled(date) && !this.isCellUnavailable(date);
194
221
  },
195
222
  isCellFocused(date) {
196
223
  return isFocused && focusedDate && isSameDay(date, focusedDate);
197
224
  },
198
225
  isCellDisabled(date) {
199
- return props.isDisabled || date.compare(startDate) < 0 || date.compare(endDate) > 0 || isInvalid(date, minValue, maxValue);
226
+ return props.isDisabled || date.compare(startDate) < 0 || date.compare(endDate) > 0 || this.isInvalid(date, minValue, maxValue);
227
+ },
228
+ isCellUnavailable(date) {
229
+ return props.isDateUnavailable && props.isDateUnavailable(date);
200
230
  },
201
231
  isPreviousVisibleRangeInvalid() {
202
- return isInvalid(startDate.subtract({days: 1}), minValue, maxValue);
232
+ return this.isInvalid(startDate.subtract({days: 1}), minValue, maxValue);
203
233
  },
204
234
  isNextVisibleRangeInvalid() {
205
- return isInvalid(endDate.add({days: 1}), minValue, maxValue);
235
+ return this.isInvalid(endDate.add({days: 1}), minValue, maxValue);
206
236
  }
207
237
  };
208
238
  }
@@ -10,31 +10,46 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {alignCenter} from './utils';
14
- import {Calendar, CalendarDate, Duration, GregorianCalendar, toCalendar, toCalendarDate} from '@internationalized/date';
13
+ import {alignCenter, isInvalid} from './utils';
14
+ import {Calendar, CalendarDate, DateDuration, GregorianCalendar, isEqualDay, maxDate, minDate, toCalendar, toCalendarDate} from '@internationalized/date';
15
+ import {CalendarState, RangeCalendarState} from './types';
15
16
  import {DateRange, DateValue} from '@react-types/calendar';
16
17
  import {RangeCalendarProps} from '@react-types/calendar';
17
- import {RangeCalendarState} from './types';
18
18
  import {RangeValue} from '@react-types/shared';
19
19
  import {useCalendarState} from './useCalendarState';
20
20
  import {useControlledState} from '@react-stately/utils';
21
- import {useState} from 'react';
21
+ import {useMemo, useRef, useState} from 'react';
22
22
 
23
- interface RangeCalendarStateOptions<T extends DateValue> extends RangeCalendarProps<T> {
23
+ interface RangeCalendarStateOptions extends RangeCalendarProps<DateValue> {
24
+ /** The locale to display and edit the value according to. */
24
25
  locale: string,
26
+ /**
27
+ * A function that creates a [Calendar](../internationalized/date/Calendar.html)
28
+ * object for a given calendar identifier. Such a function may be imported from the
29
+ * `@internationalized/date` package, or manually implemented to include support for
30
+ * only certain calendars.
31
+ */
25
32
  createCalendar: (name: string) => Calendar,
26
- visibleDuration?: Duration
33
+ /**
34
+ * The amount of days that will be displayed at once. This affects how pagination works.
35
+ * @default {months: 1}
36
+ */
37
+ visibleDuration?: DateDuration
27
38
  }
28
39
 
29
- export function useRangeCalendarState<T extends DateValue>(props: RangeCalendarStateOptions<T>): RangeCalendarState {
40
+ /**
41
+ * Provides state management for a range calendar component.
42
+ * A range calendar displays one or more date grids and allows users to select a contiguous range of dates.
43
+ */
44
+ export function useRangeCalendarState(props: RangeCalendarStateOptions): RangeCalendarState {
30
45
  let {value: valueProp, defaultValue, onChange, createCalendar, locale, visibleDuration = {months: 1}, minValue, maxValue, ...calendarProps} = props;
31
46
  let [value, setValue] = useControlledState<DateRange>(
32
47
  valueProp,
33
- defaultValue,
48
+ defaultValue || null,
34
49
  onChange
35
50
  );
36
51
 
37
- let [anchorDate, setAnchorDate] = useState(null);
52
+ let [anchorDate, setAnchorDateState] = useState(null);
38
53
  let alignment: 'center' | 'start' = 'center';
39
54
  if (value && value.start && value.end) {
40
55
  let start = alignCenter(toCalendarDate(value.start), visibleDuration, locale, minValue, maxValue);
@@ -45,17 +60,53 @@ export function useRangeCalendarState<T extends DateValue>(props: RangeCalendarS
45
60
  }
46
61
  }
47
62
 
63
+ // Available range must be stored in a ref so we have access to the updated version immediately in `isInvalid`.
64
+ let availableRangeRef = useRef<RangeValue<DateValue>>(null);
65
+ let [availableRange, setAvailableRange] = useState<RangeValue<DateValue>>(null);
66
+ let min = useMemo(() => maxDate(minValue, availableRange?.start), [minValue, availableRange]);
67
+ let max = useMemo(() => minDate(maxValue, availableRange?.end), [maxValue, availableRange]);
68
+
48
69
  let calendar = useCalendarState({
49
70
  ...calendarProps,
50
71
  value: value && value.start,
51
72
  createCalendar,
52
73
  locale,
53
74
  visibleDuration,
54
- minValue,
55
- maxValue,
75
+ minValue: min,
76
+ maxValue: max,
56
77
  selectionAlignment: alignment
57
78
  });
58
79
 
80
+ let updateAvailableRange = (date) => {
81
+ if (date && props.isDateUnavailable && !props.allowsNonContiguousRanges) {
82
+ availableRangeRef.current = {
83
+ start: nextUnavailableDate(date, calendar, -1),
84
+ end: nextUnavailableDate(date, calendar, 1)
85
+ };
86
+ setAvailableRange(availableRangeRef.current);
87
+ } else {
88
+ availableRangeRef.current = null;
89
+ setAvailableRange(null);
90
+ }
91
+ };
92
+
93
+ // If the visible range changes, we need to update the available range.
94
+ let lastVisibleRange = useRef(calendar.visibleRange);
95
+ if (!isEqualDay(calendar.visibleRange.start, lastVisibleRange.current.start) || !isEqualDay(calendar.visibleRange.end, lastVisibleRange.current.end)) {
96
+ updateAvailableRange(anchorDate);
97
+ lastVisibleRange.current = calendar.visibleRange;
98
+ }
99
+
100
+ let setAnchorDate = (date: CalendarDate) => {
101
+ if (date) {
102
+ setAnchorDateState(date);
103
+ updateAvailableRange(date);
104
+ } else {
105
+ setAnchorDateState(null);
106
+ updateAvailableRange(null);
107
+ }
108
+ };
109
+
59
110
  let highlightedRange = anchorDate ? makeRange(anchorDate, calendar.focusedDate) : value && makeRange(value.start, value.end);
60
111
  let selectDate = (date: CalendarDate) => {
61
112
  if (props.isReadOnly) {
@@ -93,7 +144,10 @@ export function useRangeCalendarState<T extends DateValue>(props: RangeCalendarS
93
144
  }
94
145
  },
95
146
  isSelected(date) {
96
- return highlightedRange && date.compare(highlightedRange.start) >= 0 && date.compare(highlightedRange.end) <= 0;
147
+ return highlightedRange && date.compare(highlightedRange.start) >= 0 && date.compare(highlightedRange.end) <= 0 && !calendar.isCellDisabled(date) && !calendar.isCellUnavailable(date);
148
+ },
149
+ isInvalid(date) {
150
+ return calendar.isInvalid(date) || isInvalid(date, availableRangeRef.current?.start, availableRangeRef.current?.end);
97
151
  },
98
152
  isDragging,
99
153
  setDragging
@@ -124,3 +178,19 @@ function convertValue(newValue: CalendarDate, oldValue: DateValue) {
124
178
 
125
179
  return newValue;
126
180
  }
181
+
182
+ function nextUnavailableDate(anchorDate: CalendarDate, state: CalendarState, dir: number) {
183
+ let nextDate = anchorDate.add({days: dir});
184
+ while (
185
+ (dir < 0 ? nextDate.compare(state.visibleRange.start) >= 0 : nextDate.compare(state.visibleRange.end) <= 0) &&
186
+ !state.isCellUnavailable(nextDate)
187
+ ) {
188
+ nextDate = nextDate.add({days: dir});
189
+ }
190
+
191
+ if (state.isCellUnavailable(nextDate)) {
192
+ return nextDate.add({days: -dir});
193
+ }
194
+
195
+ return null;
196
+ }
package/src/utils.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import {
13
13
  CalendarDate,
14
- Duration,
14
+ DateDuration,
15
15
  maxDate,
16
16
  minDate,
17
17
  startOfMonth,
@@ -26,8 +26,8 @@ export function isInvalid(date: CalendarDate, minValue: DateValue, maxValue: Dat
26
26
  (maxValue != null && date.compare(maxValue) > 0);
27
27
  }
28
28
 
29
- export function alignCenter(date: CalendarDate, duration: Duration, locale: string, minValue?: DateValue, maxValue?: DateValue) {
30
- let halfDuration: Duration = {};
29
+ export function alignCenter(date: CalendarDate, duration: DateDuration, locale: string, minValue?: DateValue, maxValue?: DateValue) {
30
+ let halfDuration: DateDuration = {};
31
31
  for (let key in duration) {
32
32
  halfDuration[key] = Math.floor(duration[key] / 2);
33
33
  if (halfDuration[key] > 0 && duration[key] % 2 === 0) {
@@ -39,7 +39,7 @@ export function alignCenter(date: CalendarDate, duration: Duration, locale: stri
39
39
  return constrainStart(date, aligned, duration, locale, minValue, maxValue);
40
40
  }
41
41
 
42
- export function alignStart(date: CalendarDate, duration: Duration, locale: string, minValue?: DateValue, maxValue?: DateValue) {
42
+ export function alignStart(date: CalendarDate, duration: DateDuration, locale: string, minValue?: DateValue, maxValue?: DateValue) {
43
43
  // align to the start of the largest unit
44
44
  let aligned = date;
45
45
  if (duration.years) {
@@ -53,7 +53,7 @@ export function alignStart(date: CalendarDate, duration: Duration, locale: strin
53
53
  return constrainStart(date, aligned, duration, locale, minValue, maxValue);
54
54
  }
55
55
 
56
- export function alignEnd(date: CalendarDate, duration: Duration, locale: string, minValue?: DateValue, maxValue?: DateValue) {
56
+ export function alignEnd(date: CalendarDate, duration: DateDuration, locale: string, minValue?: DateValue, maxValue?: DateValue) {
57
57
  let d = {...duration};
58
58
  // subtract 1 from the smallest unit
59
59
  if (duration.days) {
@@ -73,7 +73,7 @@ export function alignEnd(date: CalendarDate, duration: Duration, locale: string,
73
73
  export function constrainStart(
74
74
  date: CalendarDate,
75
75
  aligned: CalendarDate,
76
- duration: Duration,
76
+ duration: DateDuration,
77
77
  locale: string,
78
78
  minValue: DateValue,
79
79
  maxValue: DateValue) {