Dashboard: Add accessible landmark markup (#36753)

* Add landmark markup

* Make panel titles h2

* Descibe panel landmarks

* Add nav elements and heading 1

* Reset line height and remove margin

* Make focus styles visible

* Change nav to section

* Add desc

* Fix focus styles cutoff
This commit is contained in:
Tobias Skarhed 2021-07-16 21:48:47 +02:00 committed by GitHub
parent 2172920095
commit cfd06775c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 80 additions and 56 deletions

View File

@ -37,9 +37,9 @@ export const Components = {
}, },
Panels: { Panels: {
Panel: { Panel: {
title: (title: string) => `Panel header title item ${title}`, title: (title: string) => `Panel header ${title}`,
headerItems: (item: string) => `Panel header item ${item}`, headerItems: (item: string) => `Panel header item ${item}`,
containerByTitle: (title: string) => `Panel container title ${title}`, containerByTitle: (title: string) => `${title} panel`,
headerCornerInfo: (mode: string) => `Panel header ${mode}`, headerCornerInfo: (mode: string) => `Panel header ${mode}`,
}, },
Visualization: { Visualization: {
@ -157,13 +157,13 @@ export const Components = {
}, },
PageToolbar: { PageToolbar: {
container: () => '.page-toolbar', container: () => '.page-toolbar',
item: (tooltip: string) => `Page toolbar button ${tooltip}`, item: (tooltip: string) => `${tooltip}`,
}, },
QueryEditorToolbarItem: { QueryEditorToolbarItem: {
button: (title: string) => `QueryEditor toolbar item button ${title}`, button: (title: string) => `QueryEditor toolbar item button ${title}`,
}, },
BackButton: { BackButton: {
backArrow: 'Go Back button', backArrow: 'Go Back',
}, },
OptionsGroup: { OptionsGroup: {
group: (title?: string) => (title ? `Options group ${title}` : 'Options group'), group: (title?: string) => (title ? `Options group ${title}` : 'Options group'),

View File

@ -63,6 +63,7 @@ export const PageToolbar: FC<Props> = React.memo(
/> />
</div> </div>
)} )}
<nav className={styles.navElement}>
{parent && parentHref && ( {parent && parentHref && (
<> <>
<Link className={cx(styles.titleText, styles.parentLink, styles.titleLink)} href={parentHref}> <Link className={cx(styles.titleText, styles.parentLink, styles.titleLink)} href={parentHref}>
@ -76,11 +77,14 @@ export const PageToolbar: FC<Props> = React.memo(
</> </>
)} )}
{titleHref && ( {titleHref && (
<h1 className={styles.h1Styles}>
<Link className={cx(styles.titleText, styles.titleLink)} href={titleHref}> <Link className={cx(styles.titleText, styles.titleLink)} href={titleHref}>
{title} {title}
</Link> </Link>
</h1>
)} )}
{!titleHref && <div className={styles.titleText}>{title}</div>} {!titleHref && <h1 className={styles.titleText}>{title}</h1>}
</nav>
{leftItems?.map((child, index) => ( {leftItems?.map((child, index) => (
<div className={styles.leftActionItem} key={index}> <div className={styles.leftActionItem} key={index}>
{child} {child}
@ -147,6 +151,14 @@ const getStyles = (theme: GrafanaTheme2) => {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
`, `,
navElement: css`
display: flex;
`,
h1Styles: css`
margin: 0;
line-height: inherit;
display: flex;
`,
parentIcon: css` parentIcon: css`
margin-left: ${theme.spacing(0.5)}; margin-left: ${theme.spacing(0.5)};
`, `,

View File

@ -97,7 +97,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
<div className="grafana-app"> <div className="grafana-app">
<Router history={locationService.getHistory()}> <Router history={locationService.getHistory()}>
<SideMenu /> <SideMenu />
<div className="main-view"> <main className="main-view">
<div <div
ref={this.container} ref={this.container}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@ -110,7 +110,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
{bodyRenderHooks.map((Hook, index) => ( {bodyRenderHooks.map((Hook, index) => (
<Hook key={index.toString()} /> <Hook key={index.toString()} />
))} ))}
</div> </main>
</Router> </Router>
</div> </div>
<LiveConnectionWarning /> <LiveConnectionWarning />

View File

@ -24,7 +24,7 @@ export const SideMenu: FC = React.memo(() => {
} }
return ( return (
<div className="sidemenu" data-testid="sidemenu"> <nav className="sidemenu" data-testid="sidemenu">
<a href={homeUrl} className="sidemenu__logo" key="logo"> <a href={homeUrl} className="sidemenu__logo" key="logo">
<Branding.MenuLogo /> <Branding.MenuLogo />
</a> </a>
@ -37,7 +37,7 @@ export const SideMenu: FC = React.memo(() => {
</div> </div>
<TopSection key="topsection" /> <TopSection key="topsection" />
<BottomSection key="bottomsection" /> <BottomSection key="bottomsection" />
</div> </nav>
); );
}); });

View File

@ -114,9 +114,10 @@ class DashNav extends PureComponent<Props> {
} }
if (canStar) { if (canStar) {
let desc = isStarred ? 'Unmark as favorite' : 'Mark as favorite';
buttons.push( buttons.push(
<DashNavButton <DashNavButton
tooltip="Mark as favorite" tooltip={desc}
icon={isStarred ? 'favorite' : 'star'} icon={isStarred ? 'favorite' : 'star'}
iconType={isStarred ? 'mono' : 'default'} iconType={isStarred ? 'mono' : 'default'}
iconSize="lg" iconSize="lg"
@ -127,11 +128,12 @@ class DashNav extends PureComponent<Props> {
} }
if (canShare) { if (canShare) {
let desc = 'Share dashboard or panel';
buttons.push( buttons.push(
<ModalsController key="button-share"> <ModalsController key="button-share">
{({ showModal, hideModal }) => ( {({ showModal, hideModal }) => (
<DashNavButton <DashNavButton
tooltip="Share dashboard or panel" tooltip={desc}
icon="share-alt" icon="share-alt"
iconSize="lg" iconSize="lg"
onClick={() => { onClick={() => {

View File

@ -317,7 +317,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
return ( return (
<div className={containerClassNames}> <div className={containerClassNames}>
{kioskMode !== KioskMode.Full && ( {kioskMode !== KioskMode.Full && (
<div aria-label={selectors.pages.Dashboard.DashNav.nav}> <header aria-label={selectors.pages.Dashboard.DashNav.nav}>
<DashNav <DashNav
dashboard={dashboard} dashboard={dashboard}
title={dashboard.title} title={dashboard.title}
@ -327,7 +327,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
kioskMode={kioskMode} kioskMode={kioskMode}
hideTimePicker={dashboard.timepicker.hidden} hideTimePicker={dashboard.timepicker.hidden}
/> />
</div> </header>
)} )}
<DashboardPrompt dashboard={dashboard} /> <DashboardPrompt dashboard={dashboard} />
@ -343,9 +343,9 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
<div className={styles.dashboardContent}> <div className={styles.dashboardContent}>
{initError && <DashboardFailed />} {initError && <DashboardFailed />}
{!editPanel && kioskMode === KioskMode.Off && ( {!editPanel && kioskMode === KioskMode.Off && (
<div aria-label={selectors.pages.Dashboard.SubMenu.submenu}> <section aria-label={selectors.pages.Dashboard.SubMenu.submenu}>
<SubMenu dashboard={dashboard} annotations={dashboard.annotations.list} links={dashboard.links} /> <SubMenu dashboard={dashboard} annotations={dashboard.annotations.list} links={dashboard.links} />
</div> </section>
)} )}
<DashboardGrid <DashboardGrid

View File

@ -434,7 +434,10 @@ export class PanelChrome extends Component<Props, State> {
}); });
return ( return (
<div className={containerClassNames} aria-label={selectors.components.Panels.Panel.containerByTitle(panel.title)}> <section
className={containerClassNames}
aria-label={selectors.components.Panels.Panel.containerByTitle(panel.title)}
>
<PanelHeader <PanelHeader
panel={panel} panel={panel}
dashboard={dashboard} dashboard={dashboard}
@ -456,7 +459,7 @@ export class PanelChrome extends Component<Props, State> {
return this.renderPanel(width, height); return this.renderPanel(width, height);
}} }}
</ErrorBoundary> </ErrorBoundary>
</div> </section>
); );
} }
} }

View File

@ -1,7 +1,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { cx } from '@emotion/css'; import { cx, css } from '@emotion/css';
import { DataLink, PanelData } from '@grafana/data'; import { DataLink, GrafanaTheme2, PanelData } from '@grafana/data';
import { Icon } from '@grafana/ui'; import { Icon, useStyles2 } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import PanelHeaderCorner from './PanelHeaderCorner'; import PanelHeaderCorner from './PanelHeaderCorner';
@ -30,6 +30,7 @@ export const PanelHeader: FC<Props> = ({ panel, error, isViewing, isEditing, dat
const onCancelQuery = () => panel.getQueryRunner().cancelQuery(); const onCancelQuery = () => panel.getQueryRunner().cancelQuery();
const title = panel.getDisplayTitle(); const title = panel.getDisplayTitle();
const className = cx('panel-header', !(isViewing || isEditing) ? 'grid-drag-handle' : ''); const className = cx('panel-header', !(isViewing || isEditing) ? 'grid-drag-handle' : '');
const styles = useStyles2(panelStyles);
return ( return (
<> <>
@ -57,7 +58,7 @@ export const PanelHeader: FC<Props> = ({ panel, error, isViewing, isEditing, dat
size="sm" size="sm"
/> />
) : null} ) : null}
<span className="panel-title-text">{title}</span> <h2 className={styles.titleText}>{title}</h2>
<Icon name="angle-down" className="panel-menu-toggle" /> <Icon name="angle-down" className="panel-menu-toggle" />
<PanelHeaderMenuWrapper panel={panel} dashboard={dashboard} show={panelMenuOpen} onClose={closeMenu} /> <PanelHeaderMenuWrapper panel={panel} dashboard={dashboard} show={panelMenuOpen} onClose={closeMenu} />
{data.request && data.request.timeInfo && ( {data.request && data.request.timeInfo && (
@ -73,3 +74,25 @@ export const PanelHeader: FC<Props> = ({ panel, error, isViewing, isEditing, dat
</> </>
); );
}; };
const panelStyles = (theme: GrafanaTheme2) => {
return {
titleText: css`
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: calc(100% - 38px);
cursor: pointer;
font-weight: ${theme.typography.fontWeightMedium};
font-size: ${theme.typography.body.fontSize};
margin: 0;
&:hover {
color: ${theme.colors.text.primary};
}
.panel-has-alert & {
max-width: calc(100% - 54px);
}
`,
};
};

View File

@ -33,9 +33,9 @@ export const PanelHeaderMenuTrigger: FC<Props> = ({ children, ...divProps }) =>
); );
return ( return (
<div {...divProps} className="panel-title-container" onClick={onMenuToggle} onMouseDown={onMouseDown}> <header {...divProps} className="panel-title-container" onClick={onMenuToggle} onMouseDown={onMouseDown}>
{children({ panelMenuOpen, closeMenu: () => setPanelMenuOpen(false) })} {children({ panelMenuOpen, closeMenu: () => setPanelMenuOpen(false) })}
</div> </header>
); );
}; };

View File

@ -37,22 +37,6 @@ $panel-header-no-title-zindex: 1;
align-items: center; align-items: center;
} }
.panel-title-text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: calc(100% - 38px);
cursor: pointer;
font-weight: $font-weight-semi-bold;
&:hover {
color: $link-hover-color;
}
.panel-has-alert & {
max-width: calc(100% - 54px);
}
}
.panel-menu-container { .panel-menu-container {
width: 0px; width: 0px;
height: 19px; height: 19px;