mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
A11y: Add focus styles to dashboard toolbar links (#35895)
This commit is contained in:
parent
83860bdcc3
commit
f5bd325354
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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" />,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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()}
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user