@react-stately/calendar 3.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright 2020 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ export * from './types';
14
+ export * from './useCalendarState';
15
+ export * from './useRangeCalendarState';
package/src/types.ts ADDED
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Copyright 2020 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {CalendarDate} from '@internationalized/date';
14
+ import {DateValue} from '@react-types/calendar';
15
+ import {RangeValue} from '@react-types/shared';
16
+
17
+ export interface CalendarStateBase {
18
+ isDisabled: boolean,
19
+ isReadOnly: boolean,
20
+ visibleRange: RangeValue<CalendarDate>,
21
+ timeZone: string,
22
+ focusedDate: CalendarDate,
23
+ setFocusedDate(value: CalendarDate): void,
24
+ focusNextDay(): void,
25
+ focusPreviousDay(): void,
26
+ focusNextRow(): void,
27
+ focusPreviousRow(): void,
28
+ focusNextPage(): void,
29
+ focusPreviousPage(): void,
30
+ focusPageStart(): void,
31
+ focusPageEnd(): void,
32
+ focusNextSection(): void,
33
+ focusPreviousSection(): void,
34
+ focusNextPage(): void,
35
+ focusPreviousPage(): void,
36
+ selectFocusedDate(): void,
37
+ selectDate(date: CalendarDate): void,
38
+ isFocused: boolean,
39
+ setFocused(value: boolean): void,
40
+ isInvalid(date: CalendarDate): boolean,
41
+ isSelected(date: CalendarDate): boolean,
42
+ isCellFocused(date: CalendarDate): boolean,
43
+ isCellDisabled(date: CalendarDate): boolean,
44
+ isPreviousVisibleRangeInvalid(): boolean,
45
+ isNextVisibleRangeInvalid(): boolean
46
+ }
47
+
48
+ export interface CalendarState extends CalendarStateBase {
49
+ value: CalendarDate,
50
+ setValue(value: CalendarDate): void
51
+ }
52
+
53
+ export interface RangeCalendarState extends CalendarStateBase {
54
+ value: RangeValue<DateValue>,
55
+ setValue(value: RangeValue<DateValue>): void,
56
+ highlightDate(date: CalendarDate): void,
57
+ anchorDate: CalendarDate | null,
58
+ setAnchorDate(date: CalendarDate | null): void,
59
+ highlightedRange: RangeValue<CalendarDate>,
60
+ isDragging: boolean,
61
+ setDragging(isDragging: boolean): void
62
+ }
@@ -0,0 +1,208 @@
1
+ /*
2
+ * Copyright 2020 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {alignCenter, alignEnd, alignStart, constrainStart, constrainValue, isInvalid} from './utils';
14
+ import {
15
+ Calendar,
16
+ CalendarDate,
17
+ Duration,
18
+ GregorianCalendar,
19
+ isSameDay,
20
+ toCalendar,
21
+ toCalendarDate,
22
+ today
23
+ } from '@internationalized/date';
24
+ import {CalendarProps, DateValue} from '@react-types/calendar';
25
+ import {CalendarState} from './types';
26
+ import {useControlledState} from '@react-stately/utils';
27
+ import {useDateFormatter} from '@react-aria/i18n';
28
+ import {useEffect, useMemo, useRef, useState} from 'react';
29
+
30
+ interface CalendarStateOptions<T extends DateValue> extends CalendarProps<T> {
31
+ locale: string,
32
+ createCalendar: (name: string) => Calendar,
33
+ visibleDuration?: Duration,
34
+ selectionAlignment?: 'start' | 'center' | 'end'
35
+ }
36
+
37
+ export function useCalendarState<T extends DateValue>(props: CalendarStateOptions<T>): CalendarState {
38
+ let defaultFormatter = useDateFormatter();
39
+ let resolvedOptions = useMemo(() => defaultFormatter.resolvedOptions(), [defaultFormatter]);
40
+ let {
41
+ locale,
42
+ createCalendar,
43
+ timeZone = resolvedOptions.timeZone,
44
+ visibleDuration = {months: 1},
45
+ minValue,
46
+ maxValue,
47
+ selectionAlignment
48
+ } = props;
49
+
50
+ let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]);
51
+
52
+ let [value, setControlledValue] = useControlledState<DateValue>(props.value, props.defaultValue, props.onChange);
53
+ 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]);
55
+ let [startDate, setStartDate] = useState(() => {
56
+ switch (selectionAlignment) {
57
+ case 'start':
58
+ return alignStart(defaultDate, visibleDuration, locale, minValue, maxValue);
59
+ case 'end':
60
+ return alignEnd(defaultDate, visibleDuration, locale, minValue, maxValue);
61
+ case 'center':
62
+ default:
63
+ return alignCenter(defaultDate, visibleDuration, locale, minValue, maxValue);
64
+ }
65
+ });
66
+ let [focusedDate, setFocusedDate] = useState(defaultDate);
67
+ let [isFocused, setFocused] = useState(props.autoFocus || false);
68
+
69
+ let endDate = useMemo(() => startDate.add(visibleDuration).subtract({days: 1}), [startDate, visibleDuration]);
70
+
71
+ // Reset focused date and visible range when calendar changes.
72
+ 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]);
81
+
82
+ // Sets focus to a specific cell date
83
+ function focusCell(date: CalendarDate) {
84
+ // date = constrain(focusedDate, date, visibleDuration, locale, minValue, maxValue);
85
+ 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
+ setFocusedDate(date);
95
+ }
96
+
97
+ function setValue(newValue: CalendarDate) {
98
+ if (!props.isDisabled && !props.isReadOnly) {
99
+ // The display calendar should not have any effect on the emitted value.
100
+ // Emit dates in the same calendar as the original value, if any, otherwise gregorian.
101
+ newValue = toCalendar(newValue, value?.calendar || new GregorianCalendar());
102
+
103
+ // Preserve time if the input value had one.
104
+ if (value && 'hour' in value) {
105
+ setControlledValue(value.set(newValue));
106
+ } else {
107
+ setControlledValue(newValue);
108
+ }
109
+ }
110
+ }
111
+
112
+ return {
113
+ isDisabled: props.isDisabled,
114
+ isReadOnly: props.isReadOnly,
115
+ value: calendarDateValue,
116
+ setValue,
117
+ visibleRange: {
118
+ start: startDate,
119
+ end: endDate
120
+ },
121
+ focusedDate,
122
+ timeZone,
123
+ setFocusedDate(date) {
124
+ setFocusedDate(date);
125
+ setFocused(true);
126
+ },
127
+ focusNextDay() {
128
+ focusCell(focusedDate.add({days: 1}));
129
+ },
130
+ focusPreviousDay() {
131
+ focusCell(focusedDate.subtract({days: 1}));
132
+ },
133
+ focusNextRow() {
134
+ if (visibleDuration.days) {
135
+ this.focusNextPage();
136
+ } else if (visibleDuration.weeks || visibleDuration.months || visibleDuration.years) {
137
+ focusCell(focusedDate.add({weeks: 1}));
138
+ }
139
+ },
140
+ focusPreviousRow() {
141
+ if (visibleDuration.days) {
142
+ this.focusPreviousPage();
143
+ } else if (visibleDuration.weeks || visibleDuration.months || visibleDuration.years) {
144
+ focusCell(focusedDate.subtract({weeks: 1}));
145
+ }
146
+ },
147
+ focusNextPage() {
148
+ let start = startDate.add(visibleDuration);
149
+ setStartDate(constrainStart(focusedDate, start, visibleDuration, locale, minValue, maxValue));
150
+ setFocusedDate(constrainValue(focusedDate.add(visibleDuration), minValue, maxValue));
151
+ },
152
+ focusPreviousPage() {
153
+ let start = startDate.subtract(visibleDuration);
154
+ setStartDate(constrainStart(focusedDate, start, visibleDuration, locale, minValue, maxValue));
155
+ setFocusedDate(constrainValue(focusedDate.subtract(visibleDuration), minValue, maxValue));
156
+ },
157
+ focusPageStart() {
158
+ focusCell(startDate);
159
+ },
160
+ focusPageEnd() {
161
+ focusCell(endDate);
162
+ },
163
+ focusNextSection() {
164
+ if (visibleDuration.days) {
165
+ this.focusNextPage();
166
+ } else if (visibleDuration.weeks) {
167
+ focusCell(focusedDate.add({months: 1}));
168
+ } else if (visibleDuration.months || visibleDuration.years) {
169
+ focusCell(focusedDate.add({years: 1}));
170
+ }
171
+ },
172
+ focusPreviousSection() {
173
+ if (visibleDuration.days) {
174
+ this.focusPreviousPage();
175
+ } else if (visibleDuration.weeks) {
176
+ focusCell(focusedDate.subtract({months: 1}));
177
+ } else if (visibleDuration.months || visibleDuration.years) {
178
+ focusCell(focusedDate.subtract({years: 1}));
179
+ }
180
+ },
181
+ selectFocusedDate() {
182
+ setValue(focusedDate);
183
+ },
184
+ selectDate(date) {
185
+ setValue(date);
186
+ },
187
+ isFocused,
188
+ setFocused,
189
+ isInvalid(date) {
190
+ return isInvalid(date, minValue, maxValue);
191
+ },
192
+ isSelected(date) {
193
+ return calendarDateValue != null && isSameDay(date, calendarDateValue);
194
+ },
195
+ isCellFocused(date) {
196
+ return isFocused && focusedDate && isSameDay(date, focusedDate);
197
+ },
198
+ isCellDisabled(date) {
199
+ return props.isDisabled || date.compare(startDate) < 0 || date.compare(endDate) > 0 || isInvalid(date, minValue, maxValue);
200
+ },
201
+ isPreviousVisibleRangeInvalid() {
202
+ return isInvalid(startDate.subtract({days: 1}), minValue, maxValue);
203
+ },
204
+ isNextVisibleRangeInvalid() {
205
+ return isInvalid(endDate.add({days: 1}), minValue, maxValue);
206
+ }
207
+ };
208
+ }
@@ -0,0 +1,126 @@
1
+ /*
2
+ * Copyright 2020 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {alignCenter} from './utils';
14
+ import {Calendar, CalendarDate, Duration, GregorianCalendar, toCalendar, toCalendarDate} from '@internationalized/date';
15
+ import {DateRange, DateValue} from '@react-types/calendar';
16
+ import {RangeCalendarProps} from '@react-types/calendar';
17
+ import {RangeCalendarState} from './types';
18
+ import {RangeValue} from '@react-types/shared';
19
+ import {useCalendarState} from './useCalendarState';
20
+ import {useControlledState} from '@react-stately/utils';
21
+ import {useState} from 'react';
22
+
23
+ interface RangeCalendarStateOptions<T extends DateValue> extends RangeCalendarProps<T> {
24
+ locale: string,
25
+ createCalendar: (name: string) => Calendar,
26
+ visibleDuration?: Duration
27
+ }
28
+
29
+ export function useRangeCalendarState<T extends DateValue>(props: RangeCalendarStateOptions<T>): RangeCalendarState {
30
+ let {value: valueProp, defaultValue, onChange, createCalendar, locale, visibleDuration = {months: 1}, minValue, maxValue, ...calendarProps} = props;
31
+ let [value, setValue] = useControlledState<DateRange>(
32
+ valueProp,
33
+ defaultValue,
34
+ onChange
35
+ );
36
+
37
+ let [anchorDate, setAnchorDate] = useState(null);
38
+ let alignment: 'center' | 'start' = 'center';
39
+ if (value && value.start && value.end) {
40
+ let start = alignCenter(toCalendarDate(value.start), visibleDuration, locale, minValue, maxValue);
41
+ let end = start.add(visibleDuration).subtract({days: 1});
42
+
43
+ if (value.end.compare(end) > 0) {
44
+ alignment = 'start';
45
+ }
46
+ }
47
+
48
+ let calendar = useCalendarState({
49
+ ...calendarProps,
50
+ value: value && value.start,
51
+ createCalendar,
52
+ locale,
53
+ visibleDuration,
54
+ minValue,
55
+ maxValue,
56
+ selectionAlignment: alignment
57
+ });
58
+
59
+ let highlightedRange = anchorDate ? makeRange(anchorDate, calendar.focusedDate) : value && makeRange(value.start, value.end);
60
+ let selectDate = (date: CalendarDate) => {
61
+ if (props.isReadOnly) {
62
+ return;
63
+ }
64
+
65
+ if (!anchorDate) {
66
+ setAnchorDate(date);
67
+ } else {
68
+ let range = makeRange(anchorDate, date);
69
+ setValue({
70
+ start: convertValue(range.start, value?.start),
71
+ end: convertValue(range.end, value?.end)
72
+ });
73
+ setAnchorDate(null);
74
+ }
75
+ };
76
+
77
+ let [isDragging, setDragging] = useState(false);
78
+
79
+ return {
80
+ ...calendar,
81
+ value,
82
+ setValue,
83
+ anchorDate,
84
+ setAnchorDate,
85
+ highlightedRange,
86
+ selectFocusedDate() {
87
+ selectDate(calendar.focusedDate);
88
+ },
89
+ selectDate,
90
+ highlightDate(date) {
91
+ if (anchorDate) {
92
+ calendar.setFocusedDate(date);
93
+ }
94
+ },
95
+ isSelected(date) {
96
+ return highlightedRange && date.compare(highlightedRange.start) >= 0 && date.compare(highlightedRange.end) <= 0;
97
+ },
98
+ isDragging,
99
+ setDragging
100
+ };
101
+ }
102
+
103
+ function makeRange(start: DateValue, end: DateValue): RangeValue<CalendarDate> {
104
+ if (!start || !end) {
105
+ return null;
106
+ }
107
+
108
+ if (end.compare(start) < 0) {
109
+ [start, end] = [end, start];
110
+ }
111
+
112
+ return {start: toCalendarDate(start), end: toCalendarDate(end)};
113
+ }
114
+
115
+ function convertValue(newValue: CalendarDate, oldValue: DateValue) {
116
+ // The display calendar should not have any effect on the emitted value.
117
+ // Emit dates in the same calendar as the original value, if any, otherwise gregorian.
118
+ newValue = toCalendar(newValue, oldValue?.calendar || new GregorianCalendar());
119
+
120
+ // Preserve time if the input value had one.
121
+ if (oldValue && 'hour' in oldValue) {
122
+ return oldValue.set(newValue);
123
+ }
124
+
125
+ return newValue;
126
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,107 @@
1
+ /*
2
+ * Copyright 2020 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import {
13
+ CalendarDate,
14
+ Duration,
15
+ maxDate,
16
+ minDate,
17
+ startOfMonth,
18
+ startOfWeek,
19
+ startOfYear,
20
+ toCalendarDate
21
+ } from '@internationalized/date';
22
+ import {DateValue} from '@react-types/calendar';
23
+
24
+ export function isInvalid(date: CalendarDate, minValue: DateValue, maxValue: DateValue) {
25
+ return (minValue != null && date.compare(minValue) < 0) ||
26
+ (maxValue != null && date.compare(maxValue) > 0);
27
+ }
28
+
29
+ export function alignCenter(date: CalendarDate, duration: Duration, locale: string, minValue?: DateValue, maxValue?: DateValue) {
30
+ let halfDuration: Duration = {};
31
+ for (let key in duration) {
32
+ halfDuration[key] = Math.floor(duration[key] / 2);
33
+ if (halfDuration[key] > 0 && duration[key] % 2 === 0) {
34
+ halfDuration[key]--;
35
+ }
36
+ }
37
+
38
+ let aligned = alignStart(date, duration, locale).subtract(halfDuration);
39
+ return constrainStart(date, aligned, duration, locale, minValue, maxValue);
40
+ }
41
+
42
+ export function alignStart(date: CalendarDate, duration: Duration, locale: string, minValue?: DateValue, maxValue?: DateValue) {
43
+ // align to the start of the largest unit
44
+ let aligned = date;
45
+ if (duration.years) {
46
+ aligned = startOfYear(date);
47
+ } else if (duration.months) {
48
+ aligned = startOfMonth(date);
49
+ } else if (duration.weeks) {
50
+ aligned = startOfWeek(date, locale);
51
+ }
52
+
53
+ return constrainStart(date, aligned, duration, locale, minValue, maxValue);
54
+ }
55
+
56
+ export function alignEnd(date: CalendarDate, duration: Duration, locale: string, minValue?: DateValue, maxValue?: DateValue) {
57
+ let d = {...duration};
58
+ // subtract 1 from the smallest unit
59
+ if (duration.days) {
60
+ d.days--;
61
+ } else if (duration.weeks) {
62
+ d.weeks--;
63
+ } else if (duration.months) {
64
+ d.months--;
65
+ } else if (duration.years) {
66
+ d.years--;
67
+ }
68
+
69
+ let aligned = alignStart(date, duration, locale).subtract(d);
70
+ return constrainStart(date, aligned, duration, locale, minValue, maxValue);
71
+ }
72
+
73
+ export function constrainStart(
74
+ date: CalendarDate,
75
+ aligned: CalendarDate,
76
+ duration: Duration,
77
+ locale: string,
78
+ minValue: DateValue,
79
+ maxValue: DateValue) {
80
+ if (minValue && date.compare(minValue) >= 0) {
81
+ aligned = maxDate(
82
+ aligned,
83
+ alignStart(toCalendarDate(minValue), duration, locale)
84
+ );
85
+ }
86
+
87
+ if (maxValue && date.compare(maxValue) <= 0) {
88
+ aligned = minDate(
89
+ aligned,
90
+ alignEnd(toCalendarDate(maxValue), duration, locale)
91
+ );
92
+ }
93
+
94
+ return aligned;
95
+ }
96
+
97
+ export function constrainValue(date: CalendarDate, minValue: DateValue, maxValue: DateValue) {
98
+ if (minValue) {
99
+ date = maxDate(date, toCalendarDate(minValue));
100
+ }
101
+
102
+ if (maxValue) {
103
+ date = minDate(date, toCalendarDate(maxValue));
104
+ }
105
+
106
+ return date;
107
+ }