Accessibility: Enable rule jsx-a11y/no-noninteractive-element-interactions (#58077)

* fixes for no-noninteractive-element-interactions

* remaining fixes

* add type="button"

* fix unit tests
This commit is contained in:
Ashley Harrison 2022-11-03 10:55:58 +00:00 committed by GitHub
parent 65bd5c65d8
commit 514d3111f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 79 additions and 65 deletions

View File

@ -81,7 +81,6 @@
"ignoreNonDOM": true "ignoreNonDOM": true
} }
], ],
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/no-static-element-interactions": "off" "jsx-a11y/no-static-element-interactions": "off"
} }
} }

View File

@ -14,12 +14,6 @@ const getStyles = (theme: GrafanaTheme2) => {
align-items: center; align-items: center;
flex-direction: row-reverse; flex-direction: row-reverse;
justify-content: space-between; justify-content: space-between;
padding: 7px 9px 7px 9px;
&:hover {
background: ${theme.colors.action.hover};
cursor: pointer;
}
`, `,
selected: css` selected: css`
background: ${theme.colors.action.selected}; background: ${theme.colors.action.selected};
@ -27,6 +21,7 @@ const getStyles = (theme: GrafanaTheme2) => {
`, `,
radio: css` radio: css`
opacity: 0; opacity: 0;
width: 0 !important;
&:focus-visible + label { &:focus-visible + label {
${getFocusStyles(theme)}; ${getFocusStyles(theme)};
@ -34,6 +29,13 @@ const getStyles = (theme: GrafanaTheme2) => {
`, `,
label: css` label: css`
cursor: pointer; cursor: pointer;
flex: 1;
padding: 7px 9px 7px 9px;
&:hover {
background: ${theme.colors.action.hover};
cursor: pointer;
}
`, `,
}; };
}; };
@ -54,7 +56,7 @@ export const TimeRangeOption = memo<Props>(({ value, onSelect, selected = false,
const id = uuidv4(); const id = uuidv4();
return ( return (
<li onClick={() => onSelect(value)} className={cx(styles.container, selected && styles.selected)}> <li className={cx(styles.container, selected && styles.selected)}>
<input <input
className={styles.radio} className={styles.radio}
checked={selected} checked={selected}

View File

@ -25,10 +25,12 @@ describe('Typeahead', () => {
render(<Typeahead origin="test" groupedItems={completionItemGroups} isOpen />); render(<Typeahead origin="test" groupedItems={completionItemGroups} isOpen />);
expect(screen.getByTestId('typeahead')).toBeInTheDocument(); expect(screen.getByTestId('typeahead')).toBeInTheDocument();
const items = screen.getAllByRole('listitem'); const groupTitles = screen.getAllByRole('listitem');
expect(items).toHaveLength(2); expect(groupTitles).toHaveLength(1);
expect(items[0]).toHaveTextContent('my group'); expect(groupTitles[0]).toHaveTextContent('my group');
expect(items[1]).toHaveTextContent('first item'); const items = screen.getAllByRole('menuitem');
expect(items).toHaveLength(1);
expect(items[0]).toHaveTextContent('first item');
}); });
it('can be rendered properly even if the size of items is large', () => { it('can be rendered properly even if the size of items is large', () => {

View File

@ -162,7 +162,7 @@ export class Typeahead extends PureComponent<Props, State> {
return ( return (
<Portal origin={origin} isOpen={isOpen} style={this.menuPosition}> <Portal origin={origin} isOpen={isOpen} style={this.menuPosition}>
<ul className="typeahead" data-testid="typeahead"> <ul role="menu" className="typeahead" data-testid="typeahead">
<FixedSizeList <FixedSizeList
ref={this.listRef} ref={this.listRef}
itemCount={allItems.length} itemCount={allItems.length}

View File

@ -22,6 +22,9 @@ interface Props {
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
typeaheadItem: css` typeaheadItem: css`
border: none;
background: none;
text-align: left;
label: type-ahead-item; label: type-ahead-item;
height: auto; height: auto;
font-family: ${theme.typography.fontFamilyMonospace}; font-family: ${theme.typography.fontFamilyMonospace};
@ -77,12 +80,15 @@ export const TypeaheadItem = (props: Props) => {
} }
return ( return (
<li <li role="none">
<button
role="menuitem"
className={className} className={className}
style={style} style={style}
onMouseDown={onClickItem} onMouseDown={onClickItem}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
type="button"
> >
{item.highlightParts !== undefined ? ( {item.highlightParts !== undefined ? (
<PartialHighlighter <PartialHighlighter
@ -98,6 +104,7 @@ export const TypeaheadItem = (props: Props) => {
highlightClassName={highlightClassName} highlightClassName={highlightClassName}
/> />
)} )}
</button>
</li> </li>
); );
}; };

View File

@ -57,7 +57,7 @@ export const AnnotationSettingsList = ({ dashboard, onNew, onEdit }: Props) => {
return ( return (
<VerticalGroup> <VerticalGroup>
{annotations.length > 0 && ( {annotations.length > 0 && (
<table className="filter-table filter-table--hover"> <table role="grid" className="filter-table filter-table--hover">
<thead> <thead>
<tr> <tr>
<th>Query name</th> <th>Query name</th>
@ -69,26 +69,26 @@ export const AnnotationSettingsList = ({ dashboard, onNew, onEdit }: Props) => {
{dashboard.annotations.list.map((annotation, idx) => ( {dashboard.annotations.list.map((annotation, idx) => (
<tr key={`${annotation.name}-${idx}`}> <tr key={`${annotation.name}-${idx}`}>
{annotation.builtIn ? ( {annotation.builtIn ? (
<td style={{ width: '90%' }} className="pointer" onClick={() => onEdit(idx)}> <td role="gridcell" style={{ width: '90%' }} className="pointer" onClick={() => onEdit(idx)}>
{getAnnotationName(annotation)} {getAnnotationName(annotation)}
</td> </td>
) : ( ) : (
<td className="pointer" onClick={() => onEdit(idx)}> <td role="gridcell" className="pointer" onClick={() => onEdit(idx)}>
{getAnnotationName(annotation)} {getAnnotationName(annotation)}
</td> </td>
)} )}
<td className="pointer" onClick={() => onEdit(idx)}> <td role="gridcell" className="pointer" onClick={() => onEdit(idx)}>
{dataSourceSrv.getInstanceSettings(annotation.datasource)?.name || annotation.datasource?.uid} {dataSourceSrv.getInstanceSettings(annotation.datasource)?.name || annotation.datasource?.uid}
</td> </td>
<td style={{ width: '1%' }}> <td role="gridcell" style={{ width: '1%' }}>
{idx !== 0 && <IconButton name="arrow-up" aria-label="arrow-up" onClick={() => onMove(idx, -1)} />} {idx !== 0 && <IconButton name="arrow-up" aria-label="arrow-up" onClick={() => onMove(idx, -1)} />}
</td> </td>
<td style={{ width: '1%' }}> <td role="gridcell" style={{ width: '1%' }}>
{dashboard.annotations.list.length > 1 && idx !== dashboard.annotations.list.length - 1 ? ( {dashboard.annotations.list.length > 1 && idx !== dashboard.annotations.list.length - 1 ? (
<IconButton name="arrow-down" aria-label="arrow-down" onClick={() => onMove(idx, 1)} /> <IconButton name="arrow-down" aria-label="arrow-down" onClick={() => onMove(idx, 1)} />
) : null} ) : null}
</td> </td>
<td style={{ width: '1%' }}> <td role="gridcell" style={{ width: '1%' }}>
{!annotation.builtIn && ( {!annotation.builtIn && (
<DeleteButton <DeleteButton
size="sm" size="sm"

View File

@ -101,7 +101,7 @@ describe('AnnotationsSettings', () => {
test('it renders empty list cta if only builtIn annotation', async () => { test('it renders empty list cta if only builtIn annotation', async () => {
setup(dashboard); setup(dashboard);
expect(screen.queryByRole('table')).toBeInTheDocument(); expect(screen.queryByRole('grid')).toBeInTheDocument();
expect(screen.getByRole('row', { name: /annotations & alerts \(built\-in\) grafana/i })).toBeInTheDocument(); expect(screen.getByRole('row', { name: /annotations & alerts \(built\-in\) grafana/i })).toBeInTheDocument();
expect( expect(
screen.getByTestId(selectors.components.CallToActionCard.buttonV2('Add annotation query')) screen.getByTestId(selectors.components.CallToActionCard.buttonV2('Add annotation query'))

View File

@ -53,7 +53,7 @@ export const LinkSettingsList: React.FC<LinkSettingsListProps> = ({ dashboard, o
return ( return (
<> <>
<table className="filter-table filter-table--hover"> <table role="grid" className="filter-table filter-table--hover">
<thead> <thead>
<tr> <tr>
<th>Type</th> <th>Type</th>
@ -64,28 +64,28 @@ export const LinkSettingsList: React.FC<LinkSettingsListProps> = ({ dashboard, o
<tbody> <tbody>
{links.map((link, idx) => ( {links.map((link, idx) => (
<tr key={`${link.title}-${idx}`}> <tr key={`${link.title}-${idx}`}>
<td className="pointer" onClick={() => onEdit(idx)}> <td role="gridcell" className="pointer" onClick={() => onEdit(idx)}>
<Icon name="external-link-alt" /> &nbsp; {link.type} <Icon name="external-link-alt" /> &nbsp; {link.type}
</td> </td>
<td> <td role="gridcell">
<HorizontalGroup> <HorizontalGroup>
{link.title && <span>{link.title}</span>} {link.title && <span>{link.title}</span>}
{link.type === 'link' && <span>{link.url}</span>} {link.type === 'link' && <span>{link.url}</span>}
{link.type === 'dashboards' && <TagList tags={link.tags ?? []} />} {link.type === 'dashboards' && <TagList tags={link.tags ?? []} />}
</HorizontalGroup> </HorizontalGroup>
</td> </td>
<td style={{ width: '1%' }}> <td style={{ width: '1%' }} role="gridcell">
{idx !== 0 && <IconButton name="arrow-up" aria-label="arrow-up" onClick={() => moveLink(idx, -1)} />} {idx !== 0 && <IconButton name="arrow-up" aria-label="arrow-up" onClick={() => moveLink(idx, -1)} />}
</td> </td>
<td style={{ width: '1%' }}> <td style={{ width: '1%' }} role="gridcell">
{links.length > 1 && idx !== links.length - 1 ? ( {links.length > 1 && idx !== links.length - 1 ? (
<IconButton name="arrow-down" aria-label="arrow-down" onClick={() => moveLink(idx, 1)} /> <IconButton name="arrow-down" aria-label="arrow-down" onClick={() => moveLink(idx, 1)} />
) : null} ) : null}
</td> </td>
<td style={{ width: '1%' }}> <td style={{ width: '1%' }} role="gridcell">
<IconButton aria-label="copy" name="copy" onClick={() => duplicateLink(link, idx)} /> <IconButton aria-label="copy" name="copy" onClick={() => duplicateLink(link, idx)} />
</td> </td>
<td style={{ width: '1%' }}> <td style={{ width: '1%' }} role="gridcell">
<DeleteButton <DeleteButton
aria-label={`Delete link with title "${link.title}"`} aria-label={`Delete link with title "${link.title}"`}
size="sm" size="sm"

View File

@ -20,7 +20,7 @@ describe('Panel header corner test', () => {
setup(); setup();
expect( expect(
screen.getByRole('region', { name: selectors.components.Panels.Panel.headerCornerInfo('info') }) screen.getByRole('button', { name: selectors.components.Panels.Panel.headerCornerInfo('info') })
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
}); });

View File

@ -86,10 +86,10 @@ export class PanelHeaderCorner extends Component<Props> {
return ( return (
<Tooltip content={content} placement="top-start" theme={theme} interactive> <Tooltip content={content} placement="top-start" theme={theme} interactive>
<section className={className} onClick={onClick} aria-label={ariaLabel}> <button type="button" className={className} onClick={onClick} aria-label={ariaLabel}>
<i aria-hidden className="fa" /> <i aria-hidden className="fa" />
<span className="panel-info-corner-inner" /> <span className="panel-info-corner-inner" />
</section> </button>
</Tooltip> </Tooltip>
); );
} }

View File

@ -18,8 +18,8 @@ export function Breadcrumb({ pathName, onPathChange, rootIcon }: Props) {
return ( return (
<ul className={styles.breadCrumb}> <ul className={styles.breadCrumb}>
{rootIcon && ( {rootIcon && (
<li onClick={() => onPathChange('')}> <li>
<Icon name={rootIcon} /> <Icon name={rootIcon} onClick={() => onPathChange('')} />
</li> </li>
)} )}
{paths.map((path, index) => { {paths.map((path, index) => {

View File

@ -54,6 +54,7 @@ export function VariableEditorList({
<table <table
className="filter-table filter-table--hover" className="filter-table filter-table--hover"
aria-label={selectors.pages.Dashboard.Settings.Variables.List.table} aria-label={selectors.pages.Dashboard.Settings.Variables.List.table}
role="grid"
> >
<thead> <thead>
<tr> <tr>

View File

@ -52,7 +52,7 @@ export function VariableEditorListRow({
...provided.draggableProps.style, ...provided.draggableProps.style,
}} }}
> >
<td className={styles.column}> <td role="gridcell" className={styles.column}>
<Button <Button
size="xs" size="xs"
fill="text" fill="text"
@ -67,6 +67,7 @@ export function VariableEditorListRow({
</Button> </Button>
</td> </td>
<td <td
role="gridcell"
className={styles.definitionColumn} className={styles.definitionColumn}
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
@ -77,15 +78,15 @@ export function VariableEditorListRow({
{definition} {definition}
</td> </td>
<td className={styles.column}> <td role="gridcell" className={styles.column}>
<VariableCheckIndicator passed={passed} /> <VariableCheckIndicator passed={passed} />
</td> </td>
<td className={styles.column}> <td role="gridcell" className={styles.column}>
<VariableUsagesButton id={variable.id} isAdhoc={isAdHoc(variable)} usages={usagesNetwork} /> <VariableUsagesButton id={variable.id} isAdhoc={isAdHoc(variable)} usages={usagesNetwork} />
</td> </td>
<td className={styles.column}> <td role="gridcell" className={styles.column}>
<IconButton <IconButton
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
@ -98,7 +99,7 @@ export function VariableEditorListRow({
/> />
</td> </td>
<td className={styles.column}> <td role="gridcell" className={styles.column}>
<IconButton <IconButton
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
@ -110,7 +111,7 @@ export function VariableEditorListRow({
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(variable.name)} aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(variable.name)}
/> />
</td> </td>
<td className={styles.column}> <td role="gridcell" className={styles.column}>
<div {...provided.dragHandleProps} className={styles.dragHandle}> <div {...provided.dragHandleProps} className={styles.dragHandle}>
<Icon name="draggabledots" size="lg" /> <Icon name="draggabledots" size="lg" />
</div> </div>

View File

@ -265,7 +265,7 @@ class LegendTable extends PureComponent<Partial<LegendComponentProps>> {
} }
return ( return (
<table> <table role="grid">
<colgroup> <colgroup>
<col style={{ width: '100%' }} /> <col style={{ width: '100%' }} />
</colgroup> </colgroup>

View File

@ -107,7 +107,7 @@ export class LegendItem extends PureComponent<LegendItemProps, LegendItemState>
if (asTable) { if (asTable) {
return ( return (
<tr className={`graph-legend-series ${seriesOptionClasses}`}> <tr className={`graph-legend-series ${seriesOptionClasses}`}>
<td> <td role="gridcell">
<div className="graph-legend-series__table-name">{seriesLabel}</div> <div className="graph-legend-series__table-name">{seriesLabel}</div>
</td> </td>
{valueItems} {valueItems}
@ -221,7 +221,7 @@ interface LegendValueProps {
function LegendValue({ value, valueName, asTable, onValueClick }: LegendValueProps) { function LegendValue({ value, valueName, asTable, onValueClick }: LegendValueProps) {
if (asTable) { if (asTable) {
return ( return (
<td className={`graph-legend-value ${valueName}`} onClick={onValueClick}> <td role="gridcell" className={`graph-legend-value ${valueName}`} onClick={onValueClick}>
{value} {value}
</td> </td>
); );

View File

@ -112,6 +112,8 @@ $panel-header-no-title-zindex: 1;
} }
.panel-info-corner { .panel-info-corner {
background: none;
border: none;
color: $text-muted; color: $text-muted;
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
@ -123,8 +125,8 @@ $panel-header-no-title-zindex: 1;
top: 0; top: 0;
.fa { .fa {
position: relative; position: absolute;
top: -2px; top: 6px;
left: 6px; left: 6px;
font-size: 75%; font-size: 75%;
z-index: $panel-header-no-title-zindex + 2; z-index: $panel-header-no-title-zindex + 2;