diff --git a/src/bottom-navigation/__tests__/bottom-navigation.test.tsx b/src/bottom-navigation/__tests__/bottom-navigation.test.tsx
index 4709b19ca4..ec7f49d630 100644
--- a/src/bottom-navigation/__tests__/bottom-navigation.test.tsx
+++ b/src/bottom-navigation/__tests__/bottom-navigation.test.tsx
@@ -26,4 +26,34 @@ describe('BottomNavigation', () => {
const moreTab = getByText('More').closest('[role="tab"]');
expect(moreTab).toHaveAttribute('aria-selected', 'true');
});
+
+ it('labels each tabpanel with the id of the tab that controls it', () => {
+ const items = Array.from({ length: 6 }).map((_, idx) => (
+
+ {`panel ${idx}`}
+
+ ));
+
+ const { getByText, getAllByRole } = render(
+ {items}
+ );
+
+ const panels = getAllByRole('tabpanel', { hidden: true });
+ const tabIds = getAllByRole('tab').map((tab) => tab.id);
+
+ // Items 0-3 each have their own tab; item 4 (and beyond) is only reachable
+ // via the "More" overflow tab, so its panel must be labelled by that tab.
+ panels.slice(0, 4).forEach((panel, idx) => {
+ expect(panel).toHaveAttribute('aria-labelledby', tabIds[idx]);
+ });
+ const moreTabId = getByText('More').closest('[role="tab"]')?.id;
+ expect(panels[4]).toHaveAttribute('aria-labelledby', moreTabId);
+
+ // Every referenced id must resolve to an element that actually exists.
+ panels.forEach((panel) => {
+ const labelledBy = panel.getAttribute('aria-labelledby');
+ expect(labelledBy).toBeTruthy();
+ expect(document.getElementById(labelledBy as string)).not.toBeNull();
+ });
+ });
});
diff --git a/src/bottom-navigation/bottom-navigation.tsx b/src/bottom-navigation/bottom-navigation.tsx
index 8a94a40e0b..0fa1517ce4 100644
--- a/src/bottom-navigation/bottom-navigation.tsx
+++ b/src/bottom-navigation/bottom-navigation.tsx
@@ -57,6 +57,13 @@ const BottomNavigation = ({
onChange({ activeKey });
}
+ const navItems = React.Children.toArray(children);
+
+ const getSelectorId = (idx) =>
+ navItems.length > MAX_SELECTORS && idx >= MAX_SELECTORS - 1
+ ? 'bottom-navigation-tab-overflow'
+ : `bottom-navigation-tab-${idx}`;
+
const renderSelectors = (navItems) =>
navItems.map((navItem: React.ReactElement, idx) => {
const isActive = activeKey === idx && !displayOverflow;
@@ -69,6 +76,7 @@ const BottomNavigation = ({
isActive ? scrollToTop(idx) : handleNavItemChange(idx);
}}
overrides={navItem.props.overrides}
+ id={`bottom-navigation-tab-${idx}`}
key={idx}
/>
);
@@ -81,12 +89,11 @@ const BottomNavigation = ({
onChange={() => setDisplayOverflow(true)}
isActive={displayOverflow || activeKey >= MAX_SELECTORS - 1}
overrides={{ Title: overrides.OverflowTitle, Selector: overrides.OverflowSelector }}
+ id="bottom-navigation-tab-overflow"
key={'more'}
/>
);
- const navItems = React.Children.toArray(children);
-
return (
{navItems.map((navItem: React.ReactElement, idx) => {
@@ -98,7 +105,7 @@ const BottomNavigation = ({
ref={(element) => NavItemPanelRefs.current.push(element)}
overrides={navItem.props.overrides}
key={idx}
- aria-labelledby={`tabs-${navItem.props.title}-tabpanel-${idx}`}
+ aria-labelledby={getSelectorId(idx)}
>
{navItem.props.children}
diff --git a/src/bottom-navigation/panel.tsx b/src/bottom-navigation/panel.tsx
index aabe376d18..367433bef5 100644
--- a/src/bottom-navigation/panel.tsx
+++ b/src/bottom-navigation/panel.tsx
@@ -10,18 +10,21 @@ import { StyledPanel } from './styled-components';
import { getOverrides } from '../helpers/overrides';
import type { PanelProps } from './types';
-export const Panel = React.forwardRef(({ isActive, overrides = {}, children }: PanelProps, ref) => {
- const [Panel, PanelProps] = getOverrides(overrides.Panel, StyledPanel);
- return (
-
- {children}
-
- );
-});
+export const Panel = React.forwardRef(
+ ({ isActive, overrides = {}, children, 'aria-labelledby': ariaLabelledBy }: PanelProps, ref) => {
+ const [Panel, PanelProps] = getOverrides(overrides.Panel, StyledPanel);
+ return (
+
+ {children}
+
+ );
+ }
+);
Panel.displayName = 'Panel';
diff --git a/src/bottom-navigation/selector.tsx b/src/bottom-navigation/selector.tsx
index 75461621af..8abaa71ace 100644
--- a/src/bottom-navigation/selector.tsx
+++ b/src/bottom-navigation/selector.tsx
@@ -11,14 +11,21 @@ import { useStyletron } from '../styles/index';
import { getOverrides } from '../helpers/overrides';
import type { SelectorProps } from './types';
-export const Selector = ({ title, icon, isActive, onChange, overrides = {} }: SelectorProps) => {
+export const Selector = ({
+ title,
+ icon,
+ isActive,
+ onChange,
+ overrides = {},
+ id,
+}: SelectorProps) => {
const [Title, TitleProps] = getOverrides(overrides.Title, StyledTitle);
const [Selector, SelectorProps] = getOverrides(overrides.Selector, StyledSelector);
const [, theme] = useStyletron();
const Icon = icon;
return (
-
+
void;