MultiCombobox: Fix a11y and enable storybook a11y tests (#113411)

fix multicombobox a11y and enable storybook a11y tests
This commit is contained in:
Ashley Harrison
2025-11-10 09:47:01 +00:00
committed by GitHub
parent 478ae025a3
commit 834ff1be3c
3 changed files with 123 additions and 107 deletions
-8
View File
@@ -597,14 +597,6 @@
"count": 5
}
},
"packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx": {
"no-restricted-syntax": {
"count": 1
},
"react-hooks/rules-of-hooks": {
"count": 5
}
},
"packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
@@ -1,7 +1,7 @@
import { action } from '@storybook/addon-actions';
import { useArgs, useEffect, useState } from '@storybook/preview-api';
import type { Meta, StoryFn, StoryObj } from '@storybook/react';
import { ComponentProps } from 'react';
import { ComponentProps, useId } from 'react';
import { Field } from '../Forms/Field';
@@ -17,8 +17,6 @@ const meta: Meta<typeof MultiCombobox> = {
docs: {
page: mdx,
},
// TODO fix a11y issue in story and remove this
a11y: { test: 'off' },
},
};
@@ -48,22 +46,47 @@ type ManyOptionsArgs = storyArgs & { numberOfOptions?: number };
type Story = StoryObj<typeof MultiCombobox>;
export const Basic: Story = {
args: commonArgs,
render: (args) => {
const [{ value }, setArgs] = useArgs();
const BasicStory: StoryFn<typeof MultiCombobox> = (args) => {
const [{ value }, setArgs] = useArgs();
const comboboxId = useId();
return (
return (
<Field label="Country">
<MultiCombobox
{...args}
id={comboboxId}
value={value}
onChange={(val) => {
onChangeAction(val);
setArgs({ value: val });
}}
/>
);
},
</Field>
);
};
export const Basic: Story = {
args: commonArgs,
render: BasicStory,
};
const WithInfoOptionStory: StoryFn<typeof MultiCombobox> = (args) => {
const [{ value }, setArgs] = useArgs();
const comboboxId = useId();
return (
<Field label="Country">
<MultiCombobox
{...args}
id={comboboxId}
value={value}
onChange={(val) => {
onChangeAction(val);
setArgs({ value: val });
}}
/>
</Field>
);
};
export const WithInfoOption: Story = {
@@ -75,44 +98,37 @@ export const WithInfoOption: Story = {
{ label: 'Can’t find your country? Select “Other” or contact an admin', value: '__INFO__', infoOption: true },
],
},
render: (args) => {
const [{ value }, setArgs] = useArgs();
return (
<MultiCombobox
{...args}
value={value}
onChange={(val) => {
onChangeAction(val);
setArgs({ value: val });
}}
/>
);
},
render: WithInfoOptionStory,
};
export const AutoSize: Story = {
args: { ...commonArgs, width: 'auto', minWidth: 20 },
render: (args) => {
const [{ value }, setArgs] = useArgs();
const AutoSizeStory: StoryFn<typeof MultiCombobox> = (args) => {
const [{ value }, setArgs] = useArgs();
const comboboxId = useId();
return (
return (
<Field label="Country">
<MultiCombobox
{...args}
id={comboboxId}
value={value}
onChange={(val) => {
action('onChange')(val);
setArgs({ value: val });
}}
/>
);
},
</Field>
);
};
export const AutoSize: Story = {
args: { ...commonArgs, width: 'auto', minWidth: 20 },
render: AutoSizeStory,
};
const ManyOptionsStory: StoryFn<ManyOptionsArgs> = ({ numberOfOptions = 1e4, ...args }) => {
const [dynamicArgs, setArgs] = useArgs();
const [options, setOptions] = useState<ComboboxOption[]>([]);
const comboboxId = useId();
useEffect(() => {
setTimeout(async () => {
@@ -123,15 +139,18 @@ const ManyOptionsStory: StoryFn<ManyOptionsArgs> = ({ numberOfOptions = 1e4, ...
const { onChange, ...rest } = args;
return (
<MultiCombobox
{...rest}
{...dynamicArgs}
options={options}
onChange={(opts) => {
setArgs({ value: opts });
onChangeAction(opts);
}}
/>
<Field label="Lots of options">
<MultiCombobox
{...rest}
{...dynamicArgs}
options={options}
id={comboboxId}
onChange={(opts) => {
setArgs({ value: opts });
onChangeAction(opts);
}}
/>
</Field>
);
};
@@ -146,8 +165,8 @@ export const ManyOptions: StoryObj<ManyOptionsArgs> = {
const ManyOptionsGroupedStory: StoryFn<ManyOptionsArgs> = ({ numberOfOptions = 1e5, ...args }) => {
const [dynamicArgs, setArgs] = useArgs();
const [options, setOptions] = useState<ComboboxOption[]>([]);
const comboboxId = useId();
useEffect(() => {
setTimeout(async () => {
@@ -157,15 +176,18 @@ const ManyOptionsGroupedStory: StoryFn<ManyOptionsArgs> = ({ numberOfOptions = 1
}, [numberOfOptions]);
const { onChange, ...rest } = args;
return (
<MultiCombobox
{...rest}
{...dynamicArgs}
options={options}
onChange={(opts) => {
setArgs({ value: opts });
onChangeAction(opts);
}}
/>
<Field label="Lots of options with groups">
<MultiCombobox
{...rest}
{...dynamicArgs}
id={comboboxId}
options={options}
onChange={(opts) => {
setArgs({ value: opts });
onChangeAction(opts);
}}
/>
</Field>
);
};
@@ -183,6 +205,28 @@ function loadOptionsWithLabels(inputValue: string) {
return fakeSearchAPI(`http://example.com/search?errorOnQuery=break&query=${inputValue}`);
}
const AsyncOptionsWithLabelsStory: StoryFn<typeof MultiCombobox> = (args) => {
const [dynamicArgs, setArgs] = useArgs();
const comboboxId = useId();
return (
<Field
label='Async options fn returns objects like { label: "Option 69", value: "69" }'
description="Search for 'break' to see an error"
>
<MultiCombobox
{...args}
{...dynamicArgs}
id={comboboxId}
onChange={(val) => {
onChangeAction(val);
setArgs({ value: val });
}}
/>
</Field>
);
};
export const AsyncOptionsWithLabels: Story = {
name: 'Async - options returns labels',
args: {
@@ -190,25 +234,7 @@ export const AsyncOptionsWithLabels: Story = {
value: [{ label: 'Option 69', value: '69' }],
placeholder: 'Select an option',
},
render: (args) => {
const [dynamicArgs, setArgs] = useArgs();
return (
<Field
label='Async options fn returns objects like { label: "Option 69", value: "69" }'
description="Search for 'break' to see an error"
>
<MultiCombobox
{...args}
{...dynamicArgs}
onChange={(val) => {
onChangeAction(val);
setArgs({ value: val });
}}
/>
</Field>
);
},
render: AsyncOptionsWithLabelsStory,
};
function loadOptionsOnlyValues(inputValue: string) {
@@ -218,6 +244,28 @@ function loadOptionsOnlyValues(inputValue: string) {
);
}
const AsyncOptionsWithOnlyValuesStory: StoryFn<typeof MultiCombobox> = (args) => {
const [dynamicArgs, setArgs] = useArgs();
const comboboxId = useId();
return (
<Field
label='Async options fn returns objects like { value: "69" }'
description="Search for 'break' to see an error"
>
<MultiCombobox
{...args}
{...dynamicArgs}
id={comboboxId}
onChange={(val) => {
onChangeAction(val);
setArgs({ value: val });
}}
/>
</Field>
);
};
export const AsyncOptionsWithOnlyValues: Story = {
name: 'Async - options returns only values',
args: {
@@ -225,23 +273,5 @@ export const AsyncOptionsWithOnlyValues: Story = {
value: [{ value: 'Option 69' }],
placeholder: 'Select an option',
},
render: (args) => {
const [dynamicArgs, setArgs] = useArgs();
return (
<Field
label='Async options fn returns objects like { value: "69" }'
description="Search for 'break' to see an error"
>
<MultiCombobox
{...args}
{...dynamicArgs}
onChange={(val) => {
onChangeAction(val);
setArgs({ value: val });
}}
/>
</Field>
);
},
render: AsyncOptionsWithOnlyValuesStory,
};
@@ -57,6 +57,7 @@ export const MultiCombobox = <T extends string | number>(props: MultiComboboxPro
'data-testid': dataTestId,
portalContainer,
prefixIcon,
id,
} = props;
const styles = useStyles2(getComboboxStyles);
@@ -152,17 +153,10 @@ export const MultiCombobox = <T extends string | number>(props: MultiComboboxPro
},
});
const {
getToggleButtonProps,
//getLabelProps,
isOpen,
highlightedIndex,
getMenuProps,
getInputProps,
getItemProps,
} = useCombobox({
const { isOpen, highlightedIndex, getMenuProps, getInputProps, getItemProps } = useCombobox({
items: options,
itemToString,
inputId: id,
inputValue,
selectedItem: null,
stateReducer: (state, actionAndChanges) => {
@@ -325,7 +319,7 @@ export const MultiCombobox = <T extends string | number>(props: MultiComboboxPro
})}
/>
<div className={multiStyles.suffix} ref={suffixMeasureRef} {...getToggleButtonProps()}>
<div className={multiStyles.suffix} ref={suffixMeasureRef}>
{isClearable && selectedItems.length > 0 && (
<Icon
name="times"