Skip to content
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
@@ -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": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export type DatePickerProps = Omit<ComponentProps<Partial<DatePickerSlots>>, 'de
positioning?: PositioningProps;
placeholder?: string;
today?: Date;
value?: Date;
value?: Date | null;
formatDate?: (date?: Date) => string;
parseDateFromString?: (dateStr: string) => Date | null;
firstDayOfWeek?: DayOfWeek;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<React.ComponentProps<typeof DatePicker>>) => {
const [value, setValue] = React.useState<Date | null>(null);

return (
<DatePicker
value={value}
allowTextInput
formatDate={date => {
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();
Expand All @@ -41,7 +60,7 @@ describe('DatePicker', () => {
popupSurface: datePickerClassNames.popupSurface,
calendar: datePickerClassNames.calendar,
},
getPortalElement: getDatepickerPopoverElement,
getPortalElement: getDatepickerPopupElement,
},
],
},
Expand Down Expand Up @@ -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(<ControlledDatePicker onSelectDate={onSelectDate} />);

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(<ControlledDatePicker formatDate={formatDate} onSelectDate={onSelectDate} />);

fireEvent.click(result.getByRole('combobox'));
result.getAllByRole('gridcell')[10].click();

expect(onSelectDate).toHaveBeenCalledTimes(1);
expect(formatDate).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,11 @@ export type DatePickerProps = Omit<ComponentProps<Partial<DatePickerSlots>>, '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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,14 @@ function usePopupVisibility(props: DatePickerProps) {
}

function useSelectedDate({ formatDate, onSelectDate, value }: DatePickerProps) {
const [selectedDate, setSelectedDateState] = useControllableState({
initialState: undefined,
const [selectedDate, setSelectedDateState] = useControllableState<Date | null | undefined>({
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) : '');
};
Expand Down Expand Up @@ -215,7 +208,7 @@ export const useDatePicker_unstable = (props: DatePickerProps, ref: React.Ref<HT
);

const dismissDatePickerPopup = React.useCallback(
(newlySelectedDate?: Date): void => {
(newlySelectedDate?: Date | null): void => {
if (open) {
setOpen(false);

Expand Down Expand Up @@ -460,7 +453,7 @@ export const useDatePicker_unstable = (props: DatePickerProps, ref: React.Ref<HT
focus,
reset() {
setOpen(false);
setSelectedDate(undefined);
setSelectedDate(null);
},
showDatePickerPopup,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,22 @@ const useStyles = makeStyles({
export const Controlled = () => {
const styles = useStyles();

const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(new Date());
const [selectedDate, setSelectedDate] = React.useState<Date | null | undefined>(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 (
<div className={styles.root}>
<Field label="Select a date">
<DatePicker
value={selectedDate}
onSelectDate={setSelectedDate as (date: Date | null | undefined) => void}
onSelectDate={setSelectedDate}
placeholder="Select a date..."
className={styles.control}
/>
Expand All @@ -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.',
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ const onFormatDate = (date?: Date): string => {
export const CustomDateFormatting = () => {
const styles = useStyles();

const [value, setValue] = React.useState<Date | undefined>();
const [value, setValue] = React.useState<Date | null | undefined>(null);
const datePickerRef = React.useRef<IDatePicker>(null);

const onClick = React.useCallback((): void => {
setValue(undefined);
setValue(null);
datePickerRef.current?.focus();
}, []);

Expand Down