A11y: enable rule jsx-a11y/anchor-is-valid (#56690)

This commit is contained in:
Laura Fernández 2022-10-21 09:13:32 +02:00 committed by GitHub
parent 7eac79b5f8
commit e402a8f27d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 169 additions and 73 deletions

View File

@ -72,7 +72,6 @@
// rules marked "off" are those left in the recommended preset we need to fix
// we should remove the corresponding line and fix them one by one
// any marked "error" contain specific overrides we'll need to keep
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/interactive-supports-focus": "off",
"jsx-a11y/label-has-associated-control": "off",

View File

@ -302,3 +302,12 @@ export function getPropertiesForVariant(theme: GrafanaTheme2, variant: ButtonVar
return getButtonVariantStyles(theme, theme.colors.primary, fill);
}
}
export const clearButtonStyles = (theme: GrafanaTheme2) => {
return css`
background: transparent;
color: ${theme.colors.text.primary};
border: none;
padding: 0;
`;
};

View File

@ -7,9 +7,9 @@ import { Segment, Icon, SegmentSection } from '@grafana/ui';
import { SegmentSyncProps } from './Segment';
const AddButton = (
<a className="gf-form-label query-part">
<span className="gf-form-label query-part">
<Icon name="plus-circle" />
</a>
</span>
);
const toOption = (value: any) => ({ label: value, value: value });

View File

@ -9,9 +9,9 @@ import { SegmentAsync, Icon, SegmentSection } from '@grafana/ui';
import { SegmentAsyncProps } from './SegmentAsync';
const AddButton = (
<a className="gf-form-label query-part">
<span className="gf-form-label query-part">
<Icon name="plus" />
</a>
</span>
);
const toOption = (value: any) => ({ label: value, value: value });

View File

@ -87,14 +87,15 @@ export const InputWithAutoFocus = () => {
{inputComponents.map((InputComponent: any, i: number) => (
<InputComponent initialValue="test" key={i} />
))}
<a
<button
type="button"
className="gf-form-label query-part"
onClick={() => {
setInputComponents([...inputComponents, InputComponent]);
}}
>
<Icon name="plus" />
</a>
</button>
</SegmentFrame>
);
};

View File

@ -191,7 +191,7 @@ export { RangeSlider } from './Slider/RangeSlider';
export { Form } from './Forms/Form';
export { sharedInputStyle } from './Forms/commonStyles';
export { InputControl } from './InputControl';
export { Button, LinkButton, type ButtonVariant, ButtonGroup, type ButtonProps } from './Button';
export { Button, LinkButton, type ButtonVariant, ButtonGroup, type ButtonProps, clearButtonStyles } from './Button';
export { ToolbarButton, ToolbarButtonRow } from './ToolbarButton';
export { ValuePicker } from './ValuePicker/ValuePicker';
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';

View File

@ -83,6 +83,10 @@ const getStyles = (theme: GrafanaTheme2) => {
&:hover small {
text-decoration: none;
}
/* Adapt styles when changing from a element into button */
background: transparent;
text-align: left;
border: none;
`,
TracePageHeaderDetailToggle: css`
label: TracePageHeaderDetailToggle;
@ -236,7 +240,8 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) {
<div className={styles.TracePageHeaderTitleRow}>
{links && links.length > 0 && <ExternalLinks links={links} className={styles.TracePageHeaderBack} />}
{canCollapse ? (
<a
<button
type="button"
className={styles.TracePageHeaderTitleLink}
onClick={onSlimViewClicked}
role="switch"
@ -249,7 +254,7 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) {
)}
/>
{title}
</a>
</button>
) : (
title
)}

View File

@ -240,6 +240,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
&:hover > small {
color: ${autoColor(theme, '#000')};
}
text-align: left;
background: transparent;
border: none;
`,
nameDetailExpanded: css`
label: nameDetailExpanded;
@ -437,7 +440,8 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId}
/>
<a
<button
type="button"
className={cx(styles.name, { [styles.nameDetailExpanded]: isDetailExpanded })}
aria-checked={isDetailExpanded}
title={labelDetail}
@ -478,7 +482,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
</span>
<small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small>
<small className={styles.endpointName}> {this.getSpanBarLabel(span, spanBarOptions, label)}</small>
</a>
</button>
{createSpanLink &&
(() => {
const links = createSpanLink(span);

View File

@ -76,8 +76,9 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
return (
<div className={classes} data-testid="dashboard-row-container">
<a
<button
className="dashboard-row__title pointer"
type="button"
data-testid={selectors.components.DashboardRow.title(title)}
onClick={this.onToggle}
>
@ -86,7 +87,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
<span className="dashboard-row__panel_count">
({count} {panels})
</span>
</a>
</button>
{canEdit && (
<div className="dashboard-row__actions">
<RowOptionsButton
@ -94,9 +95,9 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
repeat={this.props.panel.repeat}
onUpdate={this.onUpdate}
/>
<a className="pointer" onClick={this.onDelete} role="button" aria-label="Delete row">
<button type="button" className="pointer" onClick={this.onDelete} aria-label="Delete row">
<Icon name="trash-alt" />
</a>
</button>
</div>
)}
{this.props.panel.collapsed === true && (

View File

@ -21,16 +21,16 @@ export const RowOptionsButton: FC<RowOptionsButtonProps> = ({ repeat, title, onU
<ModalsController>
{({ showModal, hideModal }) => {
return (
<a
<button
type="button"
className="pointer"
role="button"
aria-label="Row options"
onClick={() => {
showModal(RowOptionsModal, { title, repeat, onDismiss: hideModal, onUpdate: onUpdateChange(hideModal) });
}}
>
<Icon name="cog" />
</a>
</button>
);
}}
</ModalsController>

View File

@ -52,9 +52,9 @@ export const REMOVE_FILTER_KEY = '-- remove filter --';
const REMOVE_VALUE = { label: REMOVE_FILTER_KEY, value: REMOVE_FILTER_KEY };
const plusSegment: ReactElement = (
<a className="gf-form-label query-part" aria-label="Add Filter">
<span className="gf-form-label query-part" aria-label="Add Filter">
<Icon name="plus" />
</a>
</span>
);
const fetchFilterKeys = async (

View File

@ -16,7 +16,7 @@ import { VariableOption, VariableWithMultiSupport, VariableWithOptions } from '.
import { toKeyedVariableIdentifier } from '../../utils';
import { VariableInput } from '../shared/VariableInput';
import { VariableLink } from '../shared/VariableLink';
import { VariableOptions } from '../shared/VariableOptions';
import VariableOptions from '../shared/VariableOptions';
import { NavigationKey, VariablePickerProps } from '../types';
import { commitChangesToVariable, filterOrSearchOptions, navigateOptions, openOptions } from './actions';

View File

@ -1,12 +1,13 @@
import { css, cx } from '@emotion/css';
import classNames from 'classnames';
import React, { PureComponent } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Tooltip } from '@grafana/ui';
import { Tooltip, Themeable2, withTheme2, clearButtonStyles } from '@grafana/ui';
import { VariableOption } from '../../types';
export interface Props extends React.HTMLProps<HTMLUListElement> {
export interface Props extends React.HTMLProps<HTMLUListElement>, Themeable2 {
multi: boolean;
values: VariableOption[];
selectedValues: VariableOption[];
@ -19,19 +20,19 @@ export interface Props extends React.HTMLProps<HTMLUListElement> {
id: string;
}
export class VariableOptions extends PureComponent<Props> {
onToggle = (option: VariableOption) => (event: React.MouseEvent<HTMLAnchorElement>) => {
class VariableOptions extends PureComponent<Props> {
onToggle = (option: VariableOption) => (event: React.MouseEvent<HTMLButtonElement>) => {
const clearOthers = event.shiftKey || event.ctrlKey || event.metaKey;
this.handleEvent(event);
this.props.onToggle(option, clearOthers);
};
onToggleAll = (event: React.MouseEvent<HTMLAnchorElement>) => {
onToggleAll = (event: React.MouseEvent<HTMLButtonElement>) => {
this.handleEvent(event);
this.props.onToggleAll();
};
handleEvent(event: React.MouseEvent<HTMLAnchorElement>) {
handleEvent(event: React.MouseEvent<HTMLButtonElement>) {
event.preventDefault();
event.stopPropagation();
}
@ -57,24 +58,30 @@ export class VariableOptions extends PureComponent<Props> {
}
renderOption(option: VariableOption, index: number) {
const { highlightIndex } = this.props;
const { highlightIndex, theme } = this.props;
const selectClass = option.selected ? 'variable-option pointer selected' : 'variable-option pointer';
const highlightClass = index === highlightIndex ? `${selectClass} highlighted` : selectClass;
return (
<li key={`${option.value}`}>
<a role="checkbox" aria-checked={option.selected} className={highlightClass} onClick={this.onToggle(option)}>
<button
role="checkbox"
type="button"
aria-checked={option.selected}
className={classNames(highlightClass, clearButtonStyles(theme), noStyledButton)}
onClick={this.onToggle(option)}
>
<span className="variable-option-icon"></span>
<span data-testid={selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(`${option.text}`)}>
{option.text}
</span>
</a>
</button>
</li>
);
}
renderMultiToggle() {
const { multi, selectedValues } = this.props;
const { multi, selectedValues, theme } = this.props;
if (!multi) {
return null;
@ -82,12 +89,12 @@ export class VariableOptions extends PureComponent<Props> {
return (
<Tooltip content={'Clear selections'} placement={'top'}>
<a
<button
className={`${
selectedValues.length > 1
? 'variable-options-column-header many-selected'
: 'variable-options-column-header'
}`}
} ${noStyledButton} ${clearButtonStyles(theme)}`}
role="checkbox"
aria-checked={selectedValues.length > 1 ? 'mixed' : 'false'}
onClick={this.onToggleAll}
@ -96,7 +103,7 @@ export class VariableOptions extends PureComponent<Props> {
>
<span className="variable-option-icon"></span>
Selected ({selectedValues.length})
</a>
</button>
</Tooltip>
);
}
@ -108,3 +115,10 @@ const listStyles = cx(
list-style-type: none;
`
);
const noStyledButton = css`
width: 100%;
text-align: left;
`;
export default withTheme2(VariableOptions);

View File

@ -10,7 +10,7 @@ import { CloudWatchDatasource } from '../datasource';
import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types';
import CloudWatchLink from './CloudWatchLink';
import { CloudWatchLogsQueryField } from './LogsQueryField';
import CloudWatchLogsQueryField from './LogsQueryField';
type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> & {
query: CloudWatchLogsQuery;

View File

@ -6,7 +6,7 @@ import { act } from 'react-dom/test-utils';
import { ExploreId } from '../../../../types';
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
import { CloudWatchLogsQueryField } from './LogsQueryField';
import CloudWatchLogsQueryField from './LogsQueryField';
jest
.spyOn(_, 'debounce')

View File

@ -1,11 +1,21 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { LanguageMap, languages as prismLanguages } from 'prismjs';
import React, { ReactNode } from 'react';
import { Node, Plugin } from 'slate';
import { Editor } from 'slate-react';
import { AbsoluteTimeRange, QueryEditorProps } from '@grafana/data';
import { BracesPlugin, LegacyForms, QueryField, SlatePrism, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import {
BracesPlugin,
LegacyForms,
QueryField,
SlatePrism,
TypeaheadInput,
TypeaheadOutput,
Themeable2,
withTheme2,
clearButtonStyles,
} from '@grafana/ui';
import { ExploreId } from 'app/types';
// Utils & Services
// dom also includes Element polyfills
@ -20,7 +30,8 @@ import { LogGroupSelector } from './LogGroupSelector';
import QueryHeader from './QueryHeader';
export interface CloudWatchLogsQueryFieldProps
extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>,
Themeable2 {
absoluteRange: AbsoluteTimeRange;
onLabelsRefresh?: () => void;
ExtraFieldElement?: ReactNode;
@ -32,6 +43,10 @@ const rowGap = css`
gap: 3px;
`;
const addPaddingToButton = css`
padding: 1px 4px;
`;
interface State {
hint:
| {
@ -44,7 +59,7 @@ interface State {
| undefined;
}
export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogsQueryFieldProps, State> {
class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogsQueryFieldProps, State> {
state: State = {
hint: undefined,
};
@ -112,7 +127,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
};
render() {
const { onRunQuery, onChange, ExtraFieldElement, data, query, datasource } = this.props;
const { onRunQuery, onChange, ExtraFieldElement, data, query, datasource, theme } = this.props;
const { region, refId, expression, logGroupNames } = query;
const { hint } = this.state;
@ -167,9 +182,13 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
<div className="query-row-break">
<div className="text-warning">
{hint.message}
<a className="text-link muted" onClick={hint.fix.action}>
<button
type="button"
className={cx(clearButtonStyles(theme), 'text-link', 'muted', addPaddingToButton)}
onClick={hint.fix.action}
>
{hint.fix.label}
</a>
</button>
</div>
</div>
)}
@ -182,3 +201,5 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
);
}
}
export default withTheme2(CloudWatchLogsQueryField);

View File

@ -2,7 +2,7 @@ import { size } from 'lodash';
import React, { useCallback, useState } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { InlineLabel, Select, InlineFormLabel, InlineSwitch, Icon } from '@grafana/ui';
import { InlineLabel, Select, InlineFormLabel, InlineSwitch, Icon, clearButtonStyles, useStyles2 } from '@grafana/ui';
import { OpenTsdbFilter, OpenTsdbQuery } from '../types';
@ -23,6 +23,8 @@ export function FilterSection({
filterTypes,
suggestTagValues,
}: FilterSectionProps) {
const buttonStyles = useStyles2(clearButtonStyles);
const [tagKeys, updTagKeys] = useState<Array<SelectableValue<string>>>();
const [keyIsLoading, updKeyIsLoading] = useState<boolean>();
@ -121,20 +123,25 @@ export function FilterSection({
return (
<InlineFormLabel key={idx} width="auto" data-testid={testIds.list + idx}>
{fil.tagk} = {fil.type}({fil.filter}), groupBy = {'' + fil.groupBy}
<a onClick={() => editFilter(fil, idx)}>
<button type="button" className={buttonStyles} onClick={() => editFilter(fil, idx)}>
<Icon name={'pen'} />
</a>
<a onClick={() => removeFilter(idx)} data-testid={testIds.remove}>
</button>
<button
type="button"
className={buttonStyles}
onClick={() => removeFilter(idx)}
data-testid={testIds.remove}
>
<Icon name={'times'} />
</a>
</button>
</InlineFormLabel>
);
})}
{!addFilterMode && (
<label className="gf-form-label query-keyword">
<a onClick={changeAddFilterMode} data-testid={testIds.open}>
<button type="button" className={buttonStyles} onClick={changeAddFilterMode} data-testid={testIds.open}>
<Icon name={'plus'} />
</a>
</button>
</label>
)}
</div>
@ -224,10 +231,12 @@ export function FilterSection({
)}
<label className="gf-form-label">
<a onClick={addFilter}>add filter</a>
<a onClick={changeAddFilterMode}>
<button type="button" className={buttonStyles} onClick={addFilter}>
add filter
</button>
<button type="button" className={buttonStyles} onClick={changeAddFilterMode}>
<Icon name={'times'} />
</a>
</button>
</label>
</div>
</div>

View File

@ -2,7 +2,7 @@ import { has, size } from 'lodash';
import React, { useCallback, useState } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { Select, InlineFormLabel, Icon } from '@grafana/ui';
import { Select, InlineFormLabel, Icon, clearButtonStyles, useStyles2 } from '@grafana/ui';
import { OpenTsdbQuery } from '../types';
@ -23,6 +23,8 @@ export function TagSection({
suggestTagValues,
tsdbVersion,
}: TagSectionProps) {
const buttonStyles = useStyles2(clearButtonStyles);
const [tagKeys, updTagKeys] = useState<Array<SelectableValue<string>>>();
const [keyIsLoading, updKeyIsLoading] = useState<boolean>();
@ -119,20 +121,25 @@ export function TagSection({
return (
<InlineFormLabel key={idx} width="auto" data-testid={testIds.list + idx}>
{tagKey}={tagValue}
<a onClick={() => editTag(tagKey, tagValue)}>
<button type="button" className={buttonStyles} onClick={() => editTag(tagKey, tagValue)}>
<Icon name={'pen'} />
</a>
<a onClick={() => removeTag(tagKey)} data-testid={testIds.remove}>
</button>
<button
type="button"
className={buttonStyles}
onClick={() => removeTag(tagKey)}
data-testid={testIds.remove}
>
<Icon name={'times'} />
</a>
</button>
</InlineFormLabel>
);
})}
{!addTagMode && (
<label className="gf-form-label query-keyword">
<a onClick={changeAddTagMode} data-testid={testIds.open}>
<button type="button" className={buttonStyles} onClick={changeAddTagMode} data-testid={testIds.open}>
<Icon name={'plus'} />
</a>
</button>
</label>
)}
</div>
@ -195,10 +202,12 @@ export function TagSection({
)}
<label className="gf-form-label">
<a onClick={addTag}>add tag</a>
<a onClick={changeAddTagMode}>
<button type="button" className={buttonStyles} onClick={addTag}>
add tag
</button>
<button type="button" className={buttonStyles} onClick={changeAddTagMode}>
<Icon name={'times'} />
</a>
</button>
</label>
</div>
</div>

View File

@ -1,3 +1,4 @@
import { cx } from '@emotion/css';
import { LanguageMap, languages as prismLanguages } from 'prismjs';
import React, { ReactNode } from 'react';
import { Plugin } from 'slate';
@ -13,6 +14,9 @@ import {
SuggestionsState,
TypeaheadInput,
TypeaheadOutput,
Themeable2,
withTheme2,
clearButtonStyles,
} from '@grafana/ui';
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
import {
@ -74,7 +78,7 @@ export function willApplySuggestion(suggestion: string, { typeaheadContext, type
return suggestion;
}
interface PromQueryFieldProps extends QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions> {
interface PromQueryFieldProps extends QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>, Themeable2 {
ExtraFieldElement?: ReactNode;
'data-testid'?: string;
}
@ -277,6 +281,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
query,
ExtraFieldElement,
history = [],
theme,
} = this.props;
const { labelBrowserVisible, syntaxLoaded, hint } = this.state;
@ -333,9 +338,13 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
<div className="prom-query-field-info text-warning">
{hint.label}{' '}
{hint.fix ? (
<a className="text-link muted" onClick={this.onClickHintFix}>
<button
type="button"
className={cx(clearButtonStyles(theme), 'text-link', 'muted')}
onClick={this.onClickHintFix}
>
{hint.fix.label}
</a>
</button>
) : null}
</div>
</div>
@ -348,4 +357,4 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
}
}
export default PromQueryField;
export default withTheme2(PromQueryField);

View File

@ -18,7 +18,13 @@ export const TutorialCard: FC<Props> = ({ card }) => {
const styles = getStyles(theme, card.done);
return (
<a className={styles.card} onClick={(event: MouseEvent<HTMLAnchorElement>) => handleTutorialClick(event, card)}>
<a
className={styles.card}
target="_blank"
rel="noreferrer"
href={`${card.href}?utm_source=grafana_gettingstarted`}
onClick={(event: MouseEvent<HTMLAnchorElement>) => handleTutorialClick(event, card)}
>
<div className={cardContent}>
<div className={styles.type}>{card.type}</div>
<div className={styles.heading}>{card.done ? 'complete' : card.heading}</div>
@ -36,7 +42,6 @@ const handleTutorialClick = (event: MouseEvent<HTMLAnchorElement>, card: Tutoria
if (!isSet) {
store.set(card.key, true);
}
window.open(`${card.href}?utm_source=grafana_gettingstarted`, '_blank');
};
const getStyles = stylesFactory((theme: GrafanaTheme, complete: boolean) => {

View File

@ -150,7 +150,8 @@ class LegendSeriesLabel extends PureComponent<LegendSeriesLabelProps & LegendSer
onColorChange={onColorChange}
onToggleAxis={onToggleAxis}
/>,
<a
<button
type="button"
className="graph-legend-alias pointer"
title={label}
key="label"
@ -158,7 +159,7 @@ class LegendSeriesLabel extends PureComponent<LegendSeriesLabelProps & LegendSer
aria-label={selectors.components.Panels.Visualization.Graph.Legend.legendItemAlias(label)}
>
{label}
</a>,
</button>,
];
}
}

View File

@ -60,6 +60,11 @@
}
}
.graph-legend-alias {
background: transparent;
border: none;
}
.graph-legend-content {
position: relative;
}

View File

@ -40,6 +40,8 @@
font-size: $font-size-h5;
font-weight: $font-weight-semi-bold;
color: $text-color;
background: transparent;
border: none;
.fa {
color: $text-muted;
@ -54,9 +56,11 @@
opacity: 0;
transition: 200ms opacity ease-in 200ms;
a {
button {
color: $text-color-weak;
padding-left: $spacer;
background: transparent;
border: none;
&:hover {
color: $link-hover-color;