A11y: Add focus styles to dashboard toolbar links (#35895)

This commit is contained in:
kay delaney 2021-06-23 12:03:44 +01:00 committed by GitHub
parent 83860bdcc3
commit f5bd325354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 76 additions and 53 deletions

View File

@ -80,4 +80,22 @@ describe('locationUtil', () => {
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
});
});
describe('updateSearchParams', () => {
beforeEach(() => {
locationUtil.initialize({
config: {} as any,
getVariablesUrlParams: (() => {}) as any,
getTimeRangeForUrl: (() => {}) as any,
});
});
test('absolute url', () => {
const newURL = locationUtil.updateSearchParams(
'http://www.domain.com:1234/test?a=1&b=2#hashtag',
'?a=newValue&newKey=hello'
);
expect(newURL).toBe('http://www.domain.com:1234/test?a=newValue&b=2&newKey=hello#hashtag');
});
});
});

View File

@ -42,6 +42,14 @@ const assureBaseUrl = (url: string): string => {
return url;
};
const updateSearchParams = (href: string, searchParams: string) => {
const curURL = new URL(href);
const urlSearchParams = new URLSearchParams(searchParams);
urlSearchParams.forEach((val, key) => curURL.searchParams.set(key, val));
return curURL.href;
};
interface LocationUtilDependencies {
config: GrafanaConfig;
getTimeRangeForUrl: () => RawTimeRange;
@ -63,6 +71,7 @@ export const locationUtil = {
},
stripBaseFromUrl,
assureBaseUrl,
updateSearchParams,
getTimeRangeUrlParams: () => {
if (!getTimeRangeUrlParams) {
return null;

View File

@ -27,8 +27,8 @@ export const Examples = () => {
pageIcon="apps"
title="A very long dashboard name"
parent="A long folder name"
onClickTitle={() => action('Title clicked')}
onClickParent={() => action('Parent clicked')}
titleHref=""
parentHref=""
leftItems={[
<IconButton name="share-alt" size="lg" key="share" />,
<IconButton name="favorite" iconType="mono" size="lg" key="favorite" />,

View File

@ -7,14 +7,16 @@ import { Icon } from '../Icon/Icon';
import { styleMixins } from '../../themes';
import { IconButton } from '../IconButton/IconButton';
import { selectors } from '@grafana/e2e-selectors';
import { Link } from '..';
import { getFocusStyles } from '../../themes/mixins';
export interface Props {
pageIcon?: IconName;
title: string;
parent?: string;
onGoBack?: () => void;
onClickTitle?: () => void;
onClickParent?: () => void;
titleHref?: string;
parentHref?: string;
leftItems?: ReactNode[];
children?: ReactNode;
className?: string;
@ -23,18 +25,7 @@ export interface Props {
/** @alpha */
export const PageToolbar: FC<Props> = React.memo(
({
title,
parent,
pageIcon,
onGoBack,
children,
onClickTitle,
onClickParent,
leftItems,
isFullscreen,
className,
}) => {
({ title, parent, pageIcon, onGoBack, children, titleHref, parentHref, leftItems, isFullscreen, className }) => {
const styles = useStyles2(getStyles);
/**
@ -56,7 +47,7 @@ export const PageToolbar: FC<Props> = React.memo(
<div className={mainStyle}>
{pageIcon && !onGoBack && (
<div className={styles.pageIcon}>
<Icon name={pageIcon} size="lg" />
<Icon name={pageIcon} size="lg" aria-hidden />
</div>
)}
{onGoBack && (
@ -72,17 +63,24 @@ export const PageToolbar: FC<Props> = React.memo(
/>
</div>
)}
{parent && onClickParent && (
<button onClick={onClickParent} className={cx(styles.titleText, styles.parentLink)}>
{parent} <span className={styles.parentIcon}>/</span>
</button>
{parent && parentHref && (
<>
<Link className={cx(styles.titleText, styles.parentLink, styles.titleLink)} href={parentHref}>
{parent} <span className={styles.parentIcon}></span>
</Link>
{titleHref && (
<span className={cx(styles.titleText, styles.titleDivider, styles.parentLink)} aria-hidden>
/
</span>
)}
</>
)}
{onClickTitle && (
<button onClick={onClickTitle} className={styles.titleText}>
{titleHref && (
<Link className={cx(styles.titleText, styles.titleLink)} href={titleHref}>
{title}
</button>
</Link>
)}
{!onClickTitle && <div className={styles.titleText}>{title}</div>}
{!titleHref && <div className={styles.titleText}>{title}</div>}
{leftItems?.map((child, index) => (
<div className={styles.leftActionItem} key={index}>
{child}
@ -109,21 +107,18 @@ PageToolbar.displayName = 'PageToolbar';
const getStyles = (theme: GrafanaTheme2) => {
const { spacing, typography } = theme;
const titleStyles = `
font-size: ${typography.size.lg};
padding: ${spacing(0.5, 1, 0.5, 1)};
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 240px;
const focusStyle = getFocusStyles(theme);
const titleStyles = css`
font-size: ${typography.size.lg};
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 240px;
border-radius: 2px;
// clear default button styles
background: none;
border: none;
@media ${styleMixins.mediaUp(theme.v1.breakpoints.xl)} {
max-width: unset;
}
@media ${styleMixins.mediaUp(theme.v1.breakpoints.xl)} {
max-width: unset;
}
`;
return {
@ -142,6 +137,7 @@ const getStyles = (theme: GrafanaTheme2) => {
display: none;
@media ${styleMixins.mediaUp(theme.v1.breakpoints.md)} {
display: flex;
padding-right: ${theme.spacing(1)};
align-items: center;
}
`,
@ -154,12 +150,17 @@ const getStyles = (theme: GrafanaTheme2) => {
parentIcon: css`
margin-left: ${theme.spacing(0.5)};
`,
titleText: css`
${titleStyles};
titleText: titleStyles,
titleLink: css`
&:focus-visible {
${focusStyle}
}
`,
titleDivider: css`
padding: ${spacing(0, 0.5, 0, 0.5)};
`,
parentLink: css`
display: none;
padding-right: 0;
@media ${styleMixins.mediaUp(theme.v1.breakpoints.md)} {
display: unset;
}

View File

@ -7,7 +7,7 @@ import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { DashNavButton } from './DashNavButton';
import { DashNavTimeControls } from './DashNavTimeControls';
import { ButtonGroup, ModalsController, ToolbarButton, PageToolbar } from '@grafana/ui';
import { textUtil } from '@grafana/data';
import { locationUtil, textUtil } from '@grafana/data';
// State
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
// Types
@ -57,10 +57,6 @@ class DashNav extends PureComponent<Props> {
super(props);
}
onFolderNameClick = () => {
locationService.partial({ search: 'open', folder: 'current' });
};
onClose = () => {
locationService.partial({ viewPanel: null });
};
@ -96,10 +92,6 @@ class DashNav extends PureComponent<Props> {
this.forceUpdate();
};
onDashboardNameClick = () => {
locationService.partial({ search: 'open' });
};
addCustomContent(actions: DashNavButtonModel[], buttons: ReactNode[]) {
actions.map((action, index) => {
const Component = action.component;
@ -250,13 +242,16 @@ class DashNav extends PureComponent<Props> {
const { isFullscreen, title, folderTitle } = this.props;
const onGoBack = isFullscreen ? this.onClose : undefined;
const titleHref = locationUtil.updateSearchParams(window.location.href, '?search=open');
const parentHref = locationUtil.updateSearchParams(window.location.href, '?search=open&folder=current');
return (
<PageToolbar
pageIcon={isFullscreen ? undefined : 'apps'}
title={title}
parent={folderTitle}
onClickTitle={this.onDashboardNameClick}
onClickParent={this.onFolderNameClick}
titleHref={titleHref}
parentHref={parentHref}
onGoBack={onGoBack}
leftItems={this.renderLeftActionsButton()}
>