Accessibility: Add `Skip to content` link (#68065)

* user essentials mob! 🔱

lastFile:public/app/core/components/AppChrome/AppChrome.tsx

* user essentials mob! 🔱

lastFile:public/app/core/components/AppChrome/AppChrome.test.tsx

* only show skiplink when page has app chrome

---------

Co-authored-by: Joao Silva <joao.silva@grafana.com>
pull/68111/head
Ashley Harrison 2 years ago committed by GitHub
parent a650ddfecd
commit a7cbb72664
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 27
      public/app/core/components/AppChrome/AppChrome.test.tsx
  2. 49
      public/app/core/components/AppChrome/AppChrome.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(<Page navId="child1">Children</Page>);
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(<Page navId="child1">Children</Page>);
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(<Page navId="child1">Children</Page>);
context.chrome.update({
chromeless: true,
});
waitFor(() => {
expect(screen.queryByRole('link', { name: 'Skip to main content' })).not.toBeInTheDocument();
});
});
});

@ -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 (
<main className={classNames('main-view', searchBarHidden && 'main-view--search-bar-hidden')}>
<div className={classNames('main-view', searchBarHidden && 'main-view--search-bar-hidden')}>
{!state.chromeless && (
<div className={cx(styles.topNav)}>
{!searchBarHidden && <TopSearchBar />}
<NavToolbar
searchBarHidden={searchBarHidden}
sectionNav={state.sectionNav.node}
pageNav={state.pageNav}
actions={state.actions}
onToggleSearchBar={chrome.onToggleSearchBar}
onToggleMegaMenu={chrome.onToggleMegaMenu}
onToggleKioskMode={chrome.onToggleKioskMode}
/>
</div>
<>
<LinkButton className={styles.skipLink} href="#pageContent">
Skip to main content
</LinkButton>
<div className={cx(styles.topNav)}>
{!searchBarHidden && <TopSearchBar />}
<NavToolbar
searchBarHidden={searchBarHidden}
sectionNav={state.sectionNav.node}
pageNav={state.pageNav}
actions={state.actions}
onToggleSearchBar={chrome.onToggleSearchBar}
onToggleMegaMenu={chrome.onToggleMegaMenu}
onToggleKioskMode={chrome.onToggleKioskMode}
/>
</div>
</>
)}
<div className={contentClass}>
<main className={contentClass} id="pageContent">
<div className={styles.panes}>
{state.layout === PageLayoutType.Standard && state.sectionNav && <SectionNav model={state.sectionNav} />}
<div className={styles.pageContainer}>{children}</div>
</div>
</div>
</main>
{!state.chromeless && (
<>
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
<CommandPalette />
</>
)}
</main>
</div>
);
}
@ -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,
},
}),
};
};

Loading…
Cancel
Save