Navigation: Improve breadcrumb accessibility (#53471)

* refactor breadcrumbs into their own folder, add appropriate accessibility

* rename Breadcrumb to BreadcrumbItem
This commit is contained in:
Ashley Harrison
2022-08-10 09:24:21 +01:00
committed by GitHub
parent 1f8b1eef75
commit 6d495a6a8e
6 changed files with 143 additions and 91 deletions

View File

@@ -1,90 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { useStyles2, Icon, IconName, LinkButton } from '@grafana/ui';
export interface Props {
breadcrumbs: Breadcrumb[];
}
export interface Breadcrumb {
text?: string;
icon?: IconName;
href?: string;
}
export function Breadcrumbs({ breadcrumbs }: Props) {
const styles = useStyles2(getStyles);
return (
<ul className={styles.breadcrumbs}>
{breadcrumbs.map((breadcrumb, index) => (
<li className={styles.breadcrumb} key={index}>
{breadcrumb.href && breadcrumb.text && (
<a className={styles.breadcrumbLink} href={breadcrumb.href}>
{breadcrumb.text}
</a>
)}
{breadcrumb.href && breadcrumb.icon && (
<LinkButton size="md" variant="secondary" fill="text" icon={breadcrumb.icon} href={breadcrumb.href} />
)}
{!breadcrumb.href && <span className={styles.breadcrumbLink}>{breadcrumb.text}</span>}
{index + 1 < breadcrumbs.length && (
<div className={styles.separator}>
<Icon name="angle-right" />
</div>
)}
</li>
))}
</ul>
);
}
export function buildBreadcrumbs(sectionNav: NavModelItem, pageNav?: NavModelItem) {
const crumbs: Breadcrumb[] = [{ icon: 'home-alt', href: '/' }];
function addCrumbs(node: NavModelItem) {
if (node.parentItem) {
addCrumbs(node.parentItem);
}
crumbs.push({ text: node.text, href: node.url });
}
addCrumbs(sectionNav);
if (pageNav) {
addCrumbs(pageNav);
}
return crumbs;
}
const getStyles = (theme: GrafanaTheme2) => {
return {
breadcrumbs: css({
display: 'flex',
alignItems: 'center',
flexWrap: 'nowrap',
fontWeight: theme.typography.fontWeightMedium,
}),
breadcrumb: css({
display: 'flex',
alignItems: 'center',
}),
separator: css({
color: theme.colors.text.secondary,
}),
breadcrumbLink: css({
alignItems: 'center',
color: theme.colors.text.primary,
display: 'flex',
padding: theme.spacing(0, 0.5),
whiteSpace: 'nowrap',
'&:hover': {
textDecoration: 'underline',
},
}),
};
};

View File

@@ -4,7 +4,9 @@ import React from 'react';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { Icon, IconButton, ToolbarButton, useStyles2 } from '@grafana/ui';
import { Breadcrumbs, buildBreadcrumbs } from './Breadcrumbs';
import { Breadcrumbs } from '../Breadcrumbs/Breadcrumbs';
import { buildBreadcrumbs } from '../Breadcrumbs/utils';
import { NavToolbarSeparator } from './NavToolbarSeparator';
import { TOP_BAR_LEVEL_HEIGHT } from './types';

View File

@@ -0,0 +1,69 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, LinkButton, useStyles2 } from '@grafana/ui';
import { Breadcrumb } from './types';
type Props = Breadcrumb & {
isCurrent: boolean;
};
export function BreadcrumbItem(props: Props) {
const styles = useStyles2(getStyles);
return (
<li className={styles.breadcrumbWrapper}>
{props.isCurrent ? (
<span className={styles.breadcrumb} aria-current="page">
{props.text}
</span>
) : (
<>
{'icon' in props ? (
<LinkButton
size="md"
variant="secondary"
fill="text"
icon={props.icon}
href={props.href}
aria-label={props.text}
/>
) : (
<a className={cx(styles.breadcrumb, styles.breadcrumbLink)} href={props.href}>
{props.text}
</a>
)}
<div className={styles.separator} aria-hidden={true}>
<Icon name="angle-right" />
</div>
</>
)}
</li>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
breadcrumb: css({
alignItems: 'center',
display: 'flex',
padding: theme.spacing(0, 0.5),
whiteSpace: 'nowrap',
}),
breadcrumbLink: css({
'&:hover': {
textDecoration: 'underline',
},
}),
breadcrumbWrapper: css({
alignItems: 'center',
color: theme.colors.text.primary,
display: 'flex',
fontWeight: theme.typography.fontWeightMedium,
}),
separator: css({
color: theme.colors.text.secondary,
}),
};
};

View File

@@ -0,0 +1,36 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { BreadcrumbItem } from './BreadcrumbItem';
import { Breadcrumb } from './types';
export interface Props {
breadcrumbs: Breadcrumb[];
}
export function Breadcrumbs({ breadcrumbs }: Props) {
const styles = useStyles2(getStyles);
return (
<nav aria-label="Breadcrumbs">
<ol className={styles.breadcrumbs}>
{breadcrumbs.map((breadcrumb, index) => (
<BreadcrumbItem {...breadcrumb} isCurrent={index === breadcrumbs.length - 1} key={index} />
))}
</ol>
</nav>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
breadcrumbs: css({
display: 'flex',
alignItems: 'center',
flexWrap: 'nowrap',
}),
};
};

View File

@@ -0,0 +1,12 @@
import { IconName } from '@grafana/ui';
interface TextBreadcrumb {
text: string;
href: string;
}
interface IconBreadcrumb extends TextBreadcrumb {
icon: IconName;
}
export type Breadcrumb = TextBreadcrumb | IconBreadcrumb;

View File

@@ -0,0 +1,23 @@
import { NavModelItem } from '@grafana/data';
import { Breadcrumb } from './types';
export function buildBreadcrumbs(sectionNav: NavModelItem, pageNav?: NavModelItem) {
const crumbs: Breadcrumb[] = [{ icon: 'home-alt', href: '/', text: 'Home' }];
function addCrumbs(node: NavModelItem) {
if (node.parentItem) {
addCrumbs(node.parentItem);
}
crumbs.push({ text: node.text, href: node.url ?? '' });
}
addCrumbs(sectionNav);
if (pageNav) {
addCrumbs(pageNav);
}
return crumbs;
}