diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 5a853d26ede..810a64613e1 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -12,10 +12,14 @@ export const Components = { TimePicker: { openButton: 'data-testid TimePicker Open Button', - fromField: 'TimePicker from field', - toField: 'TimePicker to field', + fromField: 'Time Range from field', + toField: 'Time Range to field', applyTimeRange: 'data-testid TimePicker submit button', - calendar: 'TimePicker calendar', + calendar: { + label: 'Time Range calendar', + openButton: 'Open time range calendar', + closeButton: 'Close time range Calendar', + }, absoluteTimeRangeTitle: 'data-testid-absolute-time-range-narrow', }, DataSource: { diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index f2f086f2f5f..b1b189417c5 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -40,6 +40,8 @@ "@grafana/tsconfig": "^1.0.0-rc1", "@monaco-editor/react": "4.2.2", "@popperjs/core": "2.5.4", + "@react-aria/focus": "3.4.1", + "@react-aria/overlays": "3.7.2", "@sentry/browser": "5.25.0", "ansicolor": "1.1.95", "classnames": "2.2.6", diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx index 1412a27c9b7..5e99390677d 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../themes'; import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper'; import { Icon } from '../../Icon/Icon'; -import { getBodyStyles } from '../TimeRangePicker/TimePickerCalendar'; +import { getBodyStyles } from '../TimeRangePicker/CalendarBody'; /** @public */ export interface DatePickerProps { diff --git a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx index f08eafb68d9..9df4a280f93 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx @@ -5,9 +5,10 @@ import { css, cx } from '@emotion/css'; import { dateTimeFormat, DateTime, dateTime, GrafanaTheme2, isDateTime } from '@grafana/data'; import { Button, ClickOutsideWrapper, HorizontalGroup, Icon, InlineField, Input, Portal } from '../..'; import { TimeOfDayPicker } from '../TimeOfDayPicker'; -import { getBodyStyles, getStyles as getCalendarStyles } from '../TimeRangePicker/TimePickerCalendar'; +import { getStyles as getCalendarStyles } from '../TimeRangePicker/TimePickerCalendar'; import { useStyles2, useTheme2 } from '../../../themes'; import { isValid } from '../utils'; +import { getBodyStyles } from '../TimeRangePicker/CalendarBody'; export interface Props { /** Input date for the component */ diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx index 5cd99ef46e1..23cefa9be0d 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx @@ -1,11 +1,10 @@ // Libraries -import React, { PureComponent, memo, FormEvent } from 'react'; +import React, { memo, FormEvent, createRef, useState, ReactElement } from 'react'; import { css } from '@emotion/css'; // Components import { Tooltip } from '../Tooltip/Tooltip'; import { TimePickerContent } from './TimeRangePicker/TimePickerContent'; -import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'; // Utils & Services import { stylesFactory } from '../../themes/stylesFactory'; @@ -26,6 +25,8 @@ import { Themeable } from '../../types'; import { quickOptions } from './options'; import { ButtonGroup, ToolbarButton } from '../Button'; import { selectors } from '@grafana/e2e-selectors'; +import { useOverlay } from '@react-aria/overlays'; +import { FocusScope } from '@react-aria/focus'; /** @public */ export interface TimeRangePickerProps extends Themeable { @@ -49,95 +50,80 @@ export interface State { isOpen: boolean; } -export class UnthemedTimeRangePicker extends PureComponent { - state: State = { - isOpen: false, +export function UnthemedTimeRangePicker(props: TimeRangePickerProps): ReactElement { + const [isOpen, setOpen] = useState(false); + + const { + value, + onMoveBackward, + onMoveForward, + onZoom, + timeZone, + fiscalYearStartMonth, + timeSyncButton, + isSynced, + theme, + history, + onChangeTimeZone, + onChangeFiscalYearStartMonth, + hideQuickRanges, + } = props; + + const onChange = (timeRange: TimeRange) => { + props.onChange(timeRange); + setOpen(false); }; - onChange = (timeRange: TimeRange) => { - this.props.onChange(timeRange); - this.setState({ isOpen: false }); - }; - - onOpen = (event: FormEvent) => { - const { isOpen } = this.state; + const onOpen = (event: FormEvent) => { event.stopPropagation(); event.preventDefault(); - this.setState({ isOpen: !isOpen }); + setOpen(!isOpen); }; - componentDidMount() { - window.addEventListener('keyup', this.onKeyUp); - } - - componentWillUnmount() { - window.removeEventListener('keyup', this.onKeyUp); - } - - onKeyUp = (event: KeyboardEvent) => { - if (event.code === 'Escape') { - this.onClose(); - } + const onClose = () => { + setOpen(false); }; - onClose = () => { - this.setState({ isOpen: false }); - }; + const ref = createRef(); + const { overlayProps } = useOverlay({ onClose, isOpen }, ref); - render() { - const { - value, - onMoveBackward, - onMoveForward, - onZoom, - timeZone, - fiscalYearStartMonth, - timeSyncButton, - isSynced, - theme, - history, - onChangeTimeZone, - onChangeFiscalYearStartMonth, - hideQuickRanges, - } = this.props; + const styles = getStyles(theme); + const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to); + const variant = isSynced ? 'active' : 'default'; - const { isOpen } = this.state; - const styles = getStyles(theme); - const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to); - const variant = isSynced ? 'active' : 'default'; + return ( + + {hasAbsolute && ( + + )} - return ( - - {hasAbsolute && ( - - )} - - } placement="bottom"> - - - - - {isOpen && ( - + } placement="bottom"> + + + + + {isOpen && ( + +
- - )} +
+
+ )} - {timeSyncButton} + {timeSyncButton} - {hasAbsolute && ( - - )} + {hasAbsolute && ( + + )} - - - -
- ); - } + + + +
+ ); } const ZoomOutTooltip = () => ( diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx new file mode 100644 index 00000000000..96ce9380c76 --- /dev/null +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx @@ -0,0 +1,159 @@ +import React, { useCallback } from 'react'; +import { useStyles2 } from '../../../themes'; +import Calendar from 'react-calendar'; +import { css } from '@emotion/css'; +import { Icon } from '../../Icon/Icon'; +import { TimePickerCalendarProps } from './TimePickerCalendar'; +import { GrafanaTheme2, dateTime, dateTimeParse, DateTime, TimeZone } from '@grafana/data'; + +export function Body({ onChange, from, to, timeZone }: TimePickerCalendarProps) { + const value = inputToValue(from, to); + const onCalendarChange = useOnCalendarChange(onChange, timeZone); + const styles = useStyles2(getBodyStyles); + + return ( + } + prevLabel={} + onChange={onCalendarChange} + locale="en" + /> + ); +} + +Body.displayName = 'Body'; + +export function inputToValue(from: DateTime, to: DateTime, invalidDateDefault: Date = new Date()): Date[] { + const fromAsDate = from.toDate(); + const toAsDate = to.toDate(); + const fromAsValidDate = dateTime(fromAsDate).isValid() ? fromAsDate : invalidDateDefault; + const toAsValidDate = dateTime(toAsDate).isValid() ? toAsDate : invalidDateDefault; + + if (fromAsValidDate > toAsValidDate) { + return [toAsValidDate, fromAsValidDate]; + } + return [fromAsValidDate, toAsValidDate]; +} + +function useOnCalendarChange(onChange: (from: DateTime, to: DateTime) => void, timeZone?: TimeZone) { + return useCallback( + (value: Date | Date[]) => { + if (!Array.isArray(value)) { + return console.error('onCalendarChange: should be run in selectRange={true}'); + } + + const from = dateTimeParse(dateInfo(value[0]), { timeZone }); + const to = dateTimeParse(dateInfo(value[1]), { timeZone }); + + onChange(from, to); + }, + [onChange, timeZone] + ); +} + +function dateInfo(date: Date): number[] { + return [date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()]; +} + +export const getBodyStyles = (theme: GrafanaTheme2) => { + return { + title: css` + color: ${theme.colors.text}; + background-color: ${theme.colors.background.primary}; + font-size: ${theme.typography.size.md}; + border: 1px solid transparent; + + &:hover { + position: relative; + } + `, + body: css` + z-index: ${theme.zIndex.modal}; + background-color: ${theme.colors.background.primary}; + width: 268px; + + .react-calendar__navigation__label, + .react-calendar__navigation__arrow, + .react-calendar__navigation { + padding-top: 4px; + background-color: inherit; + color: ${theme.colors.text}; + border: 0; + font-weight: ${theme.typography.fontWeightMedium}; + } + + .react-calendar__month-view__weekdays { + background-color: inherit; + text-align: center; + color: ${theme.colors.primary.text}; + + abbr { + border: 0; + text-decoration: none; + cursor: default; + display: block; + padding: 4px 0 4px 0; + } + } + + .react-calendar__month-view__days { + background-color: inherit; + } + + .react-calendar__tile, + .react-calendar__tile--now { + margin-bottom: 4px; + background-color: inherit; + height: 26px; + } + + .react-calendar__navigation__label, + .react-calendar__navigation > button:focus, + .time-picker-calendar-tile:focus { + outline: 0; + } + + .react-calendar__tile--active, + .react-calendar__tile--active:hover { + color: ${theme.colors.primary.contrastText}; + font-weight: ${theme.typography.fontWeightMedium}; + background: ${theme.colors.primary.main}; + box-shadow: none; + border: 0px; + } + + .react-calendar__tile--rangeEnd, + .react-calendar__tile--rangeStart { + padding: 0; + border: 0px; + color: ${theme.colors.primary.contrastText}; + font-weight: ${theme.typography.fontWeightMedium}; + background: ${theme.colors.primary.main}; + + abbr { + background-color: ${theme.colors.primary.main}; + border-radius: 100px; + display: block; + padding-top: 2px; + height: 26px; + } + } + + .react-calendar__tile--rangeStart { + border-top-left-radius: 20px; + border-bottom-left-radius: 20px; + } + + .react-calendar__tile--rangeEnd { + border-top-right-radius: 20px; + border-bottom-right-radius: 20px; + } + `, + }; +}; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarFooter.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarFooter.tsx new file mode 100644 index 00000000000..e12dfbda9e8 --- /dev/null +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarFooter.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { useStyles2 } from '../../../themes'; +import { Button } from '../../Button'; +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { TimePickerCalendarProps } from './TimePickerCalendar'; + +export function Footer({ onClose, onApply }: TimePickerCalendarProps) { + const styles = useStyles2(getFooterStyles); + + return ( +
+ + +
+ ); +} + +Footer.displayName = 'Footer'; + +const getFooterStyles = (theme: GrafanaTheme2) => { + return { + container: css` + background-color: ${theme.colors.background.primary}; + display: flex; + justify-content: center; + padding: 10px; + align-items: stretch; + `, + apply: css` + margin-right: 4px; + width: 100%; + justify-content: center; + `, + }; +}; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx new file mode 100644 index 00000000000..87ac740e560 --- /dev/null +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { TimePickerTitle } from './TimePickerTitle'; +import { Button } from '../../Button'; +import { selectors } from '@grafana/e2e-selectors'; +import { TimePickerCalendarProps } from './TimePickerCalendar'; +import { useStyles2 } from '../../../themes'; +import { GrafanaTheme2 } from '@grafana/data'; +import { css } from '@emotion/css'; + +export function Header({ onClose }: TimePickerCalendarProps) { + const styles = useStyles2(getHeaderStyles); + + return ( +
+ Select a time range +
+ ); +} + +Header.displayName = 'Header'; + +const getHeaderStyles = (theme: GrafanaTheme2) => { + return { + container: css` + background-color: ${theme.colors.background.primary}; + display: flex; + align-items: center; + justify-content: space-between; + padding: 7px; + `, + }; +}; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.test.tsx index 59f06a6713a..91e6bc62edc 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.test.tsx @@ -1,6 +1,6 @@ import { dateTime } from '@grafana/data'; -import { inputToValue } from './TimePickerCalendar'; +import { inputToValue } from './CalendarBody'; describe('inputToValue', () => { describe('when called with valid dates', () => { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx index 4c9a32b627b..55fcefa475a 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx @@ -1,16 +1,16 @@ -import React, { FormEvent, memo, useCallback } from 'react'; +import React, { FormEvent, memo } from 'react'; import { css } from '@emotion/css'; -import Calendar from 'react-calendar'; -import { dateTime, DateTime, dateTimeParse, GrafanaTheme2, TimeZone } from '@grafana/data'; -import { stylesFactory, useTheme2 } from '../../../themes'; -import { TimePickerTitle } from './TimePickerTitle'; -import { Button } from '../../Button'; -import { Icon } from '../../Icon/Icon'; +import { DateTime, GrafanaTheme2, TimeZone } from '@grafana/data'; +import { useTheme2 } from '../../../themes'; +import { Header } from './CalendarHeader'; import { Portal } from '../../Portal/Portal'; -import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper'; import { selectors } from '@grafana/e2e-selectors'; +import { FocusScope } from '@react-aria/focus'; +import { useOverlay } from '@react-aria/overlays'; +import { Body } from './CalendarBody'; +import { Footer } from './CalendarFooter'; -export const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed = false) => { +export const getStyles = (theme: GrafanaTheme2, isReversed = false) => { return { container: css` top: -1px; @@ -57,134 +57,9 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed = false text-align: center; `, }; -}); +}; -const getFooterStyles = stylesFactory((theme: GrafanaTheme2) => { - return { - container: css` - background-color: ${theme.colors.background.primary}; - display: flex; - justify-content: center; - padding: 10px; - align-items: stretch; - `, - apply: css` - margin-right: 4px; - width: 100%; - justify-content: center; - `, - }; -}); - -export const getBodyStyles = stylesFactory((theme: GrafanaTheme2) => { - return { - title: css` - color: ${theme.colors.text}; - background-color: ${theme.colors.background.primary}; - font-size: ${theme.typography.size.md}; - border: 1px solid transparent; - - &:hover { - position: relative; - } - `, - body: css` - z-index: ${theme.zIndex.modal}; - background-color: ${theme.colors.background.primary}; - width: 268px; - - .react-calendar__navigation__label, - .react-calendar__navigation__arrow, - .react-calendar__navigation { - padding-top: 4px; - background-color: inherit; - color: ${theme.colors.text}; - border: 0; - font-weight: ${theme.typography.fontWeightMedium}; - } - - .react-calendar__month-view__weekdays { - background-color: inherit; - text-align: center; - color: ${theme.colors.primary.text}; - - abbr { - border: 0; - text-decoration: none; - cursor: default; - display: block; - padding: 4px 0 4px 0; - } - } - - .react-calendar__month-view__days { - background-color: inherit; - } - - .react-calendar__tile, - .react-calendar__tile--now { - margin-bottom: 4px; - background-color: inherit; - height: 26px; - } - - .react-calendar__navigation__label, - .react-calendar__navigation > button:focus, - .time-picker-calendar-tile:focus { - outline: 0; - } - - .react-calendar__tile--active, - .react-calendar__tile--active:hover { - color: ${theme.colors.primary.contrastText}; - font-weight: ${theme.typography.fontWeightMedium}; - background: ${theme.colors.primary.main}; - box-shadow: none; - border: 0px; - } - - .react-calendar__tile--rangeEnd, - .react-calendar__tile--rangeStart { - padding: 0; - border: 0px; - color: ${theme.colors.primary.contrastText}; - font-weight: ${theme.typography.fontWeightMedium}; - background: ${theme.colors.primary.main}; - - abbr { - background-color: ${theme.colors.primary.main}; - border-radius: 100px; - display: block; - padding-top: 2px; - height: 26px; - } - } - - .react-calendar__tile--rangeStart { - border-top-left-radius: 20px; - border-bottom-left-radius: 20px; - } - - .react-calendar__tile--rangeEnd { - border-top-right-radius: 20px; - border-bottom-right-radius: 20px; - } - `, - }; -}); - -const getHeaderStyles = stylesFactory((theme: GrafanaTheme2) => { - return { - container: css` - background-color: ${theme.colors.background.primary}; - display: flex; - justify-content: space-between; - padding: 7px; - `, - }; -}); - -interface Props { +export interface TimePickerCalendarProps { isOpen: boolean; from: DateTime; to: DateTime; @@ -198,10 +73,12 @@ interface Props { const stopPropagation = (event: React.MouseEvent) => event.stopPropagation(); -export const TimePickerCalendar = memo((props) => { +function TimePickerCalendar(props: TimePickerCalendarProps) { const theme = useTheme2(); const styles = getStyles(theme, props.isReversed); const { isOpen, isFullscreen } = props; + const ref = React.createRef(); + const { overlayProps } = useOverlay(props, ref); if (!isOpen) { return null; @@ -209,118 +86,35 @@ export const TimePickerCalendar = memo((props) => { if (isFullscreen) { return ( - +
+
-
+ ); } return ( -
-
-
- -
-
-
+ +
+
+
+ +
+
+
+
); -}); - +} +export default memo(TimePickerCalendar); TimePickerCalendar.displayName = 'TimePickerCalendar'; - -const Header = memo(({ onClose }) => { - const theme = useTheme2(); - const styles = getHeaderStyles(theme); - - return ( -
- Select a time range - -
- ); -}); - -Header.displayName = 'Header'; - -export const Body = memo(({ onChange, from, to, timeZone }) => { - const value = inputToValue(from, to); - const theme = useTheme2(); - const onCalendarChange = useOnCalendarChange(onChange, timeZone); - const styles = getBodyStyles(theme); - - return ( - } - prevLabel={} - onChange={onCalendarChange} - locale="en" - /> - ); -}); - -Body.displayName = 'Body'; - -const Footer = memo(({ onClose, onApply }) => { - const theme = useTheme2(); - const styles = getFooterStyles(theme); - - return ( -
- - -
- ); -}); - -Footer.displayName = 'Footer'; - -export function inputToValue(from: DateTime, to: DateTime, invalidDateDefault: Date = new Date()): Date[] { - const fromAsDate = from.toDate(); - const toAsDate = to.toDate(); - const fromAsValidDate = dateTime(fromAsDate).isValid() ? fromAsDate : invalidDateDefault; - const toAsValidDate = dateTime(toAsDate).isValid() ? toAsDate : invalidDateDefault; - - if (fromAsValidDate > toAsValidDate) { - return [toAsValidDate, fromAsValidDate]; - } - return [fromAsValidDate, toAsValidDate]; -} - -function useOnCalendarChange(onChange: (from: DateTime, to: DateTime) => void, timeZone?: TimeZone) { - return useCallback( - (value: Date | Date[]) => { - if (!Array.isArray(value)) { - return console.error('onCalendarChange: should be run in selectRange={true}'); - } - - const from = dateTimeParse(dateInfo(value[0]), { timeZone }); - const to = dateTimeParse(dateInfo(value[1]), { timeZone }); - - onChange(from, to); - }, - [onChange, timeZone] - ); -} - -function dateInfo(date: Date): number[] { - return [date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()]; -} diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx index f77402fef87..07d0fafb57b 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx @@ -94,22 +94,22 @@ describe('TimePickerContent', () => { it('renders with absolute picker when absolute value and quick ranges are visible', () => { renderComponent({ value: absoluteValue, isFullscreen: false }); - expect(screen.queryByLabelText(/timepicker from field/i)).toBeInTheDocument(); + expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument(); }); it('renders with absolute picker when absolute value and quick ranges are hidden', () => { renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true }); - expect(screen.queryByLabelText(/timepicker from field/i)).toBeInTheDocument(); + expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument(); }); it('renders without absolute picker when narrow screen and quick ranges are visible', () => { renderComponent({ value: relativeValue, isFullscreen: false }); - expect(screen.queryByLabelText(/timepicker from field/i)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(/time range from field/i)).not.toBeInTheDocument(); }); it('renders with absolute picker when narrow screen and quick ranges are hidden', () => { renderComponent({ value: relativeValue, isFullscreen: false, hideQuickRanges: true }); - expect(screen.queryByLabelText(/timepicker from field/i)).toBeInTheDocument(); + expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument(); }); it('renders without timezone picker', () => { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeForm.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeForm.test.tsx index 37c0c33d503..57c6cfab340 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeForm.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeForm.test.tsx @@ -31,20 +31,22 @@ function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc'): TimeRan describe('TimeRangeForm', () => { it('should render form correcty', () => { - const { getByLabelText, getByText } = setup(); + const { getByLabelText, getByText, getAllByRole } = setup(); const { TimePicker } = selectors.components; expect(getByText('Apply time range')).toBeInTheDocument(); + expect(getAllByRole('button', { name: TimePicker.calendar.openButton })).toHaveLength(2); expect(getByLabelText(TimePicker.fromField)).toBeInTheDocument(); expect(getByLabelText(TimePicker.toField)).toBeInTheDocument(); }); - it('should display calendar when clicking the from input field', () => { - const { getByLabelText } = setup(); + it('should display calendar when clicking the calendar icon', () => { + const { getByLabelText, getAllByRole } = setup(); const { TimePicker } = selectors.components; + const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton }); - fireEvent.focus(getByLabelText(TimePicker.fromField)); - expect(getByLabelText(TimePicker.calendar)).toBeInTheDocument(); + fireEvent.click(openCalendarButton[0]); + expect(getByLabelText(TimePicker.calendar.label)).toBeInTheDocument(); }); it('should have passed time range entered in form', () => { @@ -58,26 +60,31 @@ describe('TimeRangeForm', () => { expect(getByLabelText(TimePicker.toField)).toHaveValue(toValue); }); - it('should display calendar when clicking the to input field', () => { - const { getByLabelText } = setup(); + it('should close calendar when clicking the close icon', () => { + const { queryByLabelText, getAllByRole, getByRole } = setup(); const { TimePicker } = selectors.components; + const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton }); - fireEvent.focus(getByLabelText(TimePicker.toField)); - expect(getByLabelText(TimePicker.calendar)).toBeInTheDocument(); + fireEvent.click(openCalendarButton[0]); + expect(getByRole('button', { name: TimePicker.calendar.closeButton })).toBeInTheDocument(); + + fireEvent.click(getByRole('button', { name: TimePicker.calendar.closeButton })); + expect(queryByLabelText(TimePicker.calendar.label)).toBeNull(); }); - it('should not display calendar without clicking any input field', () => { + it('should not display calendar without clicking the calendar icon', () => { const { queryByLabelText } = setup(); const { TimePicker } = selectors.components; - expect(queryByLabelText(TimePicker.calendar)).toBeNull(); + expect(queryByLabelText(TimePicker.calendar.label)).toBeNull(); }); it('should have passed time range selected in calendar', () => { - const { getByLabelText, getCalendarDayByLabelText } = setup(); + const { getAllByRole, getCalendarDayByLabelText } = setup(); const { TimePicker } = selectors.components; + const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton }); - fireEvent.focus(getByLabelText(TimePicker.toField)); + fireEvent.click(openCalendarButton[0]); const from = getCalendarDayByLabelText('June 17, 2021'); const to = getCalendarDayByLabelText('June 19, 2021'); @@ -86,10 +93,11 @@ describe('TimeRangeForm', () => { }); it('should select correct time range in calendar when having a custom time zone', () => { - const { getByLabelText, getCalendarDayByLabelText } = setup(defaultTimeRange, 'Asia/Tokyo'); + const { getAllByRole, getCalendarDayByLabelText } = setup(defaultTimeRange, 'Asia/Tokyo'); const { TimePicker } = selectors.components; + const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton }); - fireEvent.focus(getByLabelText(TimePicker.toField)); + fireEvent.click(openCalendarButton[1]); const from = getCalendarDayByLabelText('June 17, 2021'); const to = getCalendarDayByLabelText('June 19, 2021'); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeForm.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeForm.tsx index 2a42b1c7e50..904a26f760b 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeForm.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeForm.tsx @@ -18,7 +18,7 @@ import { useStyles2 } from '../../..'; import { Button } from '../../Button'; import { Field } from '../../Forms/Field'; import { Input } from '../../Input/Input'; -import { TimePickerCalendar } from './TimePickerCalendar'; +import TimePickerCalendar from './TimePickerCalendar'; interface Props { isFullscreen: boolean; @@ -65,16 +65,6 @@ export const TimeRangeForm: React.FC = (props) => { [setOpen] ); - const onFocus = useCallback( - (event: FormEvent) => { - if (!isFullscreen) { - return; - } - onOpen(event); - }, - [isFullscreen, onOpen] - ); - const onApply = useCallback( (e: FormEvent) => { e.preventDefault(); @@ -111,15 +101,21 @@ export const TimeRangeForm: React.FC = (props) => {
); - const icon = isFullscreen ? null :