Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 14 additions & 2 deletions src/TabNavList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,8 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((props, ref

// ========================= Render ========================
const hasDropdown = !!hiddenTabs.length;
const hasTabList = Boolean(id);
const tabIds = hasTabList ? tabs.map(tab => `${id}-tab-${tab.key}`).join(' ') : undefined;
const wrapPrefix = `${prefixCls}-nav-wrap`;
let pingLeft: boolean;
let pingRight: boolean;
Expand All @@ -588,15 +590,25 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((props, ref
<ResizeObserver onResize={onListHolderResize}>
<div
ref={useComposeRef(ref, containerRef)}
role="tablist"
aria-orientation={tabPositionTopOrBottom ? 'horizontal' : 'vertical'}
role={hasTabList ? undefined : 'tablist'}
aria-orientation={
hasTabList ? undefined : tabPositionTopOrBottom ? 'horizontal' : 'vertical'
}
className={clsx(`${prefixCls}-nav`, className, tabsClassNames?.header)}
style={{ ...styles?.header, ...style }}
onKeyDown={() => {
// No need animation when use keyboard
doLockAnimation();
}}
>
{hasTabList && (
<div
role="tablist"
aria-orientation={tabPositionTopOrBottom ? 'horizontal' : 'vertical'}
aria-owns={tabIds || undefined}
/>
)}

<ExtraContent ref={extraLeftRef} position="left" extra={extra} prefixCls={prefixCls} />

<ResizeObserver onResize={onListHolderResize}>
Expand Down
14 changes: 10 additions & 4 deletions tests/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ exports[`Tabs.Basic Normal 1`] = `
class="rc-tabs rc-tabs-top"
>
<div
aria-orientation="horizontal"
class="rc-tabs-nav"
role="tablist"
>
<div
aria-orientation="horizontal"
aria-owns="rc-tabs-test-tab-light rc-tabs-test-tab-bamboo rc-tabs-test-tab-cute"
role="tablist"
/>
<div
class="rc-tabs-nav-wrap rc-tabs-nav-wrap-ping-right"
>
Expand Down Expand Up @@ -108,10 +111,13 @@ exports[`Tabs.Basic Skip invalidate children 1`] = `
class="rc-tabs rc-tabs-top"
>
<div
aria-orientation="horizontal"
class="rc-tabs-nav"
role="tablist"
>
<div
aria-orientation="horizontal"
aria-owns="rc-tabs-test-tab-light"
role="tablist"
/>
<div
class="rc-tabs-nav-wrap"
>
Expand Down
51 changes: 51 additions & 0 deletions tests/accessibility.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { renderToString } from 'react-dom/server';
import type { TabsProps } from '../src';
import Tabs from '../src';

Expand Down Expand Up @@ -35,6 +36,56 @@ describe('Tabs.Accessibility', () => {
/>
);

it('should keep tablist semantics during server rendering', () => {
const html = renderToString(createTabs());

expect(html).toContain('role="tablist"');
expect(html).toContain('aria-orientation="horizontal"');
});

it('should keep auxiliary controls outside the semantic tablist', () => {
const { getByRole, getAllByRole } = render(
createTabs({
tabBarExtraContent: <button type="button">Extra action</button>,
editable: {
onEdit: jest.fn(),
},
}),
);

const tablist = getByRole('tablist');
const tabs = getAllByRole('tab');

expect(tablist).toBeEmptyDOMElement();
expect(tablist).toHaveAttribute('aria-owns', tabs.map(tab => tab.id).join(' '));
expect(tablist).not.toContainElement(getByRole('button', { name: 'Extra action' }));
getAllByRole('button', { name: 'Add tab' }).forEach(button => {
expect(tablist).not.toContainElement(button);
});
getAllByRole('button', { name: 'remove' }).forEach(button => {
expect(tablist).not.toContainElement(button);
});
});

it('should keep auxiliary controls outside an empty semantic tablist', () => {
const { getByRole, getAllByRole } = render(
<Tabs
items={[]}
tabBarExtraContent={<button type="button">Extra action</button>}
editable={{ onEdit: jest.fn() }}
/>,
);

const tablist = getByRole('tablist');

expect(tablist).toBeEmptyDOMElement();
expect(tablist).not.toHaveAttribute('aria-owns');
expect(tablist).not.toContainElement(getByRole('button', { name: 'Extra action' }));
getAllByRole('button', { name: 'Add tab' }).forEach(button => {
expect(tablist).not.toContainElement(button);
});
});

it('should support keyboard navigation', async () => {
const user = userEvent.setup();
const { getByRole } = render(createTabs());
Expand Down