@awsui/components-react
Version:
On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en
174 lines • 11.5 kB
JavaScript
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useMemo } from 'react';
import clsx from 'clsx';
import { isLastDayOfMonth, isSameDay, isSameMonth, isSameYear, isThisMonth, isToday } from 'date-fns';
import { useInternalI18n } from '../../../i18n/context';
import ScreenreaderOnly from '../../../internal/components/screenreader-only';
import { formatDate } from '../../../internal/utils/date-time';
import { MonthCalendar, YearCalendar } from '../../../internal/utils/date-time/calendar';
import { normalizeStartOfWeek } from '../../../internal/utils/locale/index.js';
import { GridCell } from './grid-cell';
import { renderDateAnnouncement, renderDayName } from './intl';
import testutilStyles from '../../test-classes/styles.css.js';
import styles from './styles.css.js';
const dayUtils = {
getItemKey: (rowIndex, rowItemIndex) => `${rowIndex}:${rowItemIndex}`,
isSameItem: (date1, date2) => isSameDay(date1, date2),
isSamePage: (date1, date2) => isSameMonth(date1, date2),
checkIfCurrentDay: date => isToday(date),
checkIfCurrentMonth: () => false,
checkIfCurrent: date => isToday(date),
getPageName: () => 'month',
};
const monthUtils = {
getItemKey: (rowIndex, rowItemIndex) => `Month ${rowIndex * 3 + rowItemIndex + 1}`,
isSameItem: (date1, date2) => isSameMonth(date1, date2),
isSamePage: (date1, date2) => isSameYear(date1, date2),
checkIfCurrentDay: () => false,
checkIfCurrentMonth: date => isThisMonth(date),
checkIfCurrent: date => isThisMonth(date),
getPageName: () => 'year',
};
/**
* Calendar grid supports two mechanisms of keyboard navigation:
* - Native screen-reader table navigation (semantic table markup);
* - Keyboard arrow-keys navigation (a custom key-down handler).
*
* The implementation largely follows the w3 example (https://www.w3.org/WAI/ARIA/apg/example-index/dialog-modal/datepicker-dialog) and shares the following issues:
* - (table navigation) Chrome+VO - weekday is announced twice when navigating to the calendar's header;
* - (table navigation) Safari+VO - "dimmed" state is announced twice;
* - (table navigation) Firefox/Chrome+NVDA - cannot use table navigation if any cell has a focus;
* - (keyboard navigation) Firefox+NVDA - every day is announced as "not selected";
* - (keyboard navigation) Safari/Chrome+VO - weekdays are not announced;
* - (keyboard navigation) Safari/Chrome+VO - days are not announced as interactive (clickable or selectable);
* - (keyboard navigation) Safari/Chrome+VO - day announcements are not interruptive and can be missed if navigating fast.
*/
export function Grid({ padDates, baseDate, selectedStartDate, selectedEndDate, rangeStartDate, rangeEndDate, focusedDate, focusedDateRef, onSelectDate, onGridKeyDownHandler, onFocusedDateChange, isDateEnabled, dateDisabledReason, locale, todayAriaLabel, currentMonthAriaLabel, ariaLabelledby, className, startOfWeek: rawStartOfWeek = 0, granularity = 'day', }) {
const baseDateTime = baseDate === null || baseDate === void 0 ? void 0 : baseDate.getTime();
const i18n = useInternalI18n('date-range-picker');
const isMonthPicker = granularity === 'month';
const startOfWeek = normalizeStartOfWeek(rawStartOfWeek, locale);
const calendar = useMemo(() => {
const startDate = rangeStartDate !== null && rangeStartDate !== void 0 ? rangeStartDate : rangeEndDate;
const endDate = rangeEndDate !== null && rangeEndDate !== void 0 ? rangeEndDate : rangeStartDate;
const selection = startDate && endDate ? [startDate, endDate] : null;
if (isMonthPicker) {
const calendarData = new YearCalendar({ baseDate, selection });
return {
header: [],
rows: calendarData.quarters,
range: calendarData.range,
};
}
const calendarData = new MonthCalendar({ padDates, startOfWeek, baseDate, selection });
return {
header: calendarData.weekdays,
rows: calendarData.weeks,
range: calendarData.range,
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[padDates, startOfWeek, baseDateTime, rangeStartDate, rangeEndDate]);
const currentAnnouncement = i18n(isMonthPicker ? 'i18nStrings.currentMonthAriaLabel' : 'i18nStrings.todayAriaLabel', isMonthPicker ? currentMonthAriaLabel : todayAriaLabel);
return (React.createElement("table", { role: "grid", "aria-labelledby": ariaLabelledby, className: clsx(styles.grid, className) },
!isMonthPicker && (React.createElement("thead", null,
React.createElement("tr", null, calendar.header.map(dayIndex => (React.createElement("th", { key: dayIndex, scope: "col", className: clsx(styles['grid-cell'], styles['day-header'], testutilStyles['day-header']) },
React.createElement("span", { "aria-hidden": "true" }, renderDayName(locale, dayIndex, 'short')),
React.createElement(ScreenreaderOnly, null, renderDayName(locale, dayIndex, 'long')))))))),
React.createElement("tbody", { onKeyDown: onGridKeyDownHandler }, calendar.rows.map((row, rowIndex) => {
const rowItems = isMonthPicker ? row.months : row.days;
const weekTestIndex = !isMonthPicker ? row.testIndex : undefined;
return (React.createElement("tr", { key: rowIndex, className: clsx({
[testutilStyles['calendar-quarter']]: isMonthPicker,
[testutilStyles['calendar-week']]: !isMonthPicker,
}), ...(!isMonthPicker && weekTestIndex
? {
['data-awsui-weekindex']: weekTestIndex,
}
: {}) }, rowItems.map(({ date, isVisible, isInRange, isSelectionTop, isSelectionBottom, isSelectionLeft, isSelectionRight }, rowItemIndex) => {
const { getItemKey, isSameItem, isSamePage, checkIfCurrent, checkIfCurrentDay, checkIfCurrentMonth, getPageName, } = isMonthPicker ? monthUtils : dayUtils;
const itemKey = getItemKey(rowIndex, rowItemIndex);
const pageName = getPageName();
const isCurrentDay = checkIfCurrentDay(date);
const isCurrentMonth = checkIfCurrentMonth(date);
const isCurrent = checkIfCurrent(date);
const isStartDate = !!selectedStartDate && isSameItem(date, selectedStartDate);
const isEndDate = !!selectedEndDate && isSameItem(date, selectedEndDate);
const isSelected = isStartDate || isEndDate;
const isFocused = !!focusedDate && isSameItem(date, focusedDate) && isSamePage(date, baseDate);
const onlyOneSelected = !!rangeStartDate && !!rangeEndDate
? isSameItem(rangeStartDate, rangeEndDate)
: !selectedStartDate || !selectedEndDate;
const isEnabled = (!isDateEnabled || isDateEnabled(date)) && isSamePage(date, baseDate);
const disabledReason = dateDisabledReason(date);
const isDisabledWithReason = !isEnabled && !!disabledReason;
const isFocusable = isFocused && (isEnabled || isDisabledWithReason);
const baseClasses = {
[testutilStyles['calendar-date']]: !isMonthPicker && isSameMonth(date, baseDate),
[testutilStyles['calendar-month']]: isMonthPicker && isSameYear(date, baseDate),
[styles.day]: !isMonthPicker,
[styles.month]: isMonthPicker,
[styles['grid-cell']]: true,
[styles['in-first-row']]: rowIndex === 0,
[styles['in-first-column']]: rowItemIndex === 0,
};
if (!isVisible) {
return (React.createElement("td", { key: itemKey, ref: isFocused ? focusedDateRef : undefined, className: clsx(baseClasses, {
[styles[`last-day-of-month`]]: !isMonthPicker && isLastDayOfMonth(date),
[styles[`last-month-of-year`]]: isMonthPicker && date.getMonth() === 11,
}) }));
}
const handlers = {};
if (isEnabled) {
handlers.onClick = () => onSelectDate(date);
handlers.onFocus = () => onFocusedDateChange(date);
}
// Screen-reader announcement for the focused day/month.
let announcement = renderDateAnnouncement({
date,
isCurrent,
locale,
granularity,
});
if (currentAnnouncement) {
if (isMonthPicker && isThisMonth(date)) {
announcement += `. ${currentAnnouncement}`;
}
else if (!isMonthPicker && isToday(date)) {
announcement += `. ${currentAnnouncement}`;
}
}
// Can't be focused.
let tabIndex = undefined;
if (isEnabled || isDisabledWithReason) {
tabIndex = isFocusable
? 0 // Next focus target.
: -1; // Can be focused programmatically.
}
return (React.createElement(GridCell, { ref: isFocused ? focusedDateRef : undefined, key: itemKey, className: clsx(baseClasses, {
[styles['in-visible-calendar']]: true,
[styles[`in-current-${pageName}`]]: isSamePage(date, baseDate),
[styles.enabled]: isEnabled,
[styles.selected]: isSelected,
[styles['start-date']]: isStartDate,
[styles['end-date']]: isEndDate,
[testutilStyles['start-date']]: isStartDate,
[testutilStyles['end-date']]: isEndDate,
[styles['no-range']]: isSelected && onlyOneSelected,
[styles['in-range']]: isInRange,
[styles['in-range-border-block-start']]: isSelectionTop,
[styles['in-range-border-block-end']]: isSelectionBottom,
[styles['in-range-border-inline-start']]: isSelectionLeft,
[styles['in-range-border-inline-end']]: isSelectionRight,
[styles.today]: isCurrentDay,
[testutilStyles.today]: isCurrentDay,
[styles['this-month']]: isCurrentMonth,
[testutilStyles['this-month']]: isCurrentMonth,
}), "aria-selected": isEnabled ? isSelected || isInRange : undefined, "aria-current": isCurrent ? 'date' : undefined, "data-date": formatDate(date, granularity), "aria-disabled": !isEnabled, tabIndex: tabIndex, disabledReason: isDisabledWithReason ? disabledReason : undefined, ...handlers },
React.createElement("span", { className: styles[`${granularity}-inner`], "aria-hidden": "true" }, isMonthPicker ? date.toLocaleString(locale, { month: 'short' }) : date.getDate()),
React.createElement(ScreenreaderOnly, null, announcement)));
})));
}))));
}
//# sourceMappingURL=grid.js.map