From 86b9648ef2cecd3ad04ad3fc5ebbcc501badf901 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 24 Jan 2023 09:13:38 -0500 Subject: [PATCH 01/57] Update prop names, make renamed currentDate prop controlled --- .../react/TimePicker/TimePicker.Example.tsx | 8 ++-- .../src/components/TimePicker/TimePicker.tsx | 38 +++++++++++-------- .../components/TimePicker/TimePicker.types.ts | 16 ++++---- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx index 7a005524f058b..f78fc4162fe57 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx @@ -15,7 +15,7 @@ const timePickerStyles: Partial = { }; const onFormatDate = (date: Date) => `Custom prefix + ${date.toLocaleTimeString()}`; -const onChange = (_: React.FormEvent, date: Date) => console.log('SELECTED DATE: ', date); +const onTimeChange = (date: Date) => console.log('SELECTED DATE: ', date); export const TimePickerBasicExample: React.FC = () => { const timeRange: ITimeRange = { @@ -31,8 +31,8 @@ export const TimePickerBasicExample: React.FC = () => { allowFreeform autoComplete="on" label={'TimePicker basic example'} - onChange={onChange} - defaultValue={new Date('November 25, 2021 09:15:00')} + onTimeChange={onTimeChange} + currentDate={new Date('November 25, 2021 09:15:00')} useComboBoxAsMenuWidth /> { label={'TimePicker with non default options'} useComboBoxAsMenuWidth timeRange={timeRange} - onChange={onChange} + onTimeChange={onTimeChange} /> = ({ useHour12 = false, timeRange, strings = getDefaultStrings(useHour12, showSeconds), - defaultValue, - onChange, + currentDate, + onTimeChange, onFormatDate, onValidateUserInput, + placeholder = strings.defaultTimePickerPlaceholder, ...rest }: ITimePickerProps) => { const [userText, setUserText] = React.useState(''); + const [selectedKey, setSelectedKey] = React.useState(); const [errorMessage, setErrorMessage] = React.useState(''); const optionsCount = getDropdownOptionsCount(increments, timeRange); - const initialValue = useConst(defaultValue || new Date()); - const baseDate: Date = React.useMemo(() => generateBaseDate(increments, timeRange, initialValue), [ - increments, - timeRange, - initialValue, - ]); + const baseDate = React.useMemo(() => { + const initialDate = currentDate || new Date(); + return generateBaseDate(increments, timeRange, initialDate); + }, [increments, timeRange, currentDate]); + + React.useEffect(() => { + if (onTimeChange && !errorMessage && userText) { + const currentChosenTime = userText; + const date = getDateFromTimeSelection(useHour12, currentDate, currentChosenTime); + onTimeChange(date); + } + }, [currentDate]); const timePickerOptions: IComboBoxOption[] = React.useMemo(() => { const optionsList = Array(optionsCount); @@ -76,10 +85,8 @@ export const TimePicker: React.FunctionComponent = ({ }); }, [baseDate, increments, optionsCount, showSeconds, onFormatDate, useHour12]); - const [selectedKey, setSelectedKey] = React.useState(timePickerOptions[0].key); - const onInputChange = React.useCallback( - (event: React.FormEvent, option?: IComboBoxOption, index?: number, value?: string): void => { + (_: React.FormEvent, option?: IComboBoxOption, _index?: number, value?: string): void => { const validateUserInput = (userInput: string): string => { let errorMessageToDisplay = ''; let regex: RegExp; @@ -114,10 +121,10 @@ export const TimePicker: React.FunctionComponent = ({ updatedUserText = option.text; } - if (onChange && !errorMessageToDisplay) { + if (onTimeChange && !errorMessageToDisplay) { const selectedTime = value || option?.text || ''; const date = getDateFromTimeSelection(useHour12, baseDate, selectedTime); - onChange(event, date); + onTimeChange(date); } setErrorMessage(errorMessageToDisplay); @@ -127,7 +134,7 @@ export const TimePicker: React.FunctionComponent = ({ [ baseDate, allowFreeform, - onChange, + onTimeChange, onFormatDate, onValidateUserInput, showSeconds, @@ -157,6 +164,7 @@ export const TimePicker: React.FunctionComponent = ({ return ( { + extends Omit { /** * Label of the component */ @@ -70,14 +68,14 @@ export interface ITimePickerProps strings?: ITimePickerStrings; /** - * Default value of the TimePicker, if any + * Controlled current date for the TimePicker, if any */ - defaultValue?: Date; + currentDate?: Date; /** * Callback issued when the time is changed */ - onChange?: (event: React.FormEvent, time: Date) => void; + onTimeChange?: (time: Date) => void; /** * Callback to localize the date strings displayed for dropdown options From 4a6766bfcca6ea71d9f000f7ecd3181c978ededc Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 24 Jan 2023 10:23:26 -0500 Subject: [PATCH 02/57] Added DatePicker and TimePicker combination example --- .../react/TimePicker/TimePicker.Example.tsx | 99 ++++++++++++------- 1 file changed, 66 insertions(+), 33 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx index f78fc4162fe57..28e8d3981e424 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx @@ -1,6 +1,10 @@ import * as React from 'react'; import { ITimeRange, TimePicker } from '@fluentui/react/lib/TimePicker'; -import { IStackTokens, Stack, IStackStyles, IComboBoxStyles, IComboBox } from '@fluentui/react'; +import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; +import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; +import { DatePicker } from '@fluentui/react/lib/DatePicker'; +import { Label } from '@fluentui/react/lib/Label'; +import { Text } from '@fluentui/react/lib/Text'; const stackStyles: Partial = { root: { maxWidth: 300 } }; const stackTokens: IStackTokens = { childrenGap: 20 }; @@ -18,43 +22,72 @@ const onFormatDate = (date: Date) => `Custom prefix + ${date.toLocaleTimeString( const onTimeChange = (date: Date) => console.log('SELECTED DATE: ', date); export const TimePickerBasicExample: React.FC = () => { + const [datePickerDate, setDatePickerDate] = React.useState(); + const [currentTime, setCurrentTime] = React.useState(); + const [currentTimeString, setCurrentTimeString] = React.useState(''); + + const onSelectDate = React.useCallback((selectedDate: Date) => { + setDatePickerDate(selectedDate); + }, []); + const timeRange: ITimeRange = { start: 8, end: 14, }; return ( - - - - - + <> + + + + + +
+ +
+ + { + setCurrentTime(time); + }} + /> +
+ {`Current selected time: ${currentTime ? currentTime.toString() : ''}`} +
+ ); }; From 2f2e08a4b67671ebec6fd23dd480820602ca2ff9 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 24 Jan 2023 10:26:30 -0500 Subject: [PATCH 03/57] Rephrase example text --- .../react-examples/src/react/TimePicker/TimePicker.Example.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx index 28e8d3981e424..9defce1bf2432 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx @@ -86,7 +86,7 @@ export const TimePickerBasicExample: React.FC = () => { }} /> - {`Current selected time: ${currentTime ? currentTime.toString() : ''}`} + {`TimePicker selected time: ${currentTime ? currentTime.toString() : ''}`} ); From d8f04f6d41bd9c93126df8dfb9c8be8f0fce0922 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 24 Jan 2023 13:01:07 -0500 Subject: [PATCH 04/57] Combine ComboBox imports --- packages/react/src/components/TimePicker/TimePicker.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 9355012d0ca1b..40357b76b63cf 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -7,8 +7,7 @@ import { ceilMinuteToIncrement, getDateFromTimeSelection, } from '@fluentui/date-time-utilities'; -import { ComboBox } from '../../ComboBox'; -import type { IComboBox, IComboBoxOption } from '../../ComboBox'; +import type { ComboBox, IComboBox, IComboBoxOption } from '../../ComboBox'; import type { ITimePickerProps, ITimeRange, ITimePickerStrings } from './TimePicker.types'; const REGEX_SHOW_SECONDS_HOUR_12 = /^((1[0-2]|0?[1-9]):([0-5][0-9]):([0-5][0-9])\s([AaPp][Mm]))$/; From 4729af96dc48ad94f4e032bdfacacd7e86c1f7b1 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 24 Jan 2023 13:24:39 -0500 Subject: [PATCH 05/57] Fixed ComboBox import and updated DateTimePicker example --- .../src/react/TimePicker/TimePicker.Example.tsx | 15 ++++++--------- .../src/components/TimePicker/TimePicker.tsx | 3 ++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx index 9defce1bf2432..4d4dbaf4eefda 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx @@ -23,13 +23,16 @@ const onTimeChange = (date: Date) => console.log('SELECTED DATE: ', date); export const TimePickerBasicExample: React.FC = () => { const [datePickerDate, setDatePickerDate] = React.useState(); - const [currentTime, setCurrentTime] = React.useState(); const [currentTimeString, setCurrentTimeString] = React.useState(''); const onSelectDate = React.useCallback((selectedDate: Date) => { setDatePickerDate(selectedDate); }, []); + const onDateTimePickerChange = React.useCallback((time: Date) => { + setCurrentTimeString(time.toString()); + }, []); + const timeRange: ITimeRange = { start: 8, end: 14, @@ -78,15 +81,9 @@ export const TimePickerBasicExample: React.FC = () => { onSelectDate={onSelectDate} minDate={new Date()} /> - { - setCurrentTime(time); - }} - /> + - {`TimePicker selected time: ${currentTime ? currentTime.toString() : ''}`} + {`TimePicker selected time: ${currentTimeString}`} ); diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 40357b76b63cf..9355012d0ca1b 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -7,7 +7,8 @@ import { ceilMinuteToIncrement, getDateFromTimeSelection, } from '@fluentui/date-time-utilities'; -import type { ComboBox, IComboBox, IComboBoxOption } from '../../ComboBox'; +import { ComboBox } from '../../ComboBox'; +import type { IComboBox, IComboBoxOption } from '../../ComboBox'; import type { ITimePickerProps, ITimeRange, ITimePickerStrings } from './TimePicker.types'; const REGEX_SHOW_SECONDS_HOUR_12 = /^((1[0-2]|0?[1-9]):([0-5][0-9]):([0-5][0-9])\s([AaPp][Mm]))$/; From a78b720c5cca8bcc96e2a724b4713d652c93370e Mon Sep 17 00:00:00 2001 From: James Wu Date: Mon, 27 Feb 2023 10:08:14 -0500 Subject: [PATCH 06/57] Moved examples into standalone files --- .../TimePicker/TimePicker.Basic.Example.tsx | 71 +++++++++++++++ .../TimePicker.CustomTimeStrings.Example.tsx | 36 ++++++++ .../TimePicker.DateTimePicker.Example.tsx | 50 +++++++++++ .../react/TimePicker/TimePicker.Example.tsx | 90 ------------------- 4 files changed, 157 insertions(+), 90 deletions(-) create mode 100644 packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx create mode 100644 packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx create mode 100644 packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx delete mode 100644 packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx new file mode 100644 index 0000000000000..2d13c1cf62b33 --- /dev/null +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { TimePicker, ITimeRange } from '@fluentui/react/lib/TimePicker'; +import { Text } from '@fluentui/react/lib/Text'; +import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; +import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; + +const stackStyles: Partial = { root: { maxWidth: 300 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; + +const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '50%', + }, +}; + +export const TimePickerBasicExample: React.FC = () => { + const [basicExampleTimeString, setBasicExampleTimeString] = React.useState(''); + const [nonDefaultOptionsExampleTimeString, setNonDefaultOptionsExampleTimeString] = React.useState(''); + + const onBasicExampleChange = React.useCallback((time: Date) => { + console.log('CLICKEDDD'); + setBasicExampleTimeString(time.toString()); + }, []); + + const onNonDefaultOptionsExampleChange = React.useCallback((time: Date) => { + setNonDefaultOptionsExampleTimeString(time.toString()); + }, []); + + const timeRange: ITimeRange = { + start: 8, + end: 14, + }; + + return ( + <> + + + {`Basic example selected time: ${ + basicExampleTimeString ? basicExampleTimeString : '' + }`} + + + {`Non default options example selected time: ${ + nonDefaultOptionsExampleTimeString ? nonDefaultOptionsExampleTimeString : '' + }`} + + + ); +}; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx new file mode 100644 index 0000000000000..4dba8fa3e4397 --- /dev/null +++ b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { TimePicker } from '@fluentui/react/lib/TimePicker'; +import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; +import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; + +const stackStyles: Partial = { root: { maxWidth: 300 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; + +const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '50%', + }, +}; + +const onFormatDate = (date: Date) => `Custom prefix + ${date.toLocaleTimeString()}`; + +export const TimePickerCustomTimeStringsExample: React.FC = () => { + return ( + <> + + + + + ); +}; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx new file mode 100644 index 0000000000000..b92bba98cd131 --- /dev/null +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { ITimeRange, TimePicker } from '@fluentui/react/lib/TimePicker'; +import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; +import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; +import { DatePicker } from '@fluentui/react/lib/DatePicker'; +import { Label } from '@fluentui/react/lib/Label'; +import { Text } from '@fluentui/react/lib/Text'; + +const stackStyles: Partial = { root: { maxWidth: 300 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; + +const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '50%', + }, +}; + +export const TimePickerDateTimePickerExample: React.FC = () => { + const [datePickerDate, setDatePickerDate] = React.useState(); + const [currentTimeString, setCurrentTimeString] = React.useState(''); + + const onSelectDate = React.useCallback((selectedDate: Date) => { + setDatePickerDate(selectedDate); + }, []); + + const onDateTimePickerChange = React.useCallback((boi: Date) => { + setCurrentTimeString(boi.toString()); + }, []); + + return ( + <> +
+ +
+ + +
+ {`TimePicker selected time: ${currentTimeString ? currentTimeString : ''}`} +
+ + ); +}; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx deleted file mode 100644 index 4d4dbaf4eefda..0000000000000 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Example.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import * as React from 'react'; -import { ITimeRange, TimePicker } from '@fluentui/react/lib/TimePicker'; -import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; -import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; -import { DatePicker } from '@fluentui/react/lib/DatePicker'; -import { Label } from '@fluentui/react/lib/Label'; -import { Text } from '@fluentui/react/lib/Text'; - -const stackStyles: Partial = { root: { maxWidth: 300 } }; -const stackTokens: IStackTokens = { childrenGap: 20 }; - -const timePickerStyles: Partial = { - optionsContainerWrapper: { - height: '500px', - }, - root: { - width: '50%', - }, -}; - -const onFormatDate = (date: Date) => `Custom prefix + ${date.toLocaleTimeString()}`; -const onTimeChange = (date: Date) => console.log('SELECTED DATE: ', date); - -export const TimePickerBasicExample: React.FC = () => { - const [datePickerDate, setDatePickerDate] = React.useState(); - const [currentTimeString, setCurrentTimeString] = React.useState(''); - - const onSelectDate = React.useCallback((selectedDate: Date) => { - setDatePickerDate(selectedDate); - }, []); - - const onDateTimePickerChange = React.useCallback((time: Date) => { - setCurrentTimeString(time.toString()); - }, []); - - const timeRange: ITimeRange = { - start: 8, - end: 14, - }; - - return ( - <> - - - - - -
- -
- - -
- {`TimePicker selected time: ${currentTimeString}`} -
- - ); -}; From 0337ed175dab71d989494516f29263a0aa8298f6 Mon Sep 17 00:00:00 2001 From: James Wu Date: Mon, 27 Feb 2023 11:26:58 -0500 Subject: [PATCH 07/57] Refactors and use new dateAnchor prop --- .../src/components/TimePicker/TimePicker.tsx | 134 ++++++++++++------ .../components/TimePicker/TimePicker.types.ts | 36 +++-- 2 files changed, 115 insertions(+), 55 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 9355012d0ca1b..9970980dd33ef 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -8,8 +8,10 @@ import { getDateFromTimeSelection, } from '@fluentui/date-time-utilities'; import { ComboBox } from '../../ComboBox'; +import { format } from '../../Utilities'; import type { IComboBox, IComboBoxOption } from '../../ComboBox'; import type { ITimePickerProps, ITimeRange, ITimePickerStrings } from './TimePicker.types'; +import { useControllableValue } from '@fluentui/react-hooks'; const REGEX_SHOW_SECONDS_HOUR_12 = /^((1[0-2]|0?[1-9]):([0-5][0-9]):([0-5][0-9])\s([AaPp][Mm]))$/; const REGEX_HIDE_SECONDS_HOUR_12 = /^((1[0-2]|0?[1-9]):[0-5][0-9]\s([AaPp][Mm]))$/; @@ -22,12 +24,14 @@ const TIME_UPPER_BOUND = 23; const getDefaultStrings = (useHour12: boolean, showSeconds: boolean): ITimePickerStrings => { const hourUnits = useHour12 ? '12-hour' : '24-hour'; const timeFormat = `hh:mm${showSeconds ? ':ss' : ''}${useHour12 ? ' AP' : ''}`; - const errorMessageToDisplay = `Enter a valid time in the ${hourUnits} format: ${timeFormat}`; + const invalidInputErrorMessage = `Enter a valid time in the ${hourUnits} format: ${timeFormat}`; const defaultTimePickerPlaceholder = `Enter or select a time`; + const timeOutOfBoundsErrorMessage = `Please enter a time within the range of {0} and {1}`; return { - invalidInputErrorMessage: errorMessageToDisplay, + invalidInputErrorMessage, defaultTimePickerPlaceholder, + timeOutOfBoundsErrorMessage, }; }; @@ -42,31 +46,45 @@ export const TimePicker: React.FunctionComponent = ({ useHour12 = false, timeRange, strings = getDefaultStrings(useHour12, showSeconds), - currentDate, - onTimeChange, + defaultValue, + value, + dateAnchor, + onChange, onFormatDate, onValidateUserInput, placeholder = strings.defaultTimePickerPlaceholder, ...rest }: ITimePickerProps) => { - const [userText, setUserText] = React.useState(''); + const [comboBoxText, setComboBoxText] = React.useState(''); const [selectedKey, setSelectedKey] = React.useState(); const [errorMessage, setErrorMessage] = React.useState(''); - const optionsCount = getDropdownOptionsCount(increments, timeRange); + const [dateStartAnchor, setDateStartAnchor] = React.useState(new Date(dateAnchor || new Date())); + const [dateEndAnchor, setDateEndAnchor] = React.useState(new Date(dateAnchor || new Date())); + + const [selectedTime, setSelectedTime] = useControllableValue(value, defaultValue); - const baseDate = React.useMemo(() => { - const initialDate = currentDate || new Date(); - return generateBaseDate(increments, timeRange, initialDate); - }, [increments, timeRange, currentDate]); + const optionsCount = getDropdownOptionsCount(increments, timeRange); React.useEffect(() => { - if (onTimeChange && !errorMessage && userText) { - const currentChosenTime = userText; - const date = getDateFromTimeSelection(useHour12, currentDate, currentChosenTime); - onTimeChange(date); + const clampedStartAnchor = new Date(dateAnchor || new Date()); + const clampedEndAnchor = new Date(dateAnchor || new Date()); + + if (timeRange) { + const clampedTimeRange = clampTimeRange(timeRange); + if (clampedStartAnchor.getHours() !== clampedTimeRange.start) { + clampedStartAnchor.setHours(clampedTimeRange.start); + clampedStartAnchor.setMinutes(0); + } + if (clampedEndAnchor.getHours() !== clampedTimeRange.end) { + clampedEndAnchor.setHours(clampedTimeRange.end); + clampedEndAnchor.setMinutes(0); + } } - }, [currentDate]); + + setDateStartAnchor(ceilMinuteToIncrement(clampedStartAnchor, increments)); + setDateEndAnchor(ceilMinuteToIncrement(clampedEndAnchor, increments)); + }, [dateAnchor, increments, timeRange]); const timePickerOptions: IComboBoxOption[] = React.useMemo(() => { const optionsList = Array(optionsCount); @@ -75,7 +93,7 @@ export const TimePicker: React.FunctionComponent = ({ } return optionsList.map((_, index) => { - const option = addMinutes(baseDate, increments * index); + const option: Date = addMinutes(dateStartAnchor, increments * index); option.setSeconds(0); const optionText = onFormatDate ? onFormatDate(option) : formatTimeString(option, showSeconds, useHour12); return { @@ -83,10 +101,28 @@ export const TimePicker: React.FunctionComponent = ({ text: optionText, }; }); - }, [baseDate, increments, optionsCount, showSeconds, onFormatDate, useHour12]); + }, [dateStartAnchor, increments, optionsCount, showSeconds, onFormatDate, useHour12]); + + const checkComboBoxTextInDropdown = React.useCallback( + (formattedTimeString: string) => + timePickerOptions.some((option: IComboBoxOption) => option.key === formattedTimeString), + [timePickerOptions], + ); + + React.useEffect(() => { + if (selectedTime && !defaultValue) { + const formattedTimeString = onFormatDate + ? onFormatDate(selectedTime) + : formatTimeString(selectedTime, showSeconds, useHour12); + if (checkComboBoxTextInDropdown(formattedTimeString)) { + setSelectedKey(formattedTimeString); + } + setComboBoxText(formatTimeString(selectedTime, showSeconds, useHour12)); + } + }, [selectedTime, defaultValue, checkComboBoxTextInDropdown, onFormatDate, showSeconds, useHour12]); const onInputChange = React.useCallback( - (_: React.FormEvent, option?: IComboBoxOption, _index?: number, value?: string): void => { + (_: React.FormEvent, option?: IComboBoxOption, _index?: number, input?: string): void => { const validateUserInput = (userInput: string): string => { let errorMessageToDisplay = ''; let regex: RegExp; @@ -98,48 +134,61 @@ export const TimePicker: React.FunctionComponent = ({ if (!regex.test(userInput)) { errorMessageToDisplay = strings.invalidInputErrorMessage; } + if (timeRange) { + const optionDate: Date = getDateFromTimeSelection(useHour12, dateStartAnchor, userInput); + if (!(optionDate >= dateStartAnchor && optionDate <= dateEndAnchor)) { + errorMessageToDisplay = format( + strings.timeOutOfBoundsErrorMessage, + dateStartAnchor.toString(), + dateEndAnchor, + toString(), + ); + } + } return errorMessageToDisplay; }; - const key = option?.key; - let updatedUserText = ''; let errorMessageToDisplay = ''; - if (value) { + if (input) { if (allowFreeform && !option) { if (!onFormatDate) { // Validate only if user did not add onFormatDate - errorMessageToDisplay = validateUserInput(value); + errorMessageToDisplay = validateUserInput(input); } else { // Use user provided validation if onFormatDate is provided if (onValidateUserInput) { - errorMessageToDisplay = onValidateUserInput(value); + errorMessageToDisplay = onValidateUserInput(input); } } } - updatedUserText = value; + setComboBoxText(input); } else if (option) { - updatedUserText = option.text; + setSelectedKey(option.key); } - if (onTimeChange && !errorMessageToDisplay) { - const selectedTime = value || option?.text || ''; - const date = getDateFromTimeSelection(useHour12, baseDate, selectedTime); - onTimeChange(date); + const userInputBoi = input || option?.text || ''; + const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, userInputBoi); + setSelectedTime(updatedTime); + + if (onChange) { + onChange(updatedTime); } setErrorMessage(errorMessageToDisplay); - setUserText(updatedUserText); - setSelectedKey(key); }, [ - baseDate, + timeRange, + dateStartAnchor, + dateEndAnchor, allowFreeform, - onTimeChange, + onChange, onFormatDate, onValidateUserInput, showSeconds, useHour12, strings.invalidInputErrorMessage, + strings.timeOutOfBoundsErrorMessage, + setSelectedTime, ], ); @@ -171,7 +220,7 @@ export const TimePicker: React.FunctionComponent = ({ errorMessage={errorMessage} options={timePickerOptions} onChange={onInputChange} - text={userText} + text={comboBoxText} //eslint-disable-next-line onKeyPress={evaluatePressedKey} /> @@ -186,16 +235,7 @@ const clampTimeRange = (timeRange: ITimeRange): ITimeRange => { }; }; -const generateBaseDate = (increments: number, timeRange: ITimeRange | undefined, baseDate: Date) => { - if (timeRange) { - const clampedTimeRange = clampTimeRange(timeRange); - baseDate.setHours(clampedTimeRange.start); - } - - return ceilMinuteToIncrement(baseDate, increments); -}; - -const getDropdownOptionsCount = (increments: number, timeRange: ITimeRange | undefined) => { +const getHoursInRange = (timeRange: ITimeRange | undefined) => { let hoursInRange = TimeConstants.HoursInOneDay; if (timeRange) { const clampedTimeRange = clampTimeRange(timeRange); @@ -205,5 +245,11 @@ const getDropdownOptionsCount = (increments: number, timeRange: ITimeRange | und hoursInRange = timeRange.end - timeRange.start; } } + + return hoursInRange; +}; + +const getDropdownOptionsCount = (increments: number, timeRange: ITimeRange | undefined) => { + const hoursInRange = getHoursInRange(timeRange); return Math.floor((TimeConstants.MinutesInOneHour * hoursInRange) / increments); }; diff --git a/packages/react/src/components/TimePicker/TimePicker.types.ts b/packages/react/src/components/TimePicker/TimePicker.types.ts index cb2fc3c91b8bf..bcf8ab26db7b3 100644 --- a/packages/react/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react/src/components/TimePicker/TimePicker.types.ts @@ -1,4 +1,5 @@ -import type { IComboBoxProps } from '../../ComboBox'; +import * as React from 'react'; +import type { IComboBox, IComboBoxProps } from '../../ComboBox'; /** * {@docCategory TimePicker} @@ -21,6 +22,8 @@ export interface ITimePickerStrings { invalidInputErrorMessage: string; /** Default placeholder text to render within ComboBox if no placeholder is provided. */ defaultTimePickerPlaceholder: string; + /** Error message to render if the user input date is out of bounds. */ + timeOutOfBoundsErrorMessage: string; } /** @@ -29,12 +32,12 @@ export interface ITimePickerStrings { export interface ITimePickerProps extends Omit { /** - * Label of the component + * Label of the component. */ label?: string; /** - * Time increments, in minutes, of the options in the dropdown + * Time increments, in minutes, of the options in the dropdown. */ increments?: number; @@ -58,32 +61,43 @@ export interface ITimePickerProps allowFreeform?: boolean; /** - * Custom time range to for time options + * Custom time range to for time options. */ timeRange?: ITimeRange; /** - * Localized strings to use in the TimePicker + * Localized strings to use in the TimePicker. */ strings?: ITimePickerStrings; /** - * Controlled current date for the TimePicker, if any + * The uncontrolled default selected time. */ - currentDate?: Date; + defaultValue?: Date; /** - * Callback issued when the time is changed + * A Date representing the selected time. If you provide this, you must maintain selection + * state by observing onChange events and passing a new value in when changed. */ - onTimeChange?: (time: Date) => void; + value?: Date; /** - * Callback to localize the date strings displayed for dropdown options + * The date in which all dropdown options are based off of. + */ + dateAnchor?: Date; + + /** + * A callback for receiving a notification when the time has been changed. + */ + onChange?: (event?: React.FormEvent, time?: Date) => void; + + /** + * Callback to localize the date strings displayed for dropdown options. */ onFormatDate?: (date: Date) => string; /** - * Callback to use custom user-input validation + * Callback to use custom user-input validation. */ onValidateUserInput?: (userInput: string) => string; } From 27c2b3c80cd63290fdea3e34a07eac8c61ec2bc7 Mon Sep 17 00:00:00 2001 From: James Wu Date: Mon, 27 Feb 2023 14:59:41 -0500 Subject: [PATCH 08/57] Updated all examples --- .../TimePicker/TimePicker.Basic.Example.tsx | 6 +- .../TimePicker.Controlled.Example.tsx | 55 +++++++++++++++++++ .../TimePicker.CustomTimeStrings.Example.tsx | 26 +++++++-- .../TimePicker.DateTimePicker.Example.tsx | 20 +------ .../src/react/TimePicker/TimePicker.doc.tsx | 28 +++++++++- 5 files changed, 108 insertions(+), 27 deletions(-) create mode 100644 packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx index 2d13c1cf62b33..8cc3f9a67d4e8 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -4,7 +4,7 @@ import { Text } from '@fluentui/react/lib/Text'; import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; -const stackStyles: Partial = { root: { maxWidth: 300 } }; +const stackStyles: Partial = { root: { width: 500 } }; const stackTokens: IStackTokens = { childrenGap: 20 }; const timePickerStyles: Partial = { @@ -12,7 +12,7 @@ const timePickerStyles: Partial = { height: '500px', }, root: { - width: '50%', + width: '500px', }, }; @@ -21,7 +21,6 @@ export const TimePickerBasicExample: React.FC = () => { const [nonDefaultOptionsExampleTimeString, setNonDefaultOptionsExampleTimeString] = React.useState(''); const onBasicExampleChange = React.useCallback((time: Date) => { - console.log('CLICKEDDD'); setBasicExampleTimeString(time.toString()); }, []); @@ -60,6 +59,7 @@ export const TimePickerBasicExample: React.FC = () => { label={'TimePicker with non default options'} useComboBoxAsMenuWidth timeRange={timeRange} + dateAnchor={new Date('February 27, 2023 08:12:00')} onChange={onNonDefaultOptionsExampleChange} /> {`Non default options example selected time: ${ diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx new file mode 100644 index 0000000000000..f6fa79b3b0c72 --- /dev/null +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { TimePicker, ITimeRange } from '@fluentui/react/lib/TimePicker'; +import { Text } from '@fluentui/react/lib/Text'; +import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; +import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; + +const stackStyles: Partial = { root: { maxWidth: 300 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; + +const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '500px', + }, +}; + +export const TimePickerControlledExample: React.FC = () => { + const dateAnchor = new Date('February 27, 2023 08:00:00'); + const [time, setTime] = React.useState(new Date(dateAnchor)); + + const [controlledTimeString, setControlledTimeString] = React.useState(''); + + const onControlledExampleChange = React.useCallback((newTime: Date) => { + newTime.setHours((newTime.getHours() + 2) % 24); + setTime(newTime); + }, []); + + React.useEffect(() => { + setControlledTimeString(time.toString()); + }, [time]); + + return ( + <> + + + {`Controlled example selected time: ${ + controlledTimeString ? controlledTimeString : '' + }`} + + + ); +}; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx index 4dba8fa3e4397..bec6d33a7961f 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx @@ -2,8 +2,9 @@ import * as React from 'react'; import { TimePicker } from '@fluentui/react/lib/TimePicker'; import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; +import { Text } from '@fluentui/react/lib/Text'; -const stackStyles: Partial = { root: { maxWidth: 300 } }; +const stackStyles: Partial = { root: { width: 500 } }; const stackTokens: IStackTokens = { childrenGap: 20 }; const timePickerStyles: Partial = { @@ -11,13 +12,25 @@ const timePickerStyles: Partial = { height: '500px', }, root: { - width: '50%', + width: '500px', }, }; -const onFormatDate = (date: Date) => `Custom prefix + ${date.toLocaleTimeString()}`; - export const TimePickerCustomTimeStringsExample: React.FC = () => { + const [customTimeString, setCustomTimeString] = React.useState(''); + const onFormatDate = React.useCallback((date: Date) => `Custom prefix + ${date.toLocaleTimeString()}`, []); + const onValidateUserInput = React.useCallback((userInput: string) => { + if (!userInput.includes('Custom prefix +')) { + return 'Your input is missing "Custom prefix +"'; + } + return ''; + }, []); + + const onChange = React.useCallback((time: Date) => { + console.log('Selected time: ', time); + setCustomTimeString(time.toString()); + }, []); + return ( <> @@ -25,11 +38,16 @@ export const TimePickerCustomTimeStringsExample: React.FC = () => { styles={timePickerStyles} // eslint-disable-next-line react/jsx-no-bind onFormatDate={onFormatDate} + onValidateUserInput={onValidateUserInput} + onChange={onChange} useHour12 allowFreeform autoComplete="on" label={'TimePicker with custom time strings'} /> + {`Custom time strings example selected time: ${ + customTimeString ? customTimeString : '' + }`} ); diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx index b92bba98cd131..3093ca338fefb 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -1,23 +1,9 @@ import * as React from 'react'; -import { ITimeRange, TimePicker } from '@fluentui/react/lib/TimePicker'; -import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; -import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; +import { TimePicker } from '@fluentui/react/lib/TimePicker'; import { DatePicker } from '@fluentui/react/lib/DatePicker'; import { Label } from '@fluentui/react/lib/Label'; import { Text } from '@fluentui/react/lib/Text'; -const stackStyles: Partial = { root: { maxWidth: 300 } }; -const stackTokens: IStackTokens = { childrenGap: 20 }; - -const timePickerStyles: Partial = { - optionsContainerWrapper: { - height: '500px', - }, - root: { - width: '50%', - }, -}; - export const TimePickerDateTimePickerExample: React.FC = () => { const [datePickerDate, setDatePickerDate] = React.useState(); const [currentTimeString, setCurrentTimeString] = React.useState(''); @@ -32,7 +18,7 @@ export const TimePickerDateTimePickerExample: React.FC = () => { return ( <> -
+
{ onSelectDate={onSelectDate} minDate={new Date()} /> - +
{`TimePicker selected time: ${currentTimeString ? currentTimeString : ''}`}
diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx index 1be35cc0bb4d6..c8d54b1ec1976 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx @@ -2,8 +2,15 @@ import * as React from 'react'; import { IDocPageProps } from '@fluentui/react/lib/common/DocPage.types'; -import { TimePickerBasicExample } from './TimePicker.Example'; -const TimePickerExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.Example.tsx') as string; +import { TimePickerBasicExample } from './TimePicker.Basic.Example'; +import { TimePickerControlledExample } from './TimePicker.Controlled.Example'; +import { TimePickerCustomTimeStringsExample } from './TimePicker.CustomTimeStrings.Example'; +import { TimePickerDateTimePickerExample } from './TimePicker.DateTimePicker.Example'; + +const TimePickerBasicExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx') as string; +const TimePickerControlledExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx') as string; +const TimePickerCustomTimeStringsExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx') as string; +const TimePickerDateTimePickerExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx') as string; export const TimePickerPageProps: IDocPageProps = { title: 'TimePicker', @@ -12,9 +19,24 @@ export const TimePickerPageProps: IDocPageProps = { examples: [ { title: 'TimePicker basic', - code: TimePickerExampleCode, + code: TimePickerBasicExampleCode, view: , }, + { + title: 'TimePicker controlled', + code: TimePickerControlledExampleCode, + view: , + }, + { + title: 'TimePicker with custom time strings', + code: TimePickerCustomTimeStringsExampleCode, + view: , + }, + { + title: 'TimePicker with DatePicker', + code: TimePickerDateTimePickerExampleCode, + view: , + }, ], overview: require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/docs/TimePickerOverview.md'), bestPractices: require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/docs/TimePickerBestPractices.md'), From 7a2b6b975a59f243c636bd22806ed8e39cc7abe5 Mon Sep 17 00:00:00 2001 From: James Wu Date: Mon, 27 Feb 2023 15:00:06 -0500 Subject: [PATCH 09/57] This should resolve the rest --- .../src/timeMath/timeMath.ts | 15 ++++---- .../src/components/TimePicker/TimePicker.tsx | 36 ++++++++++--------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/date-time-utilities/src/timeMath/timeMath.ts b/packages/date-time-utilities/src/timeMath/timeMath.ts index 73ff189d0bee3..8c7add20fea43 100644 --- a/packages/date-time-utilities/src/timeMath/timeMath.ts +++ b/packages/date-time-utilities/src/timeMath/timeMath.ts @@ -40,11 +40,11 @@ export const ceilMinuteToIncrement = (date: Date, increments: number) => { /** * Returns a date object from the selected time. * @param useHour12 - If the time picker uses 12 or 24 hour formatting - * @param baseDate - The baseline date to calculate the offset of the selected time + * @param dateStartAnchor - The baseline date to calculate the offset of the selected time * @param selectedTime - A string representing the user selected time * @returns A new date object offset from the baseDate using the selected time. */ -export const getDateFromTimeSelection = (useHour12: boolean, baseDate: Date, selectedTime: string): Date => { +export const getDateFromTimeSelection = (useHour12: boolean, dateStartAnchor: Date, selectedTime: string): Date => { const [, selectedHours, selectedMinutes, selectedSeconds, selectedAp] = TimeConstants.TimeFormatRegex.exec(selectedTime) || []; @@ -61,17 +61,20 @@ export const getDateFromTimeSelection = (useHour12: boolean, baseDate: Date, sel } let hoursOffset; - if (baseDate.getHours() > hours || (baseDate.getHours() === hours && baseDate.getMinutes() > minutes)) { - hoursOffset = TimeConstants.HoursInOneDay - baseDate.getHours() + hours; + if ( + dateStartAnchor.getHours() > hours || + (dateStartAnchor.getHours() === hours && dateStartAnchor.getMinutes() > minutes) + ) { + hoursOffset = TimeConstants.HoursInOneDay - dateStartAnchor.getHours() + hours; } else { - hoursOffset = Math.abs(baseDate.getHours() - hours); + hoursOffset = Math.abs(dateStartAnchor.getHours() - hours); } const offset = TimeConstants.MillisecondsIn1Sec * TimeConstants.MinutesInOneHour * hoursOffset * TimeConstants.SecondsInOneMinute + seconds * TimeConstants.MillisecondsIn1Sec; - const date = new Date(baseDate.getTime() + offset); + const date = new Date(dateStartAnchor.getTime() + offset); date.setMinutes(minutes); date.setSeconds(seconds); diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 9970980dd33ef..ff26b16213885 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -95,31 +95,30 @@ export const TimePicker: React.FunctionComponent = ({ return optionsList.map((_, index) => { const option: Date = addMinutes(dateStartAnchor, increments * index); option.setSeconds(0); - const optionText = onFormatDate ? onFormatDate(option) : formatTimeString(option, showSeconds, useHour12); + const formattedTimeString = formatTimeString(option, showSeconds, useHour12); + const optionText = onFormatDate ? onFormatDate(option) : formattedTimeString; return { - key: optionText, + key: formattedTimeString, text: optionText, }; }); }, [dateStartAnchor, increments, optionsCount, showSeconds, onFormatDate, useHour12]); - const checkComboBoxTextInDropdown = React.useCallback( - (formattedTimeString: string) => - timePickerOptions.some((option: IComboBoxOption) => option.key === formattedTimeString), + const getComboBoxOptionInDropdown = React.useCallback( + (optionKey: string) => timePickerOptions.find((option: IComboBoxOption) => option.key === optionKey), [timePickerOptions], ); React.useEffect(() => { if (selectedTime && !defaultValue) { - const formattedTimeString = onFormatDate - ? onFormatDate(selectedTime) - : formatTimeString(selectedTime, showSeconds, useHour12); - if (checkComboBoxTextInDropdown(formattedTimeString)) { - setSelectedKey(formattedTimeString); + const formattedTimeString = formatTimeString(selectedTime, showSeconds, useHour12); + const option = getComboBoxOptionInDropdown(formattedTimeString); + if (option) { + setSelectedKey(option.key); + setComboBoxText(option.text); } - setComboBoxText(formatTimeString(selectedTime, showSeconds, useHour12)); } - }, [selectedTime, defaultValue, checkComboBoxTextInDropdown, onFormatDate, showSeconds, useHour12]); + }, [selectedTime, defaultValue, getComboBoxOptionInDropdown, onFormatDate, showSeconds, useHour12]); const onInputChange = React.useCallback( (_: React.FormEvent, option?: IComboBoxOption, _index?: number, input?: string): void => { @@ -166,12 +165,14 @@ export const TimePicker: React.FunctionComponent = ({ setSelectedKey(option.key); } - const userInputBoi = input || option?.text || ''; - const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, userInputBoi); - setSelectedTime(updatedTime); + if (!errorMessageToDisplay) { + const userInputBoi = option?.key || ''; + const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, userInputBoi); + setSelectedTime(updatedTime); - if (onChange) { - onChange(updatedTime); + if (onChange) { + onChange(updatedTime); + } } setErrorMessage(errorMessageToDisplay); @@ -223,6 +224,7 @@ export const TimePicker: React.FunctionComponent = ({ text={comboBoxText} //eslint-disable-next-line onKeyPress={evaluatePressedKey} + useComboBoxAsMenuWidth /> ); }; From f7061336f9d2e8943c0db9bc4565ee55dc8640a9 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 28 Feb 2023 10:33:58 -0500 Subject: [PATCH 10/57] This should resolve the outstanding issues... --- .../src/components/TimePicker/TimePicker.tsx | 23 +++++++++++-------- .../components/TimePicker/TimePicker.types.ts | 5 +++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index ff26b16213885..597f5cd8ad168 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -110,15 +110,18 @@ export const TimePicker: React.FunctionComponent = ({ ); React.useEffect(() => { - if (selectedTime && !defaultValue) { + if (selectedTime) { const formattedTimeString = formatTimeString(selectedTime, showSeconds, useHour12); const option = getComboBoxOptionInDropdown(formattedTimeString); + setSelectedKey(option?.key); + if (option) { - setSelectedKey(option.key); setComboBoxText(option.text); + } else { + setComboBoxText(formattedTimeString); } } - }, [selectedTime, defaultValue, getComboBoxOptionInDropdown, onFormatDate, showSeconds, useHour12]); + }, [selectedTime, getComboBoxOptionInDropdown, onFormatDate, showSeconds, useHour12]); const onInputChange = React.useCallback( (_: React.FormEvent, option?: IComboBoxOption, _index?: number, input?: string): void => { @@ -132,8 +135,7 @@ export const TimePicker: React.FunctionComponent = ({ } if (!regex.test(userInput)) { errorMessageToDisplay = strings.invalidInputErrorMessage; - } - if (timeRange) { + } else if (timeRange) { const optionDate: Date = getDateFromTimeSelection(useHour12, dateStartAnchor, userInput); if (!(optionDate >= dateStartAnchor && optionDate <= dateEndAnchor)) { errorMessageToDisplay = format( @@ -160,19 +162,20 @@ export const TimePicker: React.FunctionComponent = ({ } } } - setComboBoxText(input); - } else if (option) { - setSelectedKey(option.key); } if (!errorMessageToDisplay) { - const userInputBoi = option?.key || ''; - const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, userInputBoi); + const timeSelection = (option?.key as string) || input || ''; + const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, timeSelection); setSelectedTime(updatedTime); if (onChange) { onChange(updatedTime); } + } else { + const timeSelection = option?.text || input || ''; + setSelectedKey(option?.key as string); + setComboBoxText(timeSelection); } setErrorMessage(errorMessageToDisplay); diff --git a/packages/react/src/components/TimePicker/TimePicker.types.ts b/packages/react/src/components/TimePicker/TimePicker.types.ts index bcf8ab26db7b3..9dc7941fd7a01 100644 --- a/packages/react/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react/src/components/TimePicker/TimePicker.types.ts @@ -30,7 +30,10 @@ export interface ITimePickerStrings { * {@docCategory TimePicker} */ export interface ITimePickerProps - extends Omit { + extends Omit< + IComboBoxProps, + 'options' | 'selectedKey' | 'defaultSelectedKey' | 'multiSelect' | 'text' | 'onChange' | 'useComboBoxAsMenuWidth' + > { /** * Label of the component. */ From 6114244e5223432b493886e77d957b7c6118aa14 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 28 Feb 2023 10:34:18 -0500 Subject: [PATCH 11/57] Updated example strings --- .../src/react/TimePicker/TimePicker.Controlled.Example.tsx | 5 ++--- .../TimePicker/TimePicker.CustomTimeStrings.Example.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx index f6fa79b3b0c72..2e793db0ff2a4 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -18,12 +18,11 @@ const timePickerStyles: Partial = { export const TimePickerControlledExample: React.FC = () => { const dateAnchor = new Date('February 27, 2023 08:00:00'); - const [time, setTime] = React.useState(new Date(dateAnchor)); + const [time, setTime] = React.useState(new Date('February 27, 2023 10:00:00')); const [controlledTimeString, setControlledTimeString] = React.useState(''); const onControlledExampleChange = React.useCallback((newTime: Date) => { - newTime.setHours((newTime.getHours() + 2) % 24); setTime(newTime); }, []); @@ -40,7 +39,7 @@ export const TimePickerControlledExample: React.FC = () => { allowFreeform increments={15} autoComplete="on" - label={'TimePicker with non default options'} + label={'Controlled TimePicker with non default options'} useComboBoxAsMenuWidth dateAnchor={dateAnchor} value={time} diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx index bec6d33a7961f..457c4b9191d57 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx @@ -41,7 +41,7 @@ export const TimePickerCustomTimeStringsExample: React.FC = () => { onValidateUserInput={onValidateUserInput} onChange={onChange} useHour12 - allowFreeform + allowFreeform={false} autoComplete="on" label={'TimePicker with custom time strings'} /> From 83704dd15e219fdfaf0fb5041d1bba55ed375f1d Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 28 Feb 2023 13:25:31 -0500 Subject: [PATCH 12/57] Updated examples --- .../TimePicker.Controlled.Example.tsx | 2 +- .../TimePicker.DateTimePicker.Example.tsx | 33 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx index 2e793db0ff2a4..5c102eee5d17c 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -4,7 +4,7 @@ import { Text } from '@fluentui/react/lib/Text'; import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; -const stackStyles: Partial = { root: { maxWidth: 300 } }; +const stackStyles: Partial = { root: { width: 500 } }; const stackTokens: IStackTokens = { childrenGap: 20 }; const timePickerStyles: Partial = { diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx index 3093ca338fefb..554e8cf90d3ec 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -6,16 +6,38 @@ import { Text } from '@fluentui/react/lib/Text'; export const TimePickerDateTimePickerExample: React.FC = () => { const [datePickerDate, setDatePickerDate] = React.useState(); + const [currentTime, setCurrentTime] = React.useState(); const [currentTimeString, setCurrentTimeString] = React.useState(''); const onSelectDate = React.useCallback((selectedDate: Date) => { setDatePickerDate(selectedDate); }, []); - const onDateTimePickerChange = React.useCallback((boi: Date) => { - setCurrentTimeString(boi.toString()); + const onDateTimePickerChange = React.useCallback((date: Date) => { + setCurrentTime(date); }, []); + React.useEffect(() => { + if (currentTime) { + setCurrentTimeString(currentTime.toString()); + } + }, [currentTime]); + + React.useEffect(() => { + if (currentTime && datePickerDate) { + const earlyDatePickerDate = new Date(datePickerDate); + const laterDatePickerDate = new Date(datePickerDate); + laterDatePickerDate.setDate(datePickerDate.getDate() + 1); + + const updatedCurrentTime = new Date(currentTime); + + if (updatedCurrentTime < earlyDatePickerDate || updatedCurrentTime > laterDatePickerDate) { + updatedCurrentTime.setDate(earlyDatePickerDate.getDate()); + setCurrentTime(updatedCurrentTime); + } + } + }, [currentTime, datePickerDate]); + return ( <>
@@ -27,7 +49,12 @@ export const TimePickerDateTimePickerExample: React.FC = () => { onSelectDate={onSelectDate} minDate={new Date()} /> - +
{`TimePicker selected time: ${currentTimeString ? currentTimeString : ''}`}
From 86ee51a077f977186e75863b58b3aac1f4a4ae06 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 28 Feb 2023 14:26:39 -0500 Subject: [PATCH 13/57] More changes to the basic example --- .../src/react/TimePicker/TimePicker.Basic.Example.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx index 8cc3f9a67d4e8..dcc7a3e80efca 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -57,7 +57,7 @@ export const TimePickerBasicExample: React.FC = () => { increments={15} autoComplete="on" label={'TimePicker with non default options'} - useComboBoxAsMenuWidth + placeholder={'Non default options placeholder'} timeRange={timeRange} dateAnchor={new Date('February 27, 2023 08:12:00')} onChange={onNonDefaultOptionsExampleChange} From caa4604cb85c0c64a89c6ad4108aea34ce4fa769 Mon Sep 17 00:00:00 2001 From: James Wu Date: Wed, 1 Mar 2023 15:00:17 -0500 Subject: [PATCH 14/57] Removed useComboBoxAsMenuWidth prop --- .../src/react/TimePicker/TimePicker.Basic.Example.tsx | 1 - .../src/react/TimePicker/TimePicker.Controlled.Example.tsx | 1 - .../react/TimePicker/TimePicker.DateTimePicker.Example.tsx | 7 +------ 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx index dcc7a3e80efca..9f3034f0cf83a 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -44,7 +44,6 @@ export const TimePickerBasicExample: React.FC = () => { label={'TimePicker basic example'} onChange={onBasicExampleChange} dateAnchor={new Date('November 25, 2021 09:15:00')} - useComboBoxAsMenuWidth /> {`Basic example selected time: ${ basicExampleTimeString ? basicExampleTimeString : '' diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx index 5c102eee5d17c..38799fe19ef0e 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -40,7 +40,6 @@ export const TimePickerControlledExample: React.FC = () => { increments={15} autoComplete="on" label={'Controlled TimePicker with non default options'} - useComboBoxAsMenuWidth dateAnchor={dateAnchor} value={time} onChange={onControlledExampleChange} diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx index 554e8cf90d3ec..0e4bb9b2d8e85 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -49,12 +49,7 @@ export const TimePickerDateTimePickerExample: React.FC = () => { onSelectDate={onSelectDate} minDate={new Date()} /> - + {`TimePicker selected time: ${currentTimeString ? currentTimeString : ''}`} From ea37cb2fc651992bb4b9d1a5b41c23f1749c2fec Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 12:55:19 -0500 Subject: [PATCH 15/57] Update formatTimeString function to return 00 if hours is 24 --- .../date-time-utilities/src/timeFormatting/index.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/date-time-utilities/src/timeFormatting/index.ts b/packages/date-time-utilities/src/timeFormatting/index.ts index cb5ba9a67b011..b2725d2f21254 100644 --- a/packages/date-time-utilities/src/timeFormatting/index.ts +++ b/packages/date-time-utilities/src/timeFormatting/index.ts @@ -4,10 +4,17 @@ * @param showSeconds - Whether to show seconds in the formatted string * @param useHour12 - Whether to use 12-hour time */ -export const formatTimeString = (date: Date, showSeconds?: boolean, useHour12?: boolean): string => - date.toLocaleTimeString([], { +export const formatTimeString = (date: Date, showSeconds?: boolean, useHour12?: boolean): string => { + let localeTimeString = date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', second: showSeconds ? '2-digit' : undefined, hour12: useHour12, }); + + if (!useHour12 && localeTimeString.slice(0, 2) === '24') { + localeTimeString = '00' + localeTimeString.slice(2); + } + + return localeTimeString; +}; From 712e838bc1bdc6f588d3993fc069f7baee8c3b00 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 13:22:01 -0500 Subject: [PATCH 16/57] Addressed comments --- .../TimePicker/TimePicker.Basic.Example.tsx | 6 +++--- .../TimePicker.Controlled.Example.tsx | 2 +- .../TimePicker.CustomTimeStrings.Example.tsx | 2 +- .../src/components/TimePicker/TimePicker.tsx | 17 +++++++---------- .../components/TimePicker/TimePicker.types.ts | 2 ++ 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx index 9f3034f0cf83a..e8b952116f6db 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -41,7 +41,7 @@ export const TimePickerBasicExample: React.FC = () => { useHour12 allowFreeform autoComplete="on" - label={'TimePicker basic example'} + label="TimePicker basic example" onChange={onBasicExampleChange} dateAnchor={new Date('November 25, 2021 09:15:00')} /> @@ -55,8 +55,8 @@ export const TimePickerBasicExample: React.FC = () => { allowFreeform increments={15} autoComplete="on" - label={'TimePicker with non default options'} - placeholder={'Non default options placeholder'} + label="TimePicker with non default options" + placeholder="Non default options placeholder" timeRange={timeRange} dateAnchor={new Date('February 27, 2023 08:12:00')} onChange={onNonDefaultOptionsExampleChange} diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx index 38799fe19ef0e..368eb18baf86b 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -39,7 +39,7 @@ export const TimePickerControlledExample: React.FC = () => { allowFreeform increments={15} autoComplete="on" - label={'Controlled TimePicker with non default options'} + label="Controlled TimePicker with non default options" dateAnchor={dateAnchor} value={time} onChange={onControlledExampleChange} diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx index 457c4b9191d57..49e148603cf2c 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx @@ -43,7 +43,7 @@ export const TimePickerCustomTimeStringsExample: React.FC = () => { useHour12 allowFreeform={false} autoComplete="on" - label={'TimePicker with custom time strings'} + label="TimePicker with custom time strings" /> {`Custom time strings example selected time: ${ customTimeString ? customTimeString : '' diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 597f5cd8ad168..cf52d0be46623 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -59,10 +59,12 @@ export const TimePicker: React.FunctionComponent = ({ const [selectedKey, setSelectedKey] = React.useState(); const [errorMessage, setErrorMessage] = React.useState(''); - const [dateStartAnchor, setDateStartAnchor] = React.useState(new Date(dateAnchor || new Date())); - const [dateEndAnchor, setDateEndAnchor] = React.useState(new Date(dateAnchor || new Date())); + const [dateStartAnchor, setDateStartAnchor] = React.useState( + new Date(dateAnchor || defaultValue || new Date()), + ); + const [dateEndAnchor, setDateEndAnchor] = React.useState(new Date(dateAnchor || defaultValue || new Date())); - const [selectedTime, setSelectedTime] = useControllableValue(value, defaultValue); + const [selectedTime, setSelectedTime] = useControllableValue(value, defaultValue); const optionsCount = getDropdownOptionsCount(increments, timeRange); @@ -114,12 +116,7 @@ export const TimePicker: React.FunctionComponent = ({ const formattedTimeString = formatTimeString(selectedTime, showSeconds, useHour12); const option = getComboBoxOptionInDropdown(formattedTimeString); setSelectedKey(option?.key); - - if (option) { - setComboBoxText(option.text); - } else { - setComboBoxText(formattedTimeString); - } + setComboBoxText(option ? option.text : formattedTimeString); } }, [selectedTime, getComboBoxOptionInDropdown, onFormatDate, showSeconds, useHour12]); @@ -137,7 +134,7 @@ export const TimePicker: React.FunctionComponent = ({ errorMessageToDisplay = strings.invalidInputErrorMessage; } else if (timeRange) { const optionDate: Date = getDateFromTimeSelection(useHour12, dateStartAnchor, userInput); - if (!(optionDate >= dateStartAnchor && optionDate <= dateEndAnchor)) { + if (optionDate < dateStartAnchor || optionDate > dateEndAnchor) { errorMessageToDisplay = format( strings.timeOutOfBoundsErrorMessage, dateStartAnchor.toString(), diff --git a/packages/react/src/components/TimePicker/TimePicker.types.ts b/packages/react/src/components/TimePicker/TimePicker.types.ts index 9dc7941fd7a01..91bb52203afa9 100644 --- a/packages/react/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react/src/components/TimePicker/TimePicker.types.ts @@ -75,12 +75,14 @@ export interface ITimePickerProps /** * The uncontrolled default selected time. + * Mutually exclusive with `value`. */ defaultValue?: Date; /** * A Date representing the selected time. If you provide this, you must maintain selection * state by observing onChange events and passing a new value in when changed. + * Mutually exclusive with `defaultValue`. */ value?: Date; From 49a7a645dddcd331c2a8a085f84b64654b620aec Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 18:00:45 -0500 Subject: [PATCH 17/57] Renamed baseDate parameter to dateStartAnchor --- packages/date-time-utilities/etc/date-time-utilities.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/date-time-utilities/etc/date-time-utilities.api.md b/packages/date-time-utilities/etc/date-time-utilities.api.md index e747d16316d40..0da93885a90d4 100644 --- a/packages/date-time-utilities/etc/date-time-utilities.api.md +++ b/packages/date-time-utilities/etc/date-time-utilities.api.md @@ -105,7 +105,7 @@ export const formatYear: (date: Date) => string; export const getBoundedDateRange: (dateRange: Date[], minDate?: Date | undefined, maxDate?: Date | undefined) => Date[]; // @public -export const getDateFromTimeSelection: (useHour12: boolean, baseDate: Date, selectedTime: string) => Date; +export const getDateFromTimeSelection: (useHour12: boolean, dateStartAnchor: Date, selectedTime: string) => Date; // @public export function getDatePartHashValue(date: Date): number; From 794406e2117f362a313ce32a4b0e76998d64c3e6 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 18:01:59 -0500 Subject: [PATCH 18/57] Added prop onGetErrorMessage to get validation result --- packages/react/src/components/TimePicker/TimePicker.types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/react/src/components/TimePicker/TimePicker.types.ts b/packages/react/src/components/TimePicker/TimePicker.types.ts index 91bb52203afa9..0749da2a9fd50 100644 --- a/packages/react/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react/src/components/TimePicker/TimePicker.types.ts @@ -105,4 +105,9 @@ export interface ITimePickerProps * Callback to use custom user-input validation. */ onValidateUserInput?: (userInput: string) => string; + + /** + * Callback to get validation result. + */ + onGetErrorMessage?: (errorMessage: string) => void; } From 3682ec873a63d411a4177e193ac97aeb28baae3d Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 18:10:47 -0500 Subject: [PATCH 19/57] Set selectedTime to invalid or undefined --- .../src/components/TimePicker/TimePicker.tsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index cf52d0be46623..2b794b8967e03 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -52,6 +52,7 @@ export const TimePicker: React.FunctionComponent = ({ onChange, onFormatDate, onValidateUserInput, + onGetErrorMessage, placeholder = strings.defaultTimePickerPlaceholder, ...rest }: ITimePickerProps) => { @@ -59,12 +60,12 @@ export const TimePicker: React.FunctionComponent = ({ const [selectedKey, setSelectedKey] = React.useState(); const [errorMessage, setErrorMessage] = React.useState(''); - const [dateStartAnchor, setDateStartAnchor] = React.useState( - new Date(dateAnchor || defaultValue || new Date()), - ); - const [dateEndAnchor, setDateEndAnchor] = React.useState(new Date(dateAnchor || defaultValue || new Date())); + const [dateStartAnchor, setDateStartAnchor] = React.useState(dateAnchor || defaultValue || new Date()); + const [dateEndAnchor, setDateEndAnchor] = React.useState(dateAnchor || defaultValue || new Date()); - const [selectedTime, setSelectedTime] = useControllableValue(value, defaultValue); + const [selectedTime, setSelectedTime] = useControllableValue(value, defaultValue, (_ev, newValue) => + onChange?.(undefined, newValue), + ); const optionsCount = getDropdownOptionsCount(increments, timeRange); @@ -161,18 +162,19 @@ export const TimePicker: React.FunctionComponent = ({ } } - if (!errorMessageToDisplay) { - const timeSelection = (option?.key as string) || input || ''; - const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, timeSelection); - setSelectedTime(updatedTime); + if (onGetErrorMessage) { + onGetErrorMessage(errorMessageToDisplay); + } - if (onChange) { - onChange(updatedTime); - } - } else { + if (errorMessage || (input !== undefined && !input.length)) { const timeSelection = option?.text || input || ''; setSelectedKey(option?.key as string); setComboBoxText(timeSelection); + setSelectedTime(errorMessage ? getDateFromTimeSelection(useHour12, dateStartAnchor, timeSelection) : undefined); + } else { + const timeSelection = (option?.key as string) || input || ''; + const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, timeSelection); + setSelectedTime(updatedTime); } setErrorMessage(errorMessageToDisplay); @@ -182,7 +184,6 @@ export const TimePicker: React.FunctionComponent = ({ dateStartAnchor, dateEndAnchor, allowFreeform, - onChange, onFormatDate, onValidateUserInput, showSeconds, @@ -190,6 +191,8 @@ export const TimePicker: React.FunctionComponent = ({ strings.invalidInputErrorMessage, strings.timeOutOfBoundsErrorMessage, setSelectedTime, + onGetErrorMessage, + errorMessage, ], ); From 01bfdc7c0618b160da8d82fea6cea14b0168c6cd Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 18:55:37 -0500 Subject: [PATCH 20/57] Clamp value to updated dateAnchor --- .../src/components/TimePicker/TimePicker.tsx | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 2b794b8967e03..cb33df6121498 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -70,8 +70,8 @@ export const TimePicker: React.FunctionComponent = ({ const optionsCount = getDropdownOptionsCount(increments, timeRange); React.useEffect(() => { - const clampedStartAnchor = new Date(dateAnchor || new Date()); - const clampedEndAnchor = new Date(dateAnchor || new Date()); + const clampedStartAnchor = dateAnchor ? new Date(dateAnchor) : new Date(); + const clampedEndAnchor = dateAnchor ? new Date(dateAnchor) : new Date(); if (timeRange) { const clampedTimeRange = clampTimeRange(timeRange); @@ -83,12 +83,34 @@ export const TimePicker: React.FunctionComponent = ({ clampedEndAnchor.setHours(clampedTimeRange.end); clampedEndAnchor.setMinutes(0); } + } else { + clampedEndAnchor.setDate(clampedStartAnchor.getDate() + 1); } + clampedStartAnchor.setMinutes(0); + clampedStartAnchor.setSeconds(0); + + clampedEndAnchor.setMinutes(0); + clampedEndAnchor.setSeconds(0); + setDateStartAnchor(ceilMinuteToIncrement(clampedStartAnchor, increments)); setDateEndAnchor(ceilMinuteToIncrement(clampedEndAnchor, increments)); }, [dateAnchor, increments, timeRange]); + React.useEffect(() => { + if (selectedTime && !isNaN(selectedTime.valueOf())) { + if (selectedTime < dateStartAnchor || selectedTime > dateEndAnchor) { + const updatedCurrentTime = new Date(dateStartAnchor); + updatedCurrentTime.setHours(selectedTime.getHours()); + updatedCurrentTime.setMinutes(selectedTime.getMinutes()); + updatedCurrentTime.setSeconds(selectedTime.getSeconds()); + updatedCurrentTime.setMilliseconds(selectedTime.getMilliseconds()); + + setSelectedTime(updatedCurrentTime); + } + } + }, [selectedTime, dateStartAnchor, dateEndAnchor, setSelectedTime]); + const timePickerOptions: IComboBoxOption[] = React.useMemo(() => { const optionsList = Array(optionsCount); for (let i = 0; i < optionsCount; i++) { @@ -113,7 +135,7 @@ export const TimePicker: React.FunctionComponent = ({ ); React.useEffect(() => { - if (selectedTime) { + if (selectedTime && !isNaN(selectedTime.valueOf())) { const formattedTimeString = formatTimeString(selectedTime, showSeconds, useHour12); const option = getComboBoxOptionInDropdown(formattedTimeString); setSelectedKey(option?.key); @@ -166,11 +188,11 @@ export const TimePicker: React.FunctionComponent = ({ onGetErrorMessage(errorMessageToDisplay); } - if (errorMessage || (input !== undefined && !input.length)) { - const timeSelection = option?.text || input || ''; + if (errorMessageToDisplay || (input !== undefined && !input.length)) { + const timeSelection = input || option?.text || ''; setSelectedKey(option?.key as string); setComboBoxText(timeSelection); - setSelectedTime(errorMessage ? getDateFromTimeSelection(useHour12, dateStartAnchor, timeSelection) : undefined); + setSelectedTime(errorMessageToDisplay ? new Date('invalid') : undefined); } else { const timeSelection = (option?.key as string) || input || ''; const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, timeSelection); @@ -192,7 +214,6 @@ export const TimePicker: React.FunctionComponent = ({ strings.timeOutOfBoundsErrorMessage, setSelectedTime, onGetErrorMessage, - errorMessage, ], ); From e79e7b0c8c5459874f2cd5ee5ad53bff400f9d74 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 18:56:37 -0500 Subject: [PATCH 21/57] Make docs look nice and readable --- .../TimePicker/TimePicker.Basic.Example.tsx | 87 ++++++++----------- .../TimePicker.Controlled.Example.tsx | 50 ++++------- .../TimePicker.CustomTimeStrings.Example.tsx | 52 ++++------- .../TimePicker.DateTimePicker.Example.tsx | 47 +++------- .../TimePicker/TimePicker.Example.Wrapper.tsx | 23 +++++ 5 files changed, 109 insertions(+), 150 deletions(-) create mode 100644 packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx index e8b952116f6db..7f6ee663b6c95 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -1,31 +1,20 @@ import * as React from 'react'; import { TimePicker, ITimeRange } from '@fluentui/react/lib/TimePicker'; import { Text } from '@fluentui/react/lib/Text'; -import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; -import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; - -const stackStyles: Partial = { root: { width: 500 } }; -const stackTokens: IStackTokens = { childrenGap: 20 }; - -const timePickerStyles: Partial = { - optionsContainerWrapper: { - height: '500px', - }, - root: { - width: '500px', - }, -}; +import { timePickerStyles, TimePickerExampleWrapper } from './TimePicker.Example.Wrapper'; export const TimePickerBasicExample: React.FC = () => { const [basicExampleTimeString, setBasicExampleTimeString] = React.useState(''); const [nonDefaultOptionsExampleTimeString, setNonDefaultOptionsExampleTimeString] = React.useState(''); + const basicDateAnchor = new Date('November 25, 2021 09:00:00'); + const nonDefaultOptionsDateAnchor = new Date('February 27, 2023 08:00:00'); - const onBasicExampleChange = React.useCallback((time: Date) => { - setBasicExampleTimeString(time.toString()); + const onBasicExampleChange = React.useCallback((_, basicExampleTime: Date) => { + setBasicExampleTimeString(basicExampleTime?.toString()); }, []); - const onNonDefaultOptionsExampleChange = React.useCallback((time: Date) => { - setNonDefaultOptionsExampleTimeString(time.toString()); + const onNonDefaultOptionsExampleChange = React.useCallback((_, nonDefaultOptionsExampleTime: Date) => { + setNonDefaultOptionsExampleTimeString(nonDefaultOptionsExampleTime?.toString()); }, []); const timeRange: ITimeRange = { @@ -34,37 +23,35 @@ export const TimePickerBasicExample: React.FC = () => { }; return ( - <> - - - {`Basic example selected time: ${ - basicExampleTimeString ? basicExampleTimeString : '' - }`} - - - {`Non default options example selected time: ${ - nonDefaultOptionsExampleTimeString ? nonDefaultOptionsExampleTimeString : '' - }`} - - + + + {`⚓ Date anchor: ${basicDateAnchor.toString()}`} + {`⌚ Selected time: ${basicExampleTimeString ? basicExampleTimeString : ''}`} + + + {`⚓ Date anchor: ${nonDefaultOptionsDateAnchor.toString()}`} + {`⌚ Selected time: ${ + nonDefaultOptionsExampleTimeString ? nonDefaultOptionsExampleTimeString : '' + }`} + ); }; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx index 368eb18baf86b..6f0be64365057 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -1,20 +1,7 @@ import * as React from 'react'; import { TimePicker, ITimeRange } from '@fluentui/react/lib/TimePicker'; import { Text } from '@fluentui/react/lib/Text'; -import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; -import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; - -const stackStyles: Partial = { root: { width: 500 } }; -const stackTokens: IStackTokens = { childrenGap: 20 }; - -const timePickerStyles: Partial = { - optionsContainerWrapper: { - height: '500px', - }, - root: { - width: '500px', - }, -}; +import { timePickerStyles, TimePickerExampleWrapper } from './TimePicker.Example.Wrapper'; export const TimePickerControlledExample: React.FC = () => { const dateAnchor = new Date('February 27, 2023 08:00:00'); @@ -22,7 +9,7 @@ export const TimePickerControlledExample: React.FC = () => { const [controlledTimeString, setControlledTimeString] = React.useState(''); - const onControlledExampleChange = React.useCallback((newTime: Date) => { + const onControlledExampleChange = React.useCallback((_, newTime: Date) => { setTime(newTime); }, []); @@ -31,23 +18,20 @@ export const TimePickerControlledExample: React.FC = () => { }, [time]); return ( - <> - - - {`Controlled example selected time: ${ - controlledTimeString ? controlledTimeString : '' - }`} - - + + + {`⚓ Date anchor: ${dateAnchor.toString()}`} + {`⌚ Selected time: ${controlledTimeString ? controlledTimeString : ''}`} + ); }; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx index 49e148603cf2c..6c9e8f66dee7d 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx @@ -1,23 +1,11 @@ import * as React from 'react'; import { TimePicker } from '@fluentui/react/lib/TimePicker'; -import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; -import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; import { Text } from '@fluentui/react/lib/Text'; - -const stackStyles: Partial = { root: { width: 500 } }; -const stackTokens: IStackTokens = { childrenGap: 20 }; - -const timePickerStyles: Partial = { - optionsContainerWrapper: { - height: '500px', - }, - root: { - width: '500px', - }, -}; +import { timePickerStyles, TimePickerExampleWrapper } from './TimePicker.Example.Wrapper'; export const TimePickerCustomTimeStringsExample: React.FC = () => { const [customTimeString, setCustomTimeString] = React.useState(''); + const dateAnchor = new Date('February 27, 2023 08:00:00'); const onFormatDate = React.useCallback((date: Date) => `Custom prefix + ${date.toLocaleTimeString()}`, []); const onValidateUserInput = React.useCallback((userInput: string) => { if (!userInput.includes('Custom prefix +')) { @@ -26,29 +14,27 @@ export const TimePickerCustomTimeStringsExample: React.FC = () => { return ''; }, []); - const onChange = React.useCallback((time: Date) => { + const onChange = React.useCallback((_, time: Date) => { console.log('Selected time: ', time); setCustomTimeString(time.toString()); }, []); return ( - <> - - - {`Custom time strings example selected time: ${ - customTimeString ? customTimeString : '' - }`} - - + + + {`⚓ Date anchor: ${dateAnchor.toString()}`} + {`⌚ Selected time: ${customTimeString ? customTimeString : ''}`} + ); }; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx index 0e4bb9b2d8e85..2b9fbd639d82f 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -3,9 +3,11 @@ import { TimePicker } from '@fluentui/react/lib/TimePicker'; import { DatePicker } from '@fluentui/react/lib/DatePicker'; import { Label } from '@fluentui/react/lib/Label'; import { Text } from '@fluentui/react/lib/Text'; +import { TimePickerExampleWrapper } from './TimePicker.Example.Wrapper'; export const TimePickerDateTimePickerExample: React.FC = () => { - const [datePickerDate, setDatePickerDate] = React.useState(); + const currentDate = new Date(); + const [datePickerDate, setDatePickerDate] = React.useState(currentDate); const [currentTime, setCurrentTime] = React.useState(); const [currentTimeString, setCurrentTimeString] = React.useState(''); @@ -13,46 +15,23 @@ export const TimePickerDateTimePickerExample: React.FC = () => { setDatePickerDate(selectedDate); }, []); - const onDateTimePickerChange = React.useCallback((date: Date) => { + const onDateTimePickerChange = React.useCallback((_, date: Date) => { setCurrentTime(date); }, []); React.useEffect(() => { - if (currentTime) { - setCurrentTimeString(currentTime.toString()); - } + setCurrentTimeString(currentTime ? currentTime.toString() : ''); }, [currentTime]); - React.useEffect(() => { - if (currentTime && datePickerDate) { - const earlyDatePickerDate = new Date(datePickerDate); - const laterDatePickerDate = new Date(datePickerDate); - laterDatePickerDate.setDate(datePickerDate.getDate() + 1); - - const updatedCurrentTime = new Date(currentTime); - - if (updatedCurrentTime < earlyDatePickerDate || updatedCurrentTime > laterDatePickerDate) { - updatedCurrentTime.setDate(earlyDatePickerDate.getDate()); - setCurrentTime(updatedCurrentTime); - } - } - }, [currentTime, datePickerDate]); - return ( - <> -
- -
- - -
- {`TimePicker selected time: ${currentTimeString ? currentTimeString : ''}`} + + +
+ +
- + {`⚓ Date anchor: ${datePickerDate.toString()}`} + {`⌚ Selected time: ${currentTimeString ? currentTimeString : ''}`} +
); }; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx new file mode 100644 index 0000000000000..39f9785c3b16e --- /dev/null +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { TimePicker, ITimeRange } from '@fluentui/react/lib/TimePicker'; +import { Text } from '@fluentui/react/lib/Text'; +import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; +import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; + +export const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '500px', + }, +}; + +const stackStyles: Partial = { root: { width: 500 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; + +export const TimePickerExampleWrapper: React.FC = ({ children }) => ( + + {children} + +); From 1ff24d6161b403ac67b615890dd9ba78cb94c846 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 19:03:23 -0500 Subject: [PATCH 22/57] Removed unneeded imports --- .../src/react/TimePicker/TimePicker.Example.Wrapper.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx index 39f9785c3b16e..e848f43baf72a 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx @@ -1,6 +1,4 @@ import * as React from 'react'; -import { TimePicker, ITimeRange } from '@fluentui/react/lib/TimePicker'; -import { Text } from '@fluentui/react/lib/Text'; import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; From c8ca9446275d2ddca5cf8a19898fd987d40b0d07 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 19:15:29 -0500 Subject: [PATCH 23/57] Updated clampedStartAnchor initialization value --- packages/react/src/components/TimePicker/TimePicker.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index cb33df6121498..ab21bd98c3b2f 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -70,8 +70,9 @@ export const TimePicker: React.FunctionComponent = ({ const optionsCount = getDropdownOptionsCount(increments, timeRange); React.useEffect(() => { - const clampedStartAnchor = dateAnchor ? new Date(dateAnchor) : new Date(); - const clampedEndAnchor = dateAnchor ? new Date(dateAnchor) : new Date(); + const clampedStartAnchor = + (dateAnchor && new Date(dateAnchor)) || (defaultValue && new Date(defaultValue)) || new Date(); + const clampedEndAnchor = new Date(clampedStartAnchor); if (timeRange) { const clampedTimeRange = clampTimeRange(timeRange); From 00ae9eca809fa718717f20cac0df661cf2306665 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 19:33:51 -0500 Subject: [PATCH 24/57] Use internalDateAnchor --- .../react/src/components/TimePicker/TimePicker.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index ab21bd98c3b2f..d3103ee5b29e7 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -69,9 +69,14 @@ export const TimePicker: React.FunctionComponent = ({ const optionsCount = getDropdownOptionsCount(increments, timeRange); + const internalDateAnchor = React.useMemo(() => dateAnchor || value || defaultValue || new Date(), [ + dateAnchor, + defaultValue, + value, + ]); + React.useEffect(() => { - const clampedStartAnchor = - (dateAnchor && new Date(dateAnchor)) || (defaultValue && new Date(defaultValue)) || new Date(); + const clampedStartAnchor = new Date(internalDateAnchor); const clampedEndAnchor = new Date(clampedStartAnchor); if (timeRange) { @@ -96,7 +101,7 @@ export const TimePicker: React.FunctionComponent = ({ setDateStartAnchor(ceilMinuteToIncrement(clampedStartAnchor, increments)); setDateEndAnchor(ceilMinuteToIncrement(clampedEndAnchor, increments)); - }, [dateAnchor, increments, timeRange]); + }, [internalDateAnchor, increments, timeRange]); React.useEffect(() => { if (selectedTime && !isNaN(selectedTime.valueOf())) { From d4f49ffc96f601cac044c190d5d1b7b2dea080ab Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 2 Mar 2023 19:41:26 -0500 Subject: [PATCH 25/57] Use fallbackDateAnchor and update DateTimePicker example --- .../react/TimePicker/TimePicker.DateTimePicker.Example.tsx | 2 +- packages/react/src/components/TimePicker/TimePicker.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx index 2b9fbd639d82f..c32fd6ecb6d68 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -6,7 +6,7 @@ import { Text } from '@fluentui/react/lib/Text'; import { TimePickerExampleWrapper } from './TimePicker.Example.Wrapper'; export const TimePickerDateTimePickerExample: React.FC = () => { - const currentDate = new Date(); + const currentDate = new Date('2023-02-01 05:00:00'); const [datePickerDate, setDatePickerDate] = React.useState(currentDate); const [currentTime, setCurrentTime] = React.useState(); const [currentTimeString, setCurrentTimeString] = React.useState(''); diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index d3103ee5b29e7..82456308dc5ed 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -11,7 +11,7 @@ import { ComboBox } from '../../ComboBox'; import { format } from '../../Utilities'; import type { IComboBox, IComboBoxOption } from '../../ComboBox'; import type { ITimePickerProps, ITimeRange, ITimePickerStrings } from './TimePicker.types'; -import { useControllableValue } from '@fluentui/react-hooks'; +import { useControllableValue, useConst } from '@fluentui/react-hooks'; const REGEX_SHOW_SECONDS_HOUR_12 = /^((1[0-2]|0?[1-9]):([0-5][0-9]):([0-5][0-9])\s([AaPp][Mm]))$/; const REGEX_HIDE_SECONDS_HOUR_12 = /^((1[0-2]|0?[1-9]):[0-5][0-9]\s([AaPp][Mm]))$/; @@ -60,6 +60,8 @@ export const TimePicker: React.FunctionComponent = ({ const [selectedKey, setSelectedKey] = React.useState(); const [errorMessage, setErrorMessage] = React.useState(''); + const fallbackDateAnchor = useConst(new Date()); + const [dateStartAnchor, setDateStartAnchor] = React.useState(dateAnchor || defaultValue || new Date()); const [dateEndAnchor, setDateEndAnchor] = React.useState(dateAnchor || defaultValue || new Date()); @@ -69,10 +71,11 @@ export const TimePicker: React.FunctionComponent = ({ const optionsCount = getDropdownOptionsCount(increments, timeRange); - const internalDateAnchor = React.useMemo(() => dateAnchor || value || defaultValue || new Date(), [ + const internalDateAnchor = React.useMemo(() => dateAnchor || value || defaultValue || fallbackDateAnchor, [ dateAnchor, defaultValue, value, + fallbackDateAnchor, ]); React.useEffect(() => { From 791eb1147ae2de846babeb162fe9fea39397b644 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 11 Apr 2023 12:36:46 -0400 Subject: [PATCH 26/57] Revert to ITimePickerProps to extend original omitted IComboBoxProps --- packages/react/src/components/TimePicker/TimePicker.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.types.ts b/packages/react/src/components/TimePicker/TimePicker.types.ts index 0749da2a9fd50..ea12914d64f1b 100644 --- a/packages/react/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react/src/components/TimePicker/TimePicker.types.ts @@ -32,7 +32,7 @@ export interface ITimePickerStrings { export interface ITimePickerProps extends Omit< IComboBoxProps, - 'options' | 'selectedKey' | 'defaultSelectedKey' | 'multiSelect' | 'text' | 'onChange' | 'useComboBoxAsMenuWidth' + 'options' | 'selectedKey' | 'defaultSelectedKey' | 'multiSelect' | 'text' | 'defaultValue' | 'onChange' > { /** * Label of the component. From c43adaa5cb1f2954a425a85792459e0d374d2c6b Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 11 Apr 2023 12:37:09 -0400 Subject: [PATCH 27/57] Addressed comments --- packages/react/src/components/TimePicker/TimePicker.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 15dfdea9b3ebc..bb21305b46123 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -168,8 +168,7 @@ export const TimePicker: React.FunctionComponent = ({ errorMessageToDisplay = format( strings.timeOutOfBoundsErrorMessage, dateStartAnchor.toString(), - dateEndAnchor, - toString(), + dateEndAnchor.toString(), ); } } From ae6be3878af0202abe3db259012e997a11428be9 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 11 Apr 2023 12:49:25 -0400 Subject: [PATCH 28/57] API snapshot update --- packages/react/etc/react.api.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index 996bd2f4c0f03..36b7f9f7f31a4 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -9379,21 +9379,26 @@ export interface IThemeSlotRule { // @public (undocumented) export interface ITimePickerProps extends Omit { allowFreeform?: boolean; + dateAnchor?: Date; defaultValue?: Date; increments?: number; label?: string; - onChange?: (event: React_2.FormEvent, time: Date) => void; + onChange?: (event?: React_2.FormEvent, time?: Date) => void; onFormatDate?: (date: Date) => string; + onGetErrorMessage?: (errorMessage: string) => void; onValidateUserInput?: (userInput: string) => string; showSeconds?: boolean; strings?: ITimePickerStrings; timeRange?: ITimeRange; useHour12?: boolean; + value?: Date; } // @public export interface ITimePickerStrings { + defaultTimePickerPlaceholder: string; invalidInputErrorMessage: string; + timeOutOfBoundsErrorMessage: string; } // @public From 4d5879890a82a896780ec2f60ce1874d12868b2b Mon Sep 17 00:00:00 2001 From: James Wu Date: Wed, 12 Apr 2023 10:44:12 -0400 Subject: [PATCH 29/57] Updated TimePicker tests and added test for new controlled component variant --- .../components/TimePicker/TimePicker.test.tsx | 76 +++- .../__snapshots__/TimePicker.test.tsx.snap | 402 ++++++++++++++++++ 2 files changed, 465 insertions(+), 13 deletions(-) create mode 100644 packages/react/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap diff --git a/packages/react/src/components/TimePicker/TimePicker.test.tsx b/packages/react/src/components/TimePicker/TimePicker.test.tsx index 8ea8849fe54d6..83a7e33e23f23 100644 --- a/packages/react/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.test.tsx @@ -1,33 +1,83 @@ import * as React from 'react'; import { TimePicker } from './TimePicker'; -// import { ITimeRange } from './TimePicker.types'; -// import { create } from '@fluentui/test-utilities'; +import { ITimeRange } from './TimePicker.types'; +import { create } from '@fluentui/test-utilities'; import { mount } from 'enzyme'; import type { IComboBox } from '../ComboBox/ComboBox.types'; import { KeyCodes } from '../../Utilities'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; describe('TimePicker', () => { - // TODO: times in this snapshot test changed and failed builds - // it('renders correctly', () => { - // const timeRange: ITimeRange = { - // start: 0, - // end: 5, - // }; - // const component = create(); - // const tree = component.toJSON(); - // expect(tree).toMatchSnapshot(); - // }); + it('renders correctly', () => { + const timeRange: ITimeRange = { + start: 0, + end: 5, + }; + const component = create(); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); it('generates the formatted option', () => { const onFormatDate = (date: Date) => { return 'custom date option'; }; const timePicker = React.createRef(); + const dateAnchor = new Date('November 25, 2021 09:00:00'); + + mount( + , + ); - mount(); expect(timePicker!.current!.selectedOptions[0].text).toBe('custom date option'); }); + it('shows controlled time correctly', () => { + let _selectedTime = new Date('February 27, 2023 10:00:00'); + const onChange = (ev: React.FormEvent, time: Date | undefined): void => { + if (time) { + _selectedTime = time; + } + }; + const dateAnchor = new Date('February 27, 2023 08:00:00'); + + const { getByRole, getAllByRole } = render( + , + ); + + const timePickerComboBox = getByRole('combobox') as HTMLInputElement; + expect(timePickerComboBox.value).toEqual('10:00:00'); + + userEvent.click(timePickerComboBox); + const timePickerOptions = getAllByRole('option') as HTMLInputElement[]; + userEvent.click(timePickerOptions[1], undefined, { skipPointerEventsCheck: true }); + + const formattedSelectedTime = _selectedTime.toLocaleTimeString([], { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + }); + + const expectedTime = '8:15:00 AM'; + expect(formattedSelectedTime).toEqual(expectedTime); + }); + describe('validates entered text when', () => { it('receives an invalid hour input for 24-hour-no-seconds format', () => { const wrapper = mount(); diff --git a/packages/react/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap b/packages/react/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap new file mode 100644 index 0000000000000..6e58797c822f8 --- /dev/null +++ b/packages/react/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap @@ -0,0 +1,402 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TimePicker renders correctly 1`] = ` +
+ +
+ + +
+
+`; From b36ac86dec1fe74f87ba5bacf39a72c754b2eb3b Mon Sep 17 00:00:00 2001 From: James Wu Date: Wed, 12 Apr 2023 11:19:20 -0400 Subject: [PATCH 30/57] Added test for handling changed base date anchor --- .../components/TimePicker/TimePicker.test.tsx | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/react/src/components/TimePicker/TimePicker.test.tsx b/packages/react/src/components/TimePicker/TimePicker.test.tsx index 83a7e33e23f23..f35914aabae2f 100644 --- a/packages/react/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.test.tsx @@ -78,6 +78,55 @@ describe('TimePicker', () => { expect(formattedSelectedTime).toEqual(expectedTime); }); + it('changes time options to match new base date', () => { + let dateAnchor = new Date('April 12, 2023 00:00:00'); + let _selectedTime: Date | undefined = undefined; + const onChange = (ev: React.FormEvent, time: Date | undefined): void => { + _selectedTime = time; + }; + + const { getByRole, getAllByRole, rerender } = render( + , + ); + + const timePickerOptionIdx = 6; + const initialTimePickerComboBox = getByRole('combobox') as HTMLInputElement; + userEvent.click(initialTimePickerComboBox); + const timePickerOptions = getAllByRole('option') as HTMLInputElement[]; + userEvent.click(timePickerOptions[timePickerOptionIdx], undefined, { skipPointerEventsCheck: true }); + expect(_selectedTime!.toString()).toEqual(new Date('April 12, 2023 01:30:00').toString()); + + dateAnchor = new Date('April 05, 2023 00:00:00'); + + rerender( + , + ); + + const updatedTimePickerComboBox = getByRole('combobox') as HTMLInputElement; + userEvent.click(updatedTimePickerComboBox); + const updatedTimePickerOptions = getAllByRole('option') as HTMLInputElement[]; + userEvent.click(updatedTimePickerOptions[timePickerOptionIdx], undefined, { skipPointerEventsCheck: true }); + expect(_selectedTime!.toString()).toEqual(new Date('April 05, 2023 01:30:00').toString()); + }); + describe('validates entered text when', () => { it('receives an invalid hour input for 24-hour-no-seconds format', () => { const wrapper = mount(); From 368ebaf88a5d096420cbc9b62e96d6bff39738c0 Mon Sep 17 00:00:00 2001 From: James Wu Date: Wed, 12 Apr 2023 12:17:12 -0400 Subject: [PATCH 31/57] Verify selected time changes on dateAnchor change --- .../react/src/components/TimePicker/TimePicker.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.test.tsx b/packages/react/src/components/TimePicker/TimePicker.test.tsx index f35914aabae2f..e453dc2302fad 100644 --- a/packages/react/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.test.tsx @@ -120,11 +120,13 @@ describe('TimePicker', () => { />, ); + expect(_selectedTime!.toString()).toEqual(new Date('April 05, 2023 01:30:00').toString()); + const updatedTimePickerComboBox = getByRole('combobox') as HTMLInputElement; userEvent.click(updatedTimePickerComboBox); const updatedTimePickerOptions = getAllByRole('option') as HTMLInputElement[]; - userEvent.click(updatedTimePickerOptions[timePickerOptionIdx], undefined, { skipPointerEventsCheck: true }); - expect(_selectedTime!.toString()).toEqual(new Date('April 05, 2023 01:30:00').toString()); + userEvent.click(updatedTimePickerOptions[timePickerOptionIdx + 1], undefined, { skipPointerEventsCheck: true }); + expect(_selectedTime!.toString()).toEqual(new Date('April 05, 2023 01:45:00').toString()); }); describe('validates entered text when', () => { From 7553c616854f4dac19d1fe699f8d19801e6aed2f Mon Sep 17 00:00:00 2001 From: James Wu Date: Wed, 12 Apr 2023 12:24:37 -0400 Subject: [PATCH 32/57] Added yarn change files --- ...ime-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json | 7 +++++++ ...luentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@fluentui-date-time-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json create mode 100644 change/@fluentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json diff --git a/change/@fluentui-date-time-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json b/change/@fluentui-date-time-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json new file mode 100644 index 0000000000000..3db44e1d50270 --- /dev/null +++ b/change/@fluentui-date-time-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Refactored getDateFromTimeSelection variable names.", + "packageName": "@fluentui/date-time-utilities", + "email": "jamwu@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json b/change/@fluentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json new file mode 100644 index 0000000000000..38e8664215d43 --- /dev/null +++ b/change/@fluentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Updated TimePicker control with controlled and uncontrolled props.", + "packageName": "@fluentui/react", + "email": "jamwu@microsoft.com", + "dependentChangeType": "patch" +} From f14f5e00287b29a8264ca424e4c45810f21fc098 Mon Sep 17 00:00:00 2001 From: James Wu Date: Wed, 12 Apr 2023 12:41:49 -0400 Subject: [PATCH 33/57] Resolve linting errors --- .../src/react/TimePicker/TimePicker.Controlled.Example.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx index 6f0be64365057..cccd1f390c298 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { TimePicker, ITimeRange } from '@fluentui/react/lib/TimePicker'; +import { TimePicker } from '@fluentui/react/lib/TimePicker'; import { Text } from '@fluentui/react/lib/Text'; import { timePickerStyles, TimePickerExampleWrapper } from './TimePicker.Example.Wrapper'; From 11d58b9be45dd8f3380866cbf23b1205fcfe23de Mon Sep 17 00:00:00 2001 From: James Wu Date: Wed, 12 Apr 2023 13:24:41 -0400 Subject: [PATCH 34/57] Resolved more linting errors --- .../src/react/TimePicker/TimePicker.Basic.Example.tsx | 3 ++- .../react/TimePicker/TimePicker.Controlled.Example.tsx | 3 ++- .../TimePicker.CustomTimeStrings.Example.tsx | 4 ++-- .../TimePicker/TimePicker.DateTimePicker.Example.tsx | 2 +- .../react/TimePicker/TimePicker.Example.Wrapper.tsx | 10 ---------- .../src/react/TimePicker/TimePickerStyles.ts | 10 ++++++++++ 6 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 packages/react-examples/src/react/TimePicker/TimePickerStyles.ts diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx index 7f6ee663b6c95..f830447d28b0c 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { TimePicker, ITimeRange } from '@fluentui/react/lib/TimePicker'; import { Text } from '@fluentui/react/lib/Text'; -import { timePickerStyles, TimePickerExampleWrapper } from './TimePicker.Example.Wrapper'; +import { TimePickerExampleWrapper } from '@fluentui/react-examples/lib/react/TimePicker/TimePicker.Example.Wrapper'; +import { timePickerStyles } from '@fluentui/react-examples/lib/react/TimePicker/TimePickerStyles'; export const TimePickerBasicExample: React.FC = () => { const [basicExampleTimeString, setBasicExampleTimeString] = React.useState(''); diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx index cccd1f390c298..da032f3c90e5e 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { TimePicker } from '@fluentui/react/lib/TimePicker'; import { Text } from '@fluentui/react/lib/Text'; -import { timePickerStyles, TimePickerExampleWrapper } from './TimePicker.Example.Wrapper'; +import { TimePickerExampleWrapper } from '@fluentui/react-examples/lib/react/TimePicker/TimePicker.Example.Wrapper'; +import { timePickerStyles } from '@fluentui/react-examples/lib/react/TimePicker/TimePickerStyles'; export const TimePickerControlledExample: React.FC = () => { const dateAnchor = new Date('February 27, 2023 08:00:00'); diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx index 6c9e8f66dee7d..686664681ce82 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { TimePicker } from '@fluentui/react/lib/TimePicker'; import { Text } from '@fluentui/react/lib/Text'; -import { timePickerStyles, TimePickerExampleWrapper } from './TimePicker.Example.Wrapper'; +import { TimePickerExampleWrapper } from '@fluentui/react-examples/lib/react/TimePicker/TimePicker.Example.Wrapper'; +import { timePickerStyles } from '@fluentui/react-examples/lib/react/TimePicker/TimePickerStyles'; export const TimePickerCustomTimeStringsExample: React.FC = () => { const [customTimeString, setCustomTimeString] = React.useState(''); @@ -23,7 +24,6 @@ export const TimePickerCustomTimeStringsExample: React.FC = () => { { const currentDate = new Date('2023-02-01 05:00:00'); diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx index e848f43baf72a..560dccff1a556 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx @@ -1,15 +1,5 @@ import * as React from 'react'; import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; -import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; - -export const timePickerStyles: Partial = { - optionsContainerWrapper: { - height: '500px', - }, - root: { - width: '500px', - }, -}; const stackStyles: Partial = { root: { width: 500 } }; const stackTokens: IStackTokens = { childrenGap: 20 }; diff --git a/packages/react-examples/src/react/TimePicker/TimePickerStyles.ts b/packages/react-examples/src/react/TimePicker/TimePickerStyles.ts new file mode 100644 index 0000000000000..8dc9af92c2040 --- /dev/null +++ b/packages/react-examples/src/react/TimePicker/TimePickerStyles.ts @@ -0,0 +1,10 @@ +import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; + +export const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '500px', + }, +}; From dcf3e7da333e690b5cbaf680efcc66d6733b79c9 Mon Sep 17 00:00:00 2001 From: James Wu Date: Wed, 12 Apr 2023 13:58:14 -0400 Subject: [PATCH 35/57] Resolved linting import error by in-lining stack and styles --- .../TimePicker/TimePicker.Basic.Example.tsx | 21 +++++++++++++------ .../TimePicker.Controlled.Example.tsx | 21 +++++++++++++------ .../TimePicker.CustomTimeStrings.Example.tsx | 21 +++++++++++++------ .../TimePicker.DateTimePicker.Example.tsx | 13 ++++++------ .../TimePicker/TimePicker.Example.Wrapper.tsx | 11 ---------- .../src/react/TimePicker/TimePickerStyles.ts | 10 --------- 6 files changed, 51 insertions(+), 46 deletions(-) delete mode 100644 packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx delete mode 100644 packages/react-examples/src/react/TimePicker/TimePickerStyles.ts diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx index f830447d28b0c..444aaad02a587 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -1,8 +1,17 @@ import * as React from 'react'; -import { TimePicker, ITimeRange } from '@fluentui/react/lib/TimePicker'; -import { Text } from '@fluentui/react/lib/Text'; -import { TimePickerExampleWrapper } from '@fluentui/react-examples/lib/react/TimePicker/TimePicker.Example.Wrapper'; -import { timePickerStyles } from '@fluentui/react-examples/lib/react/TimePicker/TimePickerStyles'; +import { TimePicker, ITimeRange, Text, IStackTokens, Stack, IStackStyles, IComboBoxStyles } from '@fluentui/react'; + +const stackStyles: Partial = { root: { width: 500 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; + +const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '500px', + }, +}; export const TimePickerBasicExample: React.FC = () => { const [basicExampleTimeString, setBasicExampleTimeString] = React.useState(''); @@ -24,7 +33,7 @@ export const TimePickerBasicExample: React.FC = () => { }; return ( - + { {`⌚ Selected time: ${ nonDefaultOptionsExampleTimeString ? nonDefaultOptionsExampleTimeString : '' }`} - + ); }; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx index da032f3c90e5e..f4002f719b88c 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -1,8 +1,17 @@ import * as React from 'react'; -import { TimePicker } from '@fluentui/react/lib/TimePicker'; -import { Text } from '@fluentui/react/lib/Text'; -import { TimePickerExampleWrapper } from '@fluentui/react-examples/lib/react/TimePicker/TimePicker.Example.Wrapper'; -import { timePickerStyles } from '@fluentui/react-examples/lib/react/TimePicker/TimePickerStyles'; +import { TimePicker, Text, IStackTokens, Stack, IStackStyles, IComboBoxStyles } from '@fluentui/react'; + +const stackStyles: Partial = { root: { width: 500 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; + +const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '500px', + }, +}; export const TimePickerControlledExample: React.FC = () => { const dateAnchor = new Date('February 27, 2023 08:00:00'); @@ -19,7 +28,7 @@ export const TimePickerControlledExample: React.FC = () => { }, [time]); return ( - + { /> {`⚓ Date anchor: ${dateAnchor.toString()}`} {`⌚ Selected time: ${controlledTimeString ? controlledTimeString : ''}`} - + ); }; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx index 686664681ce82..8698d6bc563f4 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx @@ -1,8 +1,17 @@ import * as React from 'react'; -import { TimePicker } from '@fluentui/react/lib/TimePicker'; -import { Text } from '@fluentui/react/lib/Text'; -import { TimePickerExampleWrapper } from '@fluentui/react-examples/lib/react/TimePicker/TimePicker.Example.Wrapper'; -import { timePickerStyles } from '@fluentui/react-examples/lib/react/TimePicker/TimePickerStyles'; +import { TimePicker, Text, IStackTokens, Stack, IStackStyles, IComboBoxStyles } from '@fluentui/react'; + +const stackStyles: Partial = { root: { width: 500 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; + +const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '500px', + }, +}; export const TimePickerCustomTimeStringsExample: React.FC = () => { const [customTimeString, setCustomTimeString] = React.useState(''); @@ -21,7 +30,7 @@ export const TimePickerCustomTimeStringsExample: React.FC = () => { }, []); return ( - + { /> {`⚓ Date anchor: ${dateAnchor.toString()}`} {`⌚ Selected time: ${customTimeString ? customTimeString : ''}`} - + ); }; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx index f1f7d206b81d6..233aec0b8fa66 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; -import { TimePicker } from '@fluentui/react/lib/TimePicker'; -import { DatePicker } from '@fluentui/react/lib/DatePicker'; -import { Label } from '@fluentui/react/lib/Label'; -import { Text } from '@fluentui/react/lib/Text'; -import { TimePickerExampleWrapper } from '@fluentui/react-examples/lib/react/TimePicker/TimePicker.Example.Wrapper'; +import { TimePicker, DatePicker, Label, Text, IStackTokens, Stack, IStackStyles } from '@fluentui/react'; + +const stackStyles: Partial = { root: { width: 500 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; export const TimePickerDateTimePickerExample: React.FC = () => { const currentDate = new Date('2023-02-01 05:00:00'); @@ -24,7 +23,7 @@ export const TimePickerDateTimePickerExample: React.FC = () => { }, [currentTime]); return ( - +
@@ -32,6 +31,6 @@ export const TimePickerDateTimePickerExample: React.FC = () => {
{`⚓ Date anchor: ${datePickerDate.toString()}`} {`⌚ Selected time: ${currentTimeString ? currentTimeString : ''}`} -
+ ); }; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx deleted file mode 100644 index 560dccff1a556..0000000000000 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Example.Wrapper.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; -import { IStackTokens, Stack, IStackStyles } from '@fluentui/react/lib/Stack'; - -const stackStyles: Partial = { root: { width: 500 } }; -const stackTokens: IStackTokens = { childrenGap: 20 }; - -export const TimePickerExampleWrapper: React.FC = ({ children }) => ( - - {children} - -); diff --git a/packages/react-examples/src/react/TimePicker/TimePickerStyles.ts b/packages/react-examples/src/react/TimePicker/TimePickerStyles.ts deleted file mode 100644 index 8dc9af92c2040..0000000000000 --- a/packages/react-examples/src/react/TimePicker/TimePickerStyles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IComboBoxStyles } from '@fluentui/react/lib/ComboBox'; - -export const timePickerStyles: Partial = { - optionsContainerWrapper: { - height: '500px', - }, - root: { - width: '500px', - }, -}; From 2d36ed1a7c3a86870ba8428b4263a9b118c69791 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 13 Apr 2023 15:06:30 -0400 Subject: [PATCH 36/57] Addressed comments --- ...ate-time-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json | 2 +- .../@fluentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/change/@fluentui-date-time-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json b/change/@fluentui-date-time-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json index 3db44e1d50270..27b3ee9a58068 100644 --- a/change/@fluentui-date-time-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json +++ b/change/@fluentui-date-time-utilities-d63c5a5c-4853-4fc8-b329-739b7176136a.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Refactored getDateFromTimeSelection variable names.", + "comment": "chore: Refactored getDateFromTimeSelection variable names.", "packageName": "@fluentui/date-time-utilities", "email": "jamwu@microsoft.com", "dependentChangeType": "patch" diff --git a/change/@fluentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json b/change/@fluentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json index 38e8664215d43..efa49f9d55663 100644 --- a/change/@fluentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json +++ b/change/@fluentui-react-956c059f-a412-4dce-8d66-05ce162223fc.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Updated TimePicker control with controlled and uncontrolled props.", + "comment": "feat(TimePicker): Updated TimePicker controlled and uncontrolled props to work correctly.", "packageName": "@fluentui/react", "email": "jamwu@microsoft.com", "dependentChangeType": "patch" From 270587420e152e3de4f8a003bb7de7638441b3c2 Mon Sep 17 00:00:00 2001 From: James Wu Date: Fri, 14 Apr 2023 15:46:49 -0400 Subject: [PATCH 37/57] Revert onChange prop types to avoid breaking changes and pass React.FormEvent to setSelectedTime callback --- packages/react/etc/react.api.md | 2 +- .../src/components/TimePicker/TimePicker.tsx | 20 ++++++++++++++----- .../components/TimePicker/TimePicker.types.ts | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index 36b7f9f7f31a4..ecb6cacc59a36 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -9383,7 +9383,7 @@ export interface ITimePickerProps extends Omit, time?: Date) => void; + onChange?: (event: React_2.FormEvent, time: Date) => void; onFormatDate?: (date: Date) => string; onGetErrorMessage?: (errorMessage: string) => void; onValidateUserInput?: (userInput: string) => string; diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index bb21305b46123..83897c231ba9c 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -35,6 +35,8 @@ const getDefaultStrings = (useHour12: boolean, showSeconds: boolean): ITimePicke }; }; +type HtmlElementComboBox = HTMLElement & Partial; + /** * {@docCategory TimePicker} */ @@ -65,8 +67,13 @@ export const TimePicker: React.FunctionComponent = ({ const [dateStartAnchor, setDateStartAnchor] = React.useState(dateAnchor || defaultValue || new Date()); const [dateEndAnchor, setDateEndAnchor] = React.useState(dateAnchor || defaultValue || new Date()); - const [selectedTime, setSelectedTime] = useControllableValue(value, defaultValue, (_ev: any, newTime: Date) => - onChange?.(undefined, newTime), + const [selectedTime, setSelectedTime] = useControllableValue>( + value, + defaultValue, + (ev: React.FormEvent, newTime: Date) => { + const event = ev as React.FormEvent; + onChange?.(event, newTime); + }, ); const optionsCount = getDropdownOptionsCount(increments, timeRange); @@ -151,7 +158,7 @@ export const TimePicker: React.FunctionComponent = ({ }, [selectedTime, getComboBoxOptionInDropdown, onFormatDate, showSeconds, useHour12]); const onInputChange = React.useCallback( - (_: React.FormEvent, option?: IComboBoxOption, _index?: number, input?: string): void => { + (ev: React.FormEvent, option?: IComboBoxOption, _index?: number, input?: string): void => { const validateUserInput = (userInput: string): string => { let errorMessageToDisplay = ''; let regex: RegExp; @@ -198,11 +205,14 @@ export const TimePicker: React.FunctionComponent = ({ const timeSelection = input || option?.text || ''; setSelectedKey(option?.key as string); setComboBoxText(timeSelection); - setSelectedTime(errorMessageToDisplay ? new Date('invalid') : undefined); + setSelectedTime( + errorMessageToDisplay ? new Date('invalid') : undefined, + ev as React.FormEvent, + ); } else { const timeSelection = (option?.key as string) || input || ''; const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, timeSelection); - setSelectedTime(updatedTime); + setSelectedTime(updatedTime, ev as React.FormEvent); } setErrorMessage(errorMessageToDisplay); diff --git a/packages/react/src/components/TimePicker/TimePicker.types.ts b/packages/react/src/components/TimePicker/TimePicker.types.ts index ea12914d64f1b..7625a3d110c53 100644 --- a/packages/react/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react/src/components/TimePicker/TimePicker.types.ts @@ -94,7 +94,7 @@ export interface ITimePickerProps /** * A callback for receiving a notification when the time has been changed. */ - onChange?: (event?: React.FormEvent, time?: Date) => void; + onChange?: (event: React.FormEvent, time: Date) => void; /** * Callback to localize the date strings displayed for dropdown options. From 12300fe5f0da4fd6ed4adf317884cd8c8c397de0 Mon Sep 17 00:00:00 2001 From: James Wu Date: Fri, 14 Apr 2023 16:21:00 -0400 Subject: [PATCH 38/57] Added explicit undefined pass to setSelectedTime in cases the dateAnchor changes --- packages/react/src/components/TimePicker/TimePicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 83897c231ba9c..f6dd9fdbf7e87 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -120,7 +120,7 @@ export const TimePicker: React.FunctionComponent = ({ updatedCurrentTime.setSeconds(selectedTime.getSeconds()); updatedCurrentTime.setMilliseconds(selectedTime.getMilliseconds()); - setSelectedTime(updatedCurrentTime); + setSelectedTime(updatedCurrentTime, undefined); } } }, [selectedTime, dateStartAnchor, dateEndAnchor, setSelectedTime]); From 3576bcff9322f154a74bf63668e4b479493238b5 Mon Sep 17 00:00:00 2001 From: James Wu Date: Mon, 17 Apr 2023 11:41:38 -0400 Subject: [PATCH 39/57] Updated examples and call onChange outside of useControllableValue to pass in proper event type --- .../TimePicker/TimePicker.Basic.Example.tsx | 15 ++- .../TimePicker.DateTimePicker.Example.tsx | 4 +- .../src/components/TimePicker/TimePicker.tsx | 110 +++++++++--------- 3 files changed, 66 insertions(+), 63 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx index 444aaad02a587..41a4fd0160c3a 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -1,5 +1,14 @@ import * as React from 'react'; -import { TimePicker, ITimeRange, Text, IStackTokens, Stack, IStackStyles, IComboBoxStyles } from '@fluentui/react'; +import { + TimePicker, + ITimeRange, + Text, + IStackTokens, + Stack, + IStackStyles, + IComboBoxStyles, + IComboBox, +} from '@fluentui/react'; const stackStyles: Partial = { root: { width: 500 } }; const stackTokens: IStackTokens = { childrenGap: 20 }; @@ -19,8 +28,8 @@ export const TimePickerBasicExample: React.FC = () => { const basicDateAnchor = new Date('November 25, 2021 09:00:00'); const nonDefaultOptionsDateAnchor = new Date('February 27, 2023 08:00:00'); - const onBasicExampleChange = React.useCallback((_, basicExampleTime: Date) => { - setBasicExampleTimeString(basicExampleTime?.toString()); + const onBasicExampleChange = React.useCallback((_ev: React.FormEvent, basicExampleTime: Date) => { + setBasicExampleTimeString(basicExampleTime.toString()); }, []); const onNonDefaultOptionsExampleChange = React.useCallback((_, nonDefaultOptionsExampleTime: Date) => { diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx index 233aec0b8fa66..3a3145fa65cdd 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { TimePicker, DatePicker, Label, Text, IStackTokens, Stack, IStackStyles } from '@fluentui/react'; +import { TimePicker, DatePicker, Label, Text, IStackTokens, Stack, IStackStyles, IComboBox } from '@fluentui/react'; const stackStyles: Partial = { root: { width: 500 } }; const stackTokens: IStackTokens = { childrenGap: 20 }; @@ -14,7 +14,7 @@ export const TimePickerDateTimePickerExample: React.FC = () => { setDatePickerDate(selectedDate); }, []); - const onDateTimePickerChange = React.useCallback((_, date: Date) => { + const onDateTimePickerChange = React.useCallback((_ev: React.FormEvent, date: Date) => { setCurrentTime(date); }, []); diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index f6dd9fdbf7e87..fd14fed393c7f 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -35,8 +35,6 @@ const getDefaultStrings = (useHour12: boolean, showSeconds: boolean): ITimePicke }; }; -type HtmlElementComboBox = HTMLElement & Partial; - /** * {@docCategory TimePicker} */ @@ -59,22 +57,12 @@ export const TimePicker: React.FunctionComponent = ({ ...rest }: ITimePickerProps) => { const [comboBoxText, setComboBoxText] = React.useState(''); - const [selectedKey, setSelectedKey] = React.useState(); + const [selectedKey, setSelectedKey] = React.useState(); const [errorMessage, setErrorMessage] = React.useState(''); const fallbackDateAnchor = useConst(new Date()); - const [dateStartAnchor, setDateStartAnchor] = React.useState(dateAnchor || defaultValue || new Date()); - const [dateEndAnchor, setDateEndAnchor] = React.useState(dateAnchor || defaultValue || new Date()); - - const [selectedTime, setSelectedTime] = useControllableValue>( - value, - defaultValue, - (ev: React.FormEvent, newTime: Date) => { - const event = ev as React.FormEvent; - onChange?.(event, newTime); - }, - ); + const [selectedTime, setSelectedTime] = useControllableValue(value, defaultValue); const optionsCount = getDropdownOptionsCount(increments, timeRange); @@ -83,47 +71,29 @@ export const TimePicker: React.FunctionComponent = ({ [dateAnchor, defaultValue, value, fallbackDateAnchor], ); - React.useEffect(() => { - const clampedStartAnchor = new Date(internalDateAnchor); - const clampedEndAnchor = new Date(); - - if (timeRange) { - const clampedTimeRange = clampTimeRange(timeRange); - if (clampedStartAnchor.getHours() !== clampedTimeRange.start) { - clampedStartAnchor.setHours(clampedTimeRange.start); - clampedStartAnchor.setMinutes(0); - } - if (clampedEndAnchor.getHours() !== clampedTimeRange.end) { - clampedEndAnchor.setHours(clampedTimeRange.end); - clampedEndAnchor.setMinutes(0); - } - } else { - clampedEndAnchor.setDate(clampedStartAnchor.getDate() + 1); - } - - clampedStartAnchor.setMinutes(0); - clampedStartAnchor.setSeconds(0); - - clampedEndAnchor.setMinutes(0); - clampedEndAnchor.setSeconds(0); + const dateStartAnchor = React.useMemo( + () => getDateAnchor(internalDateAnchor, 'start', increments, timeRange), + [internalDateAnchor, increments, timeRange], + ); - setDateStartAnchor(ceilMinuteToIncrement(clampedStartAnchor, increments)); - setDateEndAnchor(ceilMinuteToIncrement(clampedEndAnchor, increments)); - }, [internalDateAnchor, increments, timeRange]); + const dateEndAnchor = React.useMemo( + () => getDateAnchor(internalDateAnchor, 'end', increments, timeRange), + [internalDateAnchor, increments, timeRange], + ); - React.useEffect(() => { - if (selectedTime && !isNaN(selectedTime.valueOf())) { - if (selectedTime < dateStartAnchor || selectedTime > dateEndAnchor) { - const updatedCurrentTime = new Date(dateStartAnchor); - updatedCurrentTime.setHours(selectedTime.getHours()); - updatedCurrentTime.setMinutes(selectedTime.getMinutes()); - updatedCurrentTime.setSeconds(selectedTime.getSeconds()); - updatedCurrentTime.setMilliseconds(selectedTime.getMilliseconds()); + // React.useEffect(() => { + // if (selectedTime && !isNaN(selectedTime.valueOf())) { + // if (selectedTime < dateStartAnchor || selectedTime > dateEndAnchor) { + // const updatedCurrentTime = new Date(dateStartAnchor); + // updatedCurrentTime.setHours(selectedTime.getHours()); + // updatedCurrentTime.setMinutes(selectedTime.getMinutes()); + // updatedCurrentTime.setSeconds(selectedTime.getSeconds()); + // updatedCurrentTime.setMilliseconds(selectedTime.getMilliseconds()); - setSelectedTime(updatedCurrentTime, undefined); - } - } - }, [selectedTime, dateStartAnchor, dateEndAnchor, setSelectedTime]); + // setSelectedTime(updatedCurrentTime); + // } + // } + // }, [selectedTime, dateStartAnchor, dateEndAnchor, setSelectedTime]); const timePickerOptions: IComboBoxOption[] = React.useMemo(() => { const optionsList = Array(optionsCount); @@ -154,6 +124,8 @@ export const TimePicker: React.FunctionComponent = ({ const option = getComboBoxOptionInDropdown(formattedTimeString); setSelectedKey(option?.key); setComboBoxText(option ? option.text : formattedTimeString); + } else { + setSelectedKey(null); } }, [selectedTime, getComboBoxOptionInDropdown, onFormatDate, showSeconds, useHour12]); @@ -203,16 +175,14 @@ export const TimePicker: React.FunctionComponent = ({ if (errorMessageToDisplay || (input !== undefined && !input.length)) { const timeSelection = input || option?.text || ''; - setSelectedKey(option?.key as string); setComboBoxText(timeSelection); - setSelectedTime( - errorMessageToDisplay ? new Date('invalid') : undefined, - ev as React.FormEvent, - ); + setSelectedTime(errorMessageToDisplay ? new Date('invalid') : undefined); + onChange?.(ev, new Date('invalid')); } else { const timeSelection = (option?.key as string) || input || ''; const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, timeSelection); - setSelectedTime(updatedTime, ev as React.FormEvent); + setSelectedTime(updatedTime); + onChange?.(ev, updatedTime); } setErrorMessage(errorMessageToDisplay); @@ -230,6 +200,7 @@ export const TimePicker: React.FunctionComponent = ({ strings.timeOutOfBoundsErrorMessage, setSelectedTime, onGetErrorMessage, + onChange, ], ); @@ -270,6 +241,29 @@ export const TimePicker: React.FunctionComponent = ({ }; TimePicker.displayName = 'TimePicker'; +const getDateAnchor = ( + internalDateAnchor: Date, + startEnd: 'start' | 'end', + increments: number, + timeRange?: ITimeRange, +) => { + const clampedDateAnchor = new Date(internalDateAnchor); + if (timeRange) { + const clampedTimeRange = clampTimeRange(timeRange); + const timeRangeVal = startEnd === 'start' ? clampedTimeRange.start : clampedTimeRange.end; + if (clampedDateAnchor.getHours() !== timeRangeVal) { + clampedDateAnchor.setHours(timeRangeVal); + } + } else if (startEnd === 'end') { + clampedDateAnchor.setDate(clampedDateAnchor.getDate() + 1); + } + clampedDateAnchor.setMinutes(0); + clampedDateAnchor.setSeconds(0); + clampedDateAnchor.setMilliseconds(0); + + return ceilMinuteToIncrement(clampedDateAnchor, increments); +}; + const clampTimeRange = (timeRange: ITimeRange): ITimeRange => { return { start: Math.min(Math.max(timeRange.start, TIME_LOWER_BOUND), TIME_UPPER_BOUND), From 0e30b3898e434dcc545a9819c794bda7f9cdace4 Mon Sep 17 00:00:00 2001 From: James Wu Date: Mon, 17 Apr 2023 17:56:33 -0400 Subject: [PATCH 40/57] Control snapping of TimePicker values on DatePicker anchor change --- .../TimePicker.DateTimePicker.Example.tsx | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx index 3a3145fa65cdd..755ef1e77389f 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -4,17 +4,43 @@ import { TimePicker, DatePicker, Label, Text, IStackTokens, Stack, IStackStyles, const stackStyles: Partial = { root: { width: 500 } }; const stackTokens: IStackTokens = { childrenGap: 20 }; +const snapTimeToUpdatedDateAnchor = (datePickerDate: Date, currentTime: Date) => { + let snappedTime = new Date(currentTime); + + if (currentTime && !isNaN(currentTime.valueOf())) { + const startAnchor = new Date(datePickerDate); + const endAnchor = new Date(startAnchor); + endAnchor.setDate(startAnchor.getDate() + 1); + if (currentTime < startAnchor || currentTime > endAnchor) { + snappedTime = new Date(startAnchor); + snappedTime.setHours(currentTime.getHours()); + snappedTime.setMinutes(currentTime.getMinutes()); + snappedTime.setSeconds(currentTime.getSeconds()); + snappedTime.setMilliseconds(currentTime.getMilliseconds()); + } + } + + return snappedTime; +}; + export const TimePickerDateTimePickerExample: React.FC = () => { const currentDate = new Date('2023-02-01 05:00:00'); const [datePickerDate, setDatePickerDate] = React.useState(currentDate); const [currentTime, setCurrentTime] = React.useState(); const [currentTimeString, setCurrentTimeString] = React.useState(''); - const onSelectDate = React.useCallback((selectedDate: Date) => { - setDatePickerDate(selectedDate); - }, []); + const onSelectDate = React.useCallback( + (selectedDate: Date) => { + setDatePickerDate(selectedDate); + if (currentTime) { + const snappedTime = snapTimeToUpdatedDateAnchor(selectedDate, currentTime); + setCurrentTime(snappedTime); + } + }, + [currentTime], + ); - const onDateTimePickerChange = React.useCallback((_ev: React.FormEvent, date: Date) => { + const onTimePickerChange = React.useCallback((_ev: React.FormEvent, date: Date) => { setCurrentTime(date); }, []); @@ -27,7 +53,7 @@ export const TimePickerDateTimePickerExample: React.FC = () => {
- +
{`⚓ Date anchor: ${datePickerDate.toString()}`} {`⌚ Selected time: ${currentTimeString ? currentTimeString : ''}`} From a872cdc2df713d5665418ddd224df9866b6ea4e6 Mon Sep 17 00:00:00 2001 From: James Wu Date: Mon, 17 Apr 2023 17:57:58 -0400 Subject: [PATCH 41/57] Added tests for using defaultValue or value as date anchors --- .../components/TimePicker/TimePicker.test.tsx | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.test.tsx b/packages/react/src/components/TimePicker/TimePicker.test.tsx index e453dc2302fad..9e1194e514b20 100644 --- a/packages/react/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.test.tsx @@ -41,7 +41,7 @@ describe('TimePicker', () => { it('shows controlled time correctly', () => { let _selectedTime = new Date('February 27, 2023 10:00:00'); - const onChange = (ev: React.FormEvent, time: Date | undefined): void => { + const onChange = (_ev: React.FormEvent, time: Date): void => { if (time) { _selectedTime = time; } @@ -65,68 +65,79 @@ describe('TimePicker', () => { expect(timePickerComboBox.value).toEqual('10:00:00'); userEvent.click(timePickerComboBox); - const timePickerOptions = getAllByRole('option') as HTMLInputElement[]; - userEvent.click(timePickerOptions[1], undefined, { skipPointerEventsCheck: true }); + const timePickerOptions = getAllByRole('option') as HTMLButtonElement[]; + userEvent.click(timePickerOptions[2], undefined, { skipPointerEventsCheck: true }); const formattedSelectedTime = _selectedTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', second: '2-digit', + hour12: false, }); - const expectedTime = '8:15:00 AM'; + const expectedTime = '08:30:00'; expect(formattedSelectedTime).toEqual(expectedTime); }); - it('changes time options to match new base date', () => { - let dateAnchor = new Date('April 12, 2023 00:00:00'); - let _selectedTime: Date | undefined = undefined; - const onChange = (ev: React.FormEvent, time: Date | undefined): void => { - _selectedTime = time; + it('correctly renders options using value as date anchor', () => { + let _selectedTime = new Date('March 12, 2023 17:00:00'); + const onChange = (_ev: React.FormEvent, time: Date): void => { + if (time) { + _selectedTime = time; + } }; - const { getByRole, getAllByRole, rerender } = render( + const { getByRole, getAllByRole } = render( , ); - const timePickerOptionIdx = 6; - const initialTimePickerComboBox = getByRole('combobox') as HTMLInputElement; - userEvent.click(initialTimePickerComboBox); - const timePickerOptions = getAllByRole('option') as HTMLInputElement[]; - userEvent.click(timePickerOptions[timePickerOptionIdx], undefined, { skipPointerEventsCheck: true }); - expect(_selectedTime!.toString()).toEqual(new Date('April 12, 2023 01:30:00').toString()); + const timePickerComboBox = getByRole('combobox') as HTMLInputElement; + expect(timePickerComboBox.value).toEqual('5:00:00 PM'); - dateAnchor = new Date('April 05, 2023 00:00:00'); + userEvent.click(timePickerComboBox); + const timePickerOptions = getAllByRole('option') as HTMLButtonElement[]; + userEvent.click(timePickerOptions[2], undefined, { skipPointerEventsCheck: true }); + + const formattedSelectedTime = _selectedTime.toLocaleTimeString([], { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: true, + }); - rerender( + const expectedTime = '6:00:00 PM'; + expect(formattedSelectedTime).toEqual(expectedTime); + }); + + it('correctly renders options using default value as date anchor', () => { + const defaultValue = new Date('April 1, 2023 13:00:00'); + + const { getByRole, getAllByRole } = render( , ); - expect(_selectedTime!.toString()).toEqual(new Date('April 05, 2023 01:30:00').toString()); + const timePickerComboBox = getByRole('combobox') as HTMLInputElement; + expect(timePickerComboBox.value).toEqual('13:00'); + + userEvent.click(timePickerComboBox); + const timePickerOptions = getAllByRole('option') as HTMLButtonElement[]; + userEvent.click(timePickerOptions[2], undefined, { skipPointerEventsCheck: true }); - const updatedTimePickerComboBox = getByRole('combobox') as HTMLInputElement; - userEvent.click(updatedTimePickerComboBox); - const updatedTimePickerOptions = getAllByRole('option') as HTMLInputElement[]; - userEvent.click(updatedTimePickerOptions[timePickerOptionIdx + 1], undefined, { skipPointerEventsCheck: true }); - expect(_selectedTime!.toString()).toEqual(new Date('April 05, 2023 01:45:00').toString()); + expect(timePickerComboBox.value).toEqual('14:00'); }); describe('validates entered text when', () => { From bd3d5b69d7093d692ebc4df8ebc1d4f9bc094f3d Mon Sep 17 00:00:00 2001 From: James Wu Date: Mon, 17 Apr 2023 17:59:11 -0400 Subject: [PATCH 42/57] Took snapping logic out, fixed invalid key bug, and shared getDateAnchor function --- .../src/components/TimePicker/TimePicker.tsx | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index fd14fed393c7f..0103687f3c06c 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -81,20 +81,6 @@ export const TimePicker: React.FunctionComponent = ({ [internalDateAnchor, increments, timeRange], ); - // React.useEffect(() => { - // if (selectedTime && !isNaN(selectedTime.valueOf())) { - // if (selectedTime < dateStartAnchor || selectedTime > dateEndAnchor) { - // const updatedCurrentTime = new Date(dateStartAnchor); - // updatedCurrentTime.setHours(selectedTime.getHours()); - // updatedCurrentTime.setMinutes(selectedTime.getMinutes()); - // updatedCurrentTime.setSeconds(selectedTime.getSeconds()); - // updatedCurrentTime.setMilliseconds(selectedTime.getMilliseconds()); - - // setSelectedTime(updatedCurrentTime); - // } - // } - // }, [selectedTime, dateStartAnchor, dateEndAnchor, setSelectedTime]); - const timePickerOptions: IComboBoxOption[] = React.useMemo(() => { const optionsList = Array(optionsCount); for (let i = 0; i < optionsCount; i++) { @@ -173,18 +159,20 @@ export const TimePicker: React.FunctionComponent = ({ onGetErrorMessage(errorMessageToDisplay); } + let changedTime: Date; if (errorMessageToDisplay || (input !== undefined && !input.length)) { const timeSelection = input || option?.text || ''; setComboBoxText(timeSelection); setSelectedTime(errorMessageToDisplay ? new Date('invalid') : undefined); - onChange?.(ev, new Date('invalid')); + changedTime = new Date('invalid'); } else { const timeSelection = (option?.key as string) || input || ''; const updatedTime = getDateFromTimeSelection(useHour12, dateStartAnchor, timeSelection); setSelectedTime(updatedTime); - onChange?.(ev, updatedTime); + changedTime = updatedTime; } + onChange?.(ev, changedTime); setErrorMessage(errorMessageToDisplay); }, [ @@ -247,12 +235,12 @@ const getDateAnchor = ( increments: number, timeRange?: ITimeRange, ) => { - const clampedDateAnchor = new Date(internalDateAnchor); + const clampedDateAnchor = new Date(internalDateAnchor.getTime()); if (timeRange) { const clampedTimeRange = clampTimeRange(timeRange); - const timeRangeVal = startEnd === 'start' ? clampedTimeRange.start : clampedTimeRange.end; - if (clampedDateAnchor.getHours() !== timeRangeVal) { - clampedDateAnchor.setHours(timeRangeVal); + const timeRangeHours = startEnd === 'start' ? clampedTimeRange.start : clampedTimeRange.end; + if (clampedDateAnchor.getHours() !== timeRangeHours) { + clampedDateAnchor.setHours(timeRangeHours); } } else if (startEnd === 'end') { clampedDateAnchor.setDate(clampedDateAnchor.getDate() + 1); From 33409c88f35c9119f2b8a82359b915c1da55da97 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 18 Apr 2023 20:56:00 -0400 Subject: [PATCH 43/57] Pass in placeholder since placeholder prop no longer has default value --- .../src/react/TimePicker/TimePicker.Basic.Example.tsx | 1 + .../TimePicker/TimePicker.CustomTimeStrings.Example.tsx | 1 + .../react/TimePicker/TimePicker.DateTimePicker.Example.tsx | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx index 41a4fd0160c3a..77e60dae88f0c 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Basic.Example.tsx @@ -44,6 +44,7 @@ export const TimePickerBasicExample: React.FC = () => { return ( { return ( {
- +
{`⚓ Date anchor: ${datePickerDate.toString()}`} {`⌚ Selected time: ${currentTimeString ? currentTimeString : ''}`} From 9d1d7a4dddacbd8bb5e81fc7995d13dcdb253f66 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 18 Apr 2023 20:59:50 -0400 Subject: [PATCH 44/57] Reflect optional timeOutOfBoundsErrorMessage string --- packages/react/etc/react.api.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index ecb6cacc59a36..9cbd1bcb7e652 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -9396,9 +9396,8 @@ export interface ITimePickerProps extends Omit Date: Tue, 18 Apr 2023 21:19:27 -0400 Subject: [PATCH 45/57] Updated tests and snapshot --- packages/react/src/components/TimePicker/TimePicker.test.tsx | 4 +++- .../TimePicker/__snapshots__/TimePicker.test.tsx.snap | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.test.tsx b/packages/react/src/components/TimePicker/TimePicker.test.tsx index 9e1194e514b20..9e0ccd5d300b0 100644 --- a/packages/react/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.test.tsx @@ -14,7 +14,9 @@ describe('TimePicker', () => { start: 0, end: 5, }; - const component = create(); + const component = create( + , + ); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/packages/react/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap b/packages/react/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap index 6e58797c822f8..75e9c9945226d 100644 --- a/packages/react/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap +++ b/packages/react/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap @@ -236,7 +236,7 @@ exports[`TimePicker renders correctly 1`] = ` onKeyDown={[Function]} onKeyUp={[Function]} onTouchStart={[Function]} - placeholder="Enter or select a time" + placeholder="Select a time" role="combobox" spellCheck={false} style={ From c6b4468390c01f2f61687e52ecb6676c0c8b4a29 Mon Sep 17 00:00:00 2001 From: James Wu Date: Tue, 18 Apr 2023 21:20:50 -0400 Subject: [PATCH 46/57] Addressed comments --- .../src/components/TimePicker/TimePicker.tsx | 24 +++++-------------- .../components/TimePicker/TimePicker.types.ts | 4 +--- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 0103687f3c06c..2bef50b77ac2c 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -25,12 +25,10 @@ const getDefaultStrings = (useHour12: boolean, showSeconds: boolean): ITimePicke const hourUnits = useHour12 ? '12-hour' : '24-hour'; const timeFormat = `hh:mm${showSeconds ? ':ss' : ''}${useHour12 ? ' AP' : ''}`; const invalidInputErrorMessage = `Enter a valid time in the ${hourUnits} format: ${timeFormat}`; - const defaultTimePickerPlaceholder = `Enter or select a time`; const timeOutOfBoundsErrorMessage = `Please enter a time within the range of {0} and {1}`; return { invalidInputErrorMessage, - defaultTimePickerPlaceholder, timeOutOfBoundsErrorMessage, }; }; @@ -53,7 +51,6 @@ export const TimePicker: React.FunctionComponent = ({ onFormatDate, onValidateUserInput, onGetErrorMessage, - placeholder = strings.defaultTimePickerPlaceholder, ...rest }: ITimePickerProps) => { const [comboBoxText, setComboBoxText] = React.useState(''); @@ -66,10 +63,7 @@ export const TimePicker: React.FunctionComponent = ({ const optionsCount = getDropdownOptionsCount(increments, timeRange); - const internalDateAnchor = React.useMemo( - () => dateAnchor || value || defaultValue || fallbackDateAnchor, - [dateAnchor, defaultValue, value, fallbackDateAnchor], - ); + const internalDateAnchor = dateAnchor || value || defaultValue || fallbackDateAnchor; const dateStartAnchor = React.useMemo( () => getDateAnchor(internalDateAnchor, 'start', increments, timeRange), @@ -99,21 +93,16 @@ export const TimePicker: React.FunctionComponent = ({ }); }, [dateStartAnchor, increments, optionsCount, showSeconds, onFormatDate, useHour12]); - const getComboBoxOptionInDropdown = React.useCallback( - (optionKey: string) => timePickerOptions.find((option: IComboBoxOption) => option.key === optionKey), - [timePickerOptions], - ); - React.useEffect(() => { if (selectedTime && !isNaN(selectedTime.valueOf())) { const formattedTimeString = formatTimeString(selectedTime, showSeconds, useHour12); - const option = getComboBoxOptionInDropdown(formattedTimeString); - setSelectedKey(option?.key); - setComboBoxText(option ? option.text : formattedTimeString); + const comboboxOption = timePickerOptions.find((option: IComboBoxOption) => option.key === formattedTimeString); + setSelectedKey(comboboxOption?.key); + setComboBoxText(comboboxOption ? comboboxOption.text : formattedTimeString); } else { setSelectedKey(null); } - }, [selectedTime, getComboBoxOptionInDropdown, onFormatDate, showSeconds, useHour12]); + }, [selectedTime, timePickerOptions, onFormatDate, showSeconds, useHour12]); const onInputChange = React.useCallback( (ev: React.FormEvent, option?: IComboBoxOption, _index?: number, input?: string): void => { @@ -127,7 +116,7 @@ export const TimePicker: React.FunctionComponent = ({ } if (!regex.test(userInput)) { errorMessageToDisplay = strings.invalidInputErrorMessage; - } else if (timeRange) { + } else if (timeRange && strings.timeOutOfBoundsErrorMessage) { const optionDate: Date = getDateFromTimeSelection(useHour12, dateStartAnchor, userInput); if (optionDate < dateStartAnchor || optionDate > dateEndAnchor) { errorMessageToDisplay = format( @@ -213,7 +202,6 @@ export const TimePicker: React.FunctionComponent = ({ return ( Date: Thu, 20 Apr 2023 17:40:43 -0400 Subject: [PATCH 47/57] Removed unnecessary string state variables --- .../react/TimePicker/TimePicker.Controlled.Example.tsx | 8 +------- .../TimePicker/TimePicker.DateTimePicker.Example.tsx | 7 +------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx index f4002f719b88c..5bd608cee1cfa 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx @@ -17,16 +17,10 @@ export const TimePickerControlledExample: React.FC = () => { const dateAnchor = new Date('February 27, 2023 08:00:00'); const [time, setTime] = React.useState(new Date('February 27, 2023 10:00:00')); - const [controlledTimeString, setControlledTimeString] = React.useState(''); - const onControlledExampleChange = React.useCallback((_, newTime: Date) => { setTime(newTime); }, []); - React.useEffect(() => { - setControlledTimeString(time.toString()); - }, [time]); - return ( { onChange={onControlledExampleChange} /> {`⚓ Date anchor: ${dateAnchor.toString()}`} - {`⌚ Selected time: ${controlledTimeString ? controlledTimeString : ''}`} + {`⌚ Selected time: ${time ? time.toString() : ''}`} ); }; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx index 46146dada3379..b16e29f58d693 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx @@ -27,7 +27,6 @@ export const TimePickerDateTimePickerExample: React.FC = () => { const currentDate = new Date('2023-02-01 05:00:00'); const [datePickerDate, setDatePickerDate] = React.useState(currentDate); const [currentTime, setCurrentTime] = React.useState(); - const [currentTimeString, setCurrentTimeString] = React.useState(''); const onSelectDate = React.useCallback( (selectedDate: Date) => { @@ -44,10 +43,6 @@ export const TimePickerDateTimePickerExample: React.FC = () => { setCurrentTime(date); }, []); - React.useEffect(() => { - setCurrentTimeString(currentTime ? currentTime.toString() : ''); - }, [currentTime]); - return ( @@ -61,7 +56,7 @@ export const TimePickerDateTimePickerExample: React.FC = () => { />
{`⚓ Date anchor: ${datePickerDate.toString()}`} - {`⌚ Selected time: ${currentTimeString ? currentTimeString : ''}`} + {`⌚ Selected time: ${currentTime ? currentTime.toString() : ''}`} ); }; From 8a3b218cc46231a7ecbb2a759c3a3d14531f2e58 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 20 Apr 2023 17:42:41 -0400 Subject: [PATCH 48/57] Followed comment suggestion and replaced onGetErrorMessage with onValidationError prop --- packages/react/etc/react.api.md | 7 ++++++- packages/react/src/components/TimePicker/TimePicker.tsx | 8 ++++---- .../react/src/components/TimePicker/TimePicker.types.ts | 6 +++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index 5ac43596f2910..97d83a073a2be 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -9385,8 +9385,8 @@ export interface ITimePickerProps extends Omit, time: Date) => void; onFormatDate?: (date: Date) => string; - onGetErrorMessage?: (errorMessage: string) => void; onValidateUserInput?: (userInput: string) => string; + onValidationError?: (event: React_2.FormEvent, data: TimePickerErrorData) => void; showSeconds?: boolean; strings?: ITimePickerStrings; timeRange?: ITimeRange; @@ -11255,6 +11255,11 @@ export { TimeConstants } // @public (undocumented) export const TimePicker: React_2.FunctionComponent; +// @public (undocumented) +export type TimePickerErrorData = { + errorMessage?: string; +}; + // @public (undocumented) export const Toggle: React_2.FunctionComponent; diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 2bef50b77ac2c..9fd3fb0afcb1e 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -50,7 +50,7 @@ export const TimePicker: React.FunctionComponent = ({ onChange, onFormatDate, onValidateUserInput, - onGetErrorMessage, + onValidationError, ...rest }: ITimePickerProps) => { const [comboBoxText, setComboBoxText] = React.useState(''); @@ -144,8 +144,8 @@ export const TimePicker: React.FunctionComponent = ({ } } - if (onGetErrorMessage) { - onGetErrorMessage(errorMessageToDisplay); + if (onValidationError) { + onValidationError(ev, { errorMessage: errorMessageToDisplay }); } let changedTime: Date; @@ -176,7 +176,7 @@ export const TimePicker: React.FunctionComponent = ({ strings.invalidInputErrorMessage, strings.timeOutOfBoundsErrorMessage, setSelectedTime, - onGetErrorMessage, + onValidationError, onChange, ], ); diff --git a/packages/react/src/components/TimePicker/TimePicker.types.ts b/packages/react/src/components/TimePicker/TimePicker.types.ts index cfdd10d67a273..707aa99bb7713 100644 --- a/packages/react/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react/src/components/TimePicker/TimePicker.types.ts @@ -24,6 +24,10 @@ export interface ITimePickerStrings { timeOutOfBoundsErrorMessage?: string; } +export type TimePickerErrorData = { + errorMessage?: string; +}; + /** * {@docCategory TimePicker} */ @@ -107,5 +111,5 @@ export interface ITimePickerProps /** * Callback to get validation result. */ - onGetErrorMessage?: (errorMessage: string) => void; + onValidationError?: (event: React.FormEvent, data: TimePickerErrorData) => void; } From e5f3953b19f715bf7af97ecd0cc11396004c7f57 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 20 Apr 2023 17:43:24 -0400 Subject: [PATCH 49/57] Added onValidationError example --- .../TimePicker.ErrorValidation.Example.tsx | 73 +++++++++++++++++++ .../src/react/TimePicker/TimePicker.doc.tsx | 8 ++ 2 files changed, 81 insertions(+) create mode 100644 packages/react-examples/src/react/TimePicker/TimePicker.ErrorValidation.Example.tsx diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.ErrorValidation.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.ErrorValidation.Example.tsx new file mode 100644 index 0000000000000..c9685d7058efa --- /dev/null +++ b/packages/react-examples/src/react/TimePicker/TimePicker.ErrorValidation.Example.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { + TimePicker, + TimePickerErrorData, + ITimeRange, + Text, + IStackTokens, + Stack, + IStackStyles, + IComboBoxStyles, + PrimaryButton, + Label, +} from '@fluentui/react'; + +const stackStyles: Partial = { root: { width: 500 } }; +const stackTokens: IStackTokens = { childrenGap: 20 }; + +const timePickerStyles: Partial = { + optionsContainerWrapper: { + height: '500px', + }, + root: { + width: '500px', + }, +}; + +const timeRange: ITimeRange = { + start: 8, + end: 17, +}; + +export const TimePickerErrorValidationExample: React.FC = () => { + const dateAnchor = new Date('February 27, 2023 08:00:00'); + const [time, setTime] = React.useState(new Date('January 1, 2023 08:00:00')); + const [disableButton, setDisableButton] = React.useState(false); + + const onControlledExampleChange = React.useCallback((_, newTime: Date) => { + setTime(newTime); + }, []); + + const onValidationError = React.useCallback((_, timePickerErrorData: TimePickerErrorData) => { + if (timePickerErrorData.errorMessage !== undefined) { + console.log('Validation error message received: ', timePickerErrorData.errorMessage); + setDisableButton(timePickerErrorData.errorMessage.length > 0); + } + }, []); + + return ( + + + {`⚓ Date anchor: ${dateAnchor.toString()}`} + {`⌚ Selected time: ${time ? time.toString() : ''}`} + + + + + ); +}; diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx index a0ebb04f1a0d9..66b35ed394ccc 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx @@ -5,6 +5,7 @@ import { IDocPageProps } from '@fluentui/react/lib/common/DocPage.types'; import { TimePickerBasicExample } from './TimePicker.Basic.Example'; import { TimePickerControlledExample } from './TimePicker.Controlled.Example'; import { TimePickerCustomTimeStringsExample } from './TimePicker.CustomTimeStrings.Example'; +import { TimePickerErrorValidationExample } from './TimePicker.ErrorValidation.Example'; import { TimePickerDateTimePickerExample } from './TimePicker.DateTimePicker.Example'; const TimePickerBasicExampleCode = @@ -13,6 +14,8 @@ const TimePickerControlledExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx') as string; const TimePickerCustomTimeStringsExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx') as string; +const TimePickerErrorValidationExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.ErrorValidation.Example.tsx') as string; const TimePickerDateTimePickerExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx') as string; @@ -36,6 +39,11 @@ export const TimePickerPageProps: IDocPageProps = { code: TimePickerCustomTimeStringsExampleCode, view: , }, + { + title: 'TimePicker using onValidationError Result', + code: TimePickerErrorValidationExampleCode, + view: , + }, { title: 'TimePicker with DatePicker', code: TimePickerDateTimePickerExampleCode, From b342a02762ce7a3c7d25bef1fb6c528f23d63954 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 20 Apr 2023 17:43:45 -0400 Subject: [PATCH 50/57] Added onValidationError test case --- .../components/TimePicker/TimePicker.test.tsx | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.test.tsx b/packages/react/src/components/TimePicker/TimePicker.test.tsx index 9e0ccd5d300b0..58eac167e78bf 100644 --- a/packages/react/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { TimePicker } from './TimePicker'; -import { ITimeRange } from './TimePicker.types'; +import { ITimeRange, TimePickerErrorData } from './TimePicker.types'; import { create } from '@fluentui/test-utilities'; import { mount } from 'enzyme'; import type { IComboBox } from '../ComboBox/ComboBox.types'; @@ -142,6 +142,41 @@ describe('TimePicker', () => { expect(timePickerComboBox.value).toEqual('14:00'); }); + it('gets the error message string within the onValidationError prop', () => { + let _errorMessage: string = ''; + const onValidationError = (_ev: React.FormEvent, timePickerErrorData: TimePickerErrorData): void => { + if (timePickerErrorData.errorMessage !== undefined) { + _errorMessage = timePickerErrorData.errorMessage; + } + }; + const dateAnchor = new Date('February 27, 2023 08:00:00'); + + const { getByRole } = render( + , + ); + + const timePickerComboBox = getByRole('combobox') as HTMLInputElement; + expect(timePickerComboBox.value).toBe(''); + expect(timePickerComboBox.placeholder).toEqual('Test TimePicker'); + + userEvent.click(timePickerComboBox); + userEvent.type(timePickerComboBox, '11111 AM{enter}'); + + const errorMessageElement = getByRole('alert') as HTMLDivElement; + expect(errorMessageElement).not.toBe(null); + const expectedErrorMessage = 'Enter a valid time in the 24-hour format: hh:mm:ss'; + expect(errorMessageElement.textContent).toEqual(expectedErrorMessage); + expect(_errorMessage).toEqual(expectedErrorMessage); + }); + describe('validates entered text when', () => { it('receives an invalid hour input for 24-hour-no-seconds format', () => { const wrapper = mount(); From 80a6224f070747082e63c653b62d8b969fb109c3 Mon Sep 17 00:00:00 2001 From: James Wu Date: Thu, 20 Apr 2023 17:44:03 -0400 Subject: [PATCH 51/57] Export TimePickerErrorData --- packages/react/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index d6798ee62343c..984e1dc7f6aad 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1198,7 +1198,7 @@ export { } from './ThemeGenerator'; export type { IThemeRules, IThemeSlotRule } from './ThemeGenerator'; export { TimePicker } from './TimePicker'; -export type { ITimePickerProps, ITimePickerStrings, ITimeRange } from './TimePicker'; +export type { ITimePickerProps, ITimePickerStrings, ITimeRange, TimePickerErrorData } from './TimePicker'; export { Toggle, ToggleBase } from './Toggle'; export type { IToggle, IToggleProps, IToggleStyleProps, IToggleStyles } from './Toggle'; export { Tooltip, TooltipBase, TooltipDelay, TooltipHost, TooltipHostBase, TooltipOverflowMode } from './Tooltip'; From 44927f9026ae40bfa6a21671a485ef90e147f885 Mon Sep 17 00:00:00 2001 From: James Wu Date: Fri, 21 Apr 2023 17:27:57 -0400 Subject: [PATCH 52/57] Renamed onValidationError and TimePickerErrorData to onValidationResult and TimePickerValidationData --- packages/react/etc/react.api.md | 6 +++--- .../react/src/components/TimePicker/TimePicker.types.ts | 8 ++++++-- packages/react/src/index.ts | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index 97d83a073a2be..4eaa19a9b7df7 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -9386,7 +9386,7 @@ export interface ITimePickerProps extends Omit, time: Date) => void; onFormatDate?: (date: Date) => string; onValidateUserInput?: (userInput: string) => string; - onValidationError?: (event: React_2.FormEvent, data: TimePickerErrorData) => void; + onValidationResult?: (event: React_2.FormEvent, data: TimePickerValidationResultData) => void; showSeconds?: boolean; strings?: ITimePickerStrings; timeRange?: ITimeRange; @@ -11255,8 +11255,8 @@ export { TimeConstants } // @public (undocumented) export const TimePicker: React_2.FunctionComponent; -// @public (undocumented) -export type TimePickerErrorData = { +// @public +export type TimePickerValidationResultData = { errorMessage?: string; }; diff --git a/packages/react/src/components/TimePicker/TimePicker.types.ts b/packages/react/src/components/TimePicker/TimePicker.types.ts index 707aa99bb7713..80f153c5c034f 100644 --- a/packages/react/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react/src/components/TimePicker/TimePicker.types.ts @@ -24,7 +24,11 @@ export interface ITimePickerStrings { timeOutOfBoundsErrorMessage?: string; } -export type TimePickerErrorData = { +/** + * {@docCategory TimePicker} + * A type used to represent the TimePicker validation result. + */ +export type TimePickerValidationResultData = { errorMessage?: string; }; @@ -111,5 +115,5 @@ export interface ITimePickerProps /** * Callback to get validation result. */ - onValidationError?: (event: React.FormEvent, data: TimePickerErrorData) => void; + onValidationResult?: (event: React.FormEvent, data: TimePickerValidationResultData) => void; } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 984e1dc7f6aad..5f3d49abe0686 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1198,7 +1198,7 @@ export { } from './ThemeGenerator'; export type { IThemeRules, IThemeSlotRule } from './ThemeGenerator'; export { TimePicker } from './TimePicker'; -export type { ITimePickerProps, ITimePickerStrings, ITimeRange, TimePickerErrorData } from './TimePicker'; +export type { ITimePickerProps, ITimePickerStrings, ITimeRange, TimePickerValidationResultData } from './TimePicker'; export { Toggle, ToggleBase } from './Toggle'; export type { IToggle, IToggleProps, IToggleStyleProps, IToggleStyles } from './Toggle'; export { Tooltip, TooltipBase, TooltipDelay, TooltipHost, TooltipHostBase, TooltipOverflowMode } from './Tooltip'; From de5c8a2fa5233bdf7e7e038b0c2162adb10d8fc4 Mon Sep 17 00:00:00 2001 From: James Wu Date: Fri, 21 Apr 2023 17:28:27 -0400 Subject: [PATCH 53/57] Renamed example to reflect new callback prop --- ...x => TimePicker.ValidationResult.Example.tsx} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename packages/react-examples/src/react/TimePicker/{TimePicker.ErrorValidation.Example.tsx => TimePicker.ValidationResult.Example.tsx} (76%) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.ErrorValidation.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.ValidationResult.Example.tsx similarity index 76% rename from packages/react-examples/src/react/TimePicker/TimePicker.ErrorValidation.Example.tsx rename to packages/react-examples/src/react/TimePicker/TimePicker.ValidationResult.Example.tsx index c9685d7058efa..684300e1a576e 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.ErrorValidation.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.ValidationResult.Example.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { TimePicker, - TimePickerErrorData, + TimePickerValidationResultData, ITimeRange, Text, IStackTokens, @@ -29,7 +29,7 @@ const timeRange: ITimeRange = { end: 17, }; -export const TimePickerErrorValidationExample: React.FC = () => { +export const TimePickerValidationResultExample: React.FC = () => { const dateAnchor = new Date('February 27, 2023 08:00:00'); const [time, setTime] = React.useState(new Date('January 1, 2023 08:00:00')); const [disableButton, setDisableButton] = React.useState(false); @@ -38,10 +38,10 @@ export const TimePickerErrorValidationExample: React.FC = () => { setTime(newTime); }, []); - const onValidationError = React.useCallback((_, timePickerErrorData: TimePickerErrorData) => { - if (timePickerErrorData.errorMessage !== undefined) { - console.log('Validation error message received: ', timePickerErrorData.errorMessage); - setDisableButton(timePickerErrorData.errorMessage.length > 0); + const onValidationResult = React.useCallback((_, timePickerValidationResultData: TimePickerValidationResultData) => { + if (timePickerValidationResultData.errorMessage !== undefined) { + console.log('Validation error message received: ', timePickerValidationResultData.errorMessage); + setDisableButton(timePickerValidationResultData.errorMessage.length > 0); } }, []); @@ -53,11 +53,11 @@ export const TimePickerErrorValidationExample: React.FC = () => { useHour12 increments={15} autoComplete="on" - label="Controlled TimePicker with onValidationError Handling" + label="Controlled TimePicker with onValidationResult Handling" dateAnchor={dateAnchor} value={time} onChange={onControlledExampleChange} - onValidationError={onValidationError} + onValidationResult={onValidationResult} timeRange={timeRange} /> {`⚓ Date anchor: ${dateAnchor.toString()}`} From 2839b97b6bbdd5c15dc0e031eecd2b74a36d9276 Mon Sep 17 00:00:00 2001 From: James Wu Date: Fri, 21 Apr 2023 17:30:06 -0400 Subject: [PATCH 54/57] Use new example and fix casing --- .../TimePicker.ValidationResult.Example.tsx | 2 +- .../src/react/TimePicker/TimePicker.doc.tsx | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.ValidationResult.Example.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.ValidationResult.Example.tsx index 684300e1a576e..2d6e78b81b6c1 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.ValidationResult.Example.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.ValidationResult.Example.tsx @@ -53,7 +53,7 @@ export const TimePickerValidationResultExample: React.FC = () => { useHour12 increments={15} autoComplete="on" - label="Controlled TimePicker with onValidationResult Handling" + label="Controlled TimePicker with onValidationResult handling" dateAnchor={dateAnchor} value={time} onChange={onControlledExampleChange} diff --git a/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx b/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx index 66b35ed394ccc..d479fa84c15f7 100644 --- a/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx +++ b/packages/react-examples/src/react/TimePicker/TimePicker.doc.tsx @@ -5,7 +5,7 @@ import { IDocPageProps } from '@fluentui/react/lib/common/DocPage.types'; import { TimePickerBasicExample } from './TimePicker.Basic.Example'; import { TimePickerControlledExample } from './TimePicker.Controlled.Example'; import { TimePickerCustomTimeStringsExample } from './TimePicker.CustomTimeStrings.Example'; -import { TimePickerErrorValidationExample } from './TimePicker.ErrorValidation.Example'; +import { TimePickerValidationResultExample } from './TimePicker.ValidationResult.Example'; import { TimePickerDateTimePickerExample } from './TimePicker.DateTimePicker.Example'; const TimePickerBasicExampleCode = @@ -14,8 +14,8 @@ const TimePickerControlledExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.Controlled.Example.tsx') as string; const TimePickerCustomTimeStringsExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.CustomTimeStrings.Example.tsx') as string; -const TimePickerErrorValidationExampleCode = - require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.ErrorValidation.Example.tsx') as string; +const TimePickerValidationResultExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.ValidationResult.Example.tsx') as string; const TimePickerDateTimePickerExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/TimePicker/TimePicker.DateTimePicker.Example.tsx') as string; @@ -40,9 +40,9 @@ export const TimePickerPageProps: IDocPageProps = { view: , }, { - title: 'TimePicker using onValidationError Result', - code: TimePickerErrorValidationExampleCode, - view: , + title: 'TimePicker using onValidationResult callback', + code: TimePickerValidationResultExampleCode, + view: , }, { title: 'TimePicker with DatePicker', From 55cba397cdc947d8d3afe7b220355f384e84c2ce Mon Sep 17 00:00:00 2001 From: James Wu Date: Fri, 21 Apr 2023 17:30:51 -0400 Subject: [PATCH 55/57] Use onValidationResult and only call when stored error message differs from latest error message --- .../react/src/components/TimePicker/TimePicker.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.tsx b/packages/react/src/components/TimePicker/TimePicker.tsx index 9fd3fb0afcb1e..2442d38b065ca 100644 --- a/packages/react/src/components/TimePicker/TimePicker.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.tsx @@ -50,7 +50,7 @@ export const TimePicker: React.FunctionComponent = ({ onChange, onFormatDate, onValidateUserInput, - onValidationError, + onValidationResult, ...rest }: ITimePickerProps) => { const [comboBoxText, setComboBoxText] = React.useState(''); @@ -144,8 +144,9 @@ export const TimePicker: React.FunctionComponent = ({ } } - if (onValidationError) { - onValidationError(ev, { errorMessage: errorMessageToDisplay }); + if (onValidationResult && errorMessage !== errorMessageToDisplay) { + // only call onValidationResult if stored errorMessage state value is different from latest error message + onValidationResult(ev, { errorMessage: errorMessageToDisplay }); } let changedTime: Date; @@ -176,8 +177,9 @@ export const TimePicker: React.FunctionComponent = ({ strings.invalidInputErrorMessage, strings.timeOutOfBoundsErrorMessage, setSelectedTime, - onValidationError, + onValidationResult, onChange, + errorMessage, ], ); From 55cc1a58001a2d4b60e787de46e7b74c2b16441f Mon Sep 17 00:00:00 2001 From: James Wu Date: Fri, 21 Apr 2023 17:31:52 -0400 Subject: [PATCH 56/57] Added test for verifying onValidateResult only gets called on error message changes --- .../components/TimePicker/TimePicker.test.tsx | 67 +++++++++++++------ 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.test.tsx b/packages/react/src/components/TimePicker/TimePicker.test.tsx index 58eac167e78bf..e059c493c9b53 100644 --- a/packages/react/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { TimePicker } from './TimePicker'; -import { ITimeRange, TimePickerErrorData } from './TimePicker.types'; +import { ITimeRange, TimePickerValidationResultData } from './TimePicker.types'; import { create } from '@fluentui/test-utilities'; import { mount } from 'enzyme'; import type { IComboBox } from '../ComboBox/ComboBox.types'; @@ -52,7 +52,7 @@ describe('TimePicker', () => { const { getByRole, getAllByRole } = render( { const { getByRole, getAllByRole } = render( { const { getByRole, getAllByRole } = render( { expect(timePickerComboBox.value).toEqual('14:00'); }); - it('gets the error message string within the onValidationError prop', () => { + it('gets the error message string within the onValidationResult prop', () => { let _errorMessage: string = ''; - const onValidationError = (_ev: React.FormEvent, timePickerErrorData: TimePickerErrorData): void => { - if (timePickerErrorData.errorMessage !== undefined) { - _errorMessage = timePickerErrorData.errorMessage; - } - }; + const onValidationResult = jest.fn( + (_ev: React.FormEvent, timePickerErrorData: TimePickerValidationResultData): void => { + if (timePickerErrorData.errorMessage !== undefined) { + _errorMessage = timePickerErrorData.errorMessage; + } + }, + ); const dateAnchor = new Date('February 27, 2023 08:00:00'); + const timeRange: ITimeRange = { + start: 8, + end: 20, + }; const { getByRole } = render( , ); const timePickerComboBox = getByRole('combobox') as HTMLInputElement; - expect(timePickerComboBox.value).toBe(''); - expect(timePickerComboBox.placeholder).toEqual('Test TimePicker'); + userEvent.click(timePickerComboBox); + userEvent.type(timePickerComboBox, '10:00:00{enter}'); + expect(_errorMessage).toEqual(''); + + userEvent.clear(timePickerComboBox); userEvent.click(timePickerComboBox); userEvent.type(timePickerComboBox, '11111 AM{enter}'); - const errorMessageElement = getByRole('alert') as HTMLDivElement; - expect(errorMessageElement).not.toBe(null); - const expectedErrorMessage = 'Enter a valid time in the 24-hour format: hh:mm:ss'; - expect(errorMessageElement.textContent).toEqual(expectedErrorMessage); - expect(_errorMessage).toEqual(expectedErrorMessage); + const firstErrorMessageElement = getByRole('alert') as HTMLDivElement; + expect(firstErrorMessageElement).not.toBe(null); + expect(onValidationResult).toHaveBeenCalled(); + onValidationResult.mockClear(); + + const firstExpectedErrorMessage = 'Enter a valid time in the 24-hour format: hh:mm:ss'; + expect(firstErrorMessageElement.textContent).toEqual(firstExpectedErrorMessage); + expect(_errorMessage).toEqual(firstExpectedErrorMessage); + + // verify that onValidationResult is not called twice for the same error message + userEvent.clear(timePickerComboBox); + userEvent.click(timePickerComboBox); + userEvent.type(timePickerComboBox, '88888 AM{enter}'); + expect(onValidationResult).not.toHaveBeenCalled(); + onValidationResult.mockClear(); + + // verify that onValidationResult is finally called again for a new error message + userEvent.click(timePickerComboBox); + userEvent.type(timePickerComboBox, '03:00:00{enter}'); + expect(onValidationResult).toHaveBeenCalled(); + + const secondErrorMessageElement = getByRole('alert') as HTMLDivElement; + const secondExpectedErrorMessage = 'Please enter a time within the range'; + expect(secondErrorMessageElement.textContent).toContain(secondExpectedErrorMessage); + expect(_errorMessage).toContain(secondExpectedErrorMessage); }); describe('validates entered text when', () => { From dc8f33d899a803eca1f5b06279650482d479d25c Mon Sep 17 00:00:00 2001 From: James Wu Date: Mon, 24 Apr 2023 09:46:08 -0400 Subject: [PATCH 57/57] Split big test into two smaller tests --- .../components/TimePicker/TimePicker.test.tsx | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/packages/react/src/components/TimePicker/TimePicker.test.tsx b/packages/react/src/components/TimePicker/TimePicker.test.tsx index e059c493c9b53..c335bfea34912 100644 --- a/packages/react/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react/src/components/TimePicker/TimePicker.test.tsx @@ -141,7 +141,44 @@ describe('TimePicker', () => { expect(timePickerComboBox.value).toEqual('14:00'); }); - it('gets the error message string within the onValidationResult prop', () => { + it('shows the error message under the ComboBox on input validation error', () => { + const onValidationResult = jest.fn(); + + const dateAnchor = new Date('March 15, 2023 10:00:00'); + const timeRange: ITimeRange = { + start: 10, + end: 17, + }; + + const { getByRole } = render( + , + ); + + const timePickerComboBox = getByRole('combobox') as HTMLInputElement; + + userEvent.click(timePickerComboBox); + userEvent.type(timePickerComboBox, '10:45:00{enter}'); + expect(onValidationResult).toHaveBeenCalledTimes(0); + + userEvent.clear(timePickerComboBox); + userEvent.click(timePickerComboBox); + userEvent.type(timePickerComboBox, '11111 AM{enter}'); + + expect(onValidationResult).toHaveBeenCalledTimes(1); + const errorMessageElement = getByRole('alert') as HTMLDivElement; + expect(errorMessageElement).not.toBe(null); + }); + + it('calls onValidationResult only when the error message changes', () => { let _errorMessage: string = ''; const onValidationResult = jest.fn( (_ev: React.FormEvent, timePickerErrorData: TimePickerValidationResultData): void => { @@ -170,39 +207,28 @@ describe('TimePicker', () => { ); const timePickerComboBox = getByRole('combobox') as HTMLInputElement; - - userEvent.click(timePickerComboBox); - userEvent.type(timePickerComboBox, '10:00:00{enter}'); expect(_errorMessage).toEqual(''); - userEvent.clear(timePickerComboBox); userEvent.click(timePickerComboBox); userEvent.type(timePickerComboBox, '11111 AM{enter}'); - - const firstErrorMessageElement = getByRole('alert') as HTMLDivElement; - expect(firstErrorMessageElement).not.toBe(null); - expect(onValidationResult).toHaveBeenCalled(); - onValidationResult.mockClear(); - const firstExpectedErrorMessage = 'Enter a valid time in the 24-hour format: hh:mm:ss'; - expect(firstErrorMessageElement.textContent).toEqual(firstExpectedErrorMessage); expect(_errorMessage).toEqual(firstExpectedErrorMessage); + expect(onValidationResult).toHaveBeenCalled(); + onValidationResult.mockClear(); // verify that onValidationResult is not called twice for the same error message userEvent.clear(timePickerComboBox); userEvent.click(timePickerComboBox); userEvent.type(timePickerComboBox, '88888 AM{enter}'); + expect(_errorMessage).toEqual(firstExpectedErrorMessage); expect(onValidationResult).not.toHaveBeenCalled(); - onValidationResult.mockClear(); // verify that onValidationResult is finally called again for a new error message userEvent.click(timePickerComboBox); userEvent.type(timePickerComboBox, '03:00:00{enter}'); expect(onValidationResult).toHaveBeenCalled(); - const secondErrorMessageElement = getByRole('alert') as HTMLDivElement; const secondExpectedErrorMessage = 'Please enter a time within the range'; - expect(secondErrorMessageElement.textContent).toContain(secondExpectedErrorMessage); expect(_errorMessage).toContain(secondExpectedErrorMessage); });