fix(tabs): vertical tabs support for accessible navigation and aria-orientation (#5924)

This commit is contained in:
Hayato Hasegawa 2025-11-22 11:56:26 +09:00 committed by GitHub
parent bc4c982609
commit 5d9a05be01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 2 deletions

View File

@ -0,0 +1,5 @@
---
"@heroui/tabs": patch
---
Fix vertical tabs to use correct aria-orientation and support Up/Down arrow navigation for accessibility. (#5810)

View File

@ -539,4 +539,63 @@ describe("Tabs", () => {
expect(newTabButtons[2]).toHaveAttribute("aria-selected", "true");
});
});
test("should have correct aria-orientation for vertical tabs", () => {
const wrapper = render(
<Tabs isVertical aria-label="Vertical tabs test">
<Tab key="item1" title="Item 1">
<div>Content 1</div>
</Tab>
<Tab key="item2" title="Item 2">
<div>Content 2</div>
</Tab>
<Tab key="item3" title="Item 3">
<div>Content 3</div>
</Tab>
</Tabs>,
);
const tablist = wrapper.getByRole("tablist");
expect(tablist).toHaveAttribute("aria-orientation", "vertical");
});
test("should navigate vertical tabs with ArrowUp and ArrowDown keys", async () => {
const wrapper = render(
<Tabs isVertical aria-label="Vertical tabs keyboard test">
<Tab key="item1" title="Item 1">
<div>Content 1</div>
</Tab>
<Tab key="item2" title="Item 2">
<div>Content 2</div>
</Tab>
<Tab key="item3" title="Item 3">
<div>Content 3</div>
</Tab>
</Tabs>,
);
const tab1 = wrapper.getByRole("tab", {name: "Item 1"});
const tab2 = wrapper.getByRole("tab", {name: "Item 2"});
const tab3 = wrapper.getByRole("tab", {name: "Item 3"});
act(() => {
focus(tab1);
});
await user.keyboard("[ArrowDown]");
expect(tab1).toHaveAttribute("aria-selected", "false");
expect(tab2).toHaveAttribute("aria-selected", "true");
expect(tab3).toHaveAttribute("aria-selected", "false");
await user.keyboard("[ArrowDown]");
expect(tab1).toHaveAttribute("aria-selected", "false");
expect(tab2).toHaveAttribute("aria-selected", "false");
expect(tab3).toHaveAttribute("aria-selected", "true");
await user.keyboard("[ArrowUp]");
expect(tab1).toHaveAttribute("aria-selected", "false");
expect(tab2).toHaveAttribute("aria-selected", "true");
expect(tab3).toHaveAttribute("aria-selected", "false");
});
});

View File

@ -106,11 +106,19 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
const disableAnimation =
originalProps?.disableAnimation ?? globalContext?.disableAnimation ?? false;
const placement = (variantProps as Props).placement ?? (isVertical ? "start" : "top");
const orientation =
isVertical || placement === "start" || placement === "end" ? "vertical" : "horizontal";
const state = useTabListState<T>({
children: children as CollectionChildren<T>,
...otherProps,
});
const {tabListProps} = useTabList<T>(otherProps as AriaTabListProps<T>, state, domRef);
const {tabListProps} = useTabList<T>(
{...otherProps, orientation} as AriaTabListProps<T>,
state,
domRef,
);
const slots = useMemo(
() =>
@ -161,7 +169,6 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
[baseStyles, otherProps, slots],
);
const placement = (variantProps as Props).placement ?? (isVertical ? "start" : "top");
const getWrapperProps: PropGetter = useCallback(
(props) => ({
"data-slot": "tabWrapper",