Skip to content

Commit

Permalink
feat(ChipsInputBase): add cycle navigates between chips (#6395)
Browse files Browse the repository at this point in the history
h2. Описание

- Добавил цикличную навигацию стрелками между чипами.

    https://github.com/VKCOM/VKUI/assets/5850354/a849e56c-9912-4cd3-802a-97f224d9f03b

- Покрыл тестами `ChipsSelect`, `ChipsInputBase`, `Chip`.
- `Chip`
  - теперь принимает `readOnly`. При его передаче, скрывается кнопка удаления чипа;
  - для корректного произношения скринридером, добавил `&nbsp;` перед скрытым текстом `"Удалить <children>"`.
  • Loading branch information
inomdzhon authored Jan 23, 2024
1 parent 3dd1fcb commit 43e7905
Show file tree
Hide file tree
Showing 12 changed files with 663 additions and 287 deletions.
2 changes: 1 addition & 1 deletion packages/vkui/src/components/ChipsInput/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const Example = () => {
<FormItem htmlFor="color" top="Цвет (контролируемый компонент)">
<ChipsInput
id="color"
inputLabel="Введите цвета"
placeholder="Введите цвета"
after={
<IconButton hoverMode="opacity" label="Очистить поле" onClick={onClick}>
<Icon16Clear />
Expand Down
1 change: 1 addition & 0 deletions packages/vkui/src/components/ChipsInput/useChipsInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const useChipsInput = <O extends ChipOption>({
);

const clearInput = React.useCallback(() => {
/* istanbul ignore if */
if (!inputRef.current) {
return;
}
Expand Down
47 changes: 43 additions & 4 deletions packages/vkui/src/components/ChipsInputBase/Chip/Chip.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { baselineComponent, userEvent } from '../../../testing/utils';
import { act, render, screen } from '@testing-library/react';
import { baselineComponent, fakeTimers, userEvent } from '../../../testing/utils';
import { Chip } from './Chip';

describe('Chip', () => {
describe(Chip, () => {
baselineComponent(Chip, {
// TODO [a11y]: "Certain ARIA roles must be contained by particular parents (aria-required-parent)"
// https://dequeuniversity.com/rules/axe/4.5/aria-required-parent?application=axeAPI
Expand All @@ -12,6 +12,8 @@ describe('Chip', () => {
a11y: false,
});

fakeTimers();

it('removes chip on onRemove click', async () => {
const onRemove = jest.fn();

Expand All @@ -21,8 +23,45 @@ describe('Chip', () => {
</Chip>,
);

await userEvent.click(screen.getByRole('button'));
await act(() => userEvent.click(screen.getByRole('button')));

expect(onRemove).toHaveBeenCalled();
});

it('hides remove button if readOnly', async () => {
const result = render(<Chip value="white">Белый</Chip>);

expect(screen.getByRole('button')).toBeTruthy();

result.rerender(
<Chip value="white" readOnly>
Белый
</Chip>,
);
expect(() => screen.getByRole('button')).toThrow();
});

it.each([{ readOnly: false }, { readOnly: true }])(
'calls user events (`readOnly` prop is `$readOnly`)',
async ({ readOnly }) => {
const onFocus = jest.fn();
const onBlur = jest.fn();
render(
<Chip
value="white"
readOnly={readOnly}
data-testid="input"
tabIndex={0}
onFocus={onFocus}
onBlur={onBlur}
/>,
);

await act(() => userEvent.tab());
await act(() => userEvent.tab({ shift: true }));

expect(onFocus).toHaveBeenCalled();
expect(onBlur).toHaveBeenCalled();
},
);
});
10 changes: 6 additions & 4 deletions packages/vkui/src/components/ChipsInputBase/Chip/Chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const Chip = ({
before,
after,
disabled,
readOnly,
children,
className,
onFocus: onFocusProp,
Expand All @@ -38,17 +39,17 @@ export const Chip = ({
const focusVisibleClassName = useFocusVisibleClassName({ focusVisible });

const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
onFocus(event);
if (onFocusProp) {
onFocusProp(event);
}
onFocus(event);
};

const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
onBlur(event);
if (onBlurProp) {
onBlurProp(event);
}
onBlur(event);
};

const onRemoveWrapper = React.useCallback(
Expand All @@ -68,6 +69,7 @@ export const Chip = ({
focusVisibleClassName,
className,
)}
aria-readonly={readOnly}
aria-disabled={disabled}
onFocus={disabled ? undefined : handleFocus}
onBlur={disabled ? undefined : handleBlur}
Expand All @@ -77,7 +79,7 @@ export const Chip = ({
<Footnote className={styles['Chip__content']}>{children}</Footnote>
{hasReactNode(after) && <div className={styles['Chip__after']}>{after}</div>}
</div>
{removable && (
{!readOnly && removable && (
<div className={styles['Chip__removable']}>
<button
tabIndex={-1} // [reason]: чтобы можно было выставлять состояние фокуса только программно через `*.focus()`
Expand All @@ -86,7 +88,7 @@ export const Chip = ({
onClick={disabled ? undefined : onRemoveWrapper}
>
<VisuallyHidden>
{removeLabel} {children}
&nbsp; {removeLabel} {children}
</VisuallyHidden>
<Icon16Cancel />
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
appearance: none;
}

.ChipsInputBase__el:focus {
.ChipsInputBase__el:not(:read-only):focus {
min-inline-size: 64px;
}

Expand Down
Loading

0 comments on commit 43e7905

Please sign in to comment.