diff --git a/change/@fluentui-react-datepicker-compat-6da0cd91-e9c0-4bdc-b670-d3dca86fb9a8.json b/change/@fluentui-react-datepicker-compat-6da0cd91-e9c0-4bdc-b670-d3dca86fb9a8.json new file mode 100644 index 0000000000000..a2be4b6320f6c --- /dev/null +++ b/change/@fluentui-react-datepicker-compat-6da0cd91-e9c0-4bdc-b670-d3dca86fb9a8.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: Add null to value prop for controlled cases to work correctly.", + "packageName": "@fluentui/react-datepicker-compat", + "email": "esteban.230@hotmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-datepicker-compat/etc/react-datepicker-compat.api.md b/packages/react-components/react-datepicker-compat/etc/react-datepicker-compat.api.md index f6efab232fb88..74dac022259fb 100644 --- a/packages/react-components/react-datepicker-compat/etc/react-datepicker-compat.api.md +++ b/packages/react-components/react-datepicker-compat/etc/react-datepicker-compat.api.md @@ -175,7 +175,7 @@ export type DatePickerProps = Omit>, 'de positioning?: PositioningProps; placeholder?: string; today?: Date; - value?: Date; + value?: Date | null; formatDate?: (date?: Date) => string; parseDateFromString?: (dateStr: string) => Date | null; firstDayOfWeek?: DayOfWeek; diff --git a/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.test.tsx b/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.test.tsx index b11b6394a759e..0fca4ef12829c 100644 --- a/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.test.tsx +++ b/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.test.tsx @@ -16,13 +16,32 @@ function queryByRoleDialog(result: RenderResult) { } } -const getDatepickerPopoverElement = (result: RenderResult) => { +const getDatepickerPopupElement = (result: RenderResult) => { result.getByRole('combobox').click(); const dialog = queryByRoleDialog(result); expect(dialog).not.toBeNull(); return dialog!; }; +const ControlledDatePicker = (props: Partial>) => { + const [value, setValue] = React.useState(null); + + return ( + { + props.formatDate?.(); + return !date ? '' : date.getDate() + '/' + (date.getMonth() + 1) + '/' + (date.getFullYear() % 100); + }} + onSelectDate={date => { + props.onSelectDate?.(date); + date !== undefined && setValue(date); + }} + /> + ); +}; + describe('DatePicker', () => { beforeEach(() => { resetIdsForTests(); @@ -41,7 +60,7 @@ describe('DatePicker', () => { popupSurface: datePickerClassNames.popupSurface, calendar: datePickerClassNames.calendar, }, - getPortalElement: getDatepickerPopoverElement, + getPortalElement: getDatepickerPopupElement, }, ], }, @@ -136,4 +155,26 @@ describe('DatePicker', () => { expect(input.getAttribute('value')).toBe('15/1/20'); }); + + it('calls onSelectDate when controlled', () => { + const onSelectDate = jest.fn(); + const result = render(); + + fireEvent.click(result.getByRole('combobox')); + result.getAllByRole('gridcell')[10].click(); + + expect(onSelectDate).toHaveBeenCalledTimes(1); + }); + + it('calls onSelectDate and formatDate when controlled', () => { + const onSelectDate = jest.fn(); + const formatDate = jest.fn(); + const result = render(); + + fireEvent.click(result.getByRole('combobox')); + result.getAllByRole('gridcell')[10].click(); + + expect(onSelectDate).toHaveBeenCalledTimes(1); + expect(formatDate).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.types.ts b/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.types.ts index 7418c522045b4..e2d656ed1add5 100644 --- a/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.types.ts +++ b/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.types.ts @@ -135,8 +135,11 @@ export type DatePickerProps = Omit>, 'de /** * Default value of the DatePicker, if any + * + * When the component is controlled, `null` should be used instead of `undefined` to avoid controlled vs. uncontrolled + * ambiguity. */ - value?: Date; + value?: Date | null; /** * Optional method to format the chosen date to a string to display in the DatePicker diff --git a/packages/react-components/react-datepicker-compat/src/components/DatePicker/useDatePicker.tsx b/packages/react-components/react-datepicker-compat/src/components/DatePicker/useDatePicker.tsx index 1c1f1a2f3615e..bd257f1f5dd7d 100644 --- a/packages/react-components/react-datepicker-compat/src/components/DatePicker/useDatePicker.tsx +++ b/packages/react-components/react-datepicker-compat/src/components/DatePicker/useDatePicker.tsx @@ -66,21 +66,14 @@ function usePopupVisibility(props: DatePickerProps) { } function useSelectedDate({ formatDate, onSelectDate, value }: DatePickerProps) { - const [selectedDate, setSelectedDateState] = useControllableState({ - initialState: undefined, + const [selectedDate, setSelectedDateState] = useControllableState({ + initialState: null, state: value, }); const [formattedDate, setFormattedDate] = React.useState(() => (value && formatDate ? formatDate(value) : '')); - const setSelectedDate = (newDate: Date | undefined) => { - if ( - (selectedDate === undefined && newDate !== undefined) || - (selectedDate !== undefined && newDate === undefined) || - (newDate && selectedDate && (newDate > selectedDate || newDate < selectedDate)) - ) { - onSelectDate?.(newDate); - } - + const setSelectedDate = (newDate: Date | null | undefined) => { + onSelectDate?.(newDate); setSelectedDateState(newDate); setFormattedDate(newDate && formatDate ? formatDate(newDate) : ''); }; @@ -215,7 +208,7 @@ export const useDatePicker_unstable = (props: DatePickerProps, ref: React.Ref { + (newlySelectedDate?: Date | null): void => { if (open) { setOpen(false); @@ -460,7 +453,7 @@ export const useDatePicker_unstable = (props: DatePickerProps, ref: React.Ref { const styles = useStyles(); - const [selectedDate, setSelectedDate] = React.useState(new Date()); + const [selectedDate, setSelectedDate] = React.useState(null); const goPrevious = React.useCallback(() => { - setSelectedDate(prevSelectedDate => (prevSelectedDate ? addDays(prevSelectedDate, -1) : undefined)); + setSelectedDate(prevSelectedDate => (prevSelectedDate ? addDays(prevSelectedDate, -1) : null)); }, []); const goNext = React.useCallback(() => { - setSelectedDate(prevSelectedDate => (prevSelectedDate ? addDays(prevSelectedDate, 1) : undefined)); + setSelectedDate(prevSelectedDate => (prevSelectedDate ? addDays(prevSelectedDate, 1) : null)); }, []); return ( @@ -34,7 +34,7 @@ export const Controlled = () => { void} + onSelectDate={setSelectedDate} placeholder="Select a date..." className={styles.control} /> @@ -54,7 +54,9 @@ export const Controlled = () => { Controlled.parameters = { docs: { description: { - story: 'A DatePicker can be controlled by manually keeping track of the state and updating it.', + story: + 'A DatePicker can be controlled by manually keeping track of the state and updating it. When controlled,' + + ' the value prop should use null instead of undefined to clear the value of the DatePicker.', }, }, }; diff --git a/packages/react-components/react-datepicker-compat/stories/DatePicker/DatePickerCustomDateFormatting.stories.tsx b/packages/react-components/react-datepicker-compat/stories/DatePicker/DatePickerCustomDateFormatting.stories.tsx index e4de796c80b62..3e15e1f02ced7 100644 --- a/packages/react-components/react-datepicker-compat/stories/DatePicker/DatePickerCustomDateFormatting.stories.tsx +++ b/packages/react-components/react-datepicker-compat/stories/DatePicker/DatePickerCustomDateFormatting.stories.tsx @@ -24,11 +24,11 @@ const onFormatDate = (date?: Date): string => { export const CustomDateFormatting = () => { const styles = useStyles(); - const [value, setValue] = React.useState(); + const [value, setValue] = React.useState(null); const datePickerRef = React.useRef(null); const onClick = React.useCallback((): void => { - setValue(undefined); + setValue(null); datePickerRef.current?.focus(); }, []);