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} />; 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 ### Props
<ArgsTable of={DateTimePicker} /> <ArgsTable of={DateTimePicker} />

View File

@ -1,3 +1,4 @@
import { action } from '@storybook/addon-actions';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import React, { useState } from 'react'; import React, { useState } from 'react';
@ -8,6 +9,13 @@ import { withCenteredStory } from '../../../utils/storybook/withCenteredStory';
import { DateTimePicker } from './DateTimePicker'; import { DateTimePicker } from './DateTimePicker';
import mdx from './DateTimePicker.mdx'; 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> = { const meta: ComponentMeta<typeof DateTimePicker> = {
title: 'Pickers and Editors/TimePickers/DateTimePicker', title: 'Pickers and Editors/TimePickers/DateTimePicker',
decorators: [withCenteredStory], decorators: [withCenteredStory],
@ -19,6 +27,14 @@ const meta: ComponentMeta<typeof DateTimePicker> = {
onChange: { onChange: {
table: { disable: true }, table: { disable: true },
}, },
minDate: { control: 'date' },
maxDate: { control: 'date' },
showSeconds: { control: 'boolean' },
},
args: {
minDate: minimumDate,
maxDate: today,
showSeconds: true,
}, },
parameters: { parameters: {
docs: { docs: {
@ -27,9 +43,54 @@ const meta: ComponentMeta<typeof DateTimePicker> = {
}, },
}; };
export const Basic: ComponentStory<typeof DateTimePicker> = ({ label }) => { export const OnlyWorkingHoursEnabled: ComponentStory<typeof DateTimePicker> = ({
const [date, setDate] = useState<DateTime>(dateTime('2021-05-05 12:00:00')); label,
return <DateTimePicker label={label} date={date} onChange={setDate} />; 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 = { Basic.args = {

View File

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

View File

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