/* * 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 {announce} from '@react-aria/live-announcer'; import {AriaButtonProps} from '@react-types/button'; import {AriaLabelingProps, DOMAttributes, DOMProps} from '@react-types/shared'; import {CalendarPropsBase} from '@react-types/calendar'; import {CalendarState, RangeCalendarState} from '@react-stately/calendar'; import {filterDOMProps, mergeProps, useLabels, useSlotId, useUpdateEffect} from '@react-aria/utils'; import {hookData, useSelectedDateDescription, useVisibleRangeDescription} from './utils'; // @ts-ignore import intlMessages from '../intl/*.json'; import {useLocalizedStringFormatter} from '@react-aria/i18n'; import {useState} from 'react'; export interface CalendarAria { /** Props for the calendar grouping element. */ calendarProps: DOMAttributes, /** Props for the next button. */ nextButtonProps: AriaButtonProps, /** Props for the previous button. */ prevButtonProps: AriaButtonProps, /** Props for the error message element, if any. */ errorMessageProps: DOMAttributes, /** A description of the visible date range, for use in the calendar title. */ title: string } export function useCalendarBase(props: CalendarPropsBase & DOMProps & AriaLabelingProps, state: CalendarState | RangeCalendarState): CalendarAria { let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/calendar'); let domProps = filterDOMProps(props); let title = useVisibleRangeDescription(state.visibleRange.start, state.visibleRange.end, state.timeZone, false); let visibleRangeDescription = useVisibleRangeDescription(state.visibleRange.start, state.visibleRange.end, state.timeZone, true); // Announce when the visible date range changes useUpdateEffect(() => { // only when pressing the Previous or Next button if (!state.isFocused) { announce(visibleRangeDescription); } }, [visibleRangeDescription]); // Announce when the selected value changes let selectedDateDescription = useSelectedDateDescription(state); useUpdateEffect(() => { if (selectedDateDescription) { announce(selectedDateDescription, 'polite', 4000); } // handle an update to the caption that describes the currently selected range, to announce the new value }, [selectedDateDescription]); let errorMessageId = useSlotId([Boolean(props.errorMessage), props.isInvalid, props.validationState]); // Pass the label to the child grid elements. hookData.set(state, { ariaLabel: props['aria-label'], ariaLabelledBy: props['aria-labelledby'], errorMessageId, selectedDateDescription }); // If the next or previous buttons become disabled while they are focused, move focus to the calendar body. let [nextFocused, setNextFocused] = useState(false); let nextDisabled = props.isDisabled || state.isNextVisibleRangeInvalid(); if (nextDisabled && nextFocused) { setNextFocused(false); state.setFocused(true); } let [previousFocused, setPreviousFocused] = useState(false); let previousDisabled = props.isDisabled || state.isPreviousVisibleRangeInvalid(); if (previousDisabled && previousFocused) { setPreviousFocused(false); state.setFocused(true); } let labelProps = useLabels({ id: props['id'], 'aria-label': [props['aria-label'], visibleRangeDescription].filter(Boolean).join(', '), 'aria-labelledby': props['aria-labelledby'] }); return { calendarProps: mergeProps(domProps, labelProps, { role: 'application', 'aria-details': props['aria-details'] || undefined, 'aria-describedby': props['aria-describedby'] || undefined }), nextButtonProps: { onPress: () => state.focusNextPage(), 'aria-label': stringFormatter.format('next'), isDisabled: nextDisabled, onFocusChange: setNextFocused }, prevButtonProps: { onPress: () => state.focusPreviousPage(), 'aria-label': stringFormatter.format('previous'), isDisabled: previousDisabled, onFocusChange: setPreviousFocused }, errorMessageProps: { id: errorMessageId }, title }; }