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/'); 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; 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 { interface LocationUtilDependencies {
config: GrafanaConfig; config: GrafanaConfig;
getTimeRangeForUrl: () => RawTimeRange; getTimeRangeForUrl: () => RawTimeRange;
@ -63,6 +71,7 @@ export const locationUtil = {
}, },
stripBaseFromUrl, stripBaseFromUrl,
assureBaseUrl, assureBaseUrl,
updateSearchParams,
getTimeRangeUrlParams: () => { getTimeRangeUrlParams: () => {
if (!getTimeRangeUrlParams) { if (!getTimeRangeUrlParams) {
return null; return null;

View File

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

View File

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