MultiCombobox: Fix a11y and enable storybook a11y tests (#113411)
fix multicombobox a11y and enable storybook a11y tests
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user