mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 01:41:50 +08:00
PanelChrome: Improve accessibility landmark markup (#85863)
* Add section and use Text component to get h2 for panel * Remove heading when collapsible * Make button text truncate * Update test * Remove labelledby as it is not needed anymore * Use testid selectors in test * Remove async
This commit is contained in:
@ -155,6 +155,7 @@ export const Components = {
|
|||||||
Panels: {
|
Panels: {
|
||||||
Panel: {
|
Panel: {
|
||||||
title: (title: string) => `data-testid Panel header ${title}`,
|
title: (title: string) => `data-testid Panel header ${title}`,
|
||||||
|
content: 'data-testid panel content',
|
||||||
headerItems: (item: string) => `data-testid Panel header item ${item}`,
|
headerItems: (item: string) => `data-testid Panel header item ${item}`,
|
||||||
menuItems: (item: string) => `data-testid Panel menu item ${item}`,
|
menuItems: (item: string) => `data-testid Panel menu item ${item}`,
|
||||||
menu: (title: string) => `data-testid Panel menu ${title}`,
|
menu: (title: string) => `data-testid Panel menu ${title}`,
|
||||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import { useToggle } from 'react-use';
|
import { useToggle } from 'react-use';
|
||||||
|
|
||||||
import { LoadingState } from '@grafana/data';
|
import { LoadingState } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
import { PanelChrome, PanelChromeProps } from './PanelChrome';
|
import { PanelChrome, PanelChromeProps } from './PanelChrome';
|
||||||
|
|
||||||
@ -152,16 +153,17 @@ it('collapses the controlled panel when user clicks on the chevron or the title'
|
|||||||
|
|
||||||
expect(screen.getByText("Panel's Content")).toBeInTheDocument();
|
expect(screen.getByText("Panel's Content")).toBeInTheDocument();
|
||||||
|
|
||||||
const button = screen.getByText('Default title');
|
const button = screen.getByRole('button', { name: 'Default title' });
|
||||||
|
const content = screen.getByTestId(selectors.components.Panels.Panel.content);
|
||||||
// collapse button should have same aria-controls as the panel's content
|
// collapse button should have same aria-controls as the panel's content
|
||||||
expect(button.getAttribute('aria-controls')).toBe(button.parentElement?.parentElement?.nextElementSibling?.id);
|
expect(button.getAttribute('aria-controls')).toBe(content.id);
|
||||||
|
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
|
|
||||||
expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument();
|
expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument();
|
||||||
// aria-controls should be removed when panel is collapsed
|
// aria-controls should be removed when panel is collapsed
|
||||||
expect(button).not.toHaveAttribute('aria-controlls');
|
expect(button).not.toHaveAttribute('aria-controlls');
|
||||||
expect(button.parentElement?.parentElement?.nextElementSibling?.id).toBe(undefined);
|
expect(screen.queryByTestId(selectors.components.Panels.Panel.content)?.id).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('collapses the uncontrolled panel when user clicks on the chevron or the title', () => {
|
it('collapses the uncontrolled panel when user clicks on the chevron or the title', () => {
|
||||||
@ -169,14 +171,15 @@ it('collapses the uncontrolled panel when user clicks on the chevron or the titl
|
|||||||
|
|
||||||
expect(screen.getByText("Panel's Content")).toBeInTheDocument();
|
expect(screen.getByText("Panel's Content")).toBeInTheDocument();
|
||||||
|
|
||||||
const button = screen.getByText('Default title');
|
const button = screen.getByRole('button', { name: 'Default title' });
|
||||||
|
const content = screen.getByTestId(selectors.components.Panels.Panel.content);
|
||||||
|
|
||||||
// collapse button should have same aria-controls as the panel's content
|
// collapse button should have same aria-controls as the panel's content
|
||||||
expect(button.getAttribute('aria-controls')).toBe(button.parentElement?.parentElement?.nextElementSibling?.id);
|
expect(button.getAttribute('aria-controls')).toBe(content.id);
|
||||||
|
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
|
|
||||||
expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument();
|
expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument();
|
||||||
// aria-controls should be removed when panel is collapsed
|
// aria-controls should be removed when panel is collapsed
|
||||||
expect(button).not.toHaveAttribute('aria-controlls');
|
expect(button).not.toHaveAttribute('aria-controlls');
|
||||||
expect(button.parentElement?.parentElement?.nextElementSibling?.id).toBe(undefined);
|
expect(screen.queryByTestId(selectors.components.Panels.Panel.content)?.id).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,7 @@ import { getFocusStyles } from '../../themes/mixins';
|
|||||||
import { DelayRender } from '../../utils/DelayRender';
|
import { DelayRender } from '../../utils/DelayRender';
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
import { LoadingBar } from '../LoadingBar/LoadingBar';
|
import { LoadingBar } from '../LoadingBar/LoadingBar';
|
||||||
|
import { Text } from '../Text/Text';
|
||||||
import { Tooltip } from '../Tooltip';
|
import { Tooltip } from '../Tooltip';
|
||||||
|
|
||||||
import { HoverWidget } from './HoverWidget';
|
import { HoverWidget } from './HoverWidget';
|
||||||
@ -167,14 +168,17 @@ export function PanelChrome({
|
|||||||
<>
|
<>
|
||||||
{/* Non collapsible title */}
|
{/* Non collapsible title */}
|
||||||
{!collapsible && title && (
|
{!collapsible && title && (
|
||||||
<h6 title={typeof title === 'string' ? title : undefined} className={styles.title}>
|
<div className={styles.title}>
|
||||||
|
<Text element="h2" variant="h6" truncate title={typeof title === 'string' ? title : undefined}>
|
||||||
{title}
|
{title}
|
||||||
</h6>
|
</Text>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Collapsible title */}
|
{/* Collapsible title */}
|
||||||
{collapsible && (
|
{collapsible && (
|
||||||
<h6 className={styles.title}>
|
<div className={styles.title}>
|
||||||
|
<Text element="h2" variant="h6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.clearButtonStyles}
|
className={styles.clearButtonStyles}
|
||||||
@ -192,9 +196,12 @@ export function PanelChrome({
|
|||||||
aria-hidden={!!title}
|
aria-hidden={!!title}
|
||||||
aria-label={!title ? 'toggle collapse panel' : undefined}
|
aria-label={!title ? 'toggle collapse panel' : undefined}
|
||||||
/>
|
/>
|
||||||
|
<Text variant="h6" truncate>
|
||||||
{title}
|
{title}
|
||||||
|
</Text>
|
||||||
</button>
|
</button>
|
||||||
</h6>
|
</Text>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={cx(styles.titleItems, dragClassCancel)} data-testid="title-items-container">
|
<div className={cx(styles.titleItems, dragClassCancel)} data-testid="title-items-container">
|
||||||
@ -229,7 +236,7 @@ export function PanelChrome({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// tabIndex={0} is needed for keyboard accessibility in the plot area
|
// tabIndex={0} is needed for keyboard accessibility in the plot area
|
||||||
<div
|
<section
|
||||||
className={cx(styles.container, { [styles.transparentContainer]: isPanelTransparent })}
|
className={cx(styles.container, { [styles.transparentContainer]: isPanelTransparent })}
|
||||||
style={containerStyles}
|
style={containerStyles}
|
||||||
data-testid={testid}
|
data-testid={testid}
|
||||||
@ -287,13 +294,14 @@ export function PanelChrome({
|
|||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div
|
<div
|
||||||
id={panelContentId}
|
id={panelContentId}
|
||||||
|
data-testid={selectors.components.Panels.Panel.content}
|
||||||
className={cx(styles.content, height === undefined && styles.containNone)}
|
className={cx(styles.content, height === undefined && styles.containNone)}
|
||||||
style={contentStyle}
|
style={contentStyle}
|
||||||
>
|
>
|
||||||
{typeof children === 'function' ? children(innerWidth, innerHeight) : children}
|
{typeof children === 'function' ? children(innerWidth, innerHeight) : children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,13 +432,11 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
title: css({
|
title: css({
|
||||||
label: 'panel-title',
|
label: 'panel-title',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
marginBottom: 0, // override default h6 margin-bottom
|
|
||||||
padding: theme.spacing(0, padding),
|
padding: theme.spacing(0, padding),
|
||||||
textOverflow: 'ellipsis',
|
minWidth: 0,
|
||||||
overflow: 'hidden',
|
'& > h2': {
|
||||||
whiteSpace: 'nowrap',
|
minWidth: 0,
|
||||||
fontSize: theme.typography.h6.fontSize,
|
},
|
||||||
fontWeight: theme.typography.h6.fontWeight,
|
|
||||||
}),
|
}),
|
||||||
items: css({
|
items: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -478,14 +484,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: theme.spacing(0.5),
|
gap: theme.spacing(0.5),
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
color: theme.colors.text.primary,
|
|
||||||
border: 'none',
|
border: 'none',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
textOverflow: 'ellipsis',
|
maxWidth: '100%',
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
fontSize: theme.typography.h6.fontSize,
|
|
||||||
fontWeight: theme.typography.h6.fontWeight,
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user