{"version":3,"file":"TimeRangePicker.mjs","sources":["../../../../src/components/DateTimePickers/TimeRangePicker.tsx"],"sourcesContent":["import { css, cx } from '@emotion/css';\nimport { useDialog } from '@react-aria/dialog';\nimport { FocusScope } from '@react-aria/focus';\nimport { useOverlay } from '@react-aria/overlays';\nimport { memo, createRef, useState, useEffect } from 'react';\n\nimport {\n  rangeUtil,\n  GrafanaTheme2,\n  dateTimeFormat,\n  timeZoneFormatUserFriendly,\n  TimeOption,\n  TimeRange,\n  TimeZone,\n  dateMath,\n} from '@grafana/data';\nimport { selectors } from '@grafana/e2e-selectors';\n\nimport { useStyles2 } from '../../themes/ThemeContext';\nimport { t, Trans } from '../../utils/i18n';\nimport { ButtonGroup } from '../Button';\nimport { getModalStyles } from '../Modal/getModalStyles';\nimport { getPortalContainer } from '../Portal/Portal';\nimport { ToolbarButton } from '../ToolbarButton';\nimport { Tooltip } from '../Tooltip/Tooltip';\n\nimport { TimePickerContent } from './TimeRangePicker/TimePickerContent';\nimport { WeekStart } from './WeekStartPicker';\nimport { quickOptions } from './options';\nimport { useTimeSync } from './utils/useTimeSync';\n\n/** @public */\nexport interface TimeRangePickerProps {\n  hideText?: boolean;\n  value: TimeRange;\n  timeZone?: TimeZone;\n  fiscalYearStartMonth?: number;\n\n  /**\n   * If you handle sync state between pickers yourself use this prop to pass the sync button component.\n   * Otherwise, a default one will show automatically if sync is possible.\n   */\n  timeSyncButton?: JSX.Element;\n\n  // Use to manually set the synced styles for the time range picker if you need to control the sync state yourself.\n  isSynced?: boolean;\n\n  // Use to manually set the initial sync state for the time range picker. It will use the current value to sync.\n  initialIsSynced?: boolean;\n\n  onChange: (timeRange: TimeRange) => void;\n  onChangeTimeZone: (timeZone: TimeZone) => void;\n  onChangeFiscalYearStartMonth?: (month: number) => void;\n  onMoveBackward: () => void;\n  onMoveForward: () => void;\n  onZoom: () => void;\n  onError?: (error?: string) => void;\n  history?: TimeRange[];\n  quickRanges?: TimeOption[];\n  hideQuickRanges?: boolean;\n  widthOverride?: number;\n  isOnCanvas?: boolean;\n  onToolbarTimePickerClick?: () => void;\n  /** Which day of the week the calendar should start on. Possible values: \"saturday\", \"sunday\" or \"monday\" */\n  weekStart?: WeekStart;\n}\n\nexport interface State {\n  isOpen: boolean;\n}\n\nexport function TimeRangePicker(props: TimeRangePickerProps) {\n  const [isOpen, setOpen] = useState(false);\n\n  const {\n    value,\n    onMoveBackward,\n    onMoveForward,\n    onZoom,\n    onError,\n    timeZone,\n    fiscalYearStartMonth,\n    history,\n    onChangeTimeZone,\n    onChangeFiscalYearStartMonth,\n    quickRanges,\n    hideQuickRanges,\n    widthOverride,\n    isOnCanvas,\n    onToolbarTimePickerClick,\n    weekStart,\n    initialIsSynced,\n  } = props;\n\n  const { onChangeWithSync, isSynced, timeSyncButton } = useTimeSync({\n    initialIsSynced,\n    value,\n    onChangeProp: props.onChange,\n    isSyncedProp: props.isSynced,\n    timeSyncButtonProp: props.timeSyncButton,\n  });\n\n  const onChange = (timeRange: TimeRange) => {\n    onChangeWithSync(timeRange);\n    setOpen(false);\n  };\n\n  useEffect(() => {\n    if (isOpen && onToolbarTimePickerClick) {\n      onToolbarTimePickerClick();\n    }\n  }, [isOpen, onToolbarTimePickerClick]);\n\n  const onToolbarButtonSwitch = () => {\n    setOpen((prevState) => !prevState);\n  };\n\n  const onClose = () => {\n    setOpen(false);\n  };\n\n  const overlayRef = createRef<HTMLElement>();\n  const buttonRef = createRef<HTMLElement>();\n  const { overlayProps, underlayProps } = useOverlay(\n    {\n      onClose,\n      isDismissable: true,\n      isOpen,\n      shouldCloseOnInteractOutside: (element) => {\n        const portalContainer = getPortalContainer();\n        return !buttonRef.current?.contains(element) && !portalContainer.contains(element);\n      },\n    },\n    overlayRef\n  );\n  const { dialogProps } = useDialog({}, overlayRef);\n\n  const styles = useStyles2(getStyles);\n  const { modalBackdrop } = useStyles2(getModalStyles);\n  const hasAbsolute = !rangeUtil.isRelativeTime(value.raw.from) || !rangeUtil.isRelativeTime(value.raw.to);\n\n  const variant = isSynced ? 'active' : isOnCanvas ? 'canvas' : 'default';\n\n  const isFromAfterTo = value?.to?.isBefore(value.from);\n  const timePickerIcon = isFromAfterTo ? 'exclamation-triangle' : 'clock-nine';\n\n  const currentTimeRange = formattedRange(value, timeZone, quickRanges);\n\n  return (\n    <ButtonGroup className={styles.container}>\n      {hasAbsolute && (\n        <ToolbarButton\n          aria-label={t('time-picker.range-picker.backwards-time-aria-label', 'Move time range backwards')}\n          variant={variant}\n          onClick={onMoveBackward}\n          icon=\"angle-left\"\n          type=\"button\"\n          narrow\n        />\n      )}\n\n      <Tooltip\n        ref={buttonRef}\n        content={<TimePickerTooltip timeRange={value} timeZone={timeZone} />}\n        placement=\"bottom\"\n        interactive\n      >\n        <ToolbarButton\n          data-testid={selectors.components.TimePicker.openButton}\n          aria-label={t('time-picker.range-picker.current-time-selected', 'Time range selected: {{currentTimeRange}}', {\n            currentTimeRange,\n          })}\n          aria-controls=\"TimePickerContent\"\n          onClick={onToolbarButtonSwitch}\n          icon={timePickerIcon}\n          isOpen={isOpen}\n          type=\"button\"\n          variant={variant}\n        >\n          <TimePickerButtonLabel {...props} />\n        </ToolbarButton>\n      </Tooltip>\n      {isOpen && (\n        <div data-testid={selectors.components.TimePicker.overlayContent}>\n          <div role=\"presentation\" className={cx(modalBackdrop, styles.backdrop)} {...underlayProps} />\n          <FocusScope contain autoFocus restoreFocus>\n            <section className={styles.content} ref={overlayRef} {...overlayProps} {...dialogProps}>\n              <TimePickerContent\n                timeZone={timeZone}\n                fiscalYearStartMonth={fiscalYearStartMonth}\n                value={value}\n                onChange={onChange}\n                quickOptions={quickRanges || quickOptions}\n                history={history}\n                showHistory\n                widthOverride={widthOverride}\n                onChangeTimeZone={onChangeTimeZone}\n                onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}\n                hideQuickRanges={hideQuickRanges}\n                onError={onError}\n                weekStart={weekStart}\n              />\n            </section>\n          </FocusScope>\n        </div>\n      )}\n\n      {timeSyncButton}\n\n      {hasAbsolute && (\n        <ToolbarButton\n          aria-label={t('time-picker.range-picker.forwards-time-aria-label', 'Move time range forwards')}\n          onClick={onMoveForward}\n          icon=\"angle-right\"\n          narrow\n          type=\"button\"\n          variant={variant}\n        />\n      )}\n\n      <Tooltip content={ZoomOutTooltip} placement=\"bottom\">\n        <ToolbarButton\n          aria-label={t('time-picker.range-picker.zoom-out-button', 'Zoom out time range')}\n          onClick={onZoom}\n          icon=\"search-minus\"\n          type=\"button\"\n          variant={variant}\n        />\n      </Tooltip>\n    </ButtonGroup>\n  );\n}\n\nTimeRangePicker.displayName = 'TimeRangePicker';\n\nconst ZoomOutTooltip = () => (\n  <>\n    <Trans i18nKey=\"time-picker.range-picker.zoom-out-tooltip\">\n      Time range zoom out <br /> CTRL+Z\n    </Trans>\n  </>\n);\n\nexport const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => {\n  const styles = useStyles2(getLabelStyles);\n\n  return (\n    <>\n      {dateTimeFormat(timeRange.from, { timeZone })}\n      <div className=\"text-center\">\n        <Trans i18nKey=\"time-picker.range-picker.to\">to</Trans>\n      </div>\n      {dateTimeFormat(timeRange.to, { timeZone })}\n      <div className=\"text-center\">\n        <span className={styles.utc}>{timeZoneFormatUserFriendly(timeZone)}</span>\n      </div>\n    </>\n  );\n};\n\ntype LabelProps = Pick<TimeRangePickerProps, 'hideText' | 'value' | 'timeZone' | 'quickRanges'>;\n\nexport const TimePickerButtonLabel = memo<LabelProps>(({ hideText, value, timeZone, quickRanges }) => {\n  const styles = useStyles2(getLabelStyles);\n\n  if (hideText) {\n    return null;\n  }\n\n  return (\n    <span className={styles.container} aria-live=\"polite\" aria-atomic=\"true\">\n      <span>{formattedRange(value, timeZone, quickRanges)}</span>\n      <span className={styles.utc}>{rangeUtil.describeTimeRangeAbbreviation(value, timeZone)}</span>\n    </span>\n  );\n});\n\nTimePickerButtonLabel.displayName = 'TimePickerButtonLabel';\n\nconst formattedRange = (value: TimeRange, timeZone?: TimeZone, quickRanges?: TimeOption[]) => {\n  const adjustedTimeRange = {\n    to: dateMath.isMathString(value.raw.to) ? value.raw.to : value.to,\n    from: dateMath.isMathString(value.raw.from) ? value.raw.from : value.from,\n  };\n  return rangeUtil.describeTimeRange(adjustedTimeRange, timeZone, quickRanges);\n};\n\nconst getStyles = (theme: GrafanaTheme2) => {\n  return {\n    container: css({\n      position: 'relative',\n      display: 'flex',\n      verticalAlign: 'middle',\n    }),\n    backdrop: css({\n      display: 'none',\n      [theme.breakpoints.down('sm')]: {\n        display: 'block',\n      },\n    }),\n    content: css({\n      position: 'absolute',\n      right: 0,\n      top: '116%',\n      zIndex: theme.zIndex.dropdown,\n\n      [theme.breakpoints.down('sm')]: {\n        position: 'fixed',\n        right: '50%',\n        top: '50%',\n        transform: 'translate(50%, -50%)',\n        zIndex: theme.zIndex.modal,\n      },\n    }),\n  };\n};\n\nconst getLabelStyles = (theme: GrafanaTheme2) => {\n  return {\n    container: css({\n      display: 'flex',\n      alignItems: 'center',\n      whiteSpace: 'nowrap',\n    }),\n    utc: css({\n      color: theme.v1.palette.orange,\n      fontSize: theme.typography.size.sm,\n      paddingLeft: '6px',\n      lineHeight: '28px',\n      verticalAlign: 'bottom',\n      fontWeight: theme.typography.fontWeightMedium,\n    }),\n  };\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAsQa,MAAA,qBAAA,GAAwB,KAAiB,CAAC,EAAE,UAAU,KAAO,EAAA,QAAA,EAAU,aAAkB,KAAA;AACpG,EAAM,MAAA,MAAA,GAAS,WAAW,cAAc,CAAA;AAExC,EAAA,IAAI,QAAU,EAAA;AACZ,IAAO,OAAA,IAAA;AAAA;AAGT,EACE,uBAAA,IAAA,CAAC,UAAK,SAAW,EAAA,MAAA,CAAO,WAAW,WAAU,EAAA,QAAA,EAAS,eAAY,MAChE,EAAA,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,MAAM,EAAA,EAAA,QAAA,EAAA,cAAA,CAAe,KAAO,EAAA,QAAA,EAAU,WAAW,CAAE,EAAA,CAAA;AAAA,oBACpD,GAAA,CAAC,UAAK,SAAW,EAAA,MAAA,CAAO,KAAM,QAAU,EAAA,SAAA,CAAA,6BAAA,CAA8B,KAAO,EAAA,QAAQ,CAAE,EAAA;AAAA,GACzF,EAAA,CAAA;AAEJ,CAAC;AAED,qBAAA,CAAsB,WAAc,GAAA,uBAAA;AAEpC,MAAM,cAAiB,GAAA,CAAC,KAAkB,EAAA,QAAA,EAAqB,WAA+B,KAAA;AAC5F,EAAA,MAAM,iBAAoB,GAAA;AAAA,IACxB,EAAA,EAAI,QAAS,CAAA,YAAA,CAAa,KAAM,CAAA,GAAA,CAAI,EAAE,CAAI,GAAA,KAAA,CAAM,GAAI,CAAA,EAAA,GAAK,KAAM,CAAA,EAAA;AAAA,IAC/D,IAAA,EAAM,QAAS,CAAA,YAAA,CAAa,KAAM,CAAA,GAAA,CAAI,IAAI,CAAI,GAAA,KAAA,CAAM,GAAI,CAAA,IAAA,GAAO,KAAM,CAAA;AAAA,GACvE;AACA,EAAA,OAAO,SAAU,CAAA,iBAAA,CAAkB,iBAAmB,EAAA,QAAA,EAAU,WAAW,CAAA;AAC7E,CAAA;AAgCA,MAAM,cAAA,GAAiB,CAAC,KAAyB,KAAA;AAC/C,EAAO,OAAA;AAAA,IACL,WAAW,GAAI,CAAA;AAAA,MACb,OAAS,EAAA,MAAA;AAAA,MACT,UAAY,EAAA,QAAA;AAAA,MACZ,UAAY,EAAA;AAAA,KACb,CAAA;AAAA,IACD,KAAK,GAAI,CAAA;AAAA,MACP,KAAA,EAAO,KAAM,CAAA,EAAA,CAAG,OAAQ,CAAA,MAAA;AAAA,MACxB,QAAA,EAAU,KAAM,CAAA,UAAA,CAAW,IAAK,CAAA,EAAA;AAAA,MAChC,WAAa,EAAA,KAAA;AAAA,MACb,UAAY,EAAA,MAAA;AAAA,MACZ,aAAe,EAAA,QAAA;AAAA,MACf,UAAA,EAAY,MAAM,UAAW,CAAA;AAAA,KAC9B;AAAA,GACH;AACF,CAAA;;;;"}