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:
		@@ -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;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user