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 // we should remove the corresponding line and fix them one by one
// any marked "error" contain specific overrides we'll need to keep // any marked "error" contain specific overrides we'll need to keep
"jsx-a11y/click-events-have-key-events": "off", "jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/mouse-events-have-key-events": "off",
"jsx-a11y/no-autofocus": [ "jsx-a11y/no-autofocus": [
"error", "error",
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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