Skip to content

Commit b18417d

Browse files
authored
[CORE-626] Fix Highlight State for Workspace Settings Tab (#5383)
1 parent f8787a5 commit b18417d

File tree

2 files changed

+201
-2
lines changed

2 files changed

+201
-2
lines changed

src/components/tabBars.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useUniqueId } from '@terra-ui-packages/components';
22
import _ from 'lodash/fp';
33
import PropTypes from 'prop-types';
4-
import { Fragment, useRef } from 'react';
4+
import { Fragment, useRef, useState } from 'react';
55
import { div, h, span } from 'react-hyperscript-helpers';
66
import { Clickable } from 'src/components/common';
77
import { HorizontalNavigation } from 'src/components/keyboard-nav';
@@ -58,9 +58,12 @@ const styles = {
5858
* @param props Any additional properties to add to the container menu element
5959
*/
6060
export function TabBar({ activeTab, tabNames, displayNames = {}, refresh = _.noop, getHref, getOnClick = _.noop, children, ...props }) {
61+
const [hoveredTab, setHoveredTab] = useState(null);
62+
6163
const navTab = (i, currentTab) => {
6264
const selected = currentTab === activeTab;
6365
const href = getHref(currentTab);
66+
const isHovered = hoveredTab === currentTab;
6467

6568
return span(
6669
{
@@ -76,9 +79,11 @@ export function TabBar({ activeTab, tabNames, displayNames = {}, refresh = _.noo
7679
Clickable,
7780
{
7881
style: { ...Style.tabBar.tab, ...(selected ? Style.tabBar.active : {}) },
79-
hover: selected ? {} : Style.tabBar.hover,
82+
hover: isHovered ? Style.tabBar.hover : {},
8083
onClick: href === window.location.hash ? refresh : getOnClick(currentTab),
8184
href,
85+
onMouseEnter: () => setHoveredTab(currentTab),
86+
onMouseLeave: () => setHoveredTab(null),
8287
},
8388
[
8489
div(

src/components/tabBars.test.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { fireEvent } from '@testing-library/react';
2+
import { h } from 'react-hyperscript-helpers';
3+
import { TabBar } from 'src/components/tabBars';
4+
import { renderWithAppContexts as render } from 'src/testing/test-utils';
5+
6+
describe('TabBar', () => {
7+
const defaultProps = {
8+
'aria-label': 'Test navigation',
9+
activeTab: 'tab1',
10+
tabNames: ['tab1', 'tab2', 'tab3'],
11+
getHref: (tab) => `#${tab}`,
12+
};
13+
14+
it('renders with basic props', () => {
15+
// Arrange
16+
// (defaultProps defined above)
17+
18+
// Act
19+
const { getByRole, getByText } = render(h(TabBar, defaultProps));
20+
21+
// Assert
22+
expect(getByRole('navigation')).toBeInTheDocument();
23+
expect(getByRole('menu')).toBeInTheDocument();
24+
expect(getByText('tab1')).toBeInTheDocument();
25+
expect(getByText('tab2')).toBeInTheDocument();
26+
expect(getByText('tab3')).toBeInTheDocument();
27+
});
28+
29+
it('displays custom display names when provided', () => {
30+
// Arrange
31+
const props = {
32+
...defaultProps,
33+
displayNames: { tab1: 'First Tab', tab2: 'Second Tab' },
34+
};
35+
36+
// Act
37+
const { getByText, queryByText } = render(h(TabBar, props));
38+
39+
// Assert
40+
expect(getByText('First Tab')).toBeInTheDocument();
41+
expect(getByText('Second Tab')).toBeInTheDocument();
42+
expect(getByText('tab3')).toBeInTheDocument();
43+
expect(queryByText('tab1')).not.toBeInTheDocument();
44+
});
45+
46+
it('marks the active tab correctly', () => {
47+
// Arrange
48+
// (defaultProps defined above)
49+
50+
// Act
51+
const { getByText } = render(h(TabBar, defaultProps));
52+
53+
// Assert
54+
const activeTab = getByText('tab1').closest('[role="menuitem"]');
55+
expect(activeTab).toHaveAttribute('aria-current', 'location');
56+
57+
const inactiveTab = getByText('tab2').closest('[role="menuitem"]');
58+
expect(inactiveTab).not.toHaveAttribute('aria-current');
59+
});
60+
61+
it('sets aria attributes correctly', () => {
62+
// Arrange
63+
// (defaultProps defined above)
64+
65+
// Act
66+
const { getAllByRole } = render(h(TabBar, defaultProps));
67+
68+
// Assert
69+
const menuItems = getAllByRole('menuitem');
70+
expect(menuItems).toHaveLength(3);
71+
72+
menuItems.forEach((item, index) => {
73+
expect(item).toHaveAttribute('aria-setsize', '3');
74+
expect(item).toHaveAttribute('aria-posinset', String(index + 1));
75+
});
76+
});
77+
78+
it('calls refresh when clicking active tab with matching href', () => {
79+
// Arrange
80+
const mockRefresh = jest.fn();
81+
Object.defineProperty(window, 'location', {
82+
value: { hash: '#tab1' },
83+
writable: true,
84+
});
85+
86+
const props = {
87+
...defaultProps,
88+
refresh: mockRefresh,
89+
};
90+
91+
const { getByText } = render(h(TabBar, props));
92+
93+
// Act
94+
fireEvent.click(getByText('tab1'));
95+
96+
// Assert
97+
expect(mockRefresh).toHaveBeenCalled();
98+
});
99+
100+
it('calls getOnClick when clicking inactive tab', () => {
101+
// Arrange
102+
const mockGetOnClick = jest.fn();
103+
104+
const props = {
105+
...defaultProps,
106+
getOnClick: mockGetOnClick,
107+
};
108+
109+
const { getByText } = render(h(TabBar, props));
110+
111+
// Act
112+
fireEvent.click(getByText('tab2'));
113+
114+
// Assert
115+
expect(mockGetOnClick).toHaveBeenCalledWith('tab2');
116+
});
117+
118+
it('handles hover state correctly', () => {
119+
// Arrange
120+
// (defaultProps defined above)
121+
122+
const { getByText } = render(h(TabBar, defaultProps));
123+
const tab2 = getByText('tab2');
124+
125+
// Act
126+
fireEvent.mouseEnter(tab2);
127+
fireEvent.mouseLeave(tab2);
128+
129+
// Assert
130+
// Note: Hover state testing would require style inspection
131+
expect(tab2).toBeInTheDocument();
132+
});
133+
134+
it('passes through additional props to menu element', () => {
135+
// Arrange
136+
const props = {
137+
...defaultProps,
138+
'data-testid': 'custom-menu',
139+
};
140+
141+
// Act
142+
const { getByTestId } = render(h(TabBar, props));
143+
144+
// Assert
145+
expect(getByTestId('custom-menu')).toBeInTheDocument();
146+
});
147+
148+
it('handles empty displayNames gracefully', () => {
149+
// Arrange
150+
const props = {
151+
...defaultProps,
152+
displayNames: {},
153+
};
154+
155+
// Act
156+
const { getByText } = render(h(TabBar, props));
157+
158+
// Assert
159+
expect(getByText('tab1')).toBeInTheDocument();
160+
expect(getByText('tab2')).toBeInTheDocument();
161+
expect(getByText('tab3')).toBeInTheDocument();
162+
});
163+
164+
it('handles undefined activeTab', () => {
165+
// Arrange
166+
const props = {
167+
...defaultProps,
168+
activeTab: undefined,
169+
};
170+
171+
// Act
172+
const { getAllByRole } = render(h(TabBar, props));
173+
174+
// Assert
175+
const menuItems = getAllByRole('menuitem');
176+
menuItems.forEach((item) => {
177+
expect(item).not.toHaveAttribute('aria-current');
178+
});
179+
});
180+
181+
it('supports aria-labelledby', () => {
182+
// Arrange
183+
const props = {
184+
...defaultProps,
185+
'aria-labelledby': 'external-label',
186+
};
187+
188+
// Act
189+
const { getByRole } = render(h(TabBar, props));
190+
191+
// Assert
192+
expect(getByRole('navigation')).toHaveAttribute('aria-labelledby', 'external-label');
193+
});
194+
});

0 commit comments

Comments
 (0)