Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pink-comics-go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/select': patch
---

Fixes `Select` so that when an `aria-label` is added, VO announces both the value and the label when there is a value. Also adds `aria-current` attribute to help screen readers announce the current selection state.
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ export const DatePickerMenuSelectMonth = ({
return (
<Select
{...selectElementProps}
aria-label={`Select month - ${
monthOptions[month.getUTCMonth()].long
} selected`}
aria-label="select month"
value={month.getUTCMonth().toString()}
onChange={handleMonthOnChange}
className={cx(selectTruncateStyles, selectInputWidthStyles)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const DatePickerMenuSelectYear = ({
return (
<Select
{...selectElementProps}
aria-label={`Select year - ${month.getUTCFullYear().toString()} selected`}
aria-label="select year"
value={month.getUTCFullYear().toString()}
onChange={handleYearOnChange}
className={cx(selectTruncateStyles, selectInputWidthStyles)}
Expand Down
1 change: 1 addition & 0 deletions packages/select/src/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export const LiveExample: StoryFn<SelectProps> = ({
)}
// eslint-disable-next-line no-console
onChange={v => console.log(v)}
aria-label="hello world"
/>
);
LiveExample.parameters = {
Expand Down
45 changes: 45 additions & 0 deletions packages/select/src/Select/Select.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,51 @@ describe('packages/select', () => {
});
});

test('composes aria-label with selected value for screen readers', () => {
const { getInput } = renderSelect({
label: undefined,
'aria-label': 'Color',
defaultValue: Color.Blue,
});

const button = getInput();
expect(button).toHaveAttribute('aria-label', 'Color, Blue');
});

test('composes aria-label with placeholder when no value is selected', () => {
const { getInput } = renderSelect({
label: undefined,
'aria-label': 'Color',
placeholder: 'Choose a color',
});

const button = getInput();
expect(button).toHaveAttribute('aria-label', 'Color, Choose a color');
});

test('does not compose aria-label when a visible label is present', () => {
const { getInput } = renderSelect({
label: 'Color',
'aria-label': 'Color',
defaultValue: Color.Blue,
});

const button = getInput();
expect(button).not.toHaveAttribute('aria-label');
});

test('does not compose aria-label when aria-labelledby is present', () => {
const { getInput } = renderSelect({
label: undefined,
'aria-label': 'Color',
'aria-labelledby': 'custom-label',
defaultValue: Color.Blue,
});

const button = getInput();
expect(button).not.toHaveAttribute('aria-label');
});

test('option & group passes through data props', async () => {
const { queryByTestId } = render(
<Select label="Label">
Expand Down
18 changes: 17 additions & 1 deletion packages/select/src/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,22 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(
}),
} as const;

/**
* When aria-label is provided, we compose it with the selected value for screen readers. This is the default
* behavior for native select elements. When not doing this, only aria-label was being announced even when a value
* was selected.
*/
const composedAriaLabel = useMemo(() => {
if (!ariaLabel || label || ariaLabelledby) {
return undefined;
}

const selectedText =
selectedOption !== null ? selectedOption.props.children : placeholder;

return `${ariaLabel}, ${selectedText}`;
}, [ariaLabel, ariaLabelledby, label, placeholder, selectedOption]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit hacky, but I'm not sure how else we would achieve this. Definitely open to ideas here if there's a better way to do it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking we would set aria-current in addition to the label? AFAIK that should also be announced


return (
<LeafyGreenProvider darkMode={darkMode}>
<div
Expand Down Expand Up @@ -618,7 +634,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(
onOpen={onOpen}
onClose={onClose}
aria-labelledby={labelId}
aria-label={!label && !ariaLabelledby ? ariaLabel : undefined}
aria-label={composedAriaLabel}
aria-controls={menuId}
aria-expanded={open}
aria-describedby={descriptionId}
Expand Down
Loading