Skip to content

Commit

Permalink
fix(a11y): ChipsSelect and ChipsInput
Browse files Browse the repository at this point in the history
  • Loading branch information
inomdzhon committed Jan 16, 2024
1 parent 90afa54 commit 1303529
Show file tree
Hide file tree
Showing 19 changed files with 309 additions and 238 deletions.
10 changes: 9 additions & 1 deletion packages/vkui/src/components/ChipsInput/ChipsInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants';
import type { ChipOption } from '../ChipsInputBase/types';
import { FormItem } from '../FormItem/FormItem';
import { ChipsInput, ChipsInputProps } from './ChipsInput';

const story: Meta<ChipsInputProps<ChipOption>> = {
Expand All @@ -13,4 +15,10 @@ export default story;

type Story = StoryObj<ChipsInputProps<ChipOption>>;

export const Playground: Story = {};
export const Playground: Story = {
render: (args) => (
<FormItem top="Добавьте любимые теги" htmlFor="chips-input" style={{ width: 320 }}>
<ChipsInput {...args} id="chips-input" />
</FormItem>
),
};
12 changes: 6 additions & 6 deletions packages/vkui/src/components/ChipsInput/useChipsInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getOptionLabelDefault,
getOptionValueDefault,
} from '../ChipsInputBase/constants';
import { isValueLikeChipOptionObject } from '../ChipsInputBase/helpers';
import type {
ChipOption,
ChipOptionValue,
Expand All @@ -17,9 +18,6 @@ import type {
UseChipsInputBaseProps,
} from '../ChipsInputBase/types';

const isValueLikeOption = <O extends ChipOption>(value: O | ChipOptionValue): value is O =>
typeof value === 'object' && 'value' in value;

export const transformValue = <O extends ChipOption>(
value: O[],
getOptionValue: GetOptionValue<O>,
Expand Down Expand Up @@ -87,14 +85,16 @@ export const useChipsInput = <O extends ChipOption>({
const toggleOption: ToggleOption<O> = React.useCallback(
(nextValueProp: O | ChipOptionValue, isNewValue: boolean) => {
setValue((prevValue) => {
const isLikeOption = isValueLikeOption(nextValueProp);
const resolvedOption = isLikeOption
const isLikeObjectOption = isValueLikeChipOptionObject(nextValueProp);
const resolvedOption = isLikeObjectOption
? getNewOptionData(nextValueProp.value, nextValueProp.label)
: getNewOptionData(nextValueProp, typeof nextValueProp === 'string' ? nextValueProp : '');
const nextValue = prevValue.filter((option: O) => resolvedOption.value !== option.value);

if (isNewValue === true) {
nextValue.push(isLikeOption ? { ...nextValueProp, ...resolvedOption } : resolvedOption);
nextValue.push(
isLikeObjectOption ? { ...nextValueProp, ...resolvedOption } : resolvedOption,
);
}

return nextValue;
Expand Down
1 change: 0 additions & 1 deletion packages/vkui/src/components/ChipsInputBase/Chip/Chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export const Chip = ({
focusVisibleClassName,
className,
)}
tabIndex={-1} // [reason]: чтобы можно было выставлять состояние фокуса только программно через `*.focus()`
aria-disabled={disabled}
onFocus={disabled ? undefined : handleFocus}
onBlur={disabled ? undefined : handleBlur}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,12 @@
margin: 2px;
}

.ChipsInputBase__label {
display: flex;
justify-content: center;
flex-direction: column;
flex: 1;
margin-block: 2px 2px;
margin-inline: 10px 2px;
}

.ChipsInputBase__el {
flex: 1;
position: relative;
inline-size: 100%;
margin-block-end: 2px;
margin-block: 2px 4px;
margin-inline: 10px 2px;
padding: 0;
color: var(--vkui--color_text_primary);
background: transparent;
Expand Down Expand Up @@ -66,13 +59,10 @@
cursor: default;
}

.ChipsInputBase--hasPlaceholder .ChipsInputBase__label {
margin-inline: calc(12px - var(--vkui_internal--chips_input_base_container_gap)) 0;
}

.ChipsInputBase--hasPlaceholder .ChipsInputBase__el {
white-space: nowrap;
text-overflow: ellipsis;
margin-inline: calc(12px - var(--vkui_internal--chips_input_base_container_gap)) 0;
}

/**
Expand Down
86 changes: 61 additions & 25 deletions packages/vkui/src/components/ChipsInputBase/ChipsInputBase.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { baselineComponent, userEvent } from '../../testing/utils';
import { baselineComponent, userEvent, withRexExp } from '../../testing/utils';
import { ChipsInputBase } from './ChipsInputBase';
import type { ChipOption, ChipsInputBasePrivateProps } from './types';

const ChipsInputBaseTest = (props: ChipsInputBasePrivateProps) => (
<ChipsInputBase data-testid="chips-input" {...props} />
);
const ChipsInputBaseTest = ({
inputValue: inputValueProp,
...restProps
}: ChipsInputBasePrivateProps) => {
const [inputValue, setInputValue] = React.useState(inputValueProp);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
return (
<ChipsInputBase
data-testid="chips-input"
{...restProps}
inputValue={inputValue}
onInputChange={handleInputChange}
/>
);
};

const testOption = { value: 'red', label: 'Красный' };
const chipsInputValue: ChipOption[] = [testOption];
const TEST_OPTION = { value: 'red', label: 'Красный' };
const chipsInputValue: ChipOption[] = [TEST_OPTION];

describe('ChipsInputBase', () => {
baselineComponent(ChipsInputBase, {
baselineComponent(ChipsInputBaseTest, {
// доступность должна быть реализована в обёртках над ChipsInputBase
a11y: false,
});
Expand Down Expand Up @@ -55,30 +69,39 @@ describe('ChipsInputBase', () => {
expect(onAddChipOption).toHaveBeenCalledWith('Красный');
});

it('removes chip on hitting backspace', async () => {
it('focuses to chip on hitting backspace', async () => {
const result = render(
<ChipsInputBaseTest
value={chipsInputValue}
inputValue="0"
onAddChipOption={onAddChipOption}
onRemoveChipOption={onRemoveChipOption}
/>,
);
await userEvent.type(result.getByTestId('chips-input'), '{backspace}');
expect(onRemoveChipOption).toHaveBeenCalledWith(testOption);
const chipsInputLocator = result.getByTestId('chips-input');
await userEvent.type(chipsInputLocator, '{backspace}');
expect(chipsInputLocator).toHaveFocus();
await userEvent.type(chipsInputLocator, '{backspace}');
expect(chipsInputLocator.previousSibling).toHaveFocus();
});

it('does not delete chips on hitting backspace in readonly mode', async () => {
const result = render(
<ChipsInputBaseTest
readOnly
value={chipsInputValue}
onAddChipOption={onAddChipOption}
onRemoveChipOption={onRemoveChipOption}
/>,
);
await userEvent.type(result.getByTestId('chips-input'), '{backspace}');
expect(onRemoveChipOption).not.toHaveBeenCalled();
});
it.each(['delete', 'backspace'])(
'does not delete chips on hitting "%s" key in readonly mode',
async (type) => {
const result = render(
<ChipsInputBaseTest
readOnly
value={chipsInputValue}
onAddChipOption={onAddChipOption}
onRemoveChipOption={onRemoveChipOption}
/>,
);
const chipEl = result.getByRole('option', { name: withRexExp(TEST_OPTION.label) });
await userEvent.click(chipEl);
await userEvent.type(chipEl, `{${type}}`);
expect(onRemoveChipOption).not.toHaveBeenCalled();
},
);

it('focuses ChipsInputBase on surrounding container click', async () => {
const result = render(
Expand All @@ -92,18 +115,31 @@ describe('ChipsInputBase', () => {
expect(result.getByTestId('chips-input')).toHaveFocus();
});

it('focuses ChipsInputBase on chip click', async () => {
it('focuses on chip after click', async () => {
const result = render(
<ChipsInputBaseTest
value={chipsInputValue}
onAddChipOption={onAddChipOption}
onRemoveChipOption={onRemoveChipOption}
/>,
);
await userEvent.click(result.queryByText('Красный')!);
expect(result.getByTestId('chips-input')).toHaveFocus();
const chipEl = result.getByRole('option', { name: withRexExp(TEST_OPTION.label) });
await userEvent.click(chipEl);
expect(chipEl).toHaveFocus();
});

it.todo('focuses on input field after removing only one chip');

it.todo('focuses on nearest chip after removing one of chip');

it.todo('focuses on last focused chip after focus to component');

it.todo('focuses on last focused chip after enter to component with hitting "tab" key');

it.todo('focuses on last focused chip after hitting "shift + tab" key');

it.todo('navigates between chip with arrow buttons');

it('add value on blur event if addOnBlur=true', async () => {
const result = render(
<ChipsInputBaseTest
Expand Down
Loading

0 comments on commit 1303529

Please sign in to comment.