DateTimePicker: Add min date support to calendar (#64632)

This commit is contained in:
Francisco Montes de Oca 2023-03-30 10:04:02 -06:00 committed by GitHub
parent ff96cd1342
commit 15b76808b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 170 additions and 10 deletions

View File

@ -16,6 +16,26 @@ const [date, setDate] = useState<DateTime>(dateTime('2021-05-05 12:00:00'));
return <DateTimePicker label="Date" date={date} onChange={setDate} />;
```
### With disbled hours, minutes or seconds
```tsx
import React, { useState } from 'react';
import { DateTime, dateTime } from '@grafana/data';
import { DateTimePicker } from '@grafana/ui';
const [date, setDate] = useState<DateTime>(dateTime('2021-05-05 12:00:00'));
return (
<DateTimePicker
label="Date"
date={date}
onChange={setDate}
disabledHours={() => [0, 1, 2]}
disabledMinutes={() => [10, 15, 30]}
disabledSeconds={() => [5, 10, 15, 20]}
/>
);
```
### Props
<ArgsTable of={DateTimePicker} />

View File

@ -1,3 +1,4 @@
import { action } from '@storybook/addon-actions';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React, { useState } from 'react';
@ -8,6 +9,13 @@ import { withCenteredStory } from '../../../utils/storybook/withCenteredStory';
import { DateTimePicker } from './DateTimePicker';
import mdx from './DateTimePicker.mdx';
const today = new Date();
// minimum date is initially set to 7 days before to allow the user
// to quickly see its effects
const minimumDate = new Date();
minimumDate.setDate(minimumDate.getDate() - 7);
const meta: ComponentMeta<typeof DateTimePicker> = {
title: 'Pickers and Editors/TimePickers/DateTimePicker',
decorators: [withCenteredStory],
@ -19,6 +27,14 @@ const meta: ComponentMeta<typeof DateTimePicker> = {
onChange: {
table: { disable: true },
},
minDate: { control: 'date' },
maxDate: { control: 'date' },
showSeconds: { control: 'boolean' },
},
args: {
minDate: minimumDate,
maxDate: today,
showSeconds: true,
},
parameters: {
docs: {
@ -27,9 +43,54 @@ const meta: ComponentMeta<typeof DateTimePicker> = {
},
};
export const Basic: ComponentStory<typeof DateTimePicker> = ({ label }) => {
const [date, setDate] = useState<DateTime>(dateTime('2021-05-05 12:00:00'));
return <DateTimePicker label={label} date={date} onChange={setDate} />;
export const OnlyWorkingHoursEnabled: ComponentStory<typeof DateTimePicker> = ({
label,
minDate,
maxDate,
showSeconds,
}) => {
const [date, setDate] = useState<DateTime>(dateTime(today));
// the minDate arg can change from Date object to number, we need to handle this
// scenario to avoid a crash in the component's story.
const minDateVal = typeof minDate === 'number' ? new Date(minDate) : minDate;
const maxDateVal = typeof maxDate === 'number' ? new Date(maxDate) : maxDate;
return (
<DateTimePicker
label={label}
disabledHours={() => [0, 1, 2, 3, 4, 5, 6, 19, 20, 21, 22, 23]}
minDate={minDateVal}
maxDate={maxDateVal}
date={date}
showSeconds={showSeconds}
onChange={(newValue) => {
action('on change')(newValue);
setDate(newValue);
}}
/>
);
};
export const Basic: ComponentStory<typeof DateTimePicker> = ({ label, minDate, maxDate, showSeconds }) => {
const [date, setDate] = useState<DateTime>(dateTime(today));
// the minDate arg can change from Date object to number, we need to handle this
// scenario to avoid a crash in the component's story.
const minDateVal = typeof minDate === 'number' ? new Date(minDate) : minDate;
const maxDateVal = typeof maxDate === 'number' ? new Date(maxDate) : maxDate;
return (
<DateTimePicker
label={label}
minDate={minDateVal}
maxDate={maxDateVal}
date={date}
showSeconds={showSeconds}
onChange={(newValue) => {
action('on change')(newValue);
setDate(newValue);
}}
/>
);
};
Basic.args = {

View File

@ -25,9 +25,29 @@ export interface Props {
label?: ReactNode;
/** Set the latest selectable date */
maxDate?: Date;
/** Set the minimum selectable date */
minDate?: Date;
/** Display seconds on the time picker */
showSeconds?: boolean;
/** Set the hours that can't be selected */
disabledHours?: () => number[];
/** Set the minutes that can't be selected */
disabledMinutes?: () => number[];
/** Set the seconds that can't be selected */
disabledSeconds?: () => number[];
}
export const DateTimePicker = ({ date, maxDate, label, onChange }: Props) => {
export const DateTimePicker = ({
date,
maxDate,
minDate,
label,
onChange,
disabledHours,
disabledMinutes,
disabledSeconds,
showSeconds = true,
}: Props) => {
const [isOpen, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
@ -82,6 +102,7 @@ export const DateTimePicker = ({ date, maxDate, label, onChange }: Props) => {
onOpen={onOpen}
label={label}
ref={setMarkerElement}
showSeconds={showSeconds}
/>
{isOpen ? (
isFullscreen ? (
@ -94,8 +115,13 @@ export const DateTimePicker = ({ date, maxDate, label, onChange }: Props) => {
isFullscreen={true}
onClose={() => setOpen(false)}
maxDate={maxDate}
minDate={minDate}
ref={setSelectorElement}
style={popper.styles.popper}
showSeconds={showSeconds}
disabledHours={disabledHours}
disabledMinutes={disabledMinutes}
disabledSeconds={disabledSeconds}
/>
</div>
</FocusScope>
@ -108,9 +134,15 @@ export const DateTimePicker = ({ date, maxDate, label, onChange }: Props) => {
<div className={styles.modal}>
<DateTimeCalendar
date={date}
maxDate={maxDate}
minDate={minDate}
onChange={onApply}
isFullscreen={false}
onClose={() => setOpen(false)}
showSeconds={showSeconds}
disabledHours={disabledHours}
disabledMinutes={disabledMinutes}
disabledSeconds={disabledSeconds}
/>
</div>
</div>
@ -128,7 +160,12 @@ interface DateTimeCalendarProps {
onClose: () => void;
isFullscreen: boolean;
maxDate?: Date;
minDate?: Date;
style?: React.CSSProperties;
showSeconds?: boolean;
disabledHours?: () => number[];
disabledMinutes?: () => number[];
disabledSeconds?: () => number[];
}
interface InputProps {
@ -137,6 +174,7 @@ interface InputProps {
isFullscreen: boolean;
onChange: (date: DateTime) => void;
onOpen: (event: FormEvent<HTMLElement>) => void;
showSeconds?: boolean;
}
type InputState = {
@ -145,7 +183,8 @@ type InputState = {
};
const DateTimeInput = React.forwardRef<HTMLInputElement, InputProps>(
({ date, label, onChange, isFullscreen, onOpen }, ref) => {
({ date, label, onChange, isFullscreen, onOpen, showSeconds = true }, ref) => {
const format = showSeconds ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm';
const [internalDate, setInternalDate] = useState<InputState>(() => {
return { value: date ? dateTimeFormat(date) : dateTimeFormat(dateTime()), invalid: false };
});
@ -153,11 +192,11 @@ const DateTimeInput = React.forwardRef<HTMLInputElement, InputProps>(
useEffect(() => {
if (date) {
setInternalDate({
invalid: !isValid(dateTimeFormat(date)),
value: isDateTime(date) ? dateTimeFormat(date) : date,
invalid: !isValid(dateTimeFormat(date, { format })),
value: isDateTime(date) ? dateTimeFormat(date, { format }) : date,
});
}
}, [date]);
}, [date, format]);
const onChangeDate = useCallback((event: FormEvent<HTMLInputElement>) => {
const isInvalid = !isValid(event.currentTarget.value);
@ -199,7 +238,22 @@ const DateTimeInput = React.forwardRef<HTMLInputElement, InputProps>(
DateTimeInput.displayName = 'DateTimeInput';
const DateTimeCalendar = React.forwardRef<HTMLDivElement, DateTimeCalendarProps>(
({ date, onClose, onChange, isFullscreen, maxDate, style }, ref) => {
(
{
date,
onClose,
onChange,
isFullscreen,
maxDate,
minDate,
style,
showSeconds = true,
disabledHours,
disabledMinutes,
disabledSeconds,
},
ref
) => {
const calendarStyles = useStyles2(getBodyStyles);
const styles = useStyles2(getStyles);
const [internalDate, setInternalDate] = useState<Date>(() => {
@ -243,9 +297,17 @@ const DateTimeCalendar = React.forwardRef<HTMLDivElement, DateTimeCalendarProps>
className={calendarStyles.body}
tileClassName={calendarStyles.title}
maxDate={maxDate}
minDate={minDate}
/>
<div className={styles.time}>
<TimeOfDayPicker showSeconds={true} onChange={onChangeTime} value={dateTime(internalDate)} />
<TimeOfDayPicker
showSeconds={showSeconds}
onChange={onChangeTime}
value={dateTime(internalDate)}
disabledHours={disabledHours}
disabledMinutes={disabledMinutes}
disabledSeconds={disabledSeconds}
/>
</div>
<HorizontalGroup>
<Button type="button" onClick={() => onChange(dateTime(internalDate))}>

View File

@ -17,6 +17,9 @@ export interface Props {
minuteStep?: number;
size?: FormInputSize;
disabled?: boolean;
disabledHours?: () => number[];
disabledMinutes?: () => number[];
disabledSeconds?: () => number[];
}
export const POPUP_CLASS_NAME = 'time-of-day-picker-panel';
@ -29,6 +32,9 @@ export const TimeOfDayPicker = ({
value,
size = 'auto',
disabled,
disabledHours,
disabledMinutes,
disabledSeconds,
}: Props) => {
const styles = useStyles2(getStyles);
@ -45,6 +51,9 @@ export const TimeOfDayPicker = ({
minuteStep={minuteStep}
inputIcon={<Caret wrapperStyle={styles.caretWrapper} />}
disabled={disabled}
disabledHours={disabledHours}
disabledMinutes={disabledMinutes}
disabledSeconds={disabledSeconds}
/>
);
};
@ -93,6 +102,10 @@ const getStyles = (theme: GrafanaTheme2) => {
&:hover {
background: ${optionBgHover};
}
&.rc-time-picker-panel-select-option-disabled {
color: ${theme.colors.action.disabledText};
}
}
}

View File

@ -78,6 +78,10 @@ export const getBodyStyles = (theme: GrafanaTheme2) => {
&:hover {
position: relative;
}
&:disabled {
color: ${theme.colors.action.disabledText};
}
`,
body: css`
z-index: ${theme.zIndex.modal};