diff --git a/public/app/core/components/AppChrome/AppChrome.test.tsx b/public/app/core/components/AppChrome/AppChrome.test.tsx index 92390c093f8..a4c4a40e43b 100644 --- a/public/app/core/components/AppChrome/AppChrome.test.tsx +++ b/public/app/core/components/AppChrome/AppChrome.test.tsx @@ -1,4 +1,5 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { KBarProvider } from 'kbar'; import React, { ReactNode } from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; @@ -109,4 +110,28 @@ describe('AppChrome', () => { expect(screen.getByRole('tab', { name: 'Tab Child1' })).toBeInTheDocument(); expect(screen.getByRole('tab', { name: 'Tab pageNav child1' })).toBeInTheDocument(); }); + + it('should create a skip link to skip to main content', async () => { + setup(Children); + expect(await screen.findByRole('link', { name: 'Skip to main content' })).toBeInTheDocument(); + }); + + it('should focus the skip link on initial tab before carrying on with normal tab order', async () => { + setup(Children); + await userEvent.keyboard('{tab}'); + const skipLink = await screen.findByRole('link', { name: 'Skip to main content' }); + expect(skipLink).toHaveFocus(); + await userEvent.keyboard('{tab}'); + expect(await screen.findByRole('link', { name: 'Go to home' })).toHaveFocus(); + }); + + it('should not render a skip link if the page is chromeless', async () => { + const { context } = setup(Children); + context.chrome.update({ + chromeless: true, + }); + waitFor(() => { + expect(screen.queryByRole('link', { name: 'Skip to main content' })).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index 72489c28fa2..59939ebe768 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import React, { PropsWithChildren } from 'react'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { useStyles2, LinkButton } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { CommandPalette } from 'app/features/commandPalette/CommandPalette'; import { KioskMode } from 'app/types'; @@ -34,34 +34,39 @@ export function AppChrome({ children }: Props) { // doesn't get re-mounted when chromeless goes from true to false. return ( - + {!state.chromeless && ( - - {!searchBarHidden && } - - + <> + + Skip to main content + + + {!searchBarHidden && } + + + > )} - + {state.layout === PageLayoutType.Standard && state.sectionNav && } {children} - + {!state.chromeless && ( <> chrome.setMegaMenu(false)} /> > )} - + ); } @@ -112,5 +117,15 @@ const getStyles = (theme: GrafanaTheme2) => { flexGrow: 1, minHeight: 0, }), + skipLink: css({ + position: 'absolute', + top: -1000, + + ':focus': { + left: theme.spacing(1), + top: theme.spacing(1), + zIndex: theme.zIndex.portal, + }, + }), }; };