Skip to content
7 changes: 7 additions & 0 deletions .changeset/date-picker-aria.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@leafygreen-ui/date-picker': patch
---

[LG-3879](https://jira.mongodb.org/browse/LG-3879)
Updates ARIA labels for DatePicker menu previous/next buttons, and year/month select elements.
Hides calendar cell text, so screen-readers only read the cell's `aria-value`.
14 changes: 8 additions & 6 deletions packages/date-picker/src/DatePicker/DatePicker.testutils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,14 @@ export const renderDatePicker = (
const calendarGrid = withinElement(menuContainerEl)?.queryByRole('grid');
const calendarCells =
withinElement(menuContainerEl)?.getAllByRole('gridcell');
const leftChevron =
withinElement(menuContainerEl)?.queryByLabelText('Previous month') ||
withinElement(menuContainerEl)?.queryByLabelText('Previous valid month');
const rightChevron =
withinElement(menuContainerEl)?.queryByLabelText('Next month') ||
withinElement(menuContainerEl)?.queryByLabelText('Next valid month');

// TODO: date-picker test harnesses https://jira.mongodb.org/browse/LG-4176
const leftChevron = withinElement(menuContainerEl)?.queryByTestId(
'lg-date_picker-menu-prev_month_button',
);
const rightChevron = withinElement(menuContainerEl)?.queryByTestId(
'lg-date_picker-menu-next_month_button',
);
const monthSelect = withinElement(menuContainerEl)?.queryByLabelText(
'Select month',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,32 @@ describe('packages/date-picker/date-picker-menu', () => {
expect(grid).toHaveAttribute('aria-label', 'September 2023');
});
test('chevrons have aria labels', () => {
const { getByLabelText } = renderDatePickerMenu();
const leftChevron = getByLabelText('Previous month');
const rightChevron = getByLabelText('Next month');
const { getByTestId } = renderDatePickerMenu();
const leftChevron = getByTestId('lg-date_picker-menu-prev_month_button');
const rightChevron = getByTestId('lg-date_picker-menu-next_month_button');
expect(leftChevron).toBeInTheDocument();
expect(rightChevron).toBeInTheDocument();
expect(leftChevron).toHaveAttribute(
'aria-label',
expect.stringContaining('Previous month'),
);
expect(rightChevron).toHaveAttribute(
'aria-label',
expect.stringContaining('Next month'),
);
});
test('select menu triggers have aria labels', () => {
const { monthSelect, yearSelect } = renderDatePickerMenu();
expect(monthSelect).toBeInTheDocument();
expect(yearSelect).toBeInTheDocument();
expect(monthSelect).toHaveAttribute(
'aria-label',
expect.stringContaining('Select month'),
);
expect(yearSelect).toHaveAttribute(
'aria-label',
expect.stringContaining('Select year'),
);
});
test('select menus have correct values', () => {
const { monthSelect, yearSelect } = renderDatePickerMenu();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React, { forwardRef, MouseEventHandler } from 'react';

import { isSameUTCMonth, setUTCMonth } from '@leafygreen-ui/date-utils';
import {
getMonthName,
isSameUTCMonth,
setUTCMonth,
} from '@leafygreen-ui/date-utils';
import { SupportedLocales } from '@leafygreen-ui/date-utils';
import Icon from '@leafygreen-ui/icon';
import IconButton from '@leafygreen-ui/icon-button';
import { Icon } from '@leafygreen-ui/icon';
import { IconButton } from '@leafygreen-ui/icon-button';
import { isDefined } from '@leafygreen-ui/lib';

import { useSharedDatePickerContext } from '../../../shared/context';
import { useDatePickerContext } from '../../DatePickerContext';
Expand Down Expand Up @@ -42,6 +47,12 @@ export const DatePickerMenuHeader = forwardRef<

const isIsoFormat = locale === SupportedLocales.ISO_8601;

const formatMonth = (date: Date) => {
const monthName = getMonthName(date.getUTCMonth(), locale);
const year = date.getUTCFullYear().toString();
return `${monthName.long} ${year}`;
};

/**
* If the month is not in range and is not the last valid month
* e.g.
Expand All @@ -63,6 +74,48 @@ export const DatePickerMenuHeader = forwardRef<
return !isDateInRange && !isOnLastValidMonth;
};

/**
* Given a direction (left/right), computes the nearest valid adjacent month
*
* @example
* max: new Date(Date.UTC(2038, Month.January, 19));
* current month date: new Date(Date.UTC(2038, Month.March, 19));
* `left` chevron will change the month back to January 2038
*
* @example
* min: new Date(Date.UTC(1970, Month.January, 1));
* current month date: new Date(Date.UTC(1969, Month.November, 19));
* "right" chevron will change the month back to January 1970
*/
const getNewMonth = (dir: 'left' | 'right'): Date => {
if (isMonthInvalid(dir)) {
const closestValidDate = dir === 'left' ? max : min;
const newMonthIndex = closestValidDate.getUTCMonth();
const newMonth = setUTCMonth(closestValidDate, newMonthIndex);
return newMonth;
} else {
const increment = dir === 'left' ? -1 : 1;
const newMonthIndex = month.getUTCMonth() + increment;
const newMonth = setUTCMonth(month, newMonthIndex);
return newMonth;
}
};

const getChevronButtonLabel = (dir: 'left' | 'right') => {
const dirLabel = dir === 'left' ? 'Previous' : 'Next';
const isNewMonthInvalid = isMonthInvalid(dir);
const newMonth = getNewMonth(dir);
const newMonthString = formatMonth(newMonth);
return [
dirLabel,
isNewMonthInvalid ? 'valid ' : undefined,
'month',
`(${newMonthString})`,
]
.filter(isDefined)
.join(' ');
};

/**
* Calls the `updateMonth` helper with the appropriate month when a Chevron is clicked
*/
Expand All @@ -71,35 +124,16 @@ export const DatePickerMenuHeader = forwardRef<
e => {
e.stopPropagation();
e.preventDefault();

// e.g.
// max: new Date(Date.UTC(2038, Month.January, 19));
// current month date: new Date(Date.UTC(2038, Month.March, 19));
// left chevron will change the month back to January 2038
// e.g.
// min: new Date(Date.UTC(1970, Month.January, 1));
// current month date: new Date(Date.UTC(1969, Month.November, 19));
// right chevron will change the month back to January 1970
if (isMonthInvalid(dir)) {
const closestValidDate = dir === 'left' ? max : min;
const newMonthIndex = closestValidDate.getUTCMonth();
const newMonth = setUTCMonth(closestValidDate, newMonthIndex);
updateMonth(newMonth);
} else {
const increment = dir === 'left' ? -1 : 1;
const newMonthIndex = month.getUTCMonth() + increment;
const newMonth = setUTCMonth(month, newMonthIndex);
updateMonth(newMonth);
}
const newMonth = getNewMonth(dir);
updateMonth(newMonth);
};

return (
<div ref={fwdRef} className={menuHeaderStyles} {...rest}>
<IconButton
ref={refs.chevronButtonRefs.left}
aria-label={
isMonthInvalid('left') ? 'Previous valid month' : 'Previous month'
}
data-testid="lg-date_picker-menu-prev_month_button"
aria-label={getChevronButtonLabel('left')}
disabled={shouldChevronBeDisabled('left', month, min)}
onClick={handleChevronClick('left')}
>
Expand All @@ -120,7 +154,8 @@ export const DatePickerMenuHeader = forwardRef<
</div>
<IconButton
ref={refs.chevronButtonRefs.right}
aria-label={isMonthInvalid('right') ? 'Next valid month' : 'Next month'}
data-testid="lg-date_picker-menu-next_month_button"
aria-label={getChevronButtonLabel('right')}
disabled={shouldChevronBeDisabled('right', month, max)}
onClick={handleChevronClick('right')}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { useCallback } from 'react';

import { getLocaleMonths, setUTCMonth } from '@leafygreen-ui/date-utils';
import {
getLocaleMonths,
getMonthName,
setUTCMonth,
} from '@leafygreen-ui/date-utils';
import { cx } from '@leafygreen-ui/emotion';
import { Option, Select } from '@leafygreen-ui/select';

Expand Down Expand Up @@ -40,18 +44,18 @@ export const DatePickerMenuSelectMonth = ({
updateMonth(newMonth);
};

const monthString = getMonthName(month.getUTCMonth(), locale);

return (
<Select
{...selectElementProps}
aria-label={`Select month - ${
monthOptions[month.getUTCMonth()].long
} selected`}
aria-label={`Select month (${monthString.long} selected)`}
value={month.getUTCMonth().toString()}
onChange={handleMonthOnChange}
className={cx(selectTruncateStyles, selectInputWidthStyles)}
onEntered={() => setIsSelectOpen(true)}
onExited={() => setIsSelectOpen(false)}
placeholder={monthOptions[month.getUTCMonth()].short}
placeholder={monthString.short}
>
{monthOptions.map((m, i) => (
<Option
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,18 @@ export const DatePickerMenuSelectYear = ({
updateMonth(newMonth);
};

const yearString = month.getUTCFullYear().toString();

return (
<Select
{...selectElementProps}
aria-label={`Select year - ${month.getUTCFullYear().toString()} selected`}
value={month.getUTCFullYear().toString()}
aria-label={`Select year (${yearString} selected)`}
value={yearString}
onChange={handleYearOnChange}
className={cx(selectTruncateStyles, selectInputWidthStyles)}
onEntered={() => setIsSelectOpen(true)}
onExited={() => setIsSelectOpen(false)}
placeholder={month.getUTCFullYear().toString()}
placeholder={yearString}
>
{yearOptions.map(y => (
<Option value={y.toString()} key={y} aria-label={y.toString()}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const CalendarCell = React.forwardRef<
isHighlighted,
className,
onClick,
'aria-label': ariaLabel,
...rest
}: CalendarCellProps,
fwdRef,
Expand Down Expand Up @@ -86,6 +87,7 @@ export const CalendarCell = React.forwardRef<
role="gridcell"
data-testid="lg-date_picker-calendar_cell"
data-highlighted={isHighlighted}
aria-label={ariaLabel}
aria-current={isCurrent}
aria-selected={isActive}
aria-disabled={state === CalendarCellState.Disabled}
Expand All @@ -109,6 +111,7 @@ export const CalendarCell = React.forwardRef<
>
<div className={cx(indicatorBaseStyles, indicatorClassName)}></div>
<span
aria-hidden // hidden, since the `td` announces the value via `aria-label`
className={cx(cellTextStyles, {
[cellTextCurrentStyles]: isCurrent,
})}
Expand Down
Loading