mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -203,6 +203,12 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
onClick={this.toggleDetails}
|
onClick={this.toggleDetails}
|
||||||
onMouseEnter={this.onMouseEnter}
|
onMouseEnter={this.onMouseEnter}
|
||||||
onMouseLeave={this.onMouseLeave}
|
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 && (
|
{showDuplicates && (
|
||||||
<td className={styles.logsRowDuplicates}>
|
<td className={styles.logsRowDuplicates}>
|
||||||
@@ -241,6 +247,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
onUnpinLine={this.props.onUnpinLine}
|
onUnpinLine={this.props.onUnpinLine}
|
||||||
pinned={this.props.pinned}
|
pinned={this.props.pinned}
|
||||||
mouseIsOver={this.state.mouseIsOver}
|
mouseIsOver={this.state.mouseIsOver}
|
||||||
|
onBlur={this.onMouseLeave}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LogRowMessage
|
<LogRowMessage
|
||||||
@@ -256,6 +263,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
onUnpinLine={this.props.onUnpinLine}
|
onUnpinLine={this.props.onUnpinLine}
|
||||||
pinned={this.props.pinned}
|
pinned={this.props.pinned}
|
||||||
mouseIsOver={this.state.mouseIsOver}
|
mouseIsOver={this.state.mouseIsOver}
|
||||||
|
onBlur={this.onMouseLeave}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { SyntheticEvent, useCallback } from 'react';
|
import React, { FocusEvent, SyntheticEvent, useCallback } from 'react';
|
||||||
|
|
||||||
import { LogRowModel } from '@grafana/data';
|
import { LogRowModel } from '@grafana/data';
|
||||||
import { ClipboardButton, IconButton } from '@grafana/ui';
|
import { ClipboardButton, IconButton } from '@grafana/ui';
|
||||||
@@ -16,6 +16,7 @@ interface Props {
|
|||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
styles: LogRowStyles;
|
styles: LogRowStyles;
|
||||||
mouseIsOver: boolean;
|
mouseIsOver: boolean;
|
||||||
|
onBlur: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LogRowMenuCell = React.memo(
|
export const LogRowMenuCell = React.memo(
|
||||||
@@ -30,6 +31,7 @@ export const LogRowMenuCell = React.memo(
|
|||||||
showContextToggle,
|
showContextToggle,
|
||||||
styles,
|
styles,
|
||||||
mouseIsOver,
|
mouseIsOver,
|
||||||
|
onBlur,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const shouldShowContextToggle = showContextToggle ? showContextToggle(row) : false;
|
const shouldShowContextToggle = showContextToggle ? showContextToggle(row) : false;
|
||||||
const onLogRowClick = useCallback((e: SyntheticEvent) => {
|
const onLogRowClick = useCallback((e: SyntheticEvent) => {
|
||||||
@@ -42,11 +44,23 @@ export const LogRowMenuCell = React.memo(
|
|||||||
},
|
},
|
||||||
[onOpenContext, row]
|
[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]);
|
const getLogText = useCallback(() => logText, [logText]);
|
||||||
return (
|
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
|
// 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 && (
|
{pinned && !mouseIsOver && (
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.unPinButton}
|
className={styles.unPinButton}
|
||||||
@@ -56,6 +70,7 @@ export const LogRowMenuCell = React.memo(
|
|||||||
tooltip="Unpin line"
|
tooltip="Unpin line"
|
||||||
tooltipPlacement="top"
|
tooltipPlacement="top"
|
||||||
aria-label="Unpin line"
|
aria-label="Unpin line"
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mouseIsOver && (
|
{mouseIsOver && (
|
||||||
@@ -68,6 +83,7 @@ export const LogRowMenuCell = React.memo(
|
|||||||
tooltip="Show context"
|
tooltip="Show context"
|
||||||
tooltipPlacement="top"
|
tooltipPlacement="top"
|
||||||
aria-label="Show context"
|
aria-label="Show context"
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ClipboardButton
|
<ClipboardButton
|
||||||
@@ -79,6 +95,7 @@ export const LogRowMenuCell = React.memo(
|
|||||||
getText={getLogText}
|
getText={getLogText}
|
||||||
tooltip="Copy to clipboard"
|
tooltip="Copy to clipboard"
|
||||||
tooltipPlacement="top"
|
tooltipPlacement="top"
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
{pinned && onUnpinLine && (
|
{pinned && onUnpinLine && (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -89,6 +106,7 @@ export const LogRowMenuCell = React.memo(
|
|||||||
tooltip="Unpin line"
|
tooltip="Unpin line"
|
||||||
tooltipPlacement="top"
|
tooltipPlacement="top"
|
||||||
aria-label="Unpin line"
|
aria-label="Unpin line"
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!pinned && onPinLine && (
|
{!pinned && onPinLine && (
|
||||||
@@ -100,6 +118,7 @@ export const LogRowMenuCell = React.memo(
|
|||||||
tooltip="Pin line"
|
tooltip="Pin line"
|
||||||
tooltipPlacement="top"
|
tooltipPlacement="top"
|
||||||
aria-label="Pin line"
|
aria-label="Pin line"
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{onPermalinkClick && row.rowId !== undefined && row.uid && (
|
{onPermalinkClick && row.rowId !== undefined && row.uid && (
|
||||||
@@ -110,6 +129,7 @@ export const LogRowMenuCell = React.memo(
|
|||||||
size="md"
|
size="md"
|
||||||
name="share-alt"
|
name="share-alt"
|
||||||
onClick={() => onPermalinkClick(row)}
|
onClick={() => onPermalinkClick(row)}
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const setup = (propOverrides?: Partial<ComponentProps<typeof LogRowMessage>>, ro
|
|||||||
app: CoreApp.Explore,
|
app: CoreApp.Explore,
|
||||||
styles,
|
styles,
|
||||||
mouseIsOver: true,
|
mouseIsOver: true,
|
||||||
|
onBlur: jest.fn(),
|
||||||
...(propOverrides || {}),
|
...(propOverrides || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface Props {
|
|||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
styles: LogRowStyles;
|
styles: LogRowStyles;
|
||||||
mouseIsOver: boolean;
|
mouseIsOver: boolean;
|
||||||
|
onBlur: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LogMessageProps {
|
interface LogMessageProps {
|
||||||
@@ -75,10 +76,11 @@ export const LogRowMessage = React.memo((props: Props) => {
|
|||||||
onPinLine,
|
onPinLine,
|
||||||
pinned,
|
pinned,
|
||||||
mouseIsOver,
|
mouseIsOver,
|
||||||
|
onBlur,
|
||||||
} = props;
|
} = props;
|
||||||
const { hasAnsi, raw } = row;
|
const { hasAnsi, raw } = row;
|
||||||
const restructuredEntry = useMemo(() => restructureLog(raw, prettifyLogMessage), [raw, prettifyLogMessage]);
|
const restructuredEntry = useMemo(() => restructureLog(raw, prettifyLogMessage), [raw, prettifyLogMessage]);
|
||||||
const shouldShowMenu = useMemo(() => mouseIsOver || pinned, [mouseIsOver, pinned]);
|
const shouldShowMenu = useMemo(() => mouseIsOver || pinned || true, [mouseIsOver, pinned]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
@@ -105,6 +107,7 @@ export const LogRowMessage = React.memo((props: Props) => {
|
|||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
mouseIsOver={mouseIsOver}
|
mouseIsOver={mouseIsOver}
|
||||||
|
onBlur={onBlur}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const setup = (propOverrides: Partial<Props> = {}, detectedFields = ['place', 'p
|
|||||||
styles,
|
styles,
|
||||||
detectedFields,
|
detectedFields,
|
||||||
mouseIsOver: true,
|
mouseIsOver: true,
|
||||||
|
onBlur: jest.fn(),
|
||||||
...propOverrides,
|
...propOverrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface Props {
|
|||||||
onUnpinLine?: (row: LogRowModel) => void;
|
onUnpinLine?: (row: LogRowModel) => void;
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
mouseIsOver: boolean;
|
mouseIsOver: boolean;
|
||||||
|
onBlur: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LogRowMessageDisplayedFields = React.memo((props: Props) => {
|
export const LogRowMessageDisplayedFields = React.memo((props: Props) => {
|
||||||
@@ -65,8 +66,8 @@ export const LogRowMessageDisplayedFields = React.memo((props: Props) => {
|
|||||||
row={row}
|
row={row}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
{...rest}
|
|
||||||
mouseIsOver={mouseIsOver}
|
mouseIsOver={mouseIsOver}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user