Skip to content

Commit 0c42aba

Browse files
LG-3879: fix(DatePicker) Updates DatePicker Next/Prev button aria-labels (#3224)
* updates chevron button labels * update select labels * update calendar cell label * Create date-picker-aria.md * Update DatePickerMenu.spec.tsx * Update DatePicker.testutils.tsx * Update date-picker-aria.md * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Apply suggestion from @TheSonOfThomp --------- Co-authored-by: Copilot <[email protected]>
1 parent 9e5932c commit 0c42aba

File tree

7 files changed

+111
-40
lines changed

7 files changed

+111
-40
lines changed

.changeset/date-picker-aria.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@leafygreen-ui/date-picker': patch
3+
---
4+
5+
[LG-3879](https://jira.mongodb.org/browse/LG-3879)
6+
Updates ARIA labels for DatePicker menu previous/next buttons, and year/month select elements.
7+
Hides calendar cell text, so screen-readers only read the cell's `aria-value`.

packages/date-picker/src/DatePicker/DatePicker.testutils.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,14 @@ export const renderDatePicker = (
114114
const calendarGrid = withinElement(menuContainerEl)?.queryByRole('grid');
115115
const calendarCells =
116116
withinElement(menuContainerEl)?.getAllByRole('gridcell');
117-
const leftChevron =
118-
withinElement(menuContainerEl)?.queryByLabelText('Previous month') ||
119-
withinElement(menuContainerEl)?.queryByLabelText('Previous valid month');
120-
const rightChevron =
121-
withinElement(menuContainerEl)?.queryByLabelText('Next month') ||
122-
withinElement(menuContainerEl)?.queryByLabelText('Next valid month');
117+
118+
// TODO: date-picker test harnesses https://jira.mongodb.org/browse/LG-4176
119+
const leftChevron = withinElement(menuContainerEl)?.queryByTestId(
120+
'lg-date_picker-menu-prev_month_button',
121+
);
122+
const rightChevron = withinElement(menuContainerEl)?.queryByTestId(
123+
'lg-date_picker-menu-next_month_button',
124+
);
123125
const monthSelect = withinElement(menuContainerEl)?.queryByLabelText(
124126
'Select month',
125127
{

packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,32 @@ describe('packages/date-picker/date-picker-menu', () => {
123123
expect(grid).toHaveAttribute('aria-label', 'September 2023');
124124
});
125125
test('chevrons have aria labels', () => {
126-
const { getByLabelText } = renderDatePickerMenu();
127-
const leftChevron = getByLabelText('Previous month');
128-
const rightChevron = getByLabelText('Next month');
126+
const { getByTestId } = renderDatePickerMenu();
127+
const leftChevron = getByTestId('lg-date_picker-menu-prev_month_button');
128+
const rightChevron = getByTestId('lg-date_picker-menu-next_month_button');
129129
expect(leftChevron).toBeInTheDocument();
130130
expect(rightChevron).toBeInTheDocument();
131+
expect(leftChevron).toHaveAttribute(
132+
'aria-label',
133+
expect.stringContaining('Previous month'),
134+
);
135+
expect(rightChevron).toHaveAttribute(
136+
'aria-label',
137+
expect.stringContaining('Next month'),
138+
);
131139
});
132140
test('select menu triggers have aria labels', () => {
133141
const { monthSelect, yearSelect } = renderDatePickerMenu();
134142
expect(monthSelect).toBeInTheDocument();
135143
expect(yearSelect).toBeInTheDocument();
144+
expect(monthSelect).toHaveAttribute(
145+
'aria-label',
146+
expect.stringContaining('Select month'),
147+
);
148+
expect(yearSelect).toHaveAttribute(
149+
'aria-label',
150+
expect.stringContaining('Select year'),
151+
);
136152
});
137153
test('select menus have correct values', () => {
138154
const { monthSelect, yearSelect } = renderDatePickerMenu();

packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import React, { forwardRef, MouseEventHandler } from 'react';
22

3-
import { isSameUTCMonth, setUTCMonth } from '@leafygreen-ui/date-utils';
3+
import {
4+
getMonthName,
5+
isSameUTCMonth,
6+
setUTCMonth,
7+
} from '@leafygreen-ui/date-utils';
48
import { SupportedLocales } from '@leafygreen-ui/date-utils';
59
import { Icon } from '@leafygreen-ui/icon';
610
import { IconButton } from '@leafygreen-ui/icon-button';
11+
import { isDefined } from '@leafygreen-ui/lib';
712

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

4348
const isIsoFormat = locale === SupportedLocales.ISO_8601;
4449

50+
const formatMonth = (date: Date) => {
51+
const monthName = getMonthName(date.getUTCMonth(), locale);
52+
const year = date.getUTCFullYear().toString();
53+
return `${monthName.long} ${year}`;
54+
};
55+
4556
/**
4657
* If the month is not in range and is not the last valid month
4758
* e.g.
@@ -63,6 +74,48 @@ export const DatePickerMenuHeader = forwardRef<
6374
return !isDateInRange && !isOnLastValidMonth;
6475
};
6576

77+
/**
78+
* Given a direction (left/right), computes the nearest valid adjacent month
79+
*
80+
* @example
81+
* max: new Date(Date.UTC(2038, Month.January, 19));
82+
* current month date: new Date(Date.UTC(2038, Month.March, 19));
83+
* `left` chevron will change the month back to January 2038
84+
*
85+
* @example
86+
* min: new Date(Date.UTC(1970, Month.January, 1));
87+
* current month date: new Date(Date.UTC(1969, Month.November, 19));
88+
* "right" chevron will change the month back to January 1970
89+
*/
90+
const getNewMonth = (dir: 'left' | 'right'): Date => {
91+
if (isMonthInvalid(dir)) {
92+
const closestValidDate = dir === 'left' ? max : min;
93+
const newMonthIndex = closestValidDate.getUTCMonth();
94+
const newMonth = setUTCMonth(closestValidDate, newMonthIndex);
95+
return newMonth;
96+
} else {
97+
const increment = dir === 'left' ? -1 : 1;
98+
const newMonthIndex = month.getUTCMonth() + increment;
99+
const newMonth = setUTCMonth(month, newMonthIndex);
100+
return newMonth;
101+
}
102+
};
103+
104+
const getChevronButtonLabel = (dir: 'left' | 'right') => {
105+
const dirLabel = dir === 'left' ? 'Previous' : 'Next';
106+
const isNewMonthInvalid = isMonthInvalid(dir);
107+
const newMonth = getNewMonth(dir);
108+
const newMonthString = formatMonth(newMonth);
109+
return [
110+
dirLabel,
111+
isNewMonthInvalid ? 'valid' : undefined,
112+
'month',
113+
`(${newMonthString})`,
114+
]
115+
.filter(isDefined)
116+
.join(' ');
117+
};
118+
66119
/**
67120
* Calls the `updateMonth` helper with the appropriate month when a Chevron is clicked
68121
*/
@@ -71,35 +124,16 @@ export const DatePickerMenuHeader = forwardRef<
71124
e => {
72125
e.stopPropagation();
73126
e.preventDefault();
74-
75-
// e.g.
76-
// max: new Date(Date.UTC(2038, Month.January, 19));
77-
// current month date: new Date(Date.UTC(2038, Month.March, 19));
78-
// left chevron will change the month back to January 2038
79-
// e.g.
80-
// min: new Date(Date.UTC(1970, Month.January, 1));
81-
// current month date: new Date(Date.UTC(1969, Month.November, 19));
82-
// right chevron will change the month back to January 1970
83-
if (isMonthInvalid(dir)) {
84-
const closestValidDate = dir === 'left' ? max : min;
85-
const newMonthIndex = closestValidDate.getUTCMonth();
86-
const newMonth = setUTCMonth(closestValidDate, newMonthIndex);
87-
updateMonth(newMonth);
88-
} else {
89-
const increment = dir === 'left' ? -1 : 1;
90-
const newMonthIndex = month.getUTCMonth() + increment;
91-
const newMonth = setUTCMonth(month, newMonthIndex);
92-
updateMonth(newMonth);
93-
}
127+
const newMonth = getNewMonth(dir);
128+
updateMonth(newMonth);
94129
};
95130

96131
return (
97132
<div ref={fwdRef} className={menuHeaderStyles} {...rest}>
98133
<IconButton
99134
ref={refs.chevronButtonRefs.left}
100-
aria-label={
101-
isMonthInvalid('left') ? 'Previous valid month' : 'Previous month'
102-
}
135+
data-testid="lg-date_picker-menu-prev_month_button"
136+
aria-label={getChevronButtonLabel('left')}
103137
disabled={shouldChevronBeDisabled('left', month, min)}
104138
onClick={handleChevronClick('left')}
105139
>
@@ -120,7 +154,8 @@ export const DatePickerMenuHeader = forwardRef<
120154
</div>
121155
<IconButton
122156
ref={refs.chevronButtonRefs.right}
123-
aria-label={isMonthInvalid('right') ? 'Next valid month' : 'Next month'}
157+
data-testid="lg-date_picker-menu-next_month_button"
158+
aria-label={getChevronButtonLabel('right')}
124159
disabled={shouldChevronBeDisabled('right', month, max)}
125160
onClick={handleChevronClick('right')}
126161
>

packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuSelect/DatePickerMenuSelectMonth.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import React, { useCallback } from 'react';
22

3-
import { getLocaleMonths, setUTCMonth } from '@leafygreen-ui/date-utils';
3+
import {
4+
getLocaleMonths,
5+
getMonthName,
6+
setUTCMonth,
7+
} from '@leafygreen-ui/date-utils';
48
import { cx } from '@leafygreen-ui/emotion';
59
import { Option, Select } from '@leafygreen-ui/select';
610

@@ -40,16 +44,18 @@ export const DatePickerMenuSelectMonth = ({
4044
updateMonth(newMonth);
4145
};
4246

47+
const monthString = getMonthName(month.getUTCMonth(), locale);
48+
4349
return (
4450
<Select
4551
{...selectElementProps}
46-
aria-label="select month"
52+
aria-label={`Select month (${monthString.long} selected)`}
4753
value={month.getUTCMonth().toString()}
4854
onChange={handleMonthOnChange}
4955
className={cx(selectTruncateStyles, selectInputWidthStyles)}
5056
onEntered={() => setIsSelectOpen(true)}
5157
onExited={() => setIsSelectOpen(false)}
52-
placeholder={monthOptions[month.getUTCMonth()].short}
58+
placeholder={monthString.short}
5359
>
5460
{monthOptions.map((m, i) => (
5561
<Option

packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuSelect/DatePickerMenuSelectYear.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@ export const DatePickerMenuSelectYear = ({
3333
updateMonth(newMonth);
3434
};
3535

36+
const yearString = month.getUTCFullYear().toString();
37+
3638
return (
3739
<Select
3840
{...selectElementProps}
39-
aria-label="select year"
40-
value={month.getUTCFullYear().toString()}
41+
aria-label={`Select year (${yearString} selected)`}
42+
value={yearString}
4143
onChange={handleYearOnChange}
4244
className={cx(selectTruncateStyles, selectInputWidthStyles)}
4345
onEntered={() => setIsSelectOpen(true)}
4446
onExited={() => setIsSelectOpen(false)}
45-
placeholder={month.getUTCFullYear().toString()}
47+
placeholder={yearString}
4648
>
4749
{yearOptions.map(y => (
4850
<Option value={y.toString()} key={y} aria-label={y.toString()}>

packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const CalendarCell = React.forwardRef<
4747
isHighlighted,
4848
className,
4949
onClick,
50+
'aria-label': ariaLabel,
5051
...rest
5152
}: CalendarCellProps,
5253
fwdRef,
@@ -86,6 +87,7 @@ export const CalendarCell = React.forwardRef<
8687
role="gridcell"
8788
data-testid="lg-date_picker-calendar_cell"
8889
data-highlighted={isHighlighted}
90+
aria-label={ariaLabel}
8991
aria-current={isCurrent}
9092
aria-selected={isActive}
9193
aria-disabled={state === CalendarCellState.Disabled}
@@ -109,6 +111,7 @@ export const CalendarCell = React.forwardRef<
109111
>
110112
<div className={cx(indicatorBaseStyles, indicatorClassName)}></div>
111113
<span
114+
aria-hidden={true} // hidden, since the `td` announces the value via `aria-label`
112115
className={cx(cellTextStyles, {
113116
[cellTextCurrentStyles]: isCurrent,
114117
})}

0 commit comments

Comments
 (0)