mirror of https://github.com/grafana/grafana.git
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>
This commit is contained in:
parent
a650ddfecd
commit
a7cbb72664
|
|
@ -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…
Reference in New Issue