Skip to content

Commit

Permalink
a11y: support Tabs, TabsItem accessibilty
Browse files Browse the repository at this point in the history
  • Loading branch information
k-egor-smirnov committed Sep 22, 2022
1 parent 9e2d2c8 commit 1310f86
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 4 deletions.
13 changes: 13 additions & 0 deletions src/components/Group/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Spacing } from "../Spacing/Spacing";
import { Separator } from "../Separator/Separator";
import { hasReactNode } from "../../lib/utils";
import { Caption } from "../Typography/Caption/Caption";
import { warnOnce } from "../../lib/warnOnce";
import {
withAdaptivity,
AdaptivityProps,
Expand Down Expand Up @@ -41,6 +42,8 @@ export interface GroupProps
children?: React.ReactNode;
}

const warn = warnOnce("TabsItem");

const GroupComponent = ({
header,
description,
Expand All @@ -62,6 +65,16 @@ const GroupComponent = ({
sizeX === SizeType.COMPACT || isInsideModal ? "plain" : "card";
}

if (
process.env.NODE_ENV === "development" &&
restProps.role === "tabpanel" &&
(!restProps["aria-controls"] || !restProps["id"])
) {
warn(
'При использовани роли "tabpanel" необходимо задать значение пропов "aria-controls" и "id"'
);
}

let separatorElement = null;

if (separator !== "hide") {
Expand Down
24 changes: 21 additions & 3 deletions src/components/Tabs/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,29 @@ const Example = ({ sizeX }) => {
separator={sizeX === SizeType.REGULAR}
>
<DefaultInPanel
selected={selected}
setSelected={setSelected}
menuOpened={menuOpened}
onMenuClick={(opened) => {
setMenuOpened((prevState) => (opened ? !prevState : false));
}}
/>
</PanelHeader>

{selected === "news" && (
<Group id="tab-content-news" aria-labelledby="tab-news">
<Div>Контент новостей</Div>
</Group>
)}
{selected === "recommendations" && (
<Group
id="tab-content-recommendations"
aria-labelledby="tab-recommendations"
>
<Div>Контент рекомендаций</Div>
</Group>
)}

<Scrollable />

<PanelHeaderContext
Expand Down Expand Up @@ -65,9 +81,7 @@ const Example = ({ sizeX }) => {
);
};

const DefaultInPanel = ({ menuOpened, onMenuClick }) => {
const [selected, setSelected] = React.useState("news");

const DefaultInPanel = ({ menuOpened, onMenuClick, selected, setSelected }) => {
return (
<Tabs>
<TabsItem
Expand All @@ -85,6 +99,8 @@ const DefaultInPanel = ({ menuOpened, onMenuClick }) => {
}
setSelected("news");
}}
id="tab-news"
aria-controls="tab-content-news"
>
Новости
</TabsItem>
Expand All @@ -94,6 +110,8 @@ const DefaultInPanel = ({ menuOpened, onMenuClick }) => {
onMenuClick(false);
setSelected("recommendations");
}}
id="tab-recommendations"
aria-controls="tab-content-recommendations"
>
Интересное
</TabsItem>
Expand Down
76 changes: 75 additions & 1 deletion src/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { usePlatform } from "../../hooks/usePlatform";
import { IOS, VKCOM } from "../../lib/platform";
import { withAdaptivity, AdaptivityProps } from "../../hoc/withAdaptivity";
import { warnOnce } from "../../lib/warnOnce";
import { useGlobalEventListener } from "../../hooks/useGlobalEventListener";
import { useDOM } from "../../lib/dom";
import { pressedKey } from "../../lib/accessibility";
import "./Tabs.css";

export interface TabsProps
Expand Down Expand Up @@ -41,6 +44,9 @@ const TabsComponent = ({
...restProps
}: TabsProps) => {
const platform = usePlatform();
const { document } = useDOM();

const tabsRef = React.useRef<HTMLDivElement | null>();

if (
(mode === "buttons" || mode === "segmented") &&
Expand All @@ -65,6 +71,73 @@ const TabsComponent = ({

const withGaps = mode === "accent" || mode === "secondary";

function getTabEls(): HTMLDivElement[] {
if (!tabsRef.current) {
return [];
}

return Array.from(
// eslint-disable-next-line
tabsRef.current.querySelectorAll<HTMLDivElement>("[role=tab]")
);
}

function onDocumentKeydown(e: KeyboardEvent) {
if (!document || !tabsRef.current) {
return;
}

const key = pressedKey(e);

switch (key) {
case "ArrowLeft":
case "ArrowRight":
case "End":
case "Home": {
const tabEls = getTabEls();
const currentFocusedElIndex = tabEls.findIndex(
(el) => document.activeElement === el
);
if (currentFocusedElIndex === -1) {
return;
}

let nextIndex = 0;
if (key === "Home") {
nextIndex = 0;
} else if (key === "End") {
nextIndex = tabEls.length - 1;
} else {
const offset = key === "ArrowRight" ? 1 : -1;
nextIndex = currentFocusedElIndex + offset;
}

const nextTabEl = tabEls[nextIndex];

if (nextTabEl) {
e.preventDefault();
nextTabEl.focus();
}

break;
}
case "Space":
case "Enter": {
const tabEls = getTabEls();
const currentFocusedEl = tabEls.find(
(el) => document.activeElement === el
);
if (currentFocusedEl) {
currentFocusedEl.click();
}
}
}
}

useGlobalEventListener(document, "keydown", onDocumentKeydown, {
capture: true,
});

return (
<div
{...restProps}
Expand All @@ -77,8 +150,9 @@ const TabsComponent = ({
// TODO v5.0.0 новая адаптивность
`Tabs--sizeX-${sizeX}`
)}
role="tablist"
>
<div vkuiClass="Tabs__in">
<div vkuiClass="Tabs__in" ref={(el) => (tabsRef.current = el)}>
<TabsModeContext.Provider value={{ mode, withGaps }}>
{children}
</TabsModeContext.Provider>
Expand Down
4 changes: 4 additions & 0 deletions src/components/TabsItem/Readme.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
Элемент компонента [`Tabs`](https://vkcom.github.io/VKUI/#/Tabs). Наследует `mode` из контекста `Tabs`.
## Доступность

Чтобы скриридеры понимали, каким элементом управляет `TabsItem`, ему нужно задать `id`, и ссылаться на него в управляемом элементе с помощью `aria-labeledby`<br />
Управляемый табом элемент должен содержать параметры `id` и `aria-controls`, ссылающийся на его таб.
16 changes: 16 additions & 0 deletions src/components/TabsItem/TabsItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { useAdaptivity } from "../../hooks/useAdaptivity";
import { TabsModeContext, TabsContextProps } from "../Tabs/Tabs";
import { Headline } from "../Typography/Headline/Headline";
import { Subhead } from "../Typography/Subhead/Subhead";
import { Text } from "../Typography/Text/Text";
import { warnOnce } from "../../lib/warnOnce";
import "./TabsItem.css";

export interface TabsItemProps extends React.HTMLAttributes<HTMLElement> {
Expand Down Expand Up @@ -35,6 +37,8 @@ export interface TabsItemProps extends React.HTMLAttributes<HTMLElement> {
disabled?: boolean;
}

const warn = warnOnce("TabsItem");

/**
* @see https://vkcom.github.io/VKUI/#/TabsItem
*/
Expand Down Expand Up @@ -67,6 +71,15 @@ export const TabsItem = ({
);
}

if (process.env.NODE_ENV === "development" && !restProps["aria-controls"]) {
warn(`Передайте в "aria-controls" id контролируемого блока`, "warn");
} else if (process.env.NODE_ENV === "development" && !restProps["id"]) {
warn(
`Передайте "id" компоненту для использования в "aria-labelledby" контролируемого блока`,
"warn"
);
}

return (
<Tappable
{...restProps}
Expand All @@ -83,6 +96,9 @@ export const TabsItem = ({
activeMode="TabsItem--active"
focusVisibleMode={mode === "segmented" ? "outside" : "inside"}
hasActive={mode === "segmented"}
role="tab"
aria-selected={selected}
tabIndex={selected ? 0 : -1}
>
{before && <div vkuiClass="TabsItem__before">{before}</div>}
<Headline
Expand Down
24 changes: 24 additions & 0 deletions src/lib/accessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export enum Keys {
SPACE = "Space",
TAB = "Tab",
ESCAPE = "Escape",
HOME = "Home",
END = "End",
ARROW_LEFT = "ArrowLeft",
ARROW_RIGHT = "ArrowRight",
}

interface AccessibleKey {
Expand Down Expand Up @@ -48,6 +52,26 @@ const ACCESSIBLE_KEYS: AccessibleKey[] = [
key: ["Escape"],
keyCode: 27,
},
{
code: Keys.HOME,
key: ["Home"],
keyCode: 36,
},
{
code: Keys.END,
key: ["End"],
keyCode: 35,
},
{
code: Keys.ARROW_LEFT,
key: ["ArrowLeft"],
keyCode: 37,
},
{
code: Keys.ARROW_RIGHT,
key: ["ArrowRight"],
keyCode: 39,
},
];

export function pressedKey(
Expand Down

0 comments on commit 1310f86

Please sign in to comment.