Skip to content

Commit dbdf90d

Browse files
committed
ui-core: enable the user to type in the date picker
Signed-off-by: Clara Ni <[email protected]>
1 parent 11a8ea7 commit dbdf90d

File tree

7 files changed

+98
-21
lines changed

7 files changed

+98
-21
lines changed

ui-core/src/components/inputs/datePicker/CalendarPicker.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ export type CalendarPickerPrivateProps = {
1515
left: number;
1616
};
1717
calendarPickerRef: React.RefObject<HTMLDivElement>;
18+
selectableSlot?: CalendarSlot;
1819
};
1920

2021
export type CalendarPickerPublicProps = {
2122
initialDate?: Date;
2223
numberOfMonths?: 1 | 2 | 3;
23-
selectableSlot?: CalendarSlot;
2424
};
2525

2626
export type CalendarPickerProps = CalendarPickerPrivateProps & CalendarPickerPublicProps;

ui-core/src/components/inputs/datePicker/DatePicker.tsx

+21-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import useDatePicker from './useDatePicker';
99
import Input, { type InputProps } from '../Input';
1010

1111
type BaseDatePickerProps = {
12+
selectableSlot?: CalendarSlot;
1213
inputProps: InputProps;
1314
calendarPickerProps?: CalendarPickerPublicProps;
15+
errorMessage?: string;
16+
width?: number;
1417
};
1518

1619
export type SingleDatePickerProps = BaseDatePickerProps & {
@@ -30,6 +33,7 @@ export type DatePickerProps = SingleDatePickerProps | RangeDatePickerProps;
3033
export const DatePicker = (props: DatePickerProps) => {
3134
const {
3235
inputValue,
36+
inputIsValid,
3337
selectedSlot,
3438
showPicker,
3539
modalPosition,
@@ -38,8 +42,12 @@ export const DatePicker = (props: DatePickerProps) => {
3842
setShowPicker,
3943
handleDayClick,
4044
handleInputClick,
45+
handleInputOnChange,
4146
} = useDatePicker(props);
47+
4248
const { inputFieldWrapperClassname, ...otherInputProps } = props.inputProps;
49+
const { selectableSlot, errorMessage } = props;
50+
4351
return (
4452
<div className="date-picker">
4553
<div>
@@ -53,8 +61,19 @@ export const DatePicker = (props: DatePickerProps) => {
5361
content: <CalendarIcon />,
5462
onClickCallback: () => setShowPicker(!showPicker),
5563
}}
56-
inputFieldWrapperClassname={cx('date-picker-input', inputFieldWrapperClassname)}
64+
inputFieldWrapperClassname={cx('date-picker-input', inputFieldWrapperClassname, {
65+
'range-mode': props.isRangeMode,
66+
})}
5767
autoComplete="off"
68+
onChange={handleInputOnChange}
69+
statusWithMessage={
70+
!inputIsValid
71+
? {
72+
status: 'error',
73+
message: errorMessage ?? 'Invalid date',
74+
}
75+
: undefined
76+
}
5877
/>
5978
</div>
6079
{showPicker && (
@@ -65,6 +84,7 @@ export const DatePicker = (props: DatePickerProps) => {
6584
onDayClick={handleDayClick}
6685
modalPosition={modalPosition}
6786
calendarPickerRef={calendarPickerRef}
87+
selectableSlot={selectableSlot}
6888
/>
6989
</div>
7090
)}

ui-core/src/components/inputs/datePicker/useCalendarPicker.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useEffect, useState } from 'react';
22

33
import { type CalendarPickerProps } from './CalendarPicker';
44
import { generateSequentialDates, validateSlots } from './utils';
@@ -10,9 +10,9 @@ export default function useCalendarPicker({
1010
numberOfMonths = 1,
1111
}: Omit<CalendarPickerProps, 'modalPosition' | 'calendarPickerRef' | 'onDayClick'>) {
1212
validateSlots(selectedSlot, selectableSlot, initialDate);
13-
const initialActiveDate =
14-
initialDate ?? selectedSlot?.start ?? selectableSlot?.start ?? new Date();
15-
const [activeDate, setActiveDate] = useState<Date>(initialActiveDate);
13+
14+
const [activeDate, setActiveDate] = useState(new Date());
15+
1616
const displayedMonthsStartDates = generateSequentialDates(activeDate, numberOfMonths);
1717

1818
const activeYear = activeDate.getFullYear();
@@ -48,6 +48,10 @@ export default function useCalendarPicker({
4848
}
4949
};
5050

51+
useEffect(() => {
52+
setActiveDate(initialDate ?? selectedSlot?.start ?? selectableSlot?.start ?? new Date());
53+
}, [initialDate, selectedSlot, selectableSlot]);
54+
5155
return {
5256
displayedMonthsStartDates,
5357
showNavigationBtn,
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
11
import { useState, useRef, useEffect } from 'react';
22

33
import type { DatePickerProps } from './DatePicker';
4-
import { computeNewSelectedSlot, formatInputValue, formatValueToSlot } from './utils';
4+
import {
5+
computeNewSelectedSlot,
6+
formatInputValue,
7+
formatValueToSlot,
8+
isWithinInterval,
9+
} from './utils';
510
import { useModalPosition } from '../../../hooks/useModalPosition';
611
import useOutsideClick from '../../../hooks/useOutsideClick';
712

813
const MODAL_HORIZONTAL_OFFSET = -24;
914
const MODAL_VERTICAL_OFFSET = 3;
1015

16+
// Regex for "xx/xx/xx"
17+
const SINGLE_DATE_REGEX = /^\d{2}\/\d{2}\/\d{2}$/;
18+
1119
export default function useDatePicker(datePickerProps: DatePickerProps) {
12-
const { inputProps, value, isRangeMode, onDateChange } = datePickerProps;
20+
const { value, isRangeMode, selectableSlot, onDateChange } = datePickerProps;
1321
const [showPicker, setShowPicker] = useState(false);
1422
const [inputValue, setInputValue] = useState(formatInputValue(datePickerProps));
1523
const [selectedSlot, setSelectedSlot] = useState(formatValueToSlot(datePickerProps));
24+
const [inputIsValid, setInputIsValid] = useState(true);
1625

1726
const calendarPickerRef = useRef<HTMLDivElement>(null);
1827
const inputRef = useRef<HTMLInputElement>(null);
1928
useOutsideClick(calendarPickerRef, (e) => {
2029
// Do not close the picker if any children in input wrapper is clicked.
2130
// This wrapper include, the input itself, the trailing content (which contains the calendar icon) and the leading content
22-
if (inputRef.current && inputRef.current.parentElement?.contains(e.target as Node)) return;
31+
if (
32+
inputRef.current &&
33+
inputRef.current.parentElement?.parentElement?.contains(e.target as Node)
34+
)
35+
return;
2336
setShowPicker(false);
2437
});
2538
const { calculatePosition, modalPosition } = useModalPosition(
@@ -29,9 +42,10 @@ export default function useDatePicker(datePickerProps: DatePickerProps) {
2942
MODAL_HORIZONTAL_OFFSET
3043
);
3144

32-
const handleInputClick = (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
33-
setShowPicker(true);
34-
inputProps.onClick?.(e);
45+
const handleInputClick = () => {
46+
if (isRangeMode) {
47+
setShowPicker(true);
48+
}
3549
};
3650

3751
const handleDayClick = (clickedDate: Date) => {
@@ -43,25 +57,53 @@ export default function useDatePicker(datePickerProps: DatePickerProps) {
4357
}
4458
};
4559

60+
const handleInputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
61+
if (isRangeMode) {
62+
return;
63+
}
64+
65+
const inputIsValid = SINGLE_DATE_REGEX.test(e.target.value);
66+
setInputValue(e.target.value);
67+
68+
if (inputIsValid) {
69+
const [day, month, year] = e.target.value.split('/').map(Number);
70+
const date = new Date(year + 2000, month - 1, day);
71+
72+
if (!isNaN(date.getTime()) && isWithinInterval(date, selectableSlot) && onDateChange) {
73+
onDateChange(date);
74+
} else {
75+
setInputIsValid(false);
76+
}
77+
}
78+
};
79+
4680
useEffect(() => {
4781
if (showPicker) calculatePosition();
4882
}, [showPicker, calculatePosition]);
4983

5084
useEffect(() => {
51-
setInputValue(formatInputValue(datePickerProps));
85+
const newInput = formatInputValue(datePickerProps);
86+
if (newInput !== inputValue) {
87+
// we only set the input value if it has changed
88+
// otherwise the user loses the focus
89+
setInputValue(newInput);
90+
}
91+
setInputIsValid(true);
5292
setSelectedSlot(formatValueToSlot(datePickerProps));
5393
// eslint-disable-next-line react-hooks/exhaustive-deps
5494
}, [value]);
5595

5696
return {
5797
showPicker,
5898
inputValue,
99+
inputIsValid,
59100
selectedSlot,
60101
modalPosition,
61102
inputRef,
62103
calendarPickerRef,
63104
setShowPicker,
64105
handleDayClick,
65106
handleInputClick,
107+
handleInputOnChange,
66108
};
67109
}

ui-core/src/stories/DatePicker.stories.tsx

+10-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type CalendarSlot,
1111
} from '../components/inputs/datePicker';
1212
import '@osrd-project/ui-core/dist/theme.css';
13+
import './stories.css';
1314

1415
const now = new Date();
1516
const endSelectableDate = new Date(now);
@@ -38,11 +39,13 @@ const DatePickerStory = (props: DatePickerProps) => {
3839
);
3940
} else {
4041
return (
41-
<DatePicker
42-
{...props}
43-
value={value as SingleDatePickerProps['value']}
44-
onDateChange={onDayChange}
45-
/>
42+
<div className="date-picker-story-wrapper">
43+
<DatePicker
44+
{...props}
45+
value={value as SingleDatePickerProps['value']}
46+
onDateChange={onDayChange}
47+
/>
48+
</div>
4649
);
4750
}
4851
};
@@ -64,13 +67,14 @@ const meta: Meta<typeof DatePicker> = {
6467
},
6568
},
6669
args: {
70+
selectableSlot,
6771
calendarPickerProps: {
6872
numberOfMonths: 1,
69-
selectableSlot,
7073
},
7174
inputProps: {
7275
id: 'date-picker',
7376
label: 'Select a date',
77+
inputFieldWrapperClassname: 'date-picker-input-wrapper',
7478
},
7579
},
7680
render: (props) => <DatePickerStory {...props} />,

ui-core/src/stories/stories.css

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.date-picker-story-wrapper {
2+
.date-picker-input-wrapper {
3+
width: 300px;
4+
}
5+
}

ui-core/src/styles/inputs/datePicker.css

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
@apply text-primary-60;
66
}
77
}
8-
.input {
9-
caret-color: transparent;
8+
&.range-mode {
9+
.input {
10+
caret-color: transparent;
11+
}
1012
}
1113
}
1214

0 commit comments

Comments
 (0)