mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Improve breadcrumb accessibility (#53471)
* refactor breadcrumbs into their own folder, add appropriate accessibility * rename Breadcrumb to BreadcrumbItem
This commit is contained in:
@@ -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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
@@ -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';
|
||||
|
||||
|
69
public/app/core/components/Breadcrumbs/BreadcrumbItem.tsx
Normal file
69
public/app/core/components/Breadcrumbs/BreadcrumbItem.tsx
Normal 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,
|
||||
}),
|
||||
};
|
||||
};
|
36
public/app/core/components/Breadcrumbs/Breadcrumbs.tsx
Normal file
36
public/app/core/components/Breadcrumbs/Breadcrumbs.tsx
Normal 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',
|
||||
}),
|
||||
};
|
||||
};
|
12
public/app/core/components/Breadcrumbs/types.ts
Normal file
12
public/app/core/components/Breadcrumbs/types.ts
Normal 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;
|
23
public/app/core/components/Breadcrumbs/utils.ts
Normal file
23
public/app/core/components/Breadcrumbs/utils.ts
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user