Logs: Improved keyboard accessibility of log rows menu (#72686)

* Log Row Menu Cell: improve accessibility

* Explain implementation in comments

* Make onBlur mandatory
This commit is contained in:
Matias Chomicki 2023-08-02 18:36:53 +02:00 committed by GitHub
parent ad2705fa0b
commit 9c6a9a3977
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 39 additions and 5 deletions

View File

@ -203,6 +203,12 @@ class UnThemedLogRow extends PureComponent<Props, State> {
onClick={this.toggleDetails}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
/**
* For better accessibility support, we listen to the onFocus event here (to display the LogRowMenuCell), and
* to onBlur event in the LogRowMenuCell (to hide it). This way, the LogRowMenuCell is displayed when the user navigates
* using the keyboard.
*/
onFocus={this.onMouseEnter}
>
{showDuplicates && (
<td className={styles.logsRowDuplicates}>
@ -241,6 +247,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
onUnpinLine={this.props.onUnpinLine}
pinned={this.props.pinned}
mouseIsOver={this.state.mouseIsOver}
onBlur={this.onMouseLeave}
/>
) : (
<LogRowMessage
@ -256,6 +263,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
onUnpinLine={this.props.onUnpinLine}
pinned={this.props.pinned}
mouseIsOver={this.state.mouseIsOver}
onBlur={this.onMouseLeave}
/>
)}
</tr>

View File

@ -1,4 +1,4 @@
import React, { SyntheticEvent, useCallback } from 'react';
import React, { FocusEvent, SyntheticEvent, useCallback } from 'react';
import { LogRowModel } from '@grafana/data';
import { ClipboardButton, IconButton } from '@grafana/ui';
@ -16,6 +16,7 @@ interface Props {
pinned?: boolean;
styles: LogRowStyles;
mouseIsOver: boolean;
onBlur: () => void;
}
export const LogRowMenuCell = React.memo(
@ -30,6 +31,7 @@ export const LogRowMenuCell = React.memo(
showContextToggle,
styles,
mouseIsOver,
onBlur,
}: Props) => {
const shouldShowContextToggle = showContextToggle ? showContextToggle(row) : false;
const onLogRowClick = useCallback((e: SyntheticEvent) => {
@ -42,11 +44,23 @@ export const LogRowMenuCell = React.memo(
},
[onOpenContext, row]
);
/**
* For better accessibility support, we listen to the onBlur event here (to hide this component), and
* to onFocus in LogRow (to show this component).
*/
const handleBlur = useCallback(
(e: FocusEvent) => {
if (!e.currentTarget.contains(e.relatedTarget) && onBlur) {
onBlur();
}
},
[onBlur]
);
const getLogText = useCallback(() => logText, [logText]);
return (
// TODO: fix keyboard a11y
// We keep this click listener here to prevent the row from being selected when clicking on the menu.
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<span className={`log-row-menu ${styles.rowMenu}`} onClick={onLogRowClick}>
<span className={`log-row-menu ${styles.rowMenu}`} onClick={onLogRowClick} onBlur={handleBlur}>
{pinned && !mouseIsOver && (
<IconButton
className={styles.unPinButton}
@ -56,6 +70,7 @@ export const LogRowMenuCell = React.memo(
tooltip="Unpin line"
tooltipPlacement="top"
aria-label="Unpin line"
tabIndex={0}
/>
)}
{mouseIsOver && (
@ -68,6 +83,7 @@ export const LogRowMenuCell = React.memo(
tooltip="Show context"
tooltipPlacement="top"
aria-label="Show context"
tabIndex={0}
/>
)}
<ClipboardButton
@ -79,6 +95,7 @@ export const LogRowMenuCell = React.memo(
getText={getLogText}
tooltip="Copy to clipboard"
tooltipPlacement="top"
tabIndex={0}
/>
{pinned && onUnpinLine && (
<IconButton
@ -89,6 +106,7 @@ export const LogRowMenuCell = React.memo(
tooltip="Unpin line"
tooltipPlacement="top"
aria-label="Unpin line"
tabIndex={0}
/>
)}
{!pinned && onPinLine && (
@ -100,6 +118,7 @@ export const LogRowMenuCell = React.memo(
tooltip="Pin line"
tooltipPlacement="top"
aria-label="Pin line"
tabIndex={0}
/>
)}
{onPermalinkClick && row.rowId !== undefined && row.uid && (
@ -110,6 +129,7 @@ export const LogRowMenuCell = React.memo(
size="md"
name="share-alt"
onClick={() => onPermalinkClick(row)}
tabIndex={0}
/>
)}
</>

View File

@ -19,6 +19,7 @@ const setup = (propOverrides?: Partial<ComponentProps<typeof LogRowMessage>>, ro
app: CoreApp.Explore,
styles,
mouseIsOver: true,
onBlur: jest.fn(),
...(propOverrides || {}),
};

View File

@ -22,6 +22,7 @@ interface Props {
pinned?: boolean;
styles: LogRowStyles;
mouseIsOver: boolean;
onBlur: () => void;
}
interface LogMessageProps {
@ -75,10 +76,11 @@ export const LogRowMessage = React.memo((props: Props) => {
onPinLine,
pinned,
mouseIsOver,
onBlur,
} = props;
const { hasAnsi, raw } = row;
const restructuredEntry = useMemo(() => restructureLog(raw, prettifyLogMessage), [raw, prettifyLogMessage]);
const shouldShowMenu = useMemo(() => mouseIsOver || pinned, [mouseIsOver, pinned]);
const shouldShowMenu = useMemo(() => mouseIsOver || pinned || true, [mouseIsOver, pinned]);
return (
<>
{
@ -105,6 +107,7 @@ export const LogRowMessage = React.memo((props: Props) => {
pinned={pinned}
styles={styles}
mouseIsOver={mouseIsOver}
onBlur={onBlur}
/>
)}
</td>

View File

@ -21,6 +21,7 @@ const setup = (propOverrides: Partial<Props> = {}, detectedFields = ['place', 'p
styles,
detectedFields,
mouseIsOver: true,
onBlur: jest.fn(),
...propOverrides,
};

View File

@ -20,6 +20,7 @@ export interface Props {
onUnpinLine?: (row: LogRowModel) => void;
pinned?: boolean;
mouseIsOver: boolean;
onBlur: () => void;
}
export const LogRowMessageDisplayedFields = React.memo((props: Props) => {
@ -65,8 +66,8 @@ export const LogRowMessageDisplayedFields = React.memo((props: Props) => {
row={row}
styles={styles}
pinned={pinned}
{...rest}
mouseIsOver={mouseIsOver}
{...rest}
/>
)}
</td>