Skip to content

Commit 1a2ef16

Browse files
committed
chore(Tabs): Fix re-render bug
When tabs were clicked, I was seeing the page re-render except for the isNav tabs. Also added additional tests per CodeRabbit. Assisted-by: Cursor
1 parent 9122887 commit 1a2ef16

2 files changed

Lines changed: 101 additions & 1 deletion

File tree

packages/react-core/src/components/Tabs/Tabs.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,15 @@ class Tabs extends Component<TabsProps, TabsState> {
246246
tabContentRef: React.RefObject<any>
247247
) {
248248
const { shownKeys, initialActiveKey } = this.state;
249-
const { onSelect, defaultActiveKey } = this.props;
249+
const { onSelect, defaultActiveKey, component, isNav } = this.props;
250+
251+
if (event.currentTarget instanceof HTMLAnchorElement && event.currentTarget.hasAttribute('href')) {
252+
event.preventDefault();
253+
254+
if (canUseDOM && (component === TabsComponent.nav || isNav)) {
255+
window.history.pushState(null, '', event.currentTarget.href);
256+
}
257+
}
250258

251259
// if defaultActiveKey Tabs are uncontrolled, set new active key internally
252260
if (defaultActiveKey !== undefined) {

packages/react-core/src/components/Tabs/__tests__/Tabs.test.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,58 @@ describe('hash-based nav selection', () => {
147147
expect(screen.getByRole('tab', { name: 'Tab item 1' })).toHaveAttribute('aria-selected', 'false');
148148
});
149149

150+
test('should select the nav tab that matches the current URL hash when isNav is enabled', () => {
151+
render(
152+
<Tabs activeKey={0} onSelect={() => undefined} isNav>
153+
<Tab eventKey={0} title={<TabTitleText>Tab item 1</TabTitleText>} href="#/items/1">
154+
Tab item 1
155+
</Tab>
156+
<Tab eventKey={1} title={<TabTitleText>Tab item 2</TabTitleText>} href="#/items/2">
157+
Tab item 2
158+
</Tab>
159+
</Tabs>
160+
);
161+
162+
expect(screen.getByRole('tab', { name: 'Tab item 2' })).toHaveAttribute('aria-selected', 'true');
163+
expect(screen.getByRole('tab', { name: 'Tab item 1' })).toHaveAttribute('aria-selected', 'false');
164+
});
165+
166+
test('should fall back to activeKey when no tab href matches the current hash', () => {
167+
window.location.hash = '#/items/unknown';
168+
169+
render(
170+
<Tabs activeKey={0} onSelect={() => undefined} component="nav">
171+
<Tab eventKey={0} title={<TabTitleText>Tab item 1</TabTitleText>} href="#/items/1">
172+
Tab item 1
173+
</Tab>
174+
<Tab eventKey={1} title={<TabTitleText>Tab item 2</TabTitleText>} href="#/items/2">
175+
Tab item 2
176+
</Tab>
177+
</Tabs>
178+
);
179+
180+
expect(screen.getByRole('tab', { name: 'Tab item 1' })).toHaveAttribute('aria-selected', 'true');
181+
expect(screen.getByRole('tab', { name: 'Tab item 2' })).toHaveAttribute('aria-selected', 'false');
182+
});
183+
184+
test('should fall back to activeKey when the URL hash is empty', () => {
185+
window.location.hash = '';
186+
187+
render(
188+
<Tabs activeKey={0} onSelect={() => undefined} component="nav">
189+
<Tab eventKey={0} title={<TabTitleText>Tab item 1</TabTitleText>} href="#/items/1">
190+
Tab item 1
191+
</Tab>
192+
<Tab eventKey={1} title={<TabTitleText>Tab item 2</TabTitleText>} href="#/items/2">
193+
Tab item 2
194+
</Tab>
195+
</Tabs>
196+
);
197+
198+
expect(screen.getByRole('tab', { name: 'Tab item 1' })).toHaveAttribute('aria-selected', 'true');
199+
expect(screen.getByRole('tab', { name: 'Tab item 2' })).toHaveAttribute('aria-selected', 'false');
200+
});
201+
150202
test('should respect later controlled selections after the initial hash match', async () => {
151203
const user = userEvent.setup();
152204
const ControlledTabs = () => {
@@ -241,6 +293,46 @@ describe('hash-based nav selection', () => {
241293
expect(screen.getByRole('tab', { name: 'Tab item 1' })).toHaveAttribute('aria-selected', 'true');
242294
expect(screen.getByRole('tab', { name: 'Tab item 2' })).toHaveAttribute('aria-selected', 'false');
243295
});
296+
297+
test('should prevent default anchor navigation when selecting a tab with an href', async () => {
298+
const user = userEvent.setup();
299+
window.location.hash = '';
300+
301+
render(
302+
<Tabs activeKey={0} onSelect={() => undefined}>
303+
<Tab eventKey={0} title={<TabTitleText>Tab item 1</TabTitleText>} href="#/items/1">
304+
Tab item 1
305+
</Tab>
306+
<Tab eventKey={1} title={<TabTitleText>Tab item 2</TabTitleText>} href="#/items/2">
307+
Tab item 2
308+
</Tab>
309+
</Tabs>
310+
);
311+
312+
await user.click(screen.getByRole('tab', { name: 'Tab item 2' }));
313+
314+
expect(window.location.hash).toBe('');
315+
});
316+
317+
test('should update the URL hash when selecting a nav tab with an href', async () => {
318+
const user = userEvent.setup();
319+
window.location.hash = '';
320+
321+
render(
322+
<Tabs activeKey={0} onSelect={() => undefined} isNav>
323+
<Tab eventKey={0} title={<TabTitleText>Users</TabTitleText>} href="#users">
324+
Users
325+
</Tab>
326+
<Tab eventKey={1} title={<TabTitleText>Containers</TabTitleText>} href="#containers">
327+
Containers
328+
</Tab>
329+
</Tabs>
330+
);
331+
332+
await user.click(screen.getByRole('tab', { name: 'Containers' }));
333+
334+
expect(window.location.hash).toBe('#containers');
335+
});
244336
});
245337

246338
test(`Renders with class ${styles.modifiers.initializingAccent} when uncontrolled expandable component initially mounts`, async () => {

0 commit comments

Comments
 (0)