Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui-core: enable the user to type in the date picker #547

Merged
merged 1 commit into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { ChevronLeft, ChevronRight } from '@osrd-project/ui-icons';

Check warning on line 3 in ui-core/src/components/inputs/datePicker/CalendarPicker.tsx

View workflow job for this annotation

GitHub Actions / build

Unable to resolve path to module '@osrd-project/ui-icons'
import cx from 'classnames';

import Calendar from './Calendar';
Expand All @@ -15,12 +15,12 @@
left: number;
};
calendarPickerRef: React.RefObject<HTMLDivElement>;
selectableSlot?: CalendarSlot;
};

export type CalendarPickerPublicProps = {
initialDate?: Date;
numberOfMonths?: 1 | 2 | 3;
selectableSlot?: CalendarSlot;
};

export type CalendarPickerProps = CalendarPickerPrivateProps & CalendarPickerPublicProps;
Expand Down
15 changes: 14 additions & 1 deletion ui-core/src/components/inputs/datePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { Calendar as CalendarIcon } from '@osrd-project/ui-icons';

Check warning on line 3 in ui-core/src/components/inputs/datePicker/DatePicker.tsx

View workflow job for this annotation

GitHub Actions / build

Unable to resolve path to module '@osrd-project/ui-icons'
import cx from 'classnames';

import { type CalendarSlot } from '.';
Expand All @@ -9,8 +9,11 @@
import Input, { type InputProps } from '../Input';

type BaseDatePickerProps = {
selectableSlot?: CalendarSlot;
inputProps: InputProps;
calendarPickerProps?: CalendarPickerPublicProps;
errorMessages?: { invalidInput?: string; invalidDate?: string };
width?: number;
};

export type SingleDatePickerProps = BaseDatePickerProps & {
Expand All @@ -30,6 +33,7 @@
export const DatePicker = (props: DatePickerProps) => {
const {
inputValue,
statusWithMessage,
selectedSlot,
showPicker,
modalPosition,
Expand All @@ -38,8 +42,12 @@
setShowPicker,
handleDayClick,
handleInputClick,
handleInputOnChange,
} = useDatePicker(props);

const { inputFieldWrapperClassname, ...otherInputProps } = props.inputProps;
const { selectableSlot } = props;

return (
<div className="date-picker">
<div>
Expand All @@ -53,8 +61,12 @@
content: <CalendarIcon />,
onClickCallback: () => setShowPicker(!showPicker),
}}
inputFieldWrapperClassname={cx('date-picker-input', inputFieldWrapperClassname)}
inputFieldWrapperClassname={cx('date-picker-input', inputFieldWrapperClassname, {
'range-mode': props.isRangeMode,
})}
autoComplete="off"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputOnChange(e.target.value)}
statusWithMessage={statusWithMessage}
/>
</div>
{showPicker && (
Expand All @@ -65,6 +77,7 @@
onDayClick={handleDayClick}
modalPosition={modalPosition}
calendarPickerRef={calendarPickerRef}
selectableSlot={selectableSlot}
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,39 +97,35 @@ describe('useCalendarPicker', () => {

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

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

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

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

it('should navigate to the previous month correctly during the year', () => {
const { result } = renderHook(() =>
useCalendarPicker({ initialDate: new Date(2024, july, 1) })
);
const initialDate = new Date(2024, july, 1);
const { result } = renderHook(() => useCalendarPicker({ initialDate }));
act(() => result.current.handleGoToPreviousMonth());

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

it('should navigate to the previous month correctly at the start of the year', () => {
const { result } = renderHook(() =>
useCalendarPicker({ initialDate: new Date(2024, january, 1) })
);
const initialDate = new Date(2024, january, 1);
const { result } = renderHook(() => useCalendarPicker({ initialDate }));
act(() => result.current.handleGoToPreviousMonth());

expect(result.current.displayedMonthsStartDates[0].getMonth()).toBe(december);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
import { type RangeDatePickerProps, type SingleDatePickerProps } from '..';
import useDatePicker from '../useDatePicker';

const errorMessages = {
invalidDate: 'Invalid date',
invalidInput: 'Invalid input',
};

describe('useDatePicker', () => {
let onDayChange: RangeDatePickerProps['onDateChange'];
let onSlotChange: SingleDatePickerProps['onDateChange'];
Expand Down Expand Up @@ -59,22 +64,150 @@ describe('useDatePicker', () => {
});

describe('input click handling', () => {
it('should show the picker and call the input onClick callback ', () => {
const onClickCallback = vi.fn();
it('should not show the picker in single date mode', () => {
const { result } = renderHook(() =>
useDatePicker({
inputProps: { id: 'id', label: 'label', onClick: onClickCallback },
inputProps: { id: 'id', label: 'label' },
isRangeMode: false,
onDateChange: onDayChange,
})
);

act(() => {
result.current.handleInputClick({} as React.MouseEvent<HTMLInputElement, MouseEvent>);
result.current.handleInputClick();
});

expect(result.current.showPicker).toBe(false);
});

it('should show the picker in range mode', () => {
const { result } = renderHook(() =>
useDatePicker({
inputProps: { id: 'id', label: 'label' },
isRangeMode: true,
onDateChange: onDayChange,
})
);

act(() => {
result.current.handleInputClick();
});

expect(result.current.showPicker).toBe(true);
expect(onClickCallback).toHaveBeenCalled();
});
});

describe('input change handling', () => {
describe('single mode', () => {
it.each(['toto', '01/01/2024', '01/i'])(
'should show an error if the input is invalid (%i)',
(newValue) => {
const { result } = renderHook(() =>
useDatePicker({
inputProps: { id: 'id', label: 'label' },
isRangeMode: false,
onDateChange: onDayChange,
errorMessages,
})
);

act(() => {
result.current.handleInputOnChange(newValue);
});

expect(result.current.inputValue).toBe(newValue);
expect(result.current.statusWithMessage).toEqual({
status: 'error',
message: 'Invalid input',
});
expect(onDayChange).not.toBeCalled();
}
);

it('should update the input value if the date string is not complete', () => {
const { result } = renderHook(() =>
useDatePicker({
inputProps: { id: 'id', label: 'label' },
isRangeMode: false,
onDateChange: onDayChange,
errorMessages,
})
);

act(() => {
result.current.handleInputOnChange('3/04/24');
});

expect(result.current.inputValue).toBe('3/04/24');
expect(result.current.statusWithMessage).toBe(undefined);
expect(onDayChange).not.toBeCalled();
});

it('should show an error if the date is not within the selectable slot', () => {
const { result } = renderHook(() =>
useDatePicker({
selectableSlot: { start: new Date(2024, 0, 1), end: new Date(2024, 1, 1) },
inputProps: { id: 'id', label: 'label' },
isRangeMode: false,
onDateChange: onDayChange,
errorMessages,
})
);

act(() => {
result.current.handleInputOnChange('03/04/24');
});

expect(result.current.inputValue).toBe('03/04/24');
expect(result.current.statusWithMessage).toEqual({
status: 'error',
message: 'Invalid date',
});
expect(onDayChange).not.toBeCalled();
});

it('should update the parent value is the date is valid', () => {
const { result } = renderHook(() =>
useDatePicker({
inputProps: { id: 'id', label: 'label' },
isRangeMode: false,
onDateChange: onDayChange,
errorMessages,
})
);

act(() => {
result.current.handleInputOnChange('01/02/24');
});

expect(result.current.inputValue).toBe('01/02/24');
expect(result.current.statusWithMessage).toBe(undefined);
expect(onDayChange).toHaveBeenCalledWith(new Date(2024, 1, 1));
});
});

describe('range mode', () => {
it('should do nothing in range mode', () => {
const { result } = renderHook(() =>
useDatePicker({
value: { start: new Date(2024, 0, 1), end: null },
inputProps: {
id: 'id',
label: 'label',
},
isRangeMode: true,
onDateChange: onDayChange,
errorMessages,
})
);

act(() => {
result.current.handleInputOnChange('01/02/24');
});

expect(result.current.inputValue).toBe('01/01/24 - ');
expect(onDayChange).not.toBeCalled();
});
});
});
});
12 changes: 8 additions & 4 deletions ui-core/src/components/inputs/datePicker/useCalendarPicker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';

import { type CalendarPickerProps } from './CalendarPicker';
import { generateSequentialDates, validateSlots } from './utils';
Expand All @@ -10,9 +10,9 @@ export default function useCalendarPicker({
numberOfMonths = 1,
}: Omit<CalendarPickerProps, 'modalPosition' | 'calendarPickerRef' | 'onDayClick'>) {
validateSlots(selectedSlot, selectableSlot, initialDate);
const initialActiveDate =
initialDate ?? selectedSlot?.start ?? selectableSlot?.start ?? new Date();
const [activeDate, setActiveDate] = useState<Date>(initialActiveDate);

const [activeDate, setActiveDate] = useState(new Date());

const displayedMonthsStartDates = generateSequentialDates(activeDate, numberOfMonths);

const activeYear = activeDate.getFullYear();
Expand Down Expand Up @@ -48,6 +48,10 @@ export default function useCalendarPicker({
}
};

useEffect(() => {
setActiveDate(initialDate ?? selectedSlot?.start ?? selectableSlot?.start ?? new Date());
}, [initialDate, selectedSlot, selectableSlot]);

return {
displayedMonthsStartDates,
showNavigationBtn,
Expand Down
Loading
Loading