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
pull/86452/head
Tobias Skarhed 1 year ago committed by GitHub
parent 2ed7eecf2d
commit 1c2065af60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      packages/grafana-e2e-selectors/src/selectors/components.ts
  2. 17
      packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx
  3. 77
      packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx

@ -155,6 +155,7 @@ export const Components = {
Panels: {
Panel: {
title: (title: string) => `data-testid Panel header ${title}`,
content: 'data-testid panel content',
headerItems: (item: string) => `data-testid Panel header item ${item}`,
menuItems: (item: string) => `data-testid Panel menu item ${item}`,
menu: (title: string) => `data-testid Panel menu ${title}`,

@ -3,6 +3,7 @@ import React from 'react';
import { useToggle } from 'react-use';
import { LoadingState } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
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();
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
expect(button.getAttribute('aria-controls')).toBe(button.parentElement?.parentElement?.nextElementSibling?.id);
expect(button.getAttribute('aria-controls')).toBe(content.id);
fireEvent.click(button);
expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument();
// aria-controls should be removed when panel is collapsed
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', () => {
@ -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();
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
expect(button.getAttribute('aria-controls')).toBe(button.parentElement?.parentElement?.nextElementSibling?.id);
expect(button.getAttribute('aria-controls')).toBe(content.id);
fireEvent.click(button);
expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument();
// aria-controls should be removed when panel is collapsed
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 { Icon } from '../Icon/Icon';
import { LoadingBar } from '../LoadingBar/LoadingBar';
import { Text } from '../Text/Text';
import { Tooltip } from '../Tooltip';
import { HoverWidget } from './HoverWidget';
@ -167,34 +168,40 @@ export function PanelChrome({
<>
{/* Non collapsible title */}
{!collapsible && title && (
<h6 title={typeof title === 'string' ? title : undefined} className={styles.title}>
{title}
</h6>
<div className={styles.title}>
<Text element="h2" variant="h6" truncate title={typeof title === 'string' ? title : undefined}>
{title}
</Text>
</div>
)}
{/* Collapsible title */}
{collapsible && (
<h6 className={styles.title}>
<button
type="button"
className={styles.clearButtonStyles}
onClick={() => {
toggleOpen();
if (onToggleCollapse) {
onToggleCollapse(!collapsed);
}
}}
aria-expanded={!collapsed}
aria-controls={!collapsed ? panelContentId : undefined}
>
<Icon
name={!collapsed ? 'angle-down' : 'angle-right'}
aria-hidden={!!title}
aria-label={!title ? 'toggle collapse panel' : undefined}
/>
{title}
</button>
</h6>
<div className={styles.title}>
<Text element="h2" variant="h6">
<button
type="button"
className={styles.clearButtonStyles}
onClick={() => {
toggleOpen();
if (onToggleCollapse) {
onToggleCollapse(!collapsed);
}
}}
aria-expanded={!collapsed}
aria-controls={!collapsed ? panelContentId : undefined}
>
<Icon
name={!collapsed ? 'angle-down' : 'angle-right'}
aria-hidden={!!title}
aria-label={!title ? 'toggle collapse panel' : undefined}
/>
<Text variant="h6" truncate>
{title}
</Text>
</button>
</Text>
</div>
)}
<div className={cx(styles.titleItems, dragClassCancel)} data-testid="title-items-container">
@ -229,7 +236,7 @@ export function PanelChrome({
return (
// tabIndex={0} is needed for keyboard accessibility in the plot area
<div
<section
className={cx(styles.container, { [styles.transparentContainer]: isPanelTransparent })}
style={containerStyles}
data-testid={testid}
@ -287,13 +294,14 @@ export function PanelChrome({
{!collapsed && (
<div
id={panelContentId}
data-testid={selectors.components.Panels.Panel.content}
className={cx(styles.content, height === undefined && styles.containNone)}
style={contentStyle}
>
{typeof children === 'function' ? children(innerWidth, innerHeight) : children}
</div>
)}
</div>
</section>
);
}
@ -424,13 +432,11 @@ const getStyles = (theme: GrafanaTheme2) => {
title: css({
label: 'panel-title',
display: 'flex',
marginBottom: 0, // override default h6 margin-bottom
padding: theme.spacing(0, padding),
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
fontSize: theme.typography.h6.fontSize,
fontWeight: theme.typography.h6.fontWeight,
minWidth: 0,
'& > h2': {
minWidth: 0,
},
}),
items: css({
display: 'flex',
@ -478,14 +484,9 @@ const getStyles = (theme: GrafanaTheme2) => {
display: 'flex',
gap: theme.spacing(0.5),
background: 'transparent',
color: theme.colors.text.primary,
border: 'none',
padding: 0,
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
fontSize: theme.typography.h6.fontSize,
fontWeight: theme.typography.h6.fontWeight,
maxWidth: '100%',
}),
};
};

Loading…
Cancel
Save