TimePicker: Prevent TimePicker overflowing viewport on small screens (#59808)

* render timepicker in a modal style on small screens

* remove top: -1

* apply styles

* prevent bug where selecting a relative range wouldn't apply if the absolute ranges were expanded

* Revert "prevent bug where selecting a relative range wouldn't apply if the absolute ranges were expanded"

This reverts commit 7090443c12.
This commit is contained in:
Ashley Harrison 2022-12-09 15:02:26 +00:00 committed by GitHub
parent 90ece9d1f3
commit 1497ad4760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 58 additions and 42 deletions

View File

@ -7,9 +7,9 @@ import { dateTimeFormat, DateTime, dateTime, GrafanaTheme2, isDateTime } from '@
import { Button, ClickOutsideWrapper, HorizontalGroup, Icon, InlineField, Input, Portal } from '../..'; import { Button, ClickOutsideWrapper, HorizontalGroup, Icon, InlineField, Input, Portal } from '../..';
import { useStyles2, useTheme2 } from '../../../themes'; import { useStyles2, useTheme2 } from '../../../themes';
import { getModalStyles } from '../../Modal/getModalStyles';
import { TimeOfDayPicker } from '../TimeOfDayPicker'; import { TimeOfDayPicker } from '../TimeOfDayPicker';
import { getBodyStyles } from '../TimeRangePicker/CalendarBody'; import { getBodyStyles } from '../TimeRangePicker/CalendarBody';
import { getStyles as getCalendarStyles } from '../TimeRangePicker/TimePickerCalendar';
import { isValid } from '../utils'; import { isValid } from '../utils';
export interface Props { export interface Props {
@ -29,8 +29,8 @@ export const DateTimePicker: FC<Props> = ({ date, maxDate, label, onChange }) =>
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const theme = useTheme2(); const theme = useTheme2();
const { modalBackdrop } = getModalStyles(theme);
const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.values.lg}px)`); const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.values.lg}px)`);
const containerStyles = useStyles2(getCalendarStyles);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const onApply = useCallback( const onApply = useCallback(
@ -69,7 +69,7 @@ export const DateTimePicker: FC<Props> = ({ date, maxDate, label, onChange }) =>
<div className={styles.modal} onClick={stopPropagation}> <div className={styles.modal} onClick={stopPropagation}>
<DateTimeCalendar date={date} onChange={onApply} isFullscreen={false} onClose={() => setOpen(false)} /> <DateTimeCalendar date={date} onChange={onApply} isFullscreen={false} onClose={() => setOpen(false)} />
</div> </div>
<div className={containerStyles.backdrop} onClick={stopPropagation} /> <div className={modalBackdrop} onClick={stopPropagation} />
</ClickOutsideWrapper> </ClickOutsideWrapper>
</Portal> </Portal>
) )

View File

@ -1,4 +1,4 @@
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useDialog } from '@react-aria/dialog'; import { useDialog } from '@react-aria/dialog';
import { FocusScope } from '@react-aria/focus'; import { FocusScope } from '@react-aria/focus';
import { useOverlay } from '@react-aria/overlays'; import { useOverlay } from '@react-aria/overlays';
@ -16,9 +16,10 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../themes/ThemeContext'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
import { t, Trans } from '../../utils/i18n'; import { t, Trans } from '../../utils/i18n';
import { ButtonGroup } from '../Button'; import { ButtonGroup } from '../Button';
import { getModalStyles } from '../Modal/getModalStyles';
import { ToolbarButton } from '../ToolbarButton'; import { ToolbarButton } from '../ToolbarButton';
import { Tooltip } from '../Tooltip/Tooltip'; import { Tooltip } from '../Tooltip/Tooltip';
@ -85,10 +86,12 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
}; };
const ref = createRef<HTMLElement>(); const ref = createRef<HTMLElement>();
const { overlayProps } = useOverlay({ onClose, isDismissable: true, isOpen }, ref); const { overlayProps, underlayProps } = useOverlay({ onClose, isDismissable: true, isOpen }, ref);
const { dialogProps } = useDialog({}, ref); const { dialogProps } = useDialog({}, ref);
const theme = useTheme2();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { modalBackdrop } = getModalStyles(theme);
const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to); const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
const variant = isSynced ? 'active' : isOnCanvas ? 'canvas' : 'default'; const variant = isSynced ? 'active' : isOnCanvas ? 'canvas' : 'default';
@ -122,23 +125,26 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
{isOpen && ( {isOpen && (
<section ref={ref} {...overlayProps} {...dialogProps}> <>
<FocusScope contain autoFocus> <div role="presentation" className={cx(modalBackdrop, styles.backdrop)} {...underlayProps} />
<TimePickerContent <section className={styles.content} ref={ref} {...overlayProps} {...dialogProps}>
timeZone={timeZone} <FocusScope contain autoFocus>
fiscalYearStartMonth={fiscalYearStartMonth} <TimePickerContent
value={value} timeZone={timeZone}
onChange={onChange} fiscalYearStartMonth={fiscalYearStartMonth}
quickOptions={quickOptions} value={value}
history={history} onChange={onChange}
showHistory quickOptions={quickOptions}
widthOverride={widthOverride} history={history}
onChangeTimeZone={onChangeTimeZone} showHistory
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth} widthOverride={widthOverride}
hideQuickRanges={hideQuickRanges} onChangeTimeZone={onChangeTimeZone}
/> onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
</FocusScope> hideQuickRanges={hideQuickRanges}
</section> />
</FocusScope>
</section>
</>
)} )}
{timeSyncButton} {timeSyncButton}
@ -219,13 +225,33 @@ const formattedRange = (value: TimeRange, timeZone?: TimeZone) => {
return rangeUtil.describeTimeRange(adjustedTimeRange, timeZone); return rangeUtil.describeTimeRange(adjustedTimeRange, timeZone);
}; };
const getStyles = () => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
container: css` container: css`
position: relative; position: relative;
display: flex; display: flex;
vertical-align: middle; vertical-align: middle;
`, `,
backdrop: css({
display: 'none',
[theme.breakpoints.down('sm')]: {
display: 'block',
},
}),
content: css({
position: 'absolute',
right: 0,
top: '116%',
zIndex: theme.zIndex.dropdown,
[theme.breakpoints.down('sm')]: {
position: 'fixed',
right: '50%',
top: '50%',
transform: 'translate(50%, -50%)',
zIndex: theme.zIndex.modal,
},
}),
}; };
}; };

View File

@ -8,6 +8,7 @@ import { DateTime, GrafanaTheme2, TimeZone } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { useTheme2 } from '../../../themes'; import { useTheme2 } from '../../../themes';
import { getModalStyles } from '../../Modal/getModalStyles';
import { Body } from './CalendarBody'; import { Body } from './CalendarBody';
import { Footer } from './CalendarFooter'; import { Footer } from './CalendarFooter';
@ -16,7 +17,7 @@ import { Header } from './CalendarHeader';
export const getStyles = (theme: GrafanaTheme2, isReversed = false) => { export const getStyles = (theme: GrafanaTheme2, isReversed = false) => {
return { return {
container: css` container: css`
top: -1px; top: 0px;
position: absolute; position: absolute;
${isReversed ? 'left' : 'right'}: 544px; ${isReversed ? 'left' : 'right'}: 544px;
box-shadow: ${theme.shadows.z3}; box-shadow: ${theme.shadows.z3};
@ -38,26 +39,17 @@ export const getStyles = (theme: GrafanaTheme2, isReversed = false) => {
} }
`, `,
modal: css` modal: css`
box-shadow: ${theme.shadows.z3};
left: 50%;
position: fixed; position: fixed;
top: 20%; top: 50%;
width: 100%; transform: translate(-50%, -50%);
z-index: ${theme.zIndex.modal}; z-index: ${theme.zIndex.modal};
`, `,
content: css` content: css`
margin: 0 auto; margin: 0 auto;
width: 268px; width: 268px;
`, `,
backdrop: css`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: #202226;
opacity: 0.7;
z-index: ${theme.zIndex.modalBackdrop};
text-align: center;
`,
}; };
}; };
@ -77,6 +69,7 @@ const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopP
function TimePickerCalendar(props: TimePickerCalendarProps) { function TimePickerCalendar(props: TimePickerCalendarProps) {
const theme = useTheme2(); const theme = useTheme2();
const { modalBackdrop } = getModalStyles(theme);
const styles = getStyles(theme, props.isReversed); const styles = getStyles(theme, props.isReversed);
const { isOpen, isFullscreen, onClose } = props; const { isOpen, isFullscreen, onClose } = props;
const ref = React.createRef<HTMLElement>(); const ref = React.createRef<HTMLElement>();
@ -112,6 +105,7 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
return ( return (
<OverlayContainer> <OverlayContainer>
<div className={modalBackdrop} onClick={stopPropagation} />
<FocusScope contain autoFocus restoreFocus> <FocusScope contain autoFocus restoreFocus>
<section className={styles.modal} onClick={stopPropagation} ref={ref} {...overlayProps} {...dialogProps}> <section className={styles.modal} onClick={stopPropagation} ref={ref} {...overlayProps} {...dialogProps}>
<div className={styles.content} aria-label={selectors.components.TimePicker.calendar.label}> <div className={styles.content} aria-label={selectors.components.TimePicker.calendar.label}>
@ -121,7 +115,6 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
</div> </div>
</section> </section>
</FocusScope> </FocusScope>
<div className={styles.backdrop} onClick={stopPropagation} />
</OverlayContainer> </OverlayContainer>
); );
} }

View File

@ -267,10 +267,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRang
container: css` container: css`
background: ${theme.colors.background.primary}; background: ${theme.colors.background.primary};
box-shadow: ${theme.shadows.z3}; box-shadow: ${theme.shadows.z3};
position: absolute;
z-index: ${theme.zIndex.dropdown};
width: ${isFullscreen ? '546px' : '262px'}; width: ${isFullscreen ? '546px' : '262px'};
top: 116%;
border-radius: 2px; border-radius: 2px;
border: 1px solid ${theme.colors.border.weak}; border: 1px solid ${theme.colors.border.weak};
${isReversed ? 'left' : 'right'}: 0; ${isReversed ? 'left' : 'right'}: 0;