fix: [Obs Synthetics > Monitor detail][KEYBOARD]: Thumbnail images must take keyboard focus, open modal on keypress, and manage focus correctly (#187446)

Closes: https://github.com/elastic/observability-dev/issues/3687

## Description

The synthetics monitors include thumbnail screenshots that open a larger
preview window. These thumbnails must take keyboard focus, manage the
`Enter` and `Space` keypresses to open the modal, and return focus to
the originating thumbnail when the modal is closed. Screenshots attached
below.

### Steps to recreate

1. Open the
[Synthetics](https://keep-serverless-fyzdg-f07c50.kb.eu-west-1.aws.qa.elastic.cloud/app/synthetics)
view
2. Create a monitor if none exist
3. Click on that monitor and navigate to the [full monitor
detail](8b88e937-f917-4f12-9325-8ab005cffea5?locationId=us_central_qa)
view
4. Click on a thumbnail and verify the modal opens
5. Press `ESC` or the Close "X" and then press `TAB` to verify focus is
not on the thumbnail

### What was changed?: 

1. Added `tabIndex=0` was for ScreenshotImage for handle keyboard
navigation
2. `ScreenshotImage` API was sightly changed: `onMouseEnter` ->
`onFocus`; `onMouseLeave` -> `onBlur`

### Screen: 


a68df4b0-71c7-47ec-add7-41536027613c
This commit is contained in:
Alexey Antonov 2024-07-11 17:46:03 +03:00 committed by GitHub
parent 490bbdb3f6
commit 2ebd0ed3c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 30 additions and 25 deletions

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import React, { useCallback, useState, MouseEvent } from 'react';
import React, { useCallback, useState } from 'react';
import { EuiPopover, useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { POPOVER_SCREENSHOT_SIZE, ScreenshotImageSize } from '../screenshot/screenshot_size';
import { JourneyScreenshotDialog } from '../screenshot/journey_screenshot_dialog';
import { ScreenshotImage } from '../screenshot/screenshot_image';
import { ScreenshotImage, ScreenshotImageProps } from '../screenshot/screenshot_image';
export interface StepImagePopoverProps {
timestamp?: string;
@ -47,24 +47,24 @@ export const JourneyScreenshotPreview: React.FC<StepImagePopoverProps> = ({
// Only render the dialog if the image is at least once clicked
const [isImageEverClick, setIsImageEverClicked] = useState(false);
const onMouseEnter = useCallback(
(_evt: MouseEvent<HTMLImageElement>) => {
const onImgFocus: ScreenshotImageProps['onFocus'] = useCallback(
(_evt) => {
setIsImagePopoverOpen(true);
},
[setIsImagePopoverOpen]
);
const onMouseLeave = useCallback(
(_evt: MouseEvent<HTMLImageElement>) => {
const onImgBlur: ScreenshotImageProps['onBlur'] = useCallback(
(_evt) => {
setIsImagePopoverOpen(false);
},
[setIsImagePopoverOpen]
);
const onImgClick = useCallback(
(evt: MouseEvent<HTMLImageElement>) => {
// needed to prevent propagation to the table row click
const onImgClick: ScreenshotImageProps['onClick'] = useCallback(
(evt) => {
evt.stopPropagation();
setIsImageEverClicked(true);
setIsImageDialogOpen(true);
setIsImagePopoverOpen(false);
@ -92,8 +92,8 @@ export const JourneyScreenshotPreview: React.FC<StepImagePopoverProps> = ({
unavailableMessage={unavailableMessage}
borderColor={isStepFailed ? euiTheme.colors.danger : undefined}
borderRadius={borderRadius}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onImgFocus}
onBlur={onImgBlur}
onClick={onImgClick}
/>
);

View file

@ -133,10 +133,6 @@ export const JourneyScreenshotDialog = ({
animateLoading={false}
hasBorder={false}
size={'full'}
onClick={(evt) => {
// for table row click to work
evt.stopPropagation();
}}
/>
</ModalBodyStyled>

View file

@ -5,14 +5,16 @@
* 2.0.
*/
import React, { useState, MouseEventHandler } from 'react';
import { useEuiTheme, EuiThemeComputed } from '@elastic/eui';
import React, { useState } from 'react';
import { useEuiTheme, EuiThemeComputed, keys } from '@elastic/eui';
import { EmptyThumbnail } from './empty_thumbnail';
import { getConfinedScreenshotSize, ScreenshotImageSize } from './screenshot_size';
const DEFAULT_SIZE: [number, number] = [512, 512];
type ScreenshotImageCallback = (e: { stopPropagation(): void }) => void;
export interface ScreenshotImageProps {
label?: string;
isLoading: boolean;
@ -22,9 +24,9 @@ export interface ScreenshotImageProps {
borderColor?: EuiThemeComputed['border']['color'];
borderRadius?: string | number;
hasBorder?: boolean;
onMouseEnter?: MouseEventHandler<HTMLImageElement>;
onMouseLeave?: MouseEventHandler<HTMLImageElement>;
onClick?: MouseEventHandler<HTMLImageElement>;
onFocus?: ScreenshotImageCallback;
onBlur?: ScreenshotImageCallback;
onClick?: ScreenshotImageCallback;
}
export const ScreenshotImage: React.FC<ScreenshotImageProps & { imgSrc?: string }> = ({
@ -37,8 +39,8 @@ export const ScreenshotImage: React.FC<ScreenshotImageProps & { imgSrc?: string
borderRadius,
hasBorder = true,
size = [100, 64],
onMouseEnter,
onMouseLeave,
onFocus,
onBlur,
onClick,
}) => {
const { euiTheme } = useEuiTheme();
@ -70,10 +72,17 @@ export const ScreenshotImage: React.FC<ScreenshotImageProps & { imgSrc?: string
];
setNaturalSize(updatedSize);
}}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseEnter={onFocus}
onMouseLeave={onBlur}
onFocus={onFocus}
onBlur={onBlur}
onClick={onClick}
onKeyDown={undefined}
onKeyDown={(evt) => {
if (onClick && evt.key === keys.ENTER) {
onClick(evt);
}
}}
tabIndex={onClick ? 0 : undefined}
/>
) : (
<EmptyThumbnail