Skip to content

Commit

Permalink
#25730: TimePicker Default Value Fix, Controllable Usage, Example Upd…
Browse files Browse the repository at this point in the history
…ates (#26482)

* Update prop names, make renamed currentDate prop controlled

* Added DatePicker and TimePicker combination example

* Rephrase example text

* Combine ComboBox imports

* Fixed ComboBox import and updated DateTimePicker example

* Moved examples into standalone files

* Refactors and use new dateAnchor prop

* Updated all examples

* This should resolve the rest

* This should resolve the outstanding issues...

* Updated example strings

* Updated examples

* More changes to the basic example

* Removed useComboBoxAsMenuWidth prop

* Update formatTimeString function to return 00 if hours is 24

* Addressed comments

* Renamed baseDate parameter to dateStartAnchor

* Added prop onGetErrorMessage to get validation result

* Set selectedTime to invalid or undefined

* Clamp value to updated dateAnchor

* Make docs look nice and readable

* Removed unneeded imports

* Updated clampedStartAnchor initialization value

* Use internalDateAnchor

* Use fallbackDateAnchor and update DateTimePicker example

* Revert to ITimePickerProps to extend original omitted IComboBoxProps

* Addressed comments

* API snapshot update

* Updated TimePicker tests and added test for new controlled component variant

* Added test for handling changed base date anchor

* Verify selected time changes on dateAnchor change

* Added yarn change files

* Resolve linting errors

* Resolved more linting errors

* Resolved linting import error by in-lining stack and styles

* Addressed comments

* Revert onChange prop types to avoid breaking changes and pass React.FormEvent<IComboBox> to setSelectedTime callback

* Added explicit undefined pass to setSelectedTime in cases the dateAnchor changes

* Updated examples and call onChange outside of useControllableValue to pass in proper event type

* Control snapping of TimePicker values on DatePicker anchor change

* Added tests for using defaultValue or value as date anchors

* Took snapping logic out, fixed invalid key bug, and shared getDateAnchor function

* Pass in placeholder since placeholder prop no longer has default value

* Reflect optional timeOutOfBoundsErrorMessage string

* Updated tests and snapshot

* Addressed comments

* Removed unnecessary string state variables

* Followed comment suggestion and replaced onGetErrorMessage with onValidationError prop

* Added onValidationError example

* Added onValidationError test case

* Export TimePickerErrorData

* Renamed onValidationError and TimePickerErrorData to onValidationResult and TimePickerValidationData

* Renamed example to reflect new callback prop

* Use new example and fix casing

* Use onValidationResult and only call when stored error message differs from latest error message

* Added test for verifying onValidateResult only gets called on error message changes

* Split big test into two smaller tests
  • Loading branch information
CheerfulSatchel authored Apr 25, 2023
1 parent 66f1e1d commit 2d93a25
Show file tree
Hide file tree
Showing 18 changed files with 1,148 additions and 136 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: Refactored getDateFromTimeSelection variable names.",
"packageName": "@fluentui/date-time-utilities",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat(TimePicker): Updated TimePicker controlled and uncontrolled props to work correctly.",
"packageName": "@fluentui/react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 9 additions & 2 deletions packages/date-time-utilities/src/timeFormatting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
15 changes: 9 additions & 6 deletions packages/date-time-utilities/src/timeMath/timeMath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) || [];

Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import {
TimePicker,
ITimeRange,
Text,
IStackTokens,
Stack,
IStackStyles,
IComboBoxStyles,
IComboBox,
} from '@fluentui/react';

const stackStyles: Partial<IStackStyles> = { root: { width: 500 } };
const stackTokens: IStackTokens = { childrenGap: 20 };

const timePickerStyles: Partial<IComboBoxStyles> = {
optionsContainerWrapper: {
height: '500px',
},
root: {
width: '500px',
},
};

export const TimePickerBasicExample: React.FC = () => {
const [basicExampleTimeString, setBasicExampleTimeString] = React.useState<string>('');
const [nonDefaultOptionsExampleTimeString, setNonDefaultOptionsExampleTimeString] = React.useState<string>('');
const basicDateAnchor = new Date('November 25, 2021 09:00:00');
const nonDefaultOptionsDateAnchor = new Date('February 27, 2023 08:00:00');

const onBasicExampleChange = React.useCallback((_ev: React.FormEvent<IComboBox>, basicExampleTime: Date) => {
setBasicExampleTimeString(basicExampleTime.toString());
}, []);

const onNonDefaultOptionsExampleChange = React.useCallback((_, nonDefaultOptionsExampleTime: Date) => {
setNonDefaultOptionsExampleTimeString(nonDefaultOptionsExampleTime?.toString());
}, []);

const timeRange: ITimeRange = {
start: 8,
end: 14,
};

return (
<Stack tokens={stackTokens} styles={stackStyles}>
<TimePicker
placeholder="Basic example placeholder"
styles={timePickerStyles}
useHour12
allowFreeform
autoComplete="on"
label="TimePicker basic example"
onChange={onBasicExampleChange}
dateAnchor={basicDateAnchor}
/>
<Text>{`⚓ Date anchor: ${basicDateAnchor.toString()}`}</Text>
<Text>{`⌚ Selected time: ${basicExampleTimeString ? basicExampleTimeString : '<no time selected>'}`}</Text>

<TimePicker
styles={timePickerStyles}
showSeconds
allowFreeform
increments={15}
autoComplete="on"
label="TimePicker with non default options"
placeholder="Non default options placeholder"
timeRange={timeRange}
dateAnchor={nonDefaultOptionsDateAnchor}
onChange={onNonDefaultOptionsExampleChange}
/>
<Text>{`⚓ Date anchor: ${nonDefaultOptionsDateAnchor.toString()}`}</Text>
<Text>{`⌚ Selected time: ${
nonDefaultOptionsExampleTimeString ? nonDefaultOptionsExampleTimeString : '<no time selected>'
}`}</Text>
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import { TimePicker, Text, IStackTokens, Stack, IStackStyles, IComboBoxStyles } from '@fluentui/react';

const stackStyles: Partial<IStackStyles> = { root: { width: 500 } };
const stackTokens: IStackTokens = { childrenGap: 20 };

const timePickerStyles: Partial<IComboBoxStyles> = {
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<Date>(new Date('February 27, 2023 10:00:00'));

const onControlledExampleChange = React.useCallback((_, newTime: Date) => {
setTime(newTime);
}, []);

return (
<Stack tokens={stackTokens} styles={stackStyles}>
<TimePicker
styles={timePickerStyles}
showSeconds
allowFreeform
increments={15}
autoComplete="on"
label="Controlled TimePicker with non default options"
dateAnchor={dateAnchor}
value={time}
onChange={onControlledExampleChange}
/>
<Text>{`⚓ Date anchor: ${dateAnchor.toString()}`}</Text>
<Text>{`⌚ Selected time: ${time ? time.toString() : '<no time selected>'}`}</Text>
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react';
import { TimePicker, Text, IStackTokens, Stack, IStackStyles, IComboBoxStyles } from '@fluentui/react';

const stackStyles: Partial<IStackStyles> = { root: { width: 500 } };
const stackTokens: IStackTokens = { childrenGap: 20 };

const timePickerStyles: Partial<IComboBoxStyles> = {
optionsContainerWrapper: {
height: '500px',
},
root: {
width: '500px',
},
};

export const TimePickerCustomTimeStringsExample: React.FC = () => {
const [customTimeString, setCustomTimeString] = React.useState<string>('');
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 +')) {
return 'Your input is missing "Custom prefix +"';
}
return '';
}, []);

const onChange = React.useCallback((_, time: Date) => {
console.log('Selected time: ', time);
setCustomTimeString(time.toString());
}, []);

return (
<Stack tokens={stackTokens} styles={stackStyles}>
<TimePicker
placeholder="Custom time strings example placeholder"
styles={timePickerStyles}
onFormatDate={onFormatDate}
onValidateUserInput={onValidateUserInput}
onChange={onChange}
useHour12
allowFreeform={false}
dateAnchor={dateAnchor}
autoComplete="on"
label="TimePicker with custom time strings"
/>
<Text>{`⚓ Date anchor: ${dateAnchor.toString()}`}</Text>
<Text>{`⌚ Selected time: ${customTimeString ? customTimeString : '<no time selected>'}`}</Text>
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as React from 'react';
import { TimePicker, DatePicker, Label, Text, IStackTokens, Stack, IStackStyles, IComboBox } from '@fluentui/react';

const stackStyles: Partial<IStackStyles> = { 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<Date>(currentDate);
const [currentTime, setCurrentTime] = React.useState<Date>();

const onSelectDate = React.useCallback(
(selectedDate: Date) => {
setDatePickerDate(selectedDate);
if (currentTime) {
const snappedTime = snapTimeToUpdatedDateAnchor(selectedDate, currentTime);
setCurrentTime(snappedTime);
}
},
[currentTime],
);

const onTimePickerChange = React.useCallback((_ev: React.FormEvent<IComboBox>, date: Date) => {
setCurrentTime(date);
}, []);

return (
<Stack tokens={stackTokens} styles={stackStyles}>
<Label>{'DatePicker and TimePicker combination'}</Label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gridColumnGap: '3px' }}>
<DatePicker placeholder="Select a date..." value={datePickerDate} onSelectDate={onSelectDate} />
<TimePicker
placeholder="Select a time"
dateAnchor={datePickerDate}
value={currentTime}
onChange={onTimePickerChange}
/>
</div>
<Text>{`⚓ Date anchor: ${datePickerDate.toString()}`}</Text>
<Text>{`⌚ Selected time: ${currentTime ? currentTime.toString() : '<no time selected>'}`}</Text>
</Stack>
);
};

This file was deleted.

Loading

0 comments on commit 2d93a25

Please sign in to comment.