From 0da5e37bc7d3001d5844166c1bbf4c46cdc49b7f Mon Sep 17 00:00:00 2001 From: Nikhil Tomar <63502271+2nikhiltom@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:14:31 +0530 Subject: [PATCH] fix: adds arrow keys navigation functionality in the Switcher, and increases test coverage (#18115) * test: test coverage for switcher * fix: fixed arrowkey navigation in switcher, adds test * test: adds tests --- .../react/src/components/UIShell/Switcher.tsx | 14 +- .../UIShell/__tests__/Switcher-test.js | 159 ++++++++++++++++++ 2 files changed, 170 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/UIShell/Switcher.tsx b/packages/react/src/components/UIShell/Switcher.tsx index 9efb642aba53..319c7f7aae2c 100644 --- a/packages/react/src/components/UIShell/Switcher.tsx +++ b/packages/react/src/components/UIShell/Switcher.tsx @@ -81,7 +81,11 @@ const Switcher = forwardRef( }) => { const enabledIndices = React.Children.toArray(children).reduce( (acc, curr, i) => { - if (Object.keys((curr as any).props).length !== 0) { + if ( + React.isValidElement(curr) && + Object.keys((curr as any).props).length !== 0 && + getDisplayName(curr.type) === 'SwitcherItem' + ) { acc.push(i); } return acc; @@ -97,7 +101,11 @@ const Switcher = forwardRef( if (direction === -1) { return enabledIndices[enabledIndices.length - 1]; } - return 0; + return enabledIndices[0]; + case 0: + if (direction === 1) { + return enabledIndices[1]; + } default: return enabledIndices[nextIndex]; } @@ -116,7 +124,7 @@ const Switcher = forwardRef( if ( React.isValidElement(child) && child.type && - getDisplayName(child.type) === 'Switcher' + getDisplayName(child.type) === 'SwitcherItem' ) { return React.cloneElement(child as React.ReactElement, { handleSwitcherItemFocus, diff --git a/packages/react/src/components/UIShell/__tests__/Switcher-test.js b/packages/react/src/components/UIShell/__tests__/Switcher-test.js index aabd6dcff28a..0d2e779ff2e9 100644 --- a/packages/react/src/components/UIShell/__tests__/Switcher-test.js +++ b/packages/react/src/components/UIShell/__tests__/Switcher-test.js @@ -8,6 +8,8 @@ import React from 'react'; import Switcher from '../Switcher'; import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import HeaderPanel from '../HeaderPanel'; import SwitcherItem from '../SwitcherItem'; describe('Switcher', () => { @@ -65,5 +67,162 @@ describe('Switcher', () => { expect(container.firstChild).toHaveClass('custom-class'); }); + it('should correctly merge refs', () => { + const ref1 = React.createRef(); + render( + + Item 1 + Item 2 + + ); + + expect(ref1.current).not.toBeNull(); + expect(ref1.current.tagName).toBe('UL'); + }); + it('should apply aria attributes correctly', () => { + render( + + Item + + ); + + const switcher = screen.getByRole('list'); + expect(switcher).toHaveAttribute('aria-label', 'test-aria-label'); + expect(switcher).toHaveAttribute('aria-labelledby', 'test-labelledby'); + }); + }); + + describe('Switcher navigation and focus management', () => { + const renderSwitcher = () => { + return ( + + + Item 1 + + + Item 2 + + + Item 3 + + + ); + }; + + it('should focus the next valid index when moving forward', async () => { + render(renderSwitcher()); + const items = screen.getAllByRole('listitem'); + const firstLink = items[0].querySelector('a'); + const secondLink = items[1].querySelector('a'); + + await userEvent.keyboard('{Tab}'); + expect(document.activeElement).toBe(firstLink); + await userEvent.keyboard('{Tab}'); + + expect(document.activeElement).toBe(secondLink); + }); + + it('should focus the next valid index when moving backword', async () => { + render(renderSwitcher()); + + const items = screen.getAllByRole('listitem'); + const firstLink = items[0].querySelector('a'); + const secondLink = items[1].querySelector('a'); + + await userEvent.keyboard('{Tab}'); + expect(document.activeElement).toBe(firstLink); + await userEvent.keyboard('Shift+Tab'); + expect(document.activeElement).toBe(firstLink); + }); + it('should focus next SwitcherItem when pressing ArrowDown from first item', async () => { + render(renderSwitcher()); + const focusableItems = screen.getAllByRole('link'); + expect(focusableItems).toHaveLength(3); + + await userEvent.keyboard('{Tab}'); + expect(document.activeElement).toBe(focusableItems[0]); + + await userEvent.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(focusableItems[1]); + }); + it('should focus previous SwitcherItem when pressing ArrowUp from last item', async () => { + render(renderSwitcher()); + const focusableItems = screen.getAllByRole('link'); + expect(focusableItems).toHaveLength(3); + + focusableItems[2].focus(); + expect(document.activeElement).toBe(focusableItems[2]); + + await userEvent.keyboard('{ArrowUp}'); + expect(document.activeElement).toBe(focusableItems[1]); + }); + + it('should wrap to first item when pressing ArrowDown from last SwitcherItem', async () => { + render(renderSwitcher()); + const focusableItems = screen.getAllByRole('link'); + expect(focusableItems).toHaveLength(3); + + focusableItems[2].focus(); + expect(document.activeElement).toBe(focusableItems[2]); + + await userEvent.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(focusableItems[0]); + }); + + it('should wrap to last item when pressing ArrowUp from first SwitcherItem', async () => { + render(renderSwitcher()); + const focusableItems = screen.getAllByRole('link'); + expect(focusableItems).toHaveLength(3); + + focusableItems[0].focus(); + expect(document.activeElement).toBe(focusableItems[0]); + + await userEvent.keyboard('{ArrowUp}'); + expect(document.activeElement).toBe(focusableItems[2]); + expect(document.activeElement).toHaveTextContent('Item 3'); + }); + it('should skip non SwitcherItem elements', async () => { + render(renderSwitcher()); + const focusableItems = screen.getAllByRole('link'); + expect(focusableItems).toHaveLength(3); + + focusableItems[0].focus(); + expect(document.activeElement).toBe(focusableItems[0]); + expect(document.activeElement).toHaveTextContent('Item 1'); + + await userEvent.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(focusableItems[1]); + expect(document.activeElement).toHaveTextContent('Item 2'); + + await userEvent.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(focusableItems[2]); + expect(document.activeElement).toHaveTextContent('Item 3'); + }); + it('should handle keyboard navigation with mixed child types', async () => { + render( + + + Item 1 + +
Non-focusable div
+ + Item 2 + + + + Nested Item + + +
+ ); + const items = screen.getAllByRole('listitem'); + const secondItem = items[2].querySelector('a'); + secondItem?.focus(); + expect(document.activeElement).toBe(secondItem); + await userEvent.keyboard('{ArrowDown}'); + expect(document.activeElement).toHaveTextContent('Nested Item'); + }); }); });