Skip to content

Commit 4ad633a

Browse files
committed
fix(tabs): add correct aria-orientation and keyboard navigation for vertical tabs
1 parent 3737870 commit 4ad633a

File tree

3 files changed

+88
-9
lines changed

3 files changed

+88
-9
lines changed

.changeset/five-kiwis-carry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@heroui/tabs": patch
3+
---
4+
5+
Fix vertical tabs to use correct aria-orientation and support Up/Down arrow navigation for accessibility.

packages/components/tabs/__tests__/tabs.test.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,4 +539,67 @@ describe("Tabs", () => {
539539
expect(newTabButtons[2]).toHaveAttribute("aria-selected", "true");
540540
});
541541
});
542+
543+
it("should set vertical aria-orientation", () => {
544+
const wrapper = render(
545+
<Tabs isVertical aria-label="Tabs vertical test" data-testid="tabWrapper">
546+
<Tab key="item1" title="Item 1">
547+
<div>Content 1</div>
548+
</Tab>
549+
<Tab key="item2" title="Item 2">
550+
<div>Content 2</div>
551+
</Tab>
552+
<Tab key="item3" title="Item 3">
553+
<div>Content 3</div>
554+
</Tab>
555+
</Tabs>,
556+
);
557+
558+
const tablist = wrapper.getByRole("tablist");
559+
560+
expect(tablist).toHaveAttribute("aria-orientation", "vertical");
561+
});
562+
563+
it("should allow vertical tabs to navigate with up and down arrows", async () => {
564+
const wrapper = render(
565+
<Tabs isVertical aria-label="Tabs vertical test" data-testid="tabWrapper">
566+
<Tab key="item1" title="Item 1">
567+
<div>Content 1</div>
568+
</Tab>
569+
<Tab key="item2" title="Item 2">
570+
<div>Content 2</div>
571+
</Tab>
572+
<Tab key="item3" title="Item 3">
573+
<div>Content 3</div>
574+
</Tab>
575+
</Tabs>,
576+
);
577+
578+
const tab1 = wrapper.getByRole("tab", {name: "Item 1"});
579+
const tab2 = wrapper.getByRole("tab", {name: "Item 2"});
580+
const tab3 = wrapper.getByRole("tab", {name: "Item 3"});
581+
582+
expect(tab1).toHaveAttribute("aria-selected", "true");
583+
expect(tab2).toHaveAttribute("aria-selected", "false");
584+
expect(tab3).toHaveAttribute("aria-selected", "false");
585+
586+
act(() => {
587+
focus(tab1);
588+
});
589+
590+
await user.keyboard("[ArrowDown]");
591+
expect(tab1).toHaveAttribute("aria-selected", "false");
592+
expect(tab2).toHaveAttribute("aria-selected", "true");
593+
expect(tab3).toHaveAttribute("aria-selected", "false");
594+
595+
await user.keyboard("[ArrowDown]");
596+
expect(tab1).toHaveAttribute("aria-selected", "false");
597+
expect(tab2).toHaveAttribute("aria-selected", "false");
598+
expect(tab3).toHaveAttribute("aria-selected", "true");
599+
600+
await user.keyboard("[ArrowUp]");
601+
expect(tab1).toHaveAttribute("aria-selected", "false");
602+
expect(tab2).toHaveAttribute("aria-selected", "true");
603+
expect(tab3).toHaveAttribute("aria-selected", "false");
604+
});
542605
});

packages/components/tabs/src/use-tabs.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,18 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
106106
const disableAnimation =
107107
originalProps?.disableAnimation ?? globalContext?.disableAnimation ?? false;
108108

109+
const placement = (variantProps as Props).placement ?? (isVertical ? "start" : "top");
110+
const orientation = placement === "start" || placement === "end" ? "vertical" : "horizontal";
111+
109112
const state = useTabListState<T>({
110113
children: children as CollectionChildren<T>,
111114
...otherProps,
112115
});
113-
const {tabListProps} = useTabList<T>(otherProps as AriaTabListProps<T>, state, domRef);
116+
const {tabListProps} = useTabList<T>(
117+
{...(otherProps as AriaTabListProps<T>), orientation},
118+
state,
119+
domRef,
120+
);
114121

115122
const slots = useMemo(
116123
() =>
@@ -161,7 +168,6 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
161168
[baseStyles, otherProps, slots],
162169
);
163170

164-
const placement = (variantProps as Props).placement ?? (isVertical ? "start" : "top");
165171
const getWrapperProps: PropGetter = useCallback(
166172
(props) => ({
167173
"data-slot": "tabWrapper",
@@ -174,13 +180,18 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
174180
);
175181

176182
const getTabListProps: PropGetter = useCallback(
177-
(props) => ({
178-
ref: domRef,
179-
"data-slot": "tabList",
180-
className: slots.tabList({class: clsx(classNames?.tabList, props?.className)}),
181-
...mergeProps(tabListProps, props),
182-
}),
183-
[domRef, tabListProps, classNames, slots],
183+
(props) =>
184+
mergeProps(
185+
tabListProps,
186+
{
187+
ref: domRef,
188+
"data-slot": "tabList",
189+
"aria-orientation": orientation,
190+
className: slots.tabList({class: clsx(classNames?.tabList, props?.className)}),
191+
},
192+
props,
193+
),
194+
[domRef, tabListProps, classNames, slots, orientation],
184195
);
185196

186197
const getTabCursorProps: PropGetter = useCallback(

0 commit comments

Comments
 (0)