mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
RelativeTimeRangePicker: correctly trap focus in overlay (#60984)
* correctly trap focus in relativetimerangepicker overlay * add underlay Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
This commit is contained in:
parent
772e8cbf60
commit
a9e39a108c
@ -1,5 +1,8 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { FormEvent, useCallback, useState } from 'react';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
import React, { FormEvent, useCallback, useRef, useState } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
|
||||
import { RelativeTimeRange, GrafanaTheme2, TimeOption } from '@grafana/data';
|
||||
@ -7,7 +10,6 @@ import { RelativeTimeRange, GrafanaTheme2, TimeOption } from '@grafana/data';
|
||||
import { useStyles2 } from '../../../themes';
|
||||
import { Trans, t } from '../../../utils/i18n';
|
||||
import { Button } from '../../Button';
|
||||
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
|
||||
import { Field } from '../../Forms/Field';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
@ -51,6 +53,12 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps) {
|
||||
const timeOption = mapRelativeTimeRangeToOption(timeRange);
|
||||
const [from, setFrom] = useState<InputState>({ value: timeOption.from, validation: isRangeValid(timeOption.from) });
|
||||
const [to, setTo] = useState<InputState>({ value: timeOption.to, validation: isRangeValid(timeOption.to) });
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { overlayProps, underlayProps } = useOverlay(
|
||||
{ onClose: () => setIsOpen(false), isDismissable: true, isOpen },
|
||||
ref
|
||||
);
|
||||
const { dialogProps } = useDialog({}, ref);
|
||||
|
||||
const [markerElement, setMarkerElement] = useState<HTMLDivElement | null>(null);
|
||||
const [selectorElement, setSelectorElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -116,57 +124,60 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps) {
|
||||
</button>
|
||||
{isOpen && (
|
||||
<Portal>
|
||||
<ClickOutsideWrapper includeButtonPress={false} onClick={onClose}>
|
||||
<div
|
||||
className={styles.content}
|
||||
ref={setSelectorElement}
|
||||
style={popper.styles.popper}
|
||||
{...popper.attributes}
|
||||
>
|
||||
<div className={styles.body}>
|
||||
<CustomScrollbar className={styles.leftSide} hideHorizontalTrack>
|
||||
<TimeRangeList
|
||||
title={t('time-picker.time-range.example-title', 'Example time ranges')}
|
||||
options={validOptions}
|
||||
onChange={onChangeTimeOption}
|
||||
value={timeOption}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
<div className={styles.rightSide}>
|
||||
<div className={styles.title}>
|
||||
<TimePickerTitle>
|
||||
<Tooltip content={<TooltipContent />} placement="bottom" theme="info">
|
||||
<div>
|
||||
<Trans i18nKey="time-picker.time-range.specify">
|
||||
Specify time range <Icon name="info-circle" />
|
||||
</Trans>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</TimePickerTitle>
|
||||
<div role="presentation" className={styles.backdrop} {...underlayProps} />
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<div ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<div
|
||||
className={styles.content}
|
||||
ref={setSelectorElement}
|
||||
style={popper.styles.popper}
|
||||
{...popper.attributes}
|
||||
>
|
||||
<div className={styles.body}>
|
||||
<CustomScrollbar className={styles.leftSide} hideHorizontalTrack>
|
||||
<TimeRangeList
|
||||
title={t('time-picker.time-range.example-title', 'Example time ranges')}
|
||||
options={validOptions}
|
||||
onChange={onChangeTimeOption}
|
||||
value={timeOption}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
<div className={styles.rightSide}>
|
||||
<div className={styles.title}>
|
||||
<TimePickerTitle>
|
||||
<Tooltip content={<TooltipContent />} placement="bottom" theme="info">
|
||||
<div>
|
||||
<Trans i18nKey="time-picker.time-range.specify">
|
||||
Specify time range <Icon name="info-circle" />
|
||||
</Trans>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</TimePickerTitle>
|
||||
</div>
|
||||
<Field label="From" invalid={!from.validation.isValid} error={from.validation.errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onBlur={() => setFrom({ ...from, validation: isRangeValid(from.value) })}
|
||||
onChange={(event) => setFrom({ ...from, value: event.currentTarget.value })}
|
||||
value={from.value}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="To" invalid={!to.validation.isValid} error={to.validation.errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onBlur={() => setTo({ ...to, validation: isRangeValid(to.value) })}
|
||||
onChange={(event) => setTo({ ...to, value: event.currentTarget.value })}
|
||||
value={to.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button aria-label="TimePicker submit button" onClick={onApply}>
|
||||
Apply time range
|
||||
</Button>
|
||||
</div>
|
||||
<Field label="From" invalid={!from.validation.isValid} error={from.validation.errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onBlur={() => setFrom({ ...from, validation: isRangeValid(from.value) })}
|
||||
onChange={(event) => setFrom({ ...from, value: event.currentTarget.value })}
|
||||
value={from.value}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="To" invalid={!to.validation.isValid} error={to.validation.errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onBlur={() => setTo({ ...to, validation: isRangeValid(to.value) })}
|
||||
onChange={(event) => setTo({ ...to, value: event.currentTarget.value })}
|
||||
value={to.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button aria-label="TimePicker submit button" onClick={onApply}>
|
||||
Apply time range
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClickOutsideWrapper>
|
||||
</FocusScope>
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
@ -211,6 +222,14 @@ const getStyles = (fromError?: string, toError?: string) => (theme: GrafanaTheme
|
||||
const bodyHeight = bodyMinimumHeight + calculateErrorHeight(theme, fromError) + calculateErrorHeight(theme, toError);
|
||||
|
||||
return {
|
||||
backdrop: css`
|
||||
position: fixed;
|
||||
z-index: ${theme.zIndex.modalBackdrop};
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`,
|
||||
container: css`
|
||||
display: flex;
|
||||
position: relative;
|
||||
@ -246,7 +265,7 @@ const getStyles = (fromError?: string, toError?: string) => (theme: GrafanaTheme
|
||||
background: ${theme.colors.background.primary};
|
||||
box-shadow: ${theme.shadows.z3};
|
||||
position: absolute;
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
z-index: ${theme.zIndex.modal};
|
||||
width: 500px;
|
||||
top: 100%;
|
||||
border-radius: 2px;
|
||||
|
Loading…
Reference in New Issue
Block a user