TimePicker accessibility: remove stopPropagation from everywhere (#60404)

* remove stopPropagation from everywhere

* improve keyboard a11y in DateTimePicker
This commit is contained in:
Ashley Harrison 2022-12-21 13:04:47 +00:00 committed by GitHub
parent 96b7fb15de
commit 78e12b140e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 46 additions and 51 deletions

View File

@ -1,12 +1,15 @@
import { css, cx } from '@emotion/css';
import React, { FC, FormEvent, ReactNode, useCallback, useEffect, useState } from 'react';
import { useDialog } from '@react-aria/dialog';
import { FocusScope } from '@react-aria/focus';
import { useOverlay } from '@react-aria/overlays';
import React, { FC, FormEvent, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import Calendar from 'react-calendar';
import { usePopper } from 'react-popper';
import { useMedia } from 'react-use';
import { dateTimeFormat, DateTime, dateTime, GrafanaTheme2, isDateTime } from '@grafana/data';
import { Button, ClickOutsideWrapper, HorizontalGroup, Icon, InlineField, Input, Portal } from '../..';
import { Button, HorizontalGroup, Icon, InlineField, Input, Portal } from '../..';
import { useStyles2, useTheme2 } from '../../../themes';
import { getModalStyles } from '../../Modal/getModalStyles';
import { TimeOfDayPicker } from '../TimeOfDayPicker';
@ -24,11 +27,16 @@ export interface Props {
maxDate?: Date;
}
const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopPropagation();
export const DateTimePicker: FC<Props> = ({ date, maxDate, label, onChange }) => {
const [isOpen, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const { overlayProps, underlayProps } = useOverlay(
{ onClose: () => setOpen(false), isDismissable: true, isOpen },
ref
);
const { dialogProps } = useDialog({}, ref);
const theme = useTheme2();
const { modalBackdrop } = getModalStyles(theme);
const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.values.lg}px)`);
@ -70,26 +78,35 @@ export const DateTimePicker: FC<Props> = ({ date, maxDate, label, onChange }) =>
{isOpen ? (
isFullscreen ? (
<Portal>
<ClickOutsideWrapper onClick={() => setOpen(false)}>
<DateTimeCalendar
date={date}
onChange={onApply}
isFullscreen={true}
onClose={() => setOpen(false)}
maxDate={maxDate}
ref={setSelectorElement}
style={popper.styles.popper}
/>
</ClickOutsideWrapper>
<FocusScope contain autoFocus restoreFocus>
<div ref={ref} {...overlayProps} {...dialogProps}>
<DateTimeCalendar
date={date}
onChange={onApply}
isFullscreen={true}
onClose={() => setOpen(false)}
maxDate={maxDate}
ref={setSelectorElement}
style={popper.styles.popper}
/>
</div>
</FocusScope>
</Portal>
) : (
<Portal>
<ClickOutsideWrapper onClick={() => setOpen(false)}>
<div className={styles.modal} onClick={stopPropagation}>
<DateTimeCalendar date={date} onChange={onApply} isFullscreen={false} onClose={() => setOpen(false)} />
<div className={modalBackdrop} {...underlayProps} />
<FocusScope contain autoFocus restoreFocus>
<div ref={ref} {...overlayProps} {...dialogProps}>
<div className={styles.modal}>
<DateTimeCalendar
date={date}
onChange={onApply}
isFullscreen={false}
onClose={() => setOpen(false)}
/>
</div>
</div>
<div className={modalBackdrop} onClick={stopPropagation} />
</ClickOutsideWrapper>
</FocusScope>
</Portal>
)
) : null}
@ -142,16 +159,6 @@ const DateTimeInput = React.forwardRef<HTMLInputElement, InputProps>(
});
}, []);
const onFocus = useCallback(
(event: FormEvent<HTMLElement>) => {
if (!isFullscreen) {
return;
}
onOpen(event);
},
[isFullscreen, onOpen]
);
const onBlur = useCallback(() => {
if (isDateTime(internalDate.value)) {
onChange(dateTime(internalDate.value));
@ -162,18 +169,15 @@ const DateTimeInput = React.forwardRef<HTMLInputElement, InputProps>(
return (
<InlineField
label={label}
onClick={stopPropagation}
invalid={!!(internalDate.value && internalDate.invalid)}
className={css`
margin-bottom: 0;
`}
>
<Input
onClick={stopPropagation}
onChange={onChangeDate}
addonAfter={icon}
value={internalDate.value}
onFocus={onFocus}
onBlur={onBlur}
data-testid="date-time-input"
placeholder="Select date/time"
@ -217,12 +221,7 @@ const DateTimeCalendar = React.forwardRef<HTMLDivElement, DateTimeCalendarProps>
}, []);
return (
<div
className={cx(styles.container, { [styles.fullScreen]: isFullscreen })}
style={style}
onClick={stopPropagation}
ref={ref}
>
<div className={cx(styles.container, { [styles.fullScreen]: isFullscreen })} style={style} ref={ref}>
<Calendar
next2Label={null}
prev2Label={null}
@ -271,9 +270,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
`,
modal: css`
position: fixed;
top: 25%;
left: 25%;
width: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: ${theme.zIndex.modal};
max-width: 280px;
`,

View File

@ -65,8 +65,6 @@ export interface TimePickerCalendarProps {
isReversed?: boolean;
}
const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopPropagation();
function TimePickerCalendar(props: TimePickerCalendarProps) {
const theme = useTheme2();
const { modalBackdrop } = getModalStyles(theme);
@ -95,7 +93,7 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
if (isFullscreen) {
return (
<FocusScope contain restoreFocus autoFocus>
<section className={styles.container} onClick={stopPropagation} ref={ref} {...overlayProps} {...dialogProps}>
<section className={styles.container} ref={ref} {...overlayProps} {...dialogProps}>
<Header {...props} />
<Body {...props} />
</section>
@ -105,9 +103,9 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
return (
<OverlayContainer>
<div className={modalBackdrop} onClick={stopPropagation} />
<div className={modalBackdrop} />
<FocusScope contain autoFocus restoreFocus>
<section className={styles.modal} onClick={stopPropagation} ref={ref} {...overlayProps} {...dialogProps}>
<section className={styles.modal} ref={ref} {...overlayProps} {...dialogProps}>
<div className={styles.content} aria-label={selectors.components.TimePicker.calendar.label}>
<Header {...props} />
<Body {...props} />

View File

@ -10,18 +10,16 @@ interface Props {
children?: React.ReactNode;
}
const stopPropagation = (event: React.MouseEvent) => event.stopPropagation();
export const TimeZoneGroup = (props: Props) => {
const { children, label } = props;
const styles = useStyles2(getStyles);
if (!label) {
return <div onClick={stopPropagation}>{children}</div>;
return <div>{children}</div>;
}
return (
<div onClick={stopPropagation}>
<div>
<div className={styles.header}>
<span className={styles.label}>{label}</span>
</div>