mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TimePicker accessibility: remove stopPropagation
from everywhere (#60404)
* remove stopPropagation from everywhere * improve keyboard a11y in DateTimePicker
This commit is contained in:
parent
96b7fb15de
commit
78e12b140e
@ -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;
|
||||
`,
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user