mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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 { KBarProvider } from 'kbar';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
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 Child1' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('tab', { name: 'Tab pageNav 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 React, { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
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 { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
import { CommandPalette } from 'app/features/commandPalette/CommandPalette';
|
import { CommandPalette } from 'app/features/commandPalette/CommandPalette';
|
||||||
import { KioskMode } from 'app/types';
|
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.
|
// doesn't get re-mounted when chromeless goes from true to false.
|
||||||
|
|
||||||
return (
|
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 && (
|
{!state.chromeless && (
|
||||||
<div className={cx(styles.topNav)}>
|
<>
|
||||||
{!searchBarHidden && <TopSearchBar />}
|
<LinkButton className={styles.skipLink} href="#pageContent">
|
||||||
<NavToolbar
|
Skip to main content
|
||||||
searchBarHidden={searchBarHidden}
|
</LinkButton>
|
||||||
sectionNav={state.sectionNav.node}
|
<div className={cx(styles.topNav)}>
|
||||||
pageNav={state.pageNav}
|
{!searchBarHidden && <TopSearchBar />}
|
||||||
actions={state.actions}
|
<NavToolbar
|
||||||
onToggleSearchBar={chrome.onToggleSearchBar}
|
searchBarHidden={searchBarHidden}
|
||||||
onToggleMegaMenu={chrome.onToggleMegaMenu}
|
sectionNav={state.sectionNav.node}
|
||||||
onToggleKioskMode={chrome.onToggleKioskMode}
|
pageNav={state.pageNav}
|
||||||
/>
|
actions={state.actions}
|
||||||
</div>
|
onToggleSearchBar={chrome.onToggleSearchBar}
|
||||||
|
onToggleMegaMenu={chrome.onToggleMegaMenu}
|
||||||
|
onToggleKioskMode={chrome.onToggleKioskMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<div className={contentClass}>
|
<main className={contentClass} id="pageContent">
|
||||||
<div className={styles.panes}>
|
<div className={styles.panes}>
|
||||||
{state.layout === PageLayoutType.Standard && state.sectionNav && <SectionNav model={state.sectionNav} />}
|
{state.layout === PageLayoutType.Standard && state.sectionNav && <SectionNav model={state.sectionNav} />}
|
||||||
<div className={styles.pageContainer}>{children}</div>
|
<div className={styles.pageContainer}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
{!state.chromeless && (
|
{!state.chromeless && (
|
||||||
<>
|
<>
|
||||||
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
|
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,5 +117,15 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
}),
|
}),
|
||||||
|
skipLink: css({
|
||||||
|
position: 'absolute',
|
||||||
|
top: -1000,
|
||||||
|
|
||||||
|
':focus': {
|
||||||
|
left: theme.spacing(1),
|
||||||
|
top: theme.spacing(1),
|
||||||
|
zIndex: theme.zIndex.portal,
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user