Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

a11y: allow to pass axeConfig to baseline unit test #8145

Merged
merged 22 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d6046af
Accordion: Add full component a11y test
andrey-medvedev-vk Dec 19, 2024
69b18e7
Alert: warn about aria-label if title is not provided
andrey-medvedev-vk Dec 19, 2024
01c37c3
Alert: add tests for warn logic
andrey-medvedev-vk Dec 19, 2024
4603eab
Allow to pass jest-axe config
andrey-medvedev-vk Jan 14, 2025
77552d5
ChipsInputBase: Explicit set ignore for a11y errors
andrey-medvedev-vk Dec 19, 2024
30790d0
TabsItem: disable specific rule in config
andrey-medvedev-vk Dec 19, 2024
1084d73
CustomSelectOption: explicitely ignore rule
andrey-medvedev-vk Dec 19, 2024
f989ca7
Fix config variable name
andrey-medvedev-vk Dec 19, 2024
60434b2
CustomSelectInput: add placeholder to fix a11y in tests
andrey-medvedev-vk Jan 9, 2025
df00d65
ModalPage: explicitly disable a11y error
andrey-medvedev-vk Jan 9, 2025
1184eae
CardScroll: explicitly disable list rule check
andrey-medvedev-vk Jan 9, 2025
83e8f2b
DateRangeInput: enable a11y check
andrey-medvedev-vk Jan 9, 2025
309e022
DateInput: enable a11y check
andrey-medvedev-vk Jan 10, 2025
0425c0b
ChipsSelect: Explicit todo and ignore for a11y errors
andrey-medvedev-vk Jan 10, 2025
4229a34
ModalCard: ignore aria-dialog-name a11y error
andrey-medvedev-vk Jan 10, 2025
3c140ab
ModalRoot: remove ignore from baseline tests
andrey-medvedev-vk Jan 10, 2025
b2f8ed5
ChipsInputBase: explicit ignores
andrey-medvedev-vk Jan 10, 2025
dfd8316
Fix dialog name issue in ModalPage/ModalCard baseline tests
andrey-medvedev-vk Jan 14, 2025
43f7c96
Add notes about a11y to doc
andrey-medvedev-vk Jan 14, 2025
94c4994
Update a11y doc for Alert
andrey-medvedev-vk Jan 14, 2025
8919048
ModalRoot: remove a11y ignore
andrey-medvedev-vk Jan 14, 2025
b3881d7
Better formatting for aria-role in doc
andrey-medvedev-vk Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions packages/vkui/src/components/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { fireEvent, render } from '@testing-library/react';
import { baselineComponent, waitCSSKeyframesAnimation } from '../../testing/utils';
import { a11yTest, baselineComponent, waitCSSKeyframesAnimation } from '../../testing/utils';
import { Accordion } from './Accordion';

describe(Accordion, () => {
a11yTest(() => (
<Accordion>
<Accordion.Summary iconPosition="before" data-testid="summary">
Title
</Accordion.Summary>
<Accordion.Content data-testid="content">Content</Accordion.Content>
</Accordion>
));

baselineComponent(Accordion.Content);
baselineComponent(Accordion.Summary, { a11y: false });
baselineComponent((props) => <Accordion.Summary {...props}>Title</Accordion.Summary>);

it('toggles on click', async () => {
const result = render(
Expand Down
26 changes: 23 additions & 3 deletions packages/vkui/src/components/Alert/Alert.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Platform } from '../../lib/platform';
import {
baselineComponent,
fakeTimers,
setNodeEnv,
userEvent,
waitCSSKeyframesAnimation,
} from '../../testing/utils';
Expand All @@ -25,9 +26,28 @@ import typographyStyles from '../Typography/Typography.module.css';
describe('Alert', () => {
fakeTimers();

baselineComponent(Alert, {
// TODO [a11y]: "ARIA dialog and alertdialog nodes should have an accessible name (aria-dialog-name)"
a11y: false,
baselineComponent((props) => <Alert {...props} title="Alert title" onClose={noop} />, {});

it('shows warning if title and area attributes are not provided', () => {
setNodeEnv('development');
const warn = jest.spyOn(console, 'warn').mockImplementation(noop);

const component = render(<Alert onClose={noop} title="Alert title" />);
expect(warn).not.toHaveBeenCalled();

component.rerender(<Alert onClose={noop} aria-label="Alert title" />);
expect(warn).not.toHaveBeenCalled();

component.rerender(<Alert onClose={noop} aria-labelledby="labelId" />);
expect(warn).not.toHaveBeenCalled();

component.rerender(<Alert onClose={noop} />);

expect(warn.mock.calls[0][0]).toBe(
'%c[VKUI/Alert] Если "title" не используется, то необходимо задать либо "aria-label", либо "aria-labelledby" (см. правило axe aria-dialog-name)',
);

setNodeEnv('test');
});

describe('closes', () => {
Expand Down
16 changes: 15 additions & 1 deletion packages/vkui/src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { type UseFocusTrapProps } from '../../hooks/useFocusTrap';
import { usePlatform } from '../../hooks/usePlatform';
import { useCSSKeyframesAnimationController } from '../../lib/animation';
import { stopPropagation } from '../../lib/utils';
import { warnOnce } from '../../lib/warnOnce';
import type {
AlignType,
AnchorHTMLAttributesOnly,
Expand Down Expand Up @@ -81,6 +82,8 @@ export interface AlertProps
allowClickPropagation?: boolean;
}

const warn = warnOnce('Alert');

/**
* @see https://vkcom.github.io/VKUI/#/Alert
*/
Expand Down Expand Up @@ -149,6 +152,17 @@ export const Alert = ({

useScrollLock();

if (
process.env.NODE_ENV === 'development' &&
!title &&
!restProps['aria-label'] &&
!restProps['aria-labelledby']
) {
warn(
'Если "title" не используется, то необходимо задать либо "aria-label", либо "aria-labelledby" (см. правило axe aria-dialog-name)',
);
}

const handleClick = allowClickPropagation
? onClick
: (event: React.MouseEvent<HTMLElement>) => {
Expand All @@ -166,7 +180,6 @@ export const Alert = ({
getRootRef={getRootRef}
>
<FocusTrap
{...restProps}
{...animationHandlers}
onClick={handleClick}
getRootRef={elementRef}
Expand All @@ -183,6 +196,7 @@ export const Alert = ({
aria-modal
aria-labelledby={titleId}
aria-describedby={descriptionId}
{...restProps}
>
<div
className={classNames(
Expand Down
12 changes: 12 additions & 0 deletions packages/vkui/src/components/Alert/Readme.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
Начиная с VKUI v7 этот компонент можно объявить в любом месте приложения в пределах [`AppRoot`](#/AppRoot). Больше нет необходимости явно передавать его в свойство `popout` компоненту [`SplitLayout`](#/SplitLayout).

## Цифровая доступность (a11y)

`Alert` является модальным окном (`aria-role="dialog"`), а значит у него обязательно должно быть имя — его краткое название. Благодаря этому пользователи вспомогательных технологий знают, что это за элемент и какое у него содержимое.

Задать имя можно с помощью следующих способов:

- используя свойство `title`;
- используя свойство `aria-label`;
- используя свойство `aria-labelledby`;

## Типы кнопок

В Алертах особое внимание нужно уделить кнопкам. Всего есть три типа кнопок:
`cancel`, `destructive` и `default`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ const setup = ({ defaultScrollLeft = 50, cardsCount = 6 }: PrepareDataParams) =>

describe('CardScroll', () => {
baselineComponent(CardScroll, {
a11y: false,
a11yConfig: {
rules: {
// TODO [a11y]: "<ul> and <ol> must only directly contain <li>, <script> or <template> elements (list)"
// https://dequeuniversity.com/rules/axe/4.5/aria-required-parent?application=axeAPI
// see https://github.com/VKCOM/VKUI/issues/8135
list: { enabled: false },
},
},
});

it('check scroll by click arrow left', async () => {
Expand Down
16 changes: 15 additions & 1 deletion packages/vkui/src/components/ChipsInput/ChipsInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ import { baselineComponent, userEvent } from '../../testing/utils';
import { ChipsInput } from './ChipsInput';

describe(ChipsInput, () => {
baselineComponent(ChipsInput, { a11y: false });
baselineComponent(ChipsInput, {
a11yConfig: {
rules: {
// TODO: listbox не имеет label/title/labelledby
// https://dequeuniversity.com/rules/axe/4.9/aria-input-field-name?application=axeAPI
'aria-input-field-name': { enabled: false },
// TODO: combobox is not allowed as children of listbox
// https://dequeuniversity.com/rules/axe/4.9/aria-required-children?application=axeAPI
'aria-required-children': { enabled: false },
// TODO: real input has no assiciated label
// https://dequeuniversity.com/rules/axe/4.9/label?application=axeAPI
'label': { enabled: false },
},
},
});

it('check reset form event', async () => {
const onChange = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,38 @@ const BLUE_OPTION = { value: 'blue', label: 'Синий' };
const YELLOW_OPTION = { value: 'yellow', label: 'Жёлтый' };

describe(ChipsInputBase, () => {
baselineComponent(ChipsInputBaseTest, {
// доступность должна быть реализована в обёртках над ChipsInputBase
a11y: false,
});

const onAddChipOption = jest.fn();
const onRemoveChipOption = jest.fn();
const onClearOptions = jest.fn();

baselineComponent(
(props) => (
<ChipsInputBaseTest
onAddChipOption={onAddChipOption}
onRemoveChipOption={onRemoveChipOption}
onClear={onClearOptions}
value={[RED_OPTION]}
{...props}
/>
),
{
a11yConfig: {
rules: {
'nested-interactive': { enabled: false },
// TODO: real input has no assiciated label
// https://dequeuniversity.com/rules/axe/4.9/label?application=axeAPI
'label': { enabled: false },
// TODO: listbox не имеет label/title/labelledby
// https://dequeuniversity.com/rules/axe/4.9/aria-input-field-name?application=axeAPI
'aria-input-field-name': { enabled: false },
// TODO: combobox is not allowed as children of listbox
// https://dequeuniversity.com/rules/axe/4.9/aria-required-children?application=axeAPI
'aria-required-children': { enabled: false },
},
},
},
);

beforeEach(() => {
onAddChipOption.mockClear();
onRemoveChipOption.mockClear();
Expand Down Expand Up @@ -408,6 +431,6 @@ describe(ChipsInputBase, () => {
expect(screen.getByTestId('clear-button')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('clear-button'));

expect(onClearOptions).toBeCalledTimes(1);
expect(onClearOptions).toHaveBeenCalledTimes(1);
});
});
16 changes: 15 additions & 1 deletion packages/vkui/src/components/ChipsSelect/ChipsSelect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,21 @@ describe('ChipsSelect', () => {
afterEach(() => {
placementStub = undefined;
});
baselineComponent(ChipsSelect, { a11y: false });
baselineComponent(ChipsSelect, {
a11yConfig: {
rules: {
// TODO: listbox не имеет label/title/labelledby
// https://dequeuniversity.com/rules/axe/4.9/aria-input-field-name?application=axeAPI
'aria-input-field-name': { enabled: false },
// TODO: combobox is not allowed as children of listbox
// https://dequeuniversity.com/rules/axe/4.9/aria-required-children?application=axeAPI
'aria-required-children': { enabled: false },
// TODO: real input has no assiciated label
// https://dequeuniversity.com/rules/axe/4.9/label?application=axeAPI
'label': { enabled: false },
},
},
});

fakeTimers();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import { CustomSelectInput, type CustomSelectInputProps } from './CustomSelectIn
import styles from './CustomSelectInput.module.css';

describe(CustomSelectInput, () => {
baselineComponent(CustomSelectInput, {
a11y: false,
});
baselineComponent((props) => <CustomSelectInput {...props} placeholder="Select label" />);

it.each<{ props: Partial<CustomSelectInputProps>; className: string }>([
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ describe('CustomSelectOption', () => {
baselineComponent(
(props) => <CustomSelectOption {...props}>CustomSelectOption</CustomSelectOption>,
{
// 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
a11y: false,
a11yConfig: {
rules: {
// 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
'aria-required-parent': { enabled: false },
},
},
},
);

Expand Down
13 changes: 8 additions & 5 deletions packages/vkui/src/components/DateInput/DateInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { format, subDays } from 'date-fns';
import { baselineComponent, userEvent } from '../../testing/utils';
Expand Down Expand Up @@ -30,11 +31,13 @@ const convertInputsToNumbers = (inputs: HTMLElement[]) => {
};

describe('DateInput', () => {
baselineComponent(DateInput, {
// TODO [a11y]: "Elements must only use allowed ARIA attributes (aria-allowed-attr)"
// https://dequeuniversity.com/rules/axe/4.5/aria-allowed-attr?application=axeAPI
a11y: false,
});
baselineComponent((props) => (
<React.Fragment>
<label htmlFor="date-input">Date range</label>
<DateInput {...props} id="date-input" />
</React.Fragment>
));

it('should be correct input value', () => {
const onChange = jest.fn();
render(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { addDays, format } from 'date-fns';
import { baselineComponent, userEvent } from '../../testing/utils';
Expand Down Expand Up @@ -37,11 +38,12 @@ const convertInputsToNumbers = (inputs: HTMLElement[]) => {
};

describe('DateRangeInput', () => {
baselineComponent(DateRangeInput, {
// TODO [a11y]: "Elements must only use allowed ARIA attributes (aria-allowed-attr)"
// https://dequeuniversity.com/rules/axe/4.5/aria-allowed-attr?application=axeAPI
a11y: false,
});
baselineComponent((props) => (
<React.Fragment>
<label htmlFor="range-input">Date range</label>
<DateRangeInput {...props} id="range-input" />
</React.Fragment>
));

it('should be correct input value', () => {
render(
Expand Down
11 changes: 6 additions & 5 deletions packages/vkui/src/components/ModalCard/ModalCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { baselineComponent, waitCSSTransitionEnd } from '../../testing/utils';
import { Button } from '../Button/Button';
import { ConfigProvider } from '../ConfigProvider/ConfigProvider';
import { ModalPageHeader } from '../ModalPageHeader/ModalPageHeader';
import { ModalCard } from './ModalCard';
import { type ModalCardProps } from './types';

Expand All @@ -14,11 +15,11 @@ export const waitModalCardCSSTransitionEnd = async (el: HTMLElement) =>
* Большинство логики покрыто в `ModalRoot.test.tsx`
*/
describe(ModalCard, () => {
baselineComponent((p) => <ModalCard open nav="id" {...p} />, {
// TODO [a11y]: "ARIA dialog and alertdialog nodes should have an accessible name (aria-dialog-name)"
// https://dequeuniversity.com/rules/axe/4.5/aria-dialog-name?application=axeAPI
a11y: false,
});
baselineComponent((p) => (
<ModalCard open nav="id" {...p}>
<ModalPageHeader>Title</ModalPageHeader>
</ModalCard>
));

test('mount and unmount', async () => {
const result = render(<ModalCard id="host" data-testid="host" />);
Expand Down
11 changes: 11 additions & 0 deletions packages/vkui/src/components/ModalCard/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ const Example = () => {

# Спецификация

## Цифровая доступность (a11y)

`ModalCard` является модальным окном (`aria-role="dialog"`), а значит у него обязательно должно быть имя — его краткое название. Благодаря этому пользователи вспомогательных технологий знают, что это за элемент и какое у него содержимое.

Задать имя можно с помощью следующих способов:

- используя свойство `title`;
- используя свойство `aria-label`;
- используя свойство `aria-labelledby`;
- используя внутри компонент [ModalPageHeader](#/ModalPageHeader). Этот компонент сам свяжется с `ModalCard` через контекст c помощью `aria-labelledby`.

## Анатомия

Вёрстку и параметры `ModalCard` наследует от [ModalCardBase](#/ModalCardBase). Также используется компоновка из следующих внутренних
Expand Down
10 changes: 9 additions & 1 deletion packages/vkui/src/components/ModalCardBase/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,15 @@

## Цифровая доступность (a11y)

Чтобы кнопка для закрытия была доступной для ассистивных технологий, мы передаем в нее скрытый визуально текст, который сможет прочитать скринридер. Чтобы заменить текст, передайте его в `dismissLabel`.
- Если`ModalCardBase` является модальным окном, то ему надо добавить аттрибут `aria-role="dialog"`. Также у него обязательно должно быть имя — его краткое название. Благодаря этому пользователи вспомогательных технологий знают, что это за элемент и какое у него содержимое.

Задать имя можно с помощью следующих способов:

- используя свойство `title`;
- используя свойство `aria-label`;
- используя свойство `aria-labelledby`;

- Чтобы кнопка для закрытия была доступной для ассистивных технологий, мы передаем в нее скрытый визуально текст, который сможет прочитать скринридер. Чтобы заменить текст, передайте его в `dismissLabel`.

```jsx { "props": { "layout": false, "iframe": false } }
<div style={{ margin: 20 }}>
Expand Down
Loading
Loading