@grafana/ui
Version:
Grafana Components Library
318 lines (315 loc) • 10.9 kB
JavaScript
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