Skip to content

Commit 91f1a45

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

File tree

10 files changed

+256
-38
lines changed

10 files changed

+256
-38
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

+14-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+
errorMessages?: { invalidInput?: string; invalidDate?: 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+
statusWithMessage,
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 } = props;
50+
4351
return (
4452
<div className="date-picker">
4553
<div>
@@ -53,8 +61,12 @@ 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={(e: React.ChangeEvent<HTMLInputElement>) => handleInputOnChange(e.target.value)}
69+
statusWithMessage={statusWithMessage}
5870
/>
5971
</div>
6072
{showPicker && (
@@ -65,6 +77,7 @@ export const DatePicker = (props: DatePickerProps) => {
6577
onDayClick={handleDayClick}
6678
modalPosition={modalPosition}
6779
calendarPickerRef={calendarPickerRef}
80+
selectableSlot={selectableSlot}
6881
/>
6982
</div>
7083
)}

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

+8-12
Original file line numberDiff line numberDiff line change
@@ -97,39 +97,35 @@ describe('useCalendarPicker', () => {
9797

9898
describe('Navigation handling', () => {
9999
it('should navigate to the next month correctly during the year', () => {
100-
const { result } = renderHook(() =>
101-
useCalendarPicker({ initialDate: new Date(2024, july, 1) })
102-
);
100+
const initialDate = new Date(2024, july, 1);
101+
const { result } = renderHook(() => useCalendarPicker({ initialDate }));
103102
act(() => result.current.handleGoToNextMonth());
104103

105104
expect(result.current.displayedMonthsStartDates[0].getMonth()).toBe(august);
106105
expect(result.current.displayedMonthsStartDates[0].getFullYear()).toBe(2024);
107106
});
108107

109108
it('should navigate to the next month correctly at the end of the year', () => {
110-
const { result } = renderHook(() =>
111-
useCalendarPicker({ initialDate: new Date(2024, december, 1) })
112-
);
109+
const initialDate = new Date(2024, december, 1);
110+
const { result } = renderHook(() => useCalendarPicker({ initialDate }));
113111
act(() => result.current.handleGoToNextMonth());
114112

115113
expect(result.current.displayedMonthsStartDates[0].getMonth()).toBe(january);
116114
expect(result.current.displayedMonthsStartDates[0].getFullYear()).toBe(2025);
117115
});
118116

119117
it('should navigate to the previous month correctly during the year', () => {
120-
const { result } = renderHook(() =>
121-
useCalendarPicker({ initialDate: new Date(2024, july, 1) })
122-
);
118+
const initialDate = new Date(2024, july, 1);
119+
const { result } = renderHook(() => useCalendarPicker({ initialDate }));
123120
act(() => result.current.handleGoToPreviousMonth());
124121

125122
expect(result.current.displayedMonthsStartDates[0].getMonth()).toBe(june);
126123
expect(result.current.displayedMonthsStartDates[0].getFullYear()).toBe(2024);
127124
});
128125

129126
it('should navigate to the previous month correctly at the start of the year', () => {
130-
const { result } = renderHook(() =>
131-
useCalendarPicker({ initialDate: new Date(2024, january, 1) })
132-
);
127+
const initialDate = new Date(2024, january, 1);
128+
const { result } = renderHook(() => useCalendarPicker({ initialDate }));
133129
act(() => result.current.handleGoToPreviousMonth());
134130

135131
expect(result.current.displayedMonthsStartDates[0].getMonth()).toBe(december);

ui-core/src/components/inputs/datePicker/__tests__/useDatePicker.spec.ts

+138-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
44
import { type RangeDatePickerProps, type SingleDatePickerProps } from '..';
55
import useDatePicker from '../useDatePicker';
66

7+
const errorMessages = {
8+
invalidDate: 'Invalid date',
9+
invalidInput: 'Invalid input',
10+
};
11+
712
describe('useDatePicker', () => {
813
let onDayChange: RangeDatePickerProps['onDateChange'];
914
let onSlotChange: SingleDatePickerProps['onDateChange'];
@@ -59,22 +64,150 @@ describe('useDatePicker', () => {
5964
});
6065

6166
describe('input click handling', () => {
62-
it('should show the picker and call the input onClick callback ', () => {
63-
const onClickCallback = vi.fn();
67+
it('should not show the picker in single date mode', () => {
6468
const { result } = renderHook(() =>
6569
useDatePicker({
66-
inputProps: { id: 'id', label: 'label', onClick: onClickCallback },
70+
inputProps: { id: 'id', label: 'label' },
6771
isRangeMode: false,
6872
onDateChange: onDayChange,
6973
})
7074
);
7175

7276
act(() => {
73-
result.current.handleInputClick({} as React.MouseEvent<HTMLInputElement, MouseEvent>);
77+
result.current.handleInputClick();
78+
});
79+
80+
expect(result.current.showPicker).toBe(false);
81+
});
82+
83+
it('should show the picker in range mode', () => {
84+
const { result } = renderHook(() =>
85+
useDatePicker({
86+
inputProps: { id: 'id', label: 'label' },
87+
isRangeMode: true,
88+
onDateChange: onDayChange,
89+
})
90+
);
91+
92+
act(() => {
93+
result.current.handleInputClick();
7494
});
7595

7696
expect(result.current.showPicker).toBe(true);
77-
expect(onClickCallback).toHaveBeenCalled();
97+
});
98+
});
99+
100+
describe('input change handling', () => {
101+
describe('single mode', () => {
102+
it.each(['toto', '01/01/2024', '01/i'])(
103+
'should show an error if the input is invalid (%i)',
104+
(newValue) => {
105+
const { result } = renderHook(() =>
106+
useDatePicker({
107+
inputProps: { id: 'id', label: 'label' },
108+
isRangeMode: false,
109+
onDateChange: onDayChange,
110+
errorMessages,
111+
})
112+
);
113+
114+
act(() => {
115+
result.current.handleInputOnChange(newValue);
116+
});
117+
118+
expect(result.current.inputValue).toBe(newValue);
119+
expect(result.current.statusWithMessage).toEqual({
120+
status: 'error',
121+
message: 'Invalid input',
122+
});
123+
expect(onDayChange).not.toBeCalled();
124+
}
125+
);
126+
127+
it('should update the input value if the date string is not complete', () => {
128+
const { result } = renderHook(() =>
129+
useDatePicker({
130+
inputProps: { id: 'id', label: 'label' },
131+
isRangeMode: false,
132+
onDateChange: onDayChange,
133+
errorMessages,
134+
})
135+
);
136+
137+
act(() => {
138+
result.current.handleInputOnChange('3/04/24');
139+
});
140+
141+
expect(result.current.inputValue).toBe('3/04/24');
142+
expect(result.current.statusWithMessage).toBe(undefined);
143+
expect(onDayChange).not.toBeCalled();
144+
});
145+
146+
it('should show an error if the date is not within the selectable slot', () => {
147+
const { result } = renderHook(() =>
148+
useDatePicker({
149+
selectableSlot: { start: new Date(2024, 0, 1), end: new Date(2024, 1, 1) },
150+
inputProps: { id: 'id', label: 'label' },
151+
isRangeMode: false,
152+
onDateChange: onDayChange,
153+
errorMessages,
154+
})
155+
);
156+
157+
act(() => {
158+
result.current.handleInputOnChange('03/04/24');
159+
});
160+
161+
expect(result.current.inputValue).toBe('03/04/24');
162+
expect(result.current.statusWithMessage).toEqual({
163+
status: 'error',
164+
message: 'Invalid date',
165+
});
166+
expect(onDayChange).not.toBeCalled();
167+
});
168+
169+
it('should update the parent value is the date is valid', () => {
170+
const { result } = renderHook(() =>
171+
useDatePicker({
172+
inputProps: { id: 'id', label: 'label' },
173+
isRangeMode: false,
174+
onDateChange: onDayChange,
175+
errorMessages,
176+
})
177+
);
178+
179+
act(() => {
180+
result.current.handleInputOnChange('01/02/24');
181+
});
182+
183+
expect(result.current.inputValue).toBe('01/02/24');
184+
expect(result.current.statusWithMessage).toBe(undefined);
185+
expect(onDayChange).toHaveBeenCalledWith(new Date(2024, 1, 1));
186+
});
187+
});
188+
189+
describe('range mode', () => {
190+
it('should do nothing in range mode', () => {
191+
const { result } = renderHook(() =>
192+
useDatePicker({
193+
value: { start: new Date(2024, 0, 1), end: null },
194+
inputProps: {
195+
id: 'id',
196+
label: 'label',
197+
},
198+
isRangeMode: true,
199+
onDateChange: onDayChange,
200+
errorMessages,
201+
})
202+
);
203+
204+
act(() => {
205+
result.current.handleInputOnChange('01/02/24');
206+
});
207+
208+
expect(result.current.inputValue).toBe('01/01/24 - ');
209+
expect(onDayChange).not.toBeCalled();
210+
});
78211
});
79212
});
80213
});

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,

0 commit comments

Comments
 (0)