/* * Copyright 2020 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ import {CalendarDate, getWeeksInMonth, startOfWeek, today} from '@internationalized/date'; import {CalendarState, RangeCalendarState} from '@react-stately/calendar'; import {DOMAttributes} from '@react-types/shared'; import {hookData, useVisibleRangeDescription} from './utils'; import {KeyboardEvent, useMemo} from 'react'; import {mergeProps, useLabels} from '@react-aria/utils'; import {useDateFormatter, useLocale} from '@react-aria/i18n'; export interface AriaCalendarGridProps { /** * The first date displayed in the calendar grid. * Defaults to the first visible date in the calendar. * Override this to display multiple date grids in a calendar. */ startDate?: CalendarDate, /** * The last date displayed in the calendar grid. * Defaults to the last visible date in the calendar. * Override this to display multiple date grids in a calendar. */ endDate?: CalendarDate, /** * The style of weekday names to display in the calendar grid header, * e.g. single letter, abbreviation, or full day name. * @default "narrow" */ weekdayStyle?: 'narrow' | 'short' | 'long', /** * The day that starts the week. */ firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' } export interface CalendarGridAria { /** Props for the date grid element (e.g. ``). */ gridProps: DOMAttributes, /** Props for the grid header element (e.g. ``). */ headerProps: DOMAttributes, /** A list of week day abbreviations formatted for the current locale, typically used in column headers. */ weekDays: string[], /** The number of weeks in the month. */ weeksInMonth: number } /** * Provides the behavior and accessibility implementation for a calendar grid component. * A calendar grid displays a single grid of days within a calendar or range calendar which * can be keyboard navigated and selected by the user. */ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria { let { startDate = state.visibleRange.start, endDate = state.visibleRange.end, firstDayOfWeek } = props; let {direction} = useLocale(); let onKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'Enter': case ' ': e.preventDefault(); state.selectFocusedDate(); break; case 'PageUp': e.preventDefault(); e.stopPropagation(); state.focusPreviousSection(e.shiftKey); break; case 'PageDown': e.preventDefault(); e.stopPropagation(); state.focusNextSection(e.shiftKey); break; case 'End': e.preventDefault(); e.stopPropagation(); state.focusSectionEnd(); break; case 'Home': e.preventDefault(); e.stopPropagation(); state.focusSectionStart(); break; case 'ArrowLeft': e.preventDefault(); e.stopPropagation(); if (direction === 'rtl') { state.focusNextDay(); } else { state.focusPreviousDay(); } break; case 'ArrowUp': e.preventDefault(); e.stopPropagation(); state.focusPreviousRow(); break; case 'ArrowRight': e.preventDefault(); e.stopPropagation(); if (direction === 'rtl') { state.focusPreviousDay(); } else { state.focusNextDay(); } break; case 'ArrowDown': e.preventDefault(); e.stopPropagation(); state.focusNextRow(); break; case 'Escape': // Cancel the selection. if ('setAnchorDate' in state) { e.preventDefault(); state.setAnchorDate(null); } break; } }; let visibleRangeDescription = useVisibleRangeDescription(startDate, endDate, state.timeZone, true); let {ariaLabel, ariaLabelledBy} = hookData.get(state)!; let labelProps = useLabels({ 'aria-label': [ariaLabel, visibleRangeDescription].filter(Boolean).join(', '), 'aria-labelledby': ariaLabelledBy }); let dayFormatter = useDateFormatter({weekday: props.weekdayStyle || 'narrow', timeZone: state.timeZone}); let {locale} = useLocale(); let weekDays = useMemo(() => { let weekStart = startOfWeek(today(state.timeZone), locale, firstDayOfWeek); return [...new Array(7).keys()].map((index) => { let date = weekStart.add({days: index}); let dateDay = date.toDate(state.timeZone); return dayFormatter.format(dateDay); }); }, [locale, state.timeZone, dayFormatter, firstDayOfWeek]); let weeksInMonth = getWeeksInMonth(startDate, locale, firstDayOfWeek); return { gridProps: mergeProps(labelProps, { role: 'grid', 'aria-readonly': state.isReadOnly || undefined, 'aria-disabled': state.isDisabled || undefined, 'aria-multiselectable': ('highlightedRange' in state) || undefined, onKeyDown, onFocus: () => state.setFocused(true), onBlur: () => state.setFocused(false) }), headerProps: { // Column headers are hidden to screen readers to make navigating with a touch screen reader easier. // The day names are already included in the label of each cell, so there's no need to announce them twice. 'aria-hidden': true }, weekDays, weeksInMonth }; }