Navigation: Implement Keyboard Navigation (#41618)

* Navigation: Start creating new NavBarMenu component

* Navigation: Apply new NavBarMenu to NavBarNext

* Navigation: Remove everything to do with .sidemenu-open--xs

* Navigation: Ensure search is passed to NavBarMenu

* Navigation: Standardise NavBarMenuItem

* This extra check isn't needed anymore

* Navigation: Refactor <li> out of NavBarMenu

* Navigation: Combine NavBarMenuItem with DropdownChild

* use spread syntax since performance shouldn't be a concern for such small arrays

* Improve active item logic

* Ensure unique keys

* Remove this duplicate code

* Add unit tests for getActiveItem

* Add tests for NavBarMenu

* Rename mobileMenuOpen -> menuOpen in NavBarNext (since it can be used for mobile menu or megamenu)

* just use index to key the items

* Use exact versions of @react-aria packages

* Navigation: Make the dropdown header a NavBarMenuItem

* Navigation: Stop using dropdown-menu for styles

* Navigation: Add react-aria relevant packages

* Navigation: Refactor NavBarDropdown to support react aria

* Navigation: apply keyboard navigation to NavBar component

* Navigation: UseHover hook for triggering submenu on navbar

* Navigation: rename testMenu component to NavBarItemButton

* WIP

* some hacks

* Refactor: clean up keybinding events

* Navigation:  render subtitle on item menu and disable it

* Navigation: Adds react-aria types (#42113)

* Refactor: refactor out to NavBarItemWithoutMenu

* Refactor: cleaning up stuff

* Refactor: comment out unused code

* Chore: Removes section and uses items only

* Chore: fix NavBarNext

* Chore: adds tests

* Refactor: minimize props api

* Refactor: various refactors

* Refactor: rename enableAllItems

* Refactor: remove unused code

* Refactor: fix clicking on menuitems

* Refactor: use recommended onAction instead

* Navigation: Fix a11y issues on NavBar

* Navigation: Fix a11y navBar Next

* Navigation: Remove unnecessary label prop, use link.text instead

* Apply suggestions from code review

Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>

* Apply unit tests suggestions from code review

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update react-aria/menu package to latest version and apply PR suggestion

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Maria Alexandra
2021-12-06 00:58:17 -05:00
committed by GitHub
parent bf744698a1
commit e468fcf518
18 changed files with 911 additions and 401 deletions

View File

@@ -1,55 +1,144 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
import NavBarItem from './NavBarItem';
import NavBarItem, { Props } from './NavBarItem';
import userEvent from '@testing-library/user-event';
const onClickMock = jest.fn();
const defaults: Props = {
children: undefined,
link: {
text: 'Parent Node',
onClick: onClickMock,
children: [
{ text: 'Child Node 1', onClick: onClickMock, children: [] },
{ text: 'Child Node 2', onClick: onClickMock, children: [] },
],
},
};
function getTestContext(overrides: Partial<Props> = {}) {
jest.clearAllMocks();
const props = { ...defaults, ...overrides };
const { rerender } = render(
<BrowserRouter>
<NavBarItem {...props}>{props.children}</NavBarItem>
</BrowserRouter>
);
return { rerender };
}
describe('NavBarItem', () => {
it('renders the children', () => {
const mockLabel = 'Hello';
render(
<BrowserRouter>
<NavBarItem label={mockLabel}>
<div data-testid="mockChild" />
</NavBarItem>
</BrowserRouter>
);
describe('when url property is not set', () => {
it('then it renders the menu trigger as a button', () => {
getTestContext();
const child = screen.getByTestId('mockChild');
expect(child).toBeInTheDocument();
expect(screen.getAllByRole('button')).toHaveLength(1);
});
describe('and clicking on the menu trigger button', () => {
it('then the onClick handler should be called', () => {
getTestContext();
userEvent.click(screen.getByRole('button'));
expect(onClickMock).toHaveBeenCalledTimes(1);
});
});
describe('and hovering over the menu trigger button', () => {
it('then the menu items should be visible', () => {
getTestContext();
userEvent.hover(screen.getByRole('button'));
expect(screen.getByRole('menuitem', { name: 'Parent Node' })).toBeInTheDocument();
expect(screen.getByText('Child Node 1')).toBeInTheDocument();
expect(screen.getByText('Child Node 2')).toBeInTheDocument();
});
});
describe('and tabbing to the menu trigger button', () => {
it('then the menu items should be visible', () => {
getTestContext();
userEvent.tab();
expect(screen.getByText('Parent Node')).toBeInTheDocument();
expect(screen.getByText('Child Node 1')).toBeInTheDocument();
expect(screen.getByText('Child Node 2')).toBeInTheDocument();
});
});
describe('and pressing arrow right on the menu trigger button', () => {
it('then the correct menu item should receive focus', () => {
getTestContext();
userEvent.tab();
expect(screen.getAllByRole('menuitem')).toHaveLength(3);
expect(screen.getByRole('menuitem', { name: 'Parent Node' })).toHaveAttribute('tabIndex', '-1');
expect(screen.getAllByRole('menuitem')[1]).toHaveAttribute('tabIndex', '-1');
expect(screen.getAllByRole('menuitem')[2]).toHaveAttribute('tabIndex', '-1');
userEvent.keyboard('{arrowright}');
expect(screen.getAllByRole('menuitem')).toHaveLength(3);
expect(screen.getAllByRole('menuitem')[0]).toHaveAttribute('tabIndex', '0');
expect(screen.getAllByRole('menuitem')[1]).toHaveAttribute('tabIndex', '-1');
expect(screen.getAllByRole('menuitem')[2]).toHaveAttribute('tabIndex', '-1');
});
});
});
it('wraps the children in a link to the url if provided', () => {
const mockLabel = 'Hello';
const mockUrl = '/route';
render(
<BrowserRouter>
<NavBarItem label={mockLabel} url={mockUrl}>
<div data-testid="mockChild" />
</NavBarItem>
</BrowserRouter>
);
describe('when url property is set', () => {
it('then it renders the menu trigger as a link', () => {
getTestContext({ link: { ...defaults.link, url: 'https://www.grafana.com' } });
const child = screen.getByTestId('mockChild');
expect(child).toBeInTheDocument();
userEvent.click(child);
expect(window.location.pathname).toEqual(mockUrl);
});
expect(screen.getAllByRole('link')).toHaveLength(1);
expect(screen.getByRole('link')).toHaveAttribute('href', 'https://www.grafana.com');
});
it('wraps the children in an onClick if provided', () => {
const mockLabel = 'Hello';
const mockOnClick = jest.fn();
render(
<BrowserRouter>
<NavBarItem label={mockLabel} onClick={mockOnClick}>
<div data-testid="mockChild" />
</NavBarItem>
</BrowserRouter>
);
describe('and hovering over the menu trigger link', () => {
it('then the menu items should be visible', () => {
getTestContext({ link: { ...defaults.link, url: 'https://www.grafana.com' } });
const child = screen.getByTestId('mockChild');
expect(child).toBeInTheDocument();
userEvent.click(child);
expect(mockOnClick).toHaveBeenCalled();
userEvent.hover(screen.getByRole('link'));
expect(screen.getByText('Parent Node')).toBeInTheDocument();
expect(screen.getByText('Child Node 1')).toBeInTheDocument();
expect(screen.getByText('Child Node 2')).toBeInTheDocument();
});
});
describe('and tabbing to the menu trigger link', () => {
it('then the menu items should be visible', () => {
getTestContext({ link: { ...defaults.link, url: 'https://www.grafana.com' } });
userEvent.tab();
expect(screen.getByText('Parent Node')).toBeInTheDocument();
expect(screen.getByText('Child Node 1')).toBeInTheDocument();
expect(screen.getByText('Child Node 2')).toBeInTheDocument();
});
});
describe('and pressing arrow right on the menu trigger link', () => {
it('then the correct menu item should receive focus', () => {
getTestContext({ link: { ...defaults.link, url: 'https://www.grafana.com' } });
userEvent.tab();
expect(screen.getAllByRole('menuitem')).toHaveLength(3);
expect(screen.getAllByRole('menuitem')[0]).toHaveAttribute('tabIndex', '-1');
expect(screen.getAllByRole('menuitem')[1]).toHaveAttribute('tabIndex', '-1');
expect(screen.getAllByRole('menuitem')[2]).toHaveAttribute('tabIndex', '-1');
userEvent.keyboard('{arrowright}');
expect(screen.getAllByRole('menuitem')).toHaveLength(3);
expect(screen.getAllByRole('menuitem')[0]).toHaveAttribute('tabIndex', '0');
expect(screen.getAllByRole('menuitem')[1]).toHaveAttribute('tabIndex', '-1');
expect(screen.getAllByRole('menuitem')[2]).toHaveAttribute('tabIndex', '-1');
});
});
});
});