From a7cbb72664575ee62279f0a8e4c25577cbd05936 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 9 May 2023 15:22:23 +0100 Subject: [PATCH] Accessibility: Add `Skip to content` link (#68065) * user essentials mob! :trident: lastFile:public/app/core/components/AppChrome/AppChrome.tsx * user essentials mob! :trident: lastFile:public/app/core/components/AppChrome/AppChrome.test.tsx * only show skiplink when page has app chrome --------- Co-authored-by: Joao Silva --- .../components/AppChrome/AppChrome.test.tsx | 27 +++++++++- .../core/components/AppChrome/AppChrome.tsx | 49 ++++++++++++------- 2 files changed, 58 insertions(+), 18 deletions(-) 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, + }, + }), }; };