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:
Tobias Skarhed
2024-04-17 16:44:07 +02:00
committed by GitHub
parent 2ed7eecf2d
commit 1c2065af60
3 changed files with 50 additions and 45 deletions

View File

@ -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}`,

View File

@ -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);
}); });

View File

@ -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,
}), }),
}; };
}; };