Skip to content

Commit 81d51d4

Browse files
authored
Merge branch 'main' into 18097-findlast
2 parents 54dcb69 + 0da5e37 commit 81d51d4

File tree

2 files changed

+170
-3
lines changed

2 files changed

+170
-3
lines changed

packages/react/src/components/UIShell/Switcher.tsx

+11-3
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ const Switcher = forwardRef<HTMLUListElement, SwitcherProps>(
8181
}) => {
8282
const enabledIndices = React.Children.toArray(children).reduce<number[]>(
8383
(acc, curr, i) => {
84-
if (Object.keys((curr as any).props).length !== 0) {
84+
if (
85+
React.isValidElement(curr) &&
86+
Object.keys((curr as any).props).length !== 0 &&
87+
getDisplayName(curr.type) === 'SwitcherItem'
88+
) {
8589
acc.push(i);
8690
}
8791
return acc;
@@ -97,7 +101,11 @@ const Switcher = forwardRef<HTMLUListElement, SwitcherProps>(
97101
if (direction === -1) {
98102
return enabledIndices[enabledIndices.length - 1];
99103
}
100-
return 0;
104+
return enabledIndices[0];
105+
case 0:
106+
if (direction === 1) {
107+
return enabledIndices[1];
108+
}
101109
default:
102110
return enabledIndices[nextIndex];
103111
}
@@ -116,7 +124,7 @@ const Switcher = forwardRef<HTMLUListElement, SwitcherProps>(
116124
if (
117125
React.isValidElement(child) &&
118126
child.type &&
119-
getDisplayName(child.type) === 'Switcher'
127+
getDisplayName(child.type) === 'SwitcherItem'
120128
) {
121129
return React.cloneElement(child as React.ReactElement<any>, {
122130
handleSwitcherItemFocus,

packages/react/src/components/UIShell/__tests__/Switcher-test.js

+159
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import React from 'react';
99
import Switcher from '../Switcher';
1010
import { render, screen } from '@testing-library/react';
11+
import userEvent from '@testing-library/user-event';
12+
import HeaderPanel from '../HeaderPanel';
1113
import SwitcherItem from '../SwitcherItem';
1214

1315
describe('Switcher', () => {
@@ -65,5 +67,162 @@ describe('Switcher', () => {
6567

6668
expect(container.firstChild).toHaveClass('custom-class');
6769
});
70+
it('should correctly merge refs', () => {
71+
const ref1 = React.createRef();
72+
render(
73+
<Switcher ref={ref1} aria-label="test-label">
74+
<SwitcherItem aria-label="test-item">Item 1</SwitcherItem>
75+
<SwitcherItem aria-label="test-item">Item 2</SwitcherItem>
76+
</Switcher>
77+
);
78+
79+
expect(ref1.current).not.toBeNull();
80+
expect(ref1.current.tagName).toBe('UL');
81+
});
82+
it('should apply aria attributes correctly', () => {
83+
render(
84+
<Switcher
85+
aria-label="test-aria-label"
86+
aria-labelledby="test-labelledby">
87+
<SwitcherItem aria-label="item">Item</SwitcherItem>
88+
</Switcher>
89+
);
90+
91+
const switcher = screen.getByRole('list');
92+
expect(switcher).toHaveAttribute('aria-label', 'test-aria-label');
93+
expect(switcher).toHaveAttribute('aria-labelledby', 'test-labelledby');
94+
});
95+
});
96+
97+
describe('Switcher navigation and focus management', () => {
98+
const renderSwitcher = () => {
99+
return (
100+
<Switcher aria-label="test-switcher" expanded>
101+
<SwitcherItem aria-label="test-1" href="#">
102+
Item 1
103+
</SwitcherItem>
104+
<SwitcherItem aria-label="test-2" href="#">
105+
Item 2
106+
</SwitcherItem>
107+
<SwitcherItem aria-label="test-3" href="#">
108+
Item 3
109+
</SwitcherItem>
110+
</Switcher>
111+
);
112+
};
113+
114+
it('should focus the next valid index when moving forward', async () => {
115+
render(renderSwitcher());
116+
const items = screen.getAllByRole('listitem');
117+
const firstLink = items[0].querySelector('a');
118+
const secondLink = items[1].querySelector('a');
119+
120+
await userEvent.keyboard('{Tab}');
121+
expect(document.activeElement).toBe(firstLink);
122+
await userEvent.keyboard('{Tab}');
123+
124+
expect(document.activeElement).toBe(secondLink);
125+
});
126+
127+
it('should focus the next valid index when moving backword', async () => {
128+
render(renderSwitcher());
129+
130+
const items = screen.getAllByRole('listitem');
131+
const firstLink = items[0].querySelector('a');
132+
const secondLink = items[1].querySelector('a');
133+
134+
await userEvent.keyboard('{Tab}');
135+
expect(document.activeElement).toBe(firstLink);
136+
await userEvent.keyboard('Shift+Tab');
137+
expect(document.activeElement).toBe(firstLink);
138+
});
139+
it('should focus next SwitcherItem when pressing ArrowDown from first item', async () => {
140+
render(renderSwitcher());
141+
const focusableItems = screen.getAllByRole('link');
142+
expect(focusableItems).toHaveLength(3);
143+
144+
await userEvent.keyboard('{Tab}');
145+
expect(document.activeElement).toBe(focusableItems[0]);
146+
147+
await userEvent.keyboard('{ArrowDown}');
148+
expect(document.activeElement).toBe(focusableItems[1]);
149+
});
150+
it('should focus previous SwitcherItem when pressing ArrowUp from last item', async () => {
151+
render(renderSwitcher());
152+
const focusableItems = screen.getAllByRole('link');
153+
expect(focusableItems).toHaveLength(3);
154+
155+
focusableItems[2].focus();
156+
expect(document.activeElement).toBe(focusableItems[2]);
157+
158+
await userEvent.keyboard('{ArrowUp}');
159+
expect(document.activeElement).toBe(focusableItems[1]);
160+
});
161+
162+
it('should wrap to first item when pressing ArrowDown from last SwitcherItem', async () => {
163+
render(renderSwitcher());
164+
const focusableItems = screen.getAllByRole('link');
165+
expect(focusableItems).toHaveLength(3);
166+
167+
focusableItems[2].focus();
168+
expect(document.activeElement).toBe(focusableItems[2]);
169+
170+
await userEvent.keyboard('{ArrowDown}');
171+
expect(document.activeElement).toBe(focusableItems[0]);
172+
});
173+
174+
it('should wrap to last item when pressing ArrowUp from first SwitcherItem', async () => {
175+
render(renderSwitcher());
176+
const focusableItems = screen.getAllByRole('link');
177+
expect(focusableItems).toHaveLength(3);
178+
179+
focusableItems[0].focus();
180+
expect(document.activeElement).toBe(focusableItems[0]);
181+
182+
await userEvent.keyboard('{ArrowUp}');
183+
expect(document.activeElement).toBe(focusableItems[2]);
184+
expect(document.activeElement).toHaveTextContent('Item 3');
185+
});
186+
it('should skip non SwitcherItem elements', async () => {
187+
render(renderSwitcher());
188+
const focusableItems = screen.getAllByRole('link');
189+
expect(focusableItems).toHaveLength(3);
190+
191+
focusableItems[0].focus();
192+
expect(document.activeElement).toBe(focusableItems[0]);
193+
expect(document.activeElement).toHaveTextContent('Item 1');
194+
195+
await userEvent.keyboard('{ArrowDown}');
196+
expect(document.activeElement).toBe(focusableItems[1]);
197+
expect(document.activeElement).toHaveTextContent('Item 2');
198+
199+
await userEvent.keyboard('{ArrowDown}');
200+
expect(document.activeElement).toBe(focusableItems[2]);
201+
expect(document.activeElement).toHaveTextContent('Item 3');
202+
});
203+
it('should handle keyboard navigation with mixed child types', async () => {
204+
render(
205+
<Switcher aria-label="test-label">
206+
<SwitcherItem aria-label="test-aria-label-switcheritem">
207+
Item 1
208+
</SwitcherItem>
209+
<div>Non-focusable div</div>
210+
<SwitcherItem aria-label="test-aria-label-switcheritem">
211+
Item 2
212+
</SwitcherItem>
213+
<Switcher aria-label="nested-switcher">
214+
<SwitcherItem aria-label="test-aria-label-switcheritem">
215+
Nested Item
216+
</SwitcherItem>
217+
</Switcher>
218+
</Switcher>
219+
);
220+
const items = screen.getAllByRole('listitem');
221+
const secondItem = items[2].querySelector('a');
222+
secondItem?.focus();
223+
expect(document.activeElement).toBe(secondItem);
224+
await userEvent.keyboard('{ArrowDown}');
225+
expect(document.activeElement).toHaveTextContent('Nested Item');
226+
});
68227
});
69228
});

0 commit comments

Comments
 (0)