Skip to content

Commit

Permalink
Autosuggest: Fix aria-label and aria-labelledby features (#1420)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeltaranto authored Jan 16, 2024
1 parent c2e5500 commit aa43dfb
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 20 deletions.
12 changes: 12 additions & 0 deletions .changeset/wet-cows-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'braid-design-system': patch
---

---
updated:
- Autosuggest
---

**Autosuggest:** Fix aria-label and aria-labelledby features

Fixes an issue where the `aria-label` and `aria-labelledby` props provided by a consumer were being overidden internally by the `Autosuggest` component.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Alert,
List,
Stack,
Heading,
} from '../';
import { IconHelp, IconLanguage } from '../icons';

Expand Down Expand Up @@ -662,6 +663,53 @@ const docs: ComponentDocs = {
</>,
),
},
{
label: 'Indirect or hidden field labels',
description: (
<Text>
In some cases it may be necessary for a field to be labelled by
another element or even not to have a visual label. Instead of
providing a <Strong>label</Strong> either <Strong>aria-label</Strong>{' '}
or <Strong>aria-labelledby</Strong> can be provided.
</Text>
),
Example: ({ id, getState, setState, resetState }) =>
source(
<Stack space="large">
<Heading level="2" id="field1Label">
Custom field label
</Heading>

<Autosuggest
aria-labelledby="field1Label"
id={`${id}_1`}
value={getState('value')}
onChange={setState('value')}
onClear={() => resetState('value')}
suggestions={filterSuggestions([
{ text: 'Apples', value: 1 },
{ text: 'Bananas', value: 2 },
{ text: 'Broccoli', value: 3 },
{ text: 'Carrots', value: 4 },
])}
/>

<Autosuggest
aria-label="Hidden label for field"
id={`${id}_2`}
value={getState('value')}
onChange={setState('value')}
onClear={() => resetState('value')}
suggestions={filterSuggestions([
{ text: 'Apples', value: 1 },
{ text: 'Bananas', value: 2 },
{ text: 'Broccoli', value: 3 },
{ text: 'Carrots', value: 4 },
])}
/>
</Stack>,
),
},
],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,43 @@ describe('Autosuggest', () => {
expect(result).toBe(input);
});

it('associates field with aria-label correctly', () => {
const { getByLabelText } = render(
<BraidTestProvider>
<Autosuggest
id="id"
aria-label="Hidden field label"
value={{ text: '' }}
onChange={() => {}}
suggestions={[]}
/>
</BraidTestProvider>,
);

expect(getByLabelText('Hidden field label').tagName).toBe('INPUT');
expect(
getByLabelText('Hidden field label').getAttribute('aria-labelledby'),
).toBeNull();
});

it('associates field with aria-labelledby correctly', () => {
const { getByLabelText } = render(
<BraidTestProvider>
<div id="fieldLabel">My field</div>
<Autosuggest
id="id"
aria-labelledby="fieldLabel"
value={{ text: '' }}
onChange={() => {}}
suggestions={[]}
/>
</BraidTestProvider>,
);

expect(getByLabelText('My field').tagName).toBe('INPUT');
expect(getByLabelText('My field').getAttribute('aria-label')).toBeNull();
});

it('should support standard suggestions', async () => {
const { input, queryByLabelText } = renderAutosuggest({
value: { text: '' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ interface LegacyMessageSuggestion {

export type AutosuggestBaseProps<Value> = Omit<
FieldBaseProps,
'value' | 'autoComplete' | 'labelId' | 'prefix'
'value' | 'autoComplete' | 'prefix'
> & {
value: AutosuggestValue<Value>;
suggestions:
Expand Down Expand Up @@ -728,7 +728,6 @@ export const Autosuggest = forwardRef(function <Value>(
<Field
{...restProps}
id={id}
labelId={a11y.labelProps.id}
value={value.text}
prefix={undefined}
secondaryIcon={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,29 @@ interface AutosuggestProps {
highlightedIndex: number | null;
isOpen: boolean;
}

/*
Provides all the required accessibility props for the Autosuggest component,
with exception of the field label. Whether using `label`, `aria-label` or
`aria-labelledby`, the association with the input is handled via the
`fieldProps` on the Field component.
*/
export const createAccessbilityProps = ({
id,
highlightedIndex,
isOpen,
}: AutosuggestProps) => {
const menuId = `${id}-menu`;
const labelId = `${id}-label`;
const assistiveDescriptionId = `${id}-aria-description`;

return {
labelProps: {
id: labelId,
},
inputProps: {
id,
role: 'combobox',
'aria-haspopup': 'listbox',
'aria-owns': isOpen ? menuId : undefined, // backwards compatibility for screenreaders implementing ARIA 1.0
'aria-controls': menuId,
'aria-expanded': isOpen,
'aria-labelledby': labelId,
'aria-autocomplete': 'list',
'aria-activedescendant':
typeof highlightedIndex === 'number'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type ValidDropdownChildren = AllHTMLAttributes<
type SelectProps = AllHTMLAttributes<HTMLSelectElement>;
export type DropdownBaseProps = Omit<
FieldBaseProps,
'value' | 'labelId' | 'secondaryMessage' | 'prefix'
'value' | 'secondaryMessage' | 'prefix'
> & {
children: ValidDropdownChildren[] | ValidDropdownChildren;
value: NonNullable<SelectProps['value']>;
Expand Down Expand Up @@ -44,7 +44,6 @@ export const Dropdown = forwardRef<HTMLSelectElement, DropdownProps>(
<Field
{...restProps}
disabled={disabled}
labelId={undefined}
prefix={undefined}
secondaryMessage={null}
value={value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ type ChangeHandler = (value: MonthPickerValue) => void;
export type MonthPickerBaseProps = Omit<
FieldGroupBaseProps,
| 'value'
| 'labelId'
| 'aria-describedby'
| 'name'
| 'autoComplete'
Expand Down Expand Up @@ -204,7 +203,6 @@ const MonthPicker = ({
{...restProps}
icon={undefined}
prefix={undefined}
labelId={undefined}
name={undefined}
autoComplete={undefined}
secondaryMessage={null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type InputProps = AllHTMLAttributes<HTMLInputElement>;

export type PasswordFieldBaseProps = Omit<
FieldBaseProps,
'value' | 'labelId' | 'secondaryMessage' | 'icon' | 'prefix'
'value' | 'secondaryMessage' | 'icon' | 'prefix'
> & {
value: NonNullable<InputProps['value']>;
onChange: NonNullable<InputProps['onChange']>;
Expand Down Expand Up @@ -82,7 +82,6 @@ export const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
value={value}
icon={undefined}
prefix={undefined}
labelId={undefined}
disabled={disabled}
secondaryMessage={null}
alwaysShowSecondaryIcon={!disabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type InputProps = AllHTMLAttributes<HTMLInputElement>;

export type TextFieldBaseProps = Omit<
FieldBaseProps,
'value' | 'labelId' | 'secondaryMessage'
'value' | 'secondaryMessage'
> & {
value: NonNullable<InputProps['value']>;
type?: keyof typeof validTypes;
Expand Down Expand Up @@ -93,7 +93,6 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
{...restProps}
id={id}
value={value}
labelId={undefined}
secondaryMessage={
characterLimit
? getCharacterLimitStatus({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type NativeTextareaProps = AllHTMLAttributes<HTMLTextAreaElement>;

export type TextareaBaseProps = Omit<
FieldBaseProps,
'value' | 'labelId' | 'secondaryMessage' | 'icon' | 'prefix'
'value' | 'secondaryMessage' | 'icon' | 'prefix'
> & {
value: NonNullable<NativeTextareaProps['value']>;
onChange: NonNullable<NativeTextareaProps['onChange']>;
Expand Down Expand Up @@ -122,7 +122,6 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
value={value}
icon={undefined}
prefix={undefined}
labelId={undefined}
secondaryMessage={
characterLimit
? getCharacterLimitStatus({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export type FieldLabelVariant =
export interface FieldBaseProps {
id: NonNullable<FormElementProps['id']>;
value?: FormElementProps['value'];
labelId?: string;
name?: FormElementProps['name'];
disabled?: FormElementProps['disabled'];
autoComplete?: FormElementProps['autoComplete'];
Expand Down Expand Up @@ -94,7 +93,6 @@ type InternalFieldProps = FieldBaseProps &
export const Field = ({
id,
value,
labelId,
name,
disabled,
autoComplete,
Expand Down Expand Up @@ -156,7 +154,6 @@ export const Field = ({
<Stack space="xsmall">
{hasVisualLabelOrDescription ? (
<FieldLabel
id={labelId}
htmlFor={id}
label={'label' in restProps ? restProps.label : undefined}
disabled={disabled}
Expand Down

0 comments on commit aa43dfb

Please sign in to comment.