diff --git a/.betterer.results b/.betterer.results index 43158964214..980d4bc2cb8 100644 --- a/.betterer.results +++ b/.betterer.results @@ -752,20 +752,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], - "packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] - ], - "packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] - ], - "packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] - ], - "packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "2"] - ], "packages/grafana-ui/src/components/Drawer/Drawer.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 6154f3e3bdd..8e3a87ef3bf 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -16,13 +16,13 @@ export const Components = { TimePicker: { openButton: 'data-testid TimePicker Open Button', overlayContent: 'data-testid TimePicker Overlay Content', - fromField: 'Time Range from field', - toField: 'Time Range to field', + fromField: 'data-testid Time Range from field', + toField: 'data-testid Time Range to field', applyTimeRange: 'data-testid TimePicker submit button', calendar: { - label: 'Time Range calendar', - openButton: 'Open time range calendar', - closeButton: 'Close time range Calendar', + label: 'data-testid Time Range calendar', + openButton: 'data-testid Open time range calendar', + closeButton: 'data-testid Close time range Calendar', }, absoluteTimeRangeTitle: 'data-testid-absolute-time-range-narrow', }, diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarFooter.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarFooter.tsx index 4db12f18893..1f5ab28cff4 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarFooter.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarFooter.tsx @@ -1,44 +1,23 @@ -import { css } from '@emotion/css'; import React from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; - -import { useStyles2 } from '../../../themes'; import { Trans } from '../../../utils/i18n'; import { Button } from '../../Button'; +import { Stack } from '../../Layout/Stack/Stack'; 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({ - backgroundColor: theme.colors.background.primary, - display: 'flex', - justifyContent: 'center', - padding: '10px', - alignItems: 'stretch', - }), - apply: css({ - marginRight: '4px', - width: '100%', - justifyContent: 'center', - }), - }; -}; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx index f87fcca805b..c09d39d198a 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx @@ -1,44 +1,30 @@ -import { css } from '@emotion/css'; import React from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { useStyles2 } from '../../../themes'; -import { Trans } from '../../../utils/i18n'; -import { Button } from '../../Button'; +import { Trans, t } from '../../../utils/i18n'; +import { IconButton } from '../../IconButton/IconButton'; +import { Stack } from '../../Layout/Stack/Stack'; import { TimePickerCalendarProps } from './TimePickerCalendar'; import { TimePickerTitle } from './TimePickerTitle'; export function Header({ onClose }: TimePickerCalendarProps) { - const styles = useStyles2(getHeaderStyles); - return ( -
+ Select a time range -
+ ); } Header.displayName = 'Header'; - -const getHeaderStyles = (theme: GrafanaTheme2) => { - return { - container: css({ - backgroundColor: theme.colors.background.primary, - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '7px', - }), - }; -}; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx index 71b56bd5425..9b39f55f4af 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx @@ -19,27 +19,28 @@ export const getStyles = (theme: GrafanaTheme2, isReversed = false) => { container: css({ top: 0, position: 'absolute', - [`${isReversed ? 'left' : 'right'}`]: '544px', + [`${isReversed ? 'left' : 'right'}`]: '546px', // lmao + }), + + modalContainer: css({ + label: 'modalContainer', + margin: '0 auto', + }), + + calendar: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + padding: theme.spacing(1), + label: 'calendar', boxShadow: theme.shadows.z3, backgroundColor: theme.colors.background.primary, - zIndex: -1, border: `1px solid ${theme.colors.border.weak}`, - borderTopLeftRadius: theme.shape.radius.default, - borderBottomLeftRadius: theme.shape.radius.default, - - '&:after': { - display: 'block', - backgroundColor: theme.colors.background.primary, - width: '19px', - height: '100%', - content: `${!isReversed ? '" "' : '""'}`, - position: 'absolute', - top: 0, - right: '-19px', - borderLeft: `1px solid ${theme.colors.border.weak}`, - }, + borderRadius: theme.shape.radius.default, }), + modal: css({ + label: 'modal', boxShadow: theme.shadows.z3, left: '50%', position: 'fixed', @@ -47,10 +48,6 @@ export const getStyles = (theme: GrafanaTheme2, isReversed = false) => { transform: 'translate(-50%, -50%)', zIndex: theme.zIndex.modal, }), - content: css({ - margin: '0 auto', - width: '268px', - }), }; }; @@ -61,6 +58,11 @@ export interface TimePickerCalendarProps { onClose: () => void; onApply: (e: FormEvent) => void; onChange: (from: DateTime, to: DateTime) => void; + + /** + * When true, the calendar is rendered as a floating "tooltip" next to the input. + * When false, the calendar is rendered "fullscreen" in a modal. Yes. Don't ask. + */ isFullscreen: boolean; timeZone?: TimeZone; isReversed?: boolean; @@ -70,7 +72,7 @@ function TimePickerCalendar(props: TimePickerCalendarProps) { const theme = useTheme2(); const { modalBackdrop } = useStyles2(getModalStyles); const styles = getStyles(theme, props.isReversed); - const { isOpen, isFullscreen, onClose } = props; + const { isOpen, isFullscreen: isFullscreenProp, onClose } = props; const ref = React.createRef(); const { dialogProps } = useDialog( { @@ -87,17 +89,31 @@ function TimePickerCalendar(props: TimePickerCalendarProps) { ref ); + // This prop is confusingly worded, so rename it to something more intuitive. + const showInModal = !isFullscreenProp; + if (!isOpen) { return null; } - if (isFullscreen) { + const calendar = ( +
+
+ + {showInModal &&
} +
+ ); + + if (!showInModal) { return ( -
-
- -
+
{calendar}
); } @@ -105,14 +121,11 @@ function TimePickerCalendar(props: TimePickerCalendarProps) { return (
+ -
-
-
- -
-
-
+
+
{calendar}
+
); 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 40d9dc2b693..ba9c9a4f011 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx @@ -96,22 +96,22 @@ describe('TimePickerContent', () => { it('renders with absolute picker when absolute value and quick ranges are visible', () => { renderComponent({ value: absoluteValue, isFullscreen: false }); - expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument(); + expect(screen.queryByLabelText('From')).toBeInTheDocument(); }); it('renders with absolute picker when absolute value and quick ranges are hidden', () => { renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true }); - expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument(); + expect(screen.queryByLabelText('From')).toBeInTheDocument(); }); it('renders without absolute picker when narrow screen and quick ranges are visible', () => { renderComponent({ value: relativeValue, isFullscreen: false }); - expect(screen.queryByLabelText(/time range from field/i)).not.toBeInTheDocument(); + expect(screen.queryByLabelText('From')).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(/time range from field/i)).toBeInTheDocument(); + expect(screen.queryByLabelText('From')).toBeInTheDocument(); }); it('renders without timezone picker', () => { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx index a750b66d9e8..67e14033bdf 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx @@ -109,7 +109,7 @@ export const TimePickerFooter = (props: Props) => { ) : (
{ it('should render form correcty', () => { 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(); + expect(getAllByRole('button', { name: 'Open calendar' })).toHaveLength(2); + expect(getByLabelText('From')).toBeInTheDocument(); + expect(getByLabelText('To')).toBeInTheDocument(); }); 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 }); + const openCalendarButton = getAllByRole('button', { name: 'Open calendar' }); fireEvent.click(openCalendarButton[0]); expect(getByLabelText(TimePicker.calendar.label)).toBeInTheDocument(); @@ -55,24 +54,23 @@ describe('TimeRangeForm', () => { it('should have passed time range entered in form', () => { const { getByLabelText } = setup(); - const { TimePicker } = selectors.components; const fromValue = defaultTimeRange.raw.from as string; const toValue = defaultTimeRange.raw.to as string; - expect(getByLabelText(TimePicker.fromField)).toHaveValue(fromValue); - expect(getByLabelText(TimePicker.toField)).toHaveValue(toValue); + expect(getByLabelText('From')).toHaveValue(fromValue); + expect(getByLabelText('To')).toHaveValue(toValue); }); 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 }); + const openCalendarButton = getAllByRole('button', { name: 'Open calendar' }); fireEvent.click(openCalendarButton[0]); - expect(getByRole('button', { name: TimePicker.calendar.closeButton })).toBeInTheDocument(); + expect(getByRole('button', { name: 'Close calendar' })).toBeInTheDocument(); - fireEvent.click(getByRole('button', { name: TimePicker.calendar.closeButton })); + fireEvent.click(getByRole('button', { name: 'Close calendar' })); expect(queryByLabelText(TimePicker.calendar.label)).toBeNull(); }); @@ -85,8 +83,7 @@ describe('TimeRangeForm', () => { it('should have passed time range selected in calendar', () => { const { getAllByRole, getCalendarDayByLabelText } = setup(); - const { TimePicker } = selectors.components; - const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton }); + const openCalendarButton = getAllByRole('button', { name: 'Open calendar' }); fireEvent.click(openCalendarButton[0]); const from = getCalendarDayByLabelText('June 17, 2021'); @@ -98,8 +95,7 @@ describe('TimeRangeForm', () => { it('should select correct time range in calendar when having a custom time zone', () => { const { getAllByRole, getCalendarDayByLabelText } = setup(defaultTimeRange, 'Asia/Tokyo'); - const { TimePicker } = selectors.components; - const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton }); + const openCalendarButton = getAllByRole('button', { name: 'Open calendar' }); fireEvent.click(openCalendarButton[1]); const from = getCalendarDayByLabelText('June 17, 2021'); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx index a91d10282ae..4cbf3748b4d 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React, { FormEvent, useCallback, useEffect, useState } from 'react'; +import React, { FormEvent, useCallback, useEffect, useId, useState } from 'react'; import { DateTime, @@ -55,6 +55,9 @@ export const TimeRangeContent = (props: Props) => { const [to, setTo] = useState(toValue); const [isOpen, setOpen] = useState(false); + const fromFieldId = useId(); + const toFieldId = useId(); + // Synchronize internal state with external value useEffect(() => { const [fromValue, toValue] = valueToState(value.raw.from, value.raw.to, timeZone); @@ -113,7 +116,8 @@ export const TimeRangeContent = (props: Props) => { const icon = (