Accessibility: Enable jsx-a11y/mouse-events-have-key-events (#58050)

* implement VizLegend keyboard accessibility

* add onBlur/onFocus
This commit is contained in:
Ashley Harrison 2022-11-03 10:22:50 +00:00 committed by GitHub
parent e6b088fbf5
commit f76ba90078
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 90 additions and 55 deletions

View File

@ -75,7 +75,6 @@
// 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/click-events-have-key-events": "off",
"jsx-a11y/mouse-events-have-key-events": "off",
"jsx-a11y/no-autofocus": [
"error",
{

View File

@ -28,8 +28,11 @@ export function VizLegend<T>({
}: LegendProps<T>) {
const { eventBus, onToggleSeriesVisibility, onToggleLegendSort } = usePanelContext();
const onMouseEnter = useCallback(
(item: VizLegendItem, event: React.MouseEvent<HTMLElement, MouseEvent>) => {
const onMouseOver = useCallback(
(
item: VizLegendItem,
event: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.FocusEvent<HTMLButtonElement>
) => {
eventBus?.publish({
type: DataHoverEvent.type,
payload: {
@ -44,7 +47,10 @@ export function VizLegend<T>({
);
const onMouseOut = useCallback(
(item: VizLegendItem, event: React.MouseEvent<HTMLElement, MouseEvent>) => {
(
item: VizLegendItem,
event: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.FocusEvent<HTMLButtonElement>
) => {
eventBus?.publish({
type: DataHoverClearEvent.type,
payload: {
@ -59,7 +65,7 @@ export function VizLegend<T>({
);
const onLegendLabelClick = useCallback(
(item: VizLegendItem, event: React.MouseEvent<HTMLElement, MouseEvent>) => {
(item: VizLegendItem, event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (onLabelClick) {
onLabelClick(item, event);
}
@ -86,7 +92,7 @@ export function VizLegend<T>({
sortDesc={sortDesc}
onLabelClick={onLegendLabelClick}
onToggleSort={onToggleSort || onToggleLegendSort}
onLabelMouseEnter={onMouseEnter}
onLabelMouseOver={onMouseOver}
onLabelMouseOut={onMouseOut}
itemRenderer={itemRenderer}
readonly={readonly}
@ -98,7 +104,7 @@ export function VizLegend<T>({
className={className}
items={items}
placement={placement}
onLabelMouseEnter={onMouseEnter}
onLabelMouseOver={onMouseOver}
onLabelMouseOut={onMouseOut}
onLabelClick={onLegendLabelClick}
itemRenderer={itemRenderer}

View File

@ -18,7 +18,7 @@ export interface Props<T> extends VizLegendBaseProps<T> {}
export const VizLegendList = <T extends unknown>({
items,
itemRenderer,
onLabelMouseEnter,
onLabelMouseOver,
onLabelMouseOut,
onLabelClick,
placement,
@ -33,7 +33,7 @@ export const VizLegendList = <T extends unknown>({
<VizLegendListItem
item={item}
onLabelClick={onLabelClick}
onLabelMouseEnter={onLabelMouseEnter}
onLabelMouseOver={onLabelMouseOver}
onLabelMouseOut={onLabelMouseOut}
readonly={readonly}
/>

View File

@ -13,9 +13,15 @@ import { VizLegendItem } from './types';
export interface Props<T> {
item: VizLegendItem<T>;
className?: string;
onLabelClick?: (item: VizLegendItem<T>, event: React.MouseEvent<HTMLDivElement>) => void;
onLabelMouseEnter?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
onLabelMouseOut?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
onLabelClick?: (item: VizLegendItem<T>, event: React.MouseEvent<HTMLButtonElement>) => void;
onLabelMouseOver?: (
item: VizLegendItem,
event: React.MouseEvent<HTMLButtonElement> | React.FocusEvent<HTMLButtonElement>
) => void;
onLabelMouseOut?: (
item: VizLegendItem,
event: React.MouseEvent<HTMLButtonElement> | React.FocusEvent<HTMLButtonElement>
) => void;
readonly?: boolean;
}
@ -25,24 +31,24 @@ export interface Props<T> {
export const VizLegendListItem = <T = unknown,>({
item,
onLabelClick,
onLabelMouseEnter,
onLabelMouseOver,
onLabelMouseOut,
className,
readonly,
}: Props<T>) => {
const styles = useStyles2(getStyles);
const onMouseEnter = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (onLabelMouseEnter) {
onLabelMouseEnter(item, event);
const onMouseOver = useCallback(
(event: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.FocusEvent<HTMLButtonElement>) => {
if (onLabelMouseOver) {
onLabelMouseOver(item, event);
}
},
[item, onLabelMouseEnter]
[item, onLabelMouseOver]
);
const onMouseOut = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
(event: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.FocusEvent<HTMLButtonElement>) => {
if (onLabelMouseOut) {
onLabelMouseOut(item, event);
}
@ -51,7 +57,7 @@ export const VizLegendListItem = <T = unknown,>({
);
const onClick = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (onLabelClick) {
onLabelClick(item, event);
}
@ -65,14 +71,18 @@ export const VizLegendListItem = <T = unknown,>({
aria-label={selectors.components.VizLegend.seriesName(item.label)}
>
<VizLegendSeriesIcon seriesName={item.label} color={item.color} gradient={item.gradient} readonly={readonly} />
<div
onMouseEnter={onMouseEnter}
<button
disabled={readonly}
type="button"
onBlur={onMouseOut}
onFocus={onMouseOver}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
onClick={!readonly ? onClick : undefined}
className={cx(styles.label, !readonly && styles.clickable)}
onClick={onClick}
className={styles.label}
>
{item.label}
</div>
</button>
{item.getDisplayValues && <VizLegendStatsList stats={item.getDisplayValues()} />}
</div>
@ -85,10 +95,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
label: css`
label: LegendLabel;
white-space: nowrap;
`,
clickable: css`
label: LegendClickabel;
cursor: pointer;
background: none;
border: none;
font-size: inherit;
padding: 0;
`,
itemDisabled: css`
label: LegendLabelDisabled;

View File

@ -21,7 +21,7 @@ export const VizLegendTable = <T extends unknown>({
className,
onToggleSort,
onLabelClick,
onLabelMouseEnter,
onLabelMouseOver,
onLabelMouseOut,
readonly,
}: VizLegendTableProps<T>): JSX.Element => {
@ -57,7 +57,7 @@ export const VizLegendTable = <T extends unknown>({
key={`${item.label}-${index}`}
item={item}
onLabelClick={onLabelClick}
onLabelMouseEnter={onLabelMouseEnter}
onLabelMouseOver={onLabelMouseOver}
onLabelMouseOut={onLabelMouseOut}
readonly={readonly}
/>

View File

@ -13,9 +13,15 @@ export interface Props {
key?: React.Key;
item: VizLegendItem;
className?: string;
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
onLabelMouseEnter?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
onLabelMouseOut?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLButtonElement>) => void;
onLabelMouseOver?: (
item: VizLegendItem,
event: React.MouseEvent<HTMLButtonElement> | React.FocusEvent<HTMLButtonElement>
) => void;
onLabelMouseOut?: (
item: VizLegendItem,
event: React.MouseEvent<HTMLButtonElement> | React.FocusEvent<HTMLButtonElement>
) => void;
readonly?: boolean;
}
@ -25,24 +31,24 @@ export interface Props {
export const LegendTableItem: React.FunctionComponent<Props> = ({
item,
onLabelClick,
onLabelMouseEnter,
onLabelMouseOver,
onLabelMouseOut,
className,
readonly,
}) => {
const styles = useStyles2(getStyles);
const onMouseEnter = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (onLabelMouseEnter) {
onLabelMouseEnter(item, event);
const onMouseOver = useCallback(
(event: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.FocusEvent<HTMLButtonElement>) => {
if (onLabelMouseOver) {
onLabelMouseOver(item, event);
}
},
[item, onLabelMouseEnter]
[item, onLabelMouseOver]
);
const onMouseOut = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
(event: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.FocusEvent<HTMLButtonElement>) => {
if (onLabelMouseOut) {
onLabelMouseOut(item, event);
}
@ -51,7 +57,7 @@ export const LegendTableItem: React.FunctionComponent<Props> = ({
);
const onClick = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (onLabelClick) {
onLabelClick(item, event);
}
@ -64,14 +70,18 @@ export const LegendTableItem: React.FunctionComponent<Props> = ({
<td>
<span className={styles.itemWrapper}>
<VizLegendSeriesIcon color={item.color} seriesName={item.label} readonly={readonly} />
<div
onMouseEnter={onMouseEnter}
<button
disabled={readonly}
type="button"
onBlur={onMouseOut}
onFocus={onMouseOver}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
onClick={!readonly ? onClick : undefined}
className={cx(styles.label, item.disabled && styles.labelDisabled, !readonly && styles.clickable)}
className={cx(styles.label, item.disabled && styles.labelDisabled)}
>
{item.label} {item.yAxis === 2 && <span className={styles.yAxisLabel}>(right y-axis)</span>}
</div>
</button>
</span>
</td>
{item.getDisplayValues &&
@ -108,15 +118,15 @@ const getStyles = (theme: GrafanaTheme2) => {
label: css`
label: LegendLabel;
white-space: nowrap;
background: none;
border: none;
font-size: inherit;
padding: 0;
`,
labelDisabled: css`
label: LegendLabelDisabled;
color: ${theme.colors.text.disabled};
`,
clickable: css`
label: LegendClickable;
cursor: pointer;
`,
itemWrapper: css`
display: flex;
white-space: nowrap;

View File

@ -13,10 +13,16 @@ export interface VizLegendBaseProps<T> {
className?: string;
items: Array<VizLegendItem<T>>;
seriesVisibilityChangeBehavior?: SeriesVisibilityChangeBehavior;
onLabelClick?: (item: VizLegendItem<T>, event: React.MouseEvent<HTMLElement>) => void;
onLabelClick?: (item: VizLegendItem<T>, event: React.MouseEvent<HTMLButtonElement>) => void;
itemRenderer?: (item: VizLegendItem<T>, index: number) => JSX.Element;
onLabelMouseEnter?: (item: VizLegendItem, event: React.MouseEvent<HTMLElement>) => void;
onLabelMouseOut?: (item: VizLegendItem, event: React.MouseEvent<HTMLElement>) => void;
onLabelMouseOver?: (
item: VizLegendItem,
event: React.MouseEvent<HTMLButtonElement> | React.FocusEvent<HTMLButtonElement>
) => void;
onLabelMouseOut?: (
item: VizLegendItem,
event: React.MouseEvent<HTMLButtonElement> | React.FocusEvent<HTMLButtonElement>
) => void;
readonly?: boolean;
}

View File

@ -147,8 +147,10 @@ function SpanBar({
return (
<div
className={cx(styles.wrapper, className)}
onBlur={setShortLabel}
onClick={onClick}
onMouseLeave={setShortLabel}
onFocus={setLongLabel}
onMouseOut={setShortLabel}
onMouseOver={setLongLabel}
aria-hidden
data-testid={selectors.components.TraceViewer.spanBar}

View File

@ -37,8 +37,10 @@ export function GraphiteFunctionEditor({ func }: FunctionEditorProps) {
return (
<div
className={cx(styles.container, { [styles.error]: func.def.unknown })}
onBlur={() => setIsMouseOver(false)}
onFocus={() => setIsMouseOver(true)}
onMouseOver={() => setIsMouseOver(true)}
onMouseLeave={() => setIsMouseOver(false)}
onMouseOut={() => setIsMouseOver(false)}
>
<HorizontalGroup spacing="none">
<FunctionEditor