Skip to content

Commit 17b7903

Browse files
authored
Allow disabled dates in Calendar to be focused (#2909)
1 parent f42332d commit 17b7903

File tree

31 files changed

+750
-123
lines changed

31 files changed

+750
-123
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ module.exports = {
2828
sourceType: 'module'
2929
},
3030
rules: {
31-
'jsdoc/require-description-complete-sentence': [ERROR, {abbreviations: ['e.g', 'etc']}],
31+
'jsdoc/require-description-complete-sentence': [ERROR, {abbreviations: ['e.g', 'etc', 'i.e']}],
3232
'jsdoc/check-alignment': ERROR,
3333
'jsdoc/check-indentation': ERROR,
3434
'jsdoc/check-tag-names': ERROR,

packages/@adobe/spectrum-css-temp/components/calendar/index.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,23 @@ governing permissions and limitations under the License.
194194
pointer-events: none;
195195
}
196196

197+
&.is-unavailable {
198+
.spectrum-Calendar-dateText span {
199+
position: relative;
200+
201+
&:after {
202+
content: '';
203+
position: absolute;
204+
top: 50%;
205+
left: -4px;
206+
right: -4px;
207+
height: 2px;
208+
transform: rotate(-16deg);
209+
border-radius: 1px;
210+
}
211+
}
212+
}
213+
197214
&.is-outsideMonth {
198215
visibility: hidden;
199216
}

packages/@adobe/spectrum-css-temp/components/calendar/skin.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,15 @@ governing permissions and limitations under the License.
114114
color: var(--spectrum-calendar-day-text-color-disabled);
115115
}
116116
}
117+
118+
&.is-unavailable {
119+
&,
120+
&.is-today {
121+
--background: transparent;
122+
}
123+
124+
& .spectrum-Calendar-dateText span:after {
125+
background: var(--spectrum-global-color-gray-600);
126+
}
127+
}
117128
}

packages/@internationalized/date/src/queries.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,20 @@ export function getWeeksInMonth(date: DateValue, locale: string): number {
237237

238238
/** Returns the lesser of the two provider dates. */
239239
export function minDate<A extends DateValue, B extends DateValue>(a: A, b: B): A | B {
240-
return a.compare(b) <= 0 ? a : b;
240+
if (a && b) {
241+
return a.compare(b) <= 0 ? a : b;
242+
}
243+
244+
return a || b;
241245
}
242246

243247
/** Returns the greater of the two provider dates. */
244248
export function maxDate<A extends DateValue, B extends DateValue>(a: A, b: B): A | B {
245-
return a.compare(b) >= 0 ? a : b;
249+
if (a && b) {
250+
return a.compare(b) >= 0 ? a : b;
251+
}
252+
253+
return a || b;
246254
}
247255

248256
const WEEKEND_DATA = {

packages/@react-aria/calendar/docs/useCalendar.mdx

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ after_version: 3.0.0
4646

4747
There is no standalone calendar element in HTML. `<input type="date">` is close, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `useCalendar` helps achieve accessible and international calendar components that can be styled as needed.
4848

49-
* **Flexible** – Display one or more months at once, or a custom time range for use cases like a week view.
49+
* **Flexible** – Display one or more months at once, or a custom time range for use cases like a week view. Minimum and maximum values, unavailable dates, and non-contiguous selections are supported as well.
5050
* **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, and right-to-left support are available as well.
5151
* **Accessible** – Calendar cells can be navigated and selected using the keyboard, and localized screen reader messages are included to announce when the selection and visible date range change.
5252
* **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized.
@@ -57,18 +57,24 @@ There is no standalone calendar element in HTML. `<input type="date">` is close,
5757

5858
A calendar consists of a grouping element containing one or more date grids (e.g. months), and a previous and next button for navigating between date ranges. Each calendar grid consists of cells containing button elements that can be pressed and navigated to using the arrow keys to select a date.
5959

60+
### useCalendar
61+
6062
`useCalendar` returns props that you should spread onto the appropriate elements:
6163

6264
<TypeContext.Provider value={docs.links}>
6365
<InterfaceType properties={docs.links[docs.exports.useCalendar.return.id].properties} />
6466
</TypeContext.Provider>
6567

68+
### useCalendarGrid
69+
6670
`useCalendarGrid` returns props for an individual grid of dates, such as one month, along with a list of formatted weekday names in the current locale for use during rendering:
6771

6872
<TypeContext.Provider value={docs.links}>
6973
<InterfaceType properties={docs.links[docs.exports.useCalendarGrid.return.id].properties} />
7074
</TypeContext.Provider>
7175

76+
### useCalendarCell
77+
7278
`useCalendarCell` returns props for an individual cell, along with states and information useful during rendering:
7379

7480
<TypeContext.Provider value={docs.links}>
@@ -184,7 +190,7 @@ function CalendarGrid({state, ...props}) {
184190

185191
### CalendarCell
186192

187-
Finally, the `CalendarCell` component renders an individual cell in a calendar. It consists of two elements: a `<td>` to represent the grid cell, and a `<div>` to represent a button that can be clicked to select the date. The `useCalendarCell` hook also returns some information about the cell's state that can be useful for styling, as well as the formatted date string in the current locale.
193+
Finally, the `CalendarCell` component renders an individual cell in a calendar. It consists of two elements: a `<td>` to represent the grid cell, and a `<div>` to represent a button that can be clicked to select the date. The `useCalendarCell` hook also returns the formatted date string in the current locale, as well as some information about the cell's state that can be useful for styling. See [above](#usecalendarcell) for details.
188194

189195
**Note**: this component is the same as the `CalendarCell` component shown in the [useRangeCalendar](useRangeCalendar.html) docs, and you can reuse it between both `Calendar` and `RangeCalendar`.
190196

@@ -199,6 +205,7 @@ function CalendarCell({state, date}) {
199205
isSelected,
200206
isOutsideVisibleRange,
201207
isDisabled,
208+
isUnavailable,
202209
formattedDate
203210
} = useCalendarCell({date}, state, ref);
204211

@@ -208,7 +215,7 @@ function CalendarCell({state, date}) {
208215
{...buttonProps}
209216
ref={ref}
210217
hidden={isOutsideVisibleRange}
211-
className={`cell ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''}`}>
218+
className={`cell ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''} ${isUnavailable ? 'unavailable' : ''}`}>
212219
{formattedDate}
213220
</div>
214221
</td>
@@ -255,6 +262,10 @@ That's it! Now we can render an example of our `Calendar` component in action.
255262
color: white;
256263
}
257264

265+
.unavailable {
266+
color: var(--spectrum-global-color-red-600);
267+
}
268+
258269
.disabled {
259270
color: gray;
260271
}
@@ -382,6 +393,32 @@ import {today} from '@internationalized/date';
382393
<Calendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} />
383394
```
384395

396+
### Unavailable dates
397+
398+
`useCalendar` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard so that navigation is consistent, but cannot be selected by the user. In this example, they are displayed in red. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable.
399+
400+
This example includes multiple unavailable date ranges, e.g. dates when no appointments are available. In addition, all weekends are unavailable. The `minValue` prop is also used to prevent selecting dates before today.
401+
402+
403+
```tsx example
404+
import {today, isWeekend} from '@internationalized/date';
405+
import {useLocale} from '@adobe/react-spectrum';
406+
407+
function Example() {
408+
let now = today(getLocalTimeZone());
409+
let disabledRanges = [
410+
[now, now.add({days: 5})],
411+
[now.add({days: 14}), now.add({days: 16})],
412+
[now.add({days: 23}), now.add({days: 24})],
413+
];
414+
415+
let {locale} = useLocale();
416+
let isDateUnavailable = (date) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0);
417+
418+
return <Calendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} />
419+
}
420+
```
421+
385422
### Controlling the focused date
386423

387424
By default, the selected date is focused when a `Calendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `useCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `Calendar` first mounts, without controlling it.

packages/@react-aria/calendar/docs/useRangeCalendar.mdx

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ after_version: 3.0.0
4545

4646
There is no standalone range calendar element in HTML. Two separate `<input type="date">` elements could be used, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `useRangeCalendar` helps achieve accessible and international range calendar components that can be styled as needed.
4747

48-
* **Flexible** – Display one or more months at once, or a custom time range for use cases like a week view.
48+
* **Flexible** – Display one or more months at once, or a custom time range for use cases like a week view. Minimum and maximum values, unavailable dates, and non-contiguous selections are supported as well.
4949
* **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, and right-to-left support are available as well.
5050
* **Accessible** – Calendar cells can be navigated and selected using the keyboard, and localized screen reader messages are included to announce when the selection and visible date range change.
5151
* **Touch friendly** – Date ranges can be selected by dragging over dates in the calendar using a touch screen, and all interactions are accessible using touch-based screen readers.
@@ -57,18 +57,24 @@ There is no standalone range calendar element in HTML. Two separate `<input type
5757

5858
A range calendar consists of a grouping element containing one or more date grids (e.g. months), and a previous and next button for navigating through time. Each calendar grid consists of cells containing button elements that can be pressed and navigated to using the arrow keys to select a date range. Once a start date is selected, the user can navigate to another date using the keyboard or by hovering over it, and clicking it or pressing the <Keyboard>Enter</Keyboard> key commits the selected date range.
5959

60+
### useRangeCalendar
61+
6062
`useRangeCalendar` returns props that you should spread onto the appropriate elements:
6163

6264
<TypeContext.Provider value={docs.links}>
6365
<InterfaceType properties={docs.links[docs.exports.useRangeCalendar.return.id].properties} />
6466
</TypeContext.Provider>
6567

68+
### useCalendarGrid
69+
6670
`useCalendarGrid` returns props for an individual grid of dates, such as one month, along with a list of formatted weekday names in the current locale for use during rendering:
6771

6872
<TypeContext.Provider value={docs.links}>
6973
<InterfaceType properties={docs.links[docs.exports.useCalendarGrid.return.id].properties} />
7074
</TypeContext.Provider>
7175

76+
### useCalendarCell
77+
7278
`useCalendarCell` returns props for an individual cell, along with states and information useful during rendering:
7379

7480
<TypeContext.Provider value={docs.links}>
@@ -184,7 +190,7 @@ function CalendarGrid({state, ...props}) {
184190

185191
### CalendarCell
186192

187-
Finally, the `CalendarCell` component renders an individual cell in a calendar. It consists of two elements: a `<td>` to represent the grid cell, and a `<div>` to represent a button that can be clicked to select the date. The `useCalendarCell` hook also returns some information about the cell's state that can be useful for styling, as well as the formatted date string in the current locale.
193+
Finally, the `CalendarCell` component renders an individual cell in a calendar. It consists of two elements: a `<td>` to represent the grid cell, and a `<div>` to represent a button that can be clicked to select the date. The `useCalendarCell` hook also returns the formatted date string in the current locale, as well as some information about the cell's state that can be useful for styling. See [above](#usecalendarcell) for details.
188194

189195
**Note**: this component is the same as the `CalendarCell` component shown in the [useCalendar](useCalendar.html) docs, and you can reuse it between both `Calendar` and `RangeCalendar`.
190196

@@ -199,6 +205,7 @@ function CalendarCell({state, date}) {
199205
isSelected,
200206
isOutsideVisibleRange,
201207
isDisabled,
208+
isUnavailable,
202209
formattedDate
203210
} = useCalendarCell({date}, state, ref);
204211

@@ -208,7 +215,7 @@ function CalendarCell({state, date}) {
208215
{...buttonProps}
209216
ref={ref}
210217
hidden={isOutsideVisibleRange}
211-
className={`cell ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''}`}>
218+
className={`cell ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''} ${isUnavailable ? 'unavailable' : ''}`}>
212219
{formattedDate}
213220
</div>
214221
</td>
@@ -255,6 +262,10 @@ That's it! Now we can render an example of our `RangeCalendar` component in acti
255262
color: white;
256263
}
257264

265+
.unavailable {
266+
color: var(--spectrum-global-color-red-600);
267+
}
268+
258269
.disabled {
259270
color: gray;
260271
}
@@ -392,6 +403,49 @@ import {today} from '@internationalized/date';
392403
<RangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} />
393404
```
394405

406+
### Unavailable dates
407+
408+
`useRangeCalendar` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard so that navigation is consistent, but cannot be selected by the user. In this example, they are displayed in red. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable.
409+
410+
Note that by default, users may not select non-contiguous ranges, i.e. ranges that contain unavailable dates within them. Once a start date is selected, enabled dates will be restricted to subsequent dates until an unavailable date is hit. See [below](#non-contiguous-ranges) for an example of how to allow non-contiguous ranges.
411+
412+
This example includes multiple unavailable date ranges, e.g. dates when a rental house is not available. The `minValue` prop is also used to prevent selecting dates before today.
413+
414+
```tsx example
415+
import {today} from '@internationalized/date';
416+
import {useLocale} from '@react-aria/i18n';
417+
418+
function Example() {
419+
let now = today(getLocalTimeZone());
420+
let disabledRanges = [
421+
[now, now.add({days: 5})],
422+
[now.add({days: 14}), now.add({days: 16})],
423+
[now.add({days: 23}), now.add({days: 24})],
424+
];
425+
426+
let {locale} = useLocale();
427+
let isDateUnavailable = (date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0);
428+
429+
return <RangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} />
430+
}
431+
```
432+
433+
### Non-contiguous ranges
434+
435+
The `allowsNonContiguousRanges` prop enables a range to be selected even if there are unavailable dates in the middle. The value emitted in the `onChange` event will still be a single range with a `start` and `end` property, but unavailable dates will not be displayed as selected. It is up to applications to split the full selected range into multiple as needed for business logic.
436+
437+
This example prevents selecting weekends, but allows selecting ranges that span multiple weeks.
438+
439+
```tsx example
440+
import {isWeekend} from '@internationalized/date';
441+
442+
function Example() {
443+
let {locale} = useLocale();
444+
445+
return <RangeCalendar aria-label="Time off request" isDateUnavailable={date => isWeekend(date, locale)} allowsNonContiguousRanges />
446+
}
447+
```
448+
395449
### Controlling the focused date
396450

397451
By default, the first selected date is focused when a `RangeCalendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `useRangeCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `Calendar` first mounts, without controlling it.

0 commit comments

Comments
 (0)