UNPKG

@grafana/ui

Version:
318 lines (315 loc) • 10.9 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { cx, css } from '@emotion/css'; import { flip, shift, useFloating, autoUpdate } from '@floating-ui/react'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; import * as React from 'react'; import { useState, useEffect, useCallback, useRef } from 'react'; import Calendar from 'react-calendar'; import { useMedia } from 'react-use'; import { dateTimeFormat, dateTime, isDateTime, dateTimeForTimeZone, getTimeZone } from '@grafana/data'; import { Components } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../../themes/ThemeContext.mjs'; import { Button } from '../../Button/Button.mjs'; import { InlineField } from '../../Forms/InlineField.mjs'; import { Icon } from '../../Icon/Icon.mjs'; import { Input } from '../../Input/Input.mjs'; import { Stack } from '../../Layout/Stack/Stack.mjs'; import { getModalStyles } from '../../Modal/getModalStyles.mjs'; import { Portal } from '../../Portal/Portal.mjs'; import { TimeOfDayPicker, POPUP_CLASS_NAME } from '../TimeOfDayPicker.mjs'; import { getBodyStyles } from '../TimeRangePicker/CalendarBody.mjs'; import { isValid } from '../utils.mjs'; import { adjustDateForReactCalendar } from '../utils/adjustDateForReactCalendar.mjs'; const DateTimePicker = ({ date, maxDate, minDate, label, onChange, disabledHours, disabledMinutes, disabledSeconds, timeZone, showSeconds = true, clearable = false }) => { const [isOpen, setOpen] = useState(false); const ref = useRef(null); const { overlayProps, underlayProps } = useOverlay( { onClose: () => setOpen(false), isDismissable: true, isOpen, shouldCloseOnInteractOutside: (element) => { const popupElement = document.getElementsByClassName(POPUP_CLASS_NAME)[0]; return !(popupElement && popupElement.contains(element)); } }, ref ); const { dialogProps } = useDialog({}, ref); const theme = useTheme2(); const { modalBackdrop } = useStyles2(getModalStyles); const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.values.lg}px)`); const styles = useStyles2(getStyles); const middleware = [ flip({ // see https://floating-ui.com/docs/flip#combining-with-shift crossAxis: false, boundary: document.body }), shift() ]; const { refs, floatingStyles } = useFloating({ open: isOpen, placement: "bottom-start", onOpenChange: setOpen, middleware, whileElementsMounted: autoUpdate, strategy: "fixed" }); const onApply = useCallback( (date2) => { setOpen(false); onChange(date2); }, [onChange] ); const onOpen = useCallback( (event) => { event.preventDefault(); setOpen(true); }, [setOpen] ); return /* @__PURE__ */ jsxs("div", { "data-testid": "date-time-picker", style: { position: "relative" }, children: [ /* @__PURE__ */ jsx( DateTimeInput, { date, onChange, isFullscreen, onOpen, label, ref: refs.setReference, showSeconds, clearable, timeZone } ), isOpen ? isFullscreen ? /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(FocusScope, { contain: true, autoFocus: true, restoreFocus: true, children: /* @__PURE__ */ jsx("div", { ref, ...overlayProps, ...dialogProps, children: /* @__PURE__ */ jsx( DateTimeCalendar, { date, onChange: onApply, isFullscreen: true, onClose: () => setOpen(false), maxDate, minDate, ref: refs.setFloating, style: floatingStyles, showSeconds, disabledHours, disabledMinutes, disabledSeconds, timeZone } ) }) }) }) : /* @__PURE__ */ jsxs(Portal, { children: [ /* @__PURE__ */ jsx("div", { className: modalBackdrop, ...underlayProps }), /* @__PURE__ */ jsx(FocusScope, { contain: true, autoFocus: true, restoreFocus: true, children: /* @__PURE__ */ jsx("div", { ref, ...overlayProps, ...dialogProps, children: /* @__PURE__ */ jsx("div", { className: styles.modal, children: /* @__PURE__ */ jsx( DateTimeCalendar, { date, maxDate, minDate, onChange: onApply, isFullscreen: false, onClose: () => setOpen(false), showSeconds, disabledHours, disabledMinutes, disabledSeconds, timeZone } ) }) }) }) ] }) : null ] }); }; const DateTimeInput = React.forwardRef( ({ date, label, onChange, onOpen, timeZone, showSeconds = true, clearable = false }, ref) => { const styles = useStyles2(getStyles); const format = showSeconds ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD HH:mm"; const [internalDate, setInternalDate] = useState(() => { return { value: date ? dateTimeFormat(date, { timeZone }) : !clearable ? dateTimeFormat(dateTime(), { timeZone }) : "", invalid: false }; }); useEffect(() => { if (date) { const formattedDate = dateTimeFormat(date, { format, timeZone }); setInternalDate({ invalid: !isValid(formattedDate), value: isDateTime(date) ? formattedDate : date }); } }, [date, format, timeZone]); const onChangeDate = useCallback((event) => { const isInvalid = !isValid(event.currentTarget.value); setInternalDate({ value: event.currentTarget.value, invalid: isInvalid }); }, []); const onBlur = useCallback(() => { if (!internalDate.invalid && internalDate.value) { const date2 = dateTimeForTimeZone(getTimeZone({ timeZone }), internalDate.value); onChange(date2); } }, [internalDate, onChange, timeZone]); const clearInternalDate = useCallback(() => { setInternalDate({ value: "", invalid: false }); onChange(); }, [onChange]); const icon = /* @__PURE__ */ jsx( Button, { "aria-label": t("grafana-ui.date-time-picker.calendar-icon-label", "Time picker"), icon: "calendar-alt", variant: "secondary", onClick: onOpen } ); return /* @__PURE__ */ jsx(InlineField, { label, invalid: !!(internalDate.value && internalDate.invalid), className: styles.field, children: /* @__PURE__ */ jsx( Input, { onChange: onChangeDate, addonAfter: icon, value: internalDate.value, onBlur, "data-testid": Components.DateTimePicker.input, placeholder: t("grafana-ui.date-time-picker.select-placeholder", "Select date/time"), ref, suffix: clearable && internalDate.value && /* @__PURE__ */ jsx(Icon, { name: "times", className: styles.clearIcon, onClick: clearInternalDate }) } ) }); } ); DateTimeInput.displayName = "DateTimeInput"; const DateTimeCalendar = React.forwardRef( ({ date, onClose, onChange, isFullscreen, maxDate, minDate, style, showSeconds = true, disabledHours, disabledMinutes, disabledSeconds, timeZone }, ref) => { const calendarStyles = useStyles2(getBodyStyles); const styles = useStyles2(getStyles); const [timeOfDayDateTime, setTimeOfDayDateTime] = useState(() => { if (date && date.isValid()) { return dateTimeForTimeZone(getTimeZone({ timeZone }), date); } return dateTimeForTimeZone(getTimeZone({ timeZone }), /* @__PURE__ */ new Date()); }); const [reactCalendarDate, setReactCalendarDate] = useState(() => { if (date && date.isValid()) { return adjustDateForReactCalendar(date.toDate(), getTimeZone({ timeZone })); } return adjustDateForReactCalendar(/* @__PURE__ */ new Date(), getTimeZone({ timeZone })); }); const onChangeDate = useCallback((date2) => { if (date2 && !Array.isArray(date2)) { setReactCalendarDate(date2); } }, []); const onChangeTime = useCallback((date2) => { setTimeOfDayDateTime(date2); }, []); const handleApply = () => { const newDate = dateTime(timeOfDayDateTime); newDate.set("date", reactCalendarDate.getDate()); newDate.set("month", reactCalendarDate.getMonth()); newDate.set("year", reactCalendarDate.getFullYear()); onChange(newDate); }; return /* @__PURE__ */ jsxs("div", { className: cx(styles.container, { [styles.fullScreen]: isFullscreen }), style, ref, children: [ /* @__PURE__ */ jsx( Calendar, { next2Label: null, prev2Label: null, value: reactCalendarDate, nextLabel: /* @__PURE__ */ jsx(Icon, { name: "angle-right" }), nextAriaLabel: t("grafana-ui.date-time-picker.next-label", "Next month"), prevLabel: /* @__PURE__ */ jsx(Icon, { name: "angle-left" }), prevAriaLabel: t("grafana-ui.date-time-picker.previous-label", "Previous month"), onChange: onChangeDate, locale: "en", className: calendarStyles.body, tileClassName: calendarStyles.title, maxDate, minDate } ), /* @__PURE__ */ jsx("div", { className: styles.time, children: /* @__PURE__ */ jsx( TimeOfDayPicker, { showSeconds, onChange: onChangeTime, value: timeOfDayDateTime, disabledHours, disabledMinutes, disabledSeconds } ) }), /* @__PURE__ */ jsxs(Stack, { children: [ /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleApply, children: /* @__PURE__ */ jsx(Trans, { i18nKey: "grafana-ui.date-time-picker.apply", children: "Apply" }) }), /* @__PURE__ */ jsx(Button, { variant: "secondary", type: "button", onClick: onClose, children: /* @__PURE__ */ jsx(Trans, { i18nKey: "grafana-ui.date-time-picker.cancel", children: "Cancel" }) }) ] }) ] }); } ); DateTimeCalendar.displayName = "DateTimeCalendar"; const getStyles = (theme) => ({ container: css({ padding: theme.spacing(1), border: `1px ${theme.colors.border.weak} solid`, borderRadius: theme.shape.radius.default, backgroundColor: theme.colors.background.primary, zIndex: theme.zIndex.modal }), fullScreen: css({ position: "absolute" }), time: css({ marginBottom: theme.spacing(2) }), modal: css({ position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: theme.zIndex.modal, maxWidth: "280px" }), clearIcon: css({ cursor: "pointer" }), field: css({ marginBottom: 0, width: "100%" }) }); export { DateTimePicker }; //# sourceMappingURL=DateTimePicker.mjs.map