mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user