From 3217da343f332d50c0cd0e381ad5c1b35e1dbaa2 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Fri, 27 Oct 2023 15:35:15 +0200 Subject: [PATCH 01/19] doc and stories --- .../react-timepicker-compat-preview/README.md | 33 ++++++++ .../docs/Migration.md | 34 ++++++++ .../react-timepicker-compat-preview.api.md | 6 ++ .../components/TimePicker/TimePicker.types.ts | 3 + .../src/components/TimePicker/index.ts | 1 + .../src/index.ts | 9 ++- .../TimePicker/TimePickerControlled.md | 4 + .../TimePickerControlled.stories.tsx | 71 ++++++++++++++++ .../TimePickerCustomValidation.stories.tsx | 80 +++++++++++++++++++ .../TimePicker/TimePickerDescription.md | 3 + .../stories/TimePicker/TimePickerFreeform.md | 9 +++ .../TimePicker/TimePickerFreeform.stories.tsx | 53 ++++++++++++ .../TimePickerWithDatePicker.stories.tsx | 72 +++++++++++++++++ .../stories/TimePicker/index.stories.tsx | 4 + 14 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 packages/react-components/react-timepicker-compat-preview/docs/Migration.md create mode 100644 packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerControlled.md create mode 100644 packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerControlled.stories.tsx create mode 100644 packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerCustomValidation.stories.tsx create mode 100644 packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.md create mode 100644 packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx create mode 100644 packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerWithDatePicker.stories.tsx diff --git a/packages/react-components/react-timepicker-compat-preview/README.md b/packages/react-components/react-timepicker-compat-preview/README.md index 77484b107faaf3..c1d76aa833b1f1 100644 --- a/packages/react-components/react-timepicker-compat-preview/README.md +++ b/packages/react-components/react-timepicker-compat-preview/README.md @@ -3,3 +3,36 @@ **React Timepicker components for [Fluent UI React](https://react.fluentui.dev/)** These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. + +TimePicker offers a control that’s optimized for selecting a time from a drop-down list or using free-form input to enter a custom time. + +## Usage + +To import Timepicker: + +```js +import { TimePicker } from '@fluentui/react-timepicker-compat-preview'; +``` + +### Examples + +```jsx + +``` + +# Compat component + +## What makes a compat component? + +A compat component is a component taken from v8 and partially updated with the v9 toolset while keeping its original functionality and most of the original API surface. The most noticeable change being the removal of all v8 dependencies and using only v9 dependencies. While this is a good first step, this is not the final v9 component. We are working on a fully fleshed v9 replacement that will follow all v9 patterns and conventions. + +## How publishing the package will be handled + +Compat components are not added in the `@fluentui/react-components` package suite. Instead, these components should be imported from their respective package as shown above. In contrast with components that live in `@fluentui/react-components`, compat components are to be released as `0.x.x` and there won't be an unstable release (`beta/alpha`) before this release. This is due to the way we will handle versioning for changes, allowing for breaking changes when necessary. + +### Versioning for changes + +We will take a similar approach as v0 where we will follow this pattern: + +- `breaking change (major)`: Since this is a compat component, we will allow breaking changes if absolutely necessary. To accommodate for this, we will denote those changes as a minor version in semver, i.e. `0.(change will be reflected here).x`. +- `minor and patch`: These changes will be reflected in the patch version in semver as `0.x.(change will be reflected here)`. diff --git a/packages/react-components/react-timepicker-compat-preview/docs/Migration.md b/packages/react-components/react-timepicker-compat-preview/docs/Migration.md new file mode 100644 index 00000000000000..1767a7ace98af0 --- /dev/null +++ b/packages/react-components/react-timepicker-compat-preview/docs/Migration.md @@ -0,0 +1,34 @@ +# @fluentui/react-timepicker-compat-preview Migration Guide + +## Migration from v8 TimePicker + +### Property mapping + +TimePicker specific props: + +| v8 TimePicker | v9 TimePicker | +| --------------------- | ------------------------------------------------------------- | +| `dateAnchor` | `dateAnchor` | +| `defaultValue` | `defaultSelectedTime` | +| `increments` | `increment` | +| `label` | handled by `Field` | +| `onChange` | `onTimeSelect` | +| `onFormatDate` | `formatDateToTimeString` | +| `onValidateUserInput` | `validateFreeFormTime` | +| `onValidationResult` | `onTimeSelect` contains error type in `data` | +| `showSeconds` | `showSeconds` | +| `strings` | use `Field` to display error. See 'Custom Validation' example | +| `timeRange` | `startHour` and `endHour` | +| `useHour12` | `hourCycle='h11'` or `hourCycle='h12'` | +| `value` | `selectedTime` | + +V8 TimePicker is built on v8 Combobox, and v9 TimePicker compat on v9 Combobox. Please see Combobox migration guide for the rest of the props. + +\*In v9, any native HTML properties supported on an `` element may be set on ``, including the `onChange` handler. Because of this, the v8 `onChange` selection callback has been updated to `onTimeSelect`. The v9 TimePicker's `onChange` event behavior is the same as for an `` element, or the v9 Input control. + +### Validate selected time + +V8 TimePicker allows custom validation on freeform input via `onValidateUserInput`. There is no way to validate selected option from dropdown. +V9 TimePicker allows custom validation on freeform input via `validateFreeFormTime` and on selected option via `validateOption`. + +v8 TimePicker has default error messages. v9 TimePicker has no default error message - it returns an error type from `onTimeSelect` that can be used to display a custom error message. diff --git a/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md b/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md index ffbb0b7da62923..ed86ba8b9421db 100644 --- a/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md +++ b/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md @@ -12,12 +12,18 @@ import * as React_2 from 'react'; import type { SelectionEvents } from '@fluentui/react-combobox'; import type { SlotClassNames } from '@fluentui/react-utilities'; +// @public +export function formatDateToTimeString(date: Date, { hourCycle, showSeconds }?: TimeFormatOptions): string; + // @public export const TimePicker: ForwardRefComponent; // @public (undocumented) export const timePickerClassNames: SlotClassNames; +// @public +export type TimePickerErrorType = 'invalid-input' | 'out-of-bounds' | 'required-input'; + // @public export type TimePickerProps = Omit & TimeFormatOptions & { startHour?: Hour; diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts index 540efaf33f725f..1cf798f4b60f38 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts @@ -97,6 +97,7 @@ export type TimeFormatOptions = { export type TimePickerProps = Omit< ComboboxProps, // Omit children as TimePicker has predefined children + // TODO add children prop to allow custom children through render function | 'children' // Omit selection props as TimePicker has `selectedTime` props | 'defaultSelectedOptions' @@ -149,6 +150,8 @@ export type TimePickerProps = Omit< * Custom validation for the input time string from user in freeform TimePicker. */ validateFreeFormTime?: (time: string | undefined) => TimeStringValidationResult; + + // TODO: `validateOption` prop to do custom validation for the time selected from dropdown. }; /** diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/index.ts b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/index.ts index 1b2e349ebb7a9f..ec9749b359c983 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/index.ts +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/index.ts @@ -2,3 +2,4 @@ export * from './TimePicker'; export * from './TimePicker.types'; export * from './useTimePicker'; export * from './useTimePickerStyles.styles'; +export { formatDateToTimeString } from './timeMath'; diff --git a/packages/react-components/react-timepicker-compat-preview/src/index.ts b/packages/react-components/react-timepicker-compat-preview/src/index.ts index 88aacdd65e5ead..e90b420cb342cb 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/index.ts +++ b/packages/react-components/react-timepicker-compat-preview/src/index.ts @@ -1,8 +1,15 @@ -export { TimePicker, timePickerClassNames, useTimePickerStyles_unstable, useTimePicker_unstable } from './TimePicker'; +export { + TimePicker, + timePickerClassNames, + useTimePickerStyles_unstable, + useTimePicker_unstable, + formatDateToTimeString, +} from './TimePicker'; export type { TimePickerProps, TimePickerSlots, TimePickerState, TimeSelectionData, TimeSelectionEvents, + TimePickerErrorType, } from './TimePicker'; diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerControlled.md b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerControlled.md new file mode 100644 index 00000000000000..72c2be8de22a3d --- /dev/null +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerControlled.md @@ -0,0 +1,4 @@ +A TimePicker may have controlled selection and value. There are a few things to keep in mind: + +1. **Control `selectedTime` with `value` (or `defaultSelectedTime` with `defaultValue`)**: When the `selectedTime` is controlled or a `defaultSelectedTime` is provided, a controlled `value` or `defaultValue` must also be defined. Otherwise, the TimePicker will not be able to display a value before the Options are rendered. +2. **Clearing input with null**: when controlled, the `selectedTime` prop should use `null` instead of `undefined` to clear the value of the TimePicker. diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerControlled.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerControlled.stories.tsx new file mode 100644 index 00000000000000..1090c94632941a --- /dev/null +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerControlled.stories.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { Field, makeStyles } from '@fluentui/react-components'; +import { TimePicker, TimePickerProps, formatDateToTimeString } from '@fluentui/react-timepicker-compat-preview'; +import story from './TimePickerControlled.md'; + +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + rowGap: '20px', + maxWidth: '300px', + }, +}); + +const DefaultSelection = () => { + const [defaultSelectedTime] = React.useState(new Date('November 25, 2023 12:30:00')); + return ( + + + + ); +}; + +const ControlledSelection = () => { + const [selectedTime, setSelectedTime] = React.useState(new Date('November 25, 2023 12:30:00')); + const [value, setValue] = React.useState(selectedTime ? formatDateToTimeString(selectedTime) : ''); + + const onTimeSelect: TimePickerProps['onTimeSelect'] = (_ev, data) => { + setSelectedTime(data.selectedTime); + setValue(data.selectedTimeText ?? ''); + }; + const onInput = (ev: React.ChangeEvent) => { + setValue(ev.target.value); + }; + + return ( + + + + ); +}; + +export const Controlled = () => { + const styles = useStyles(); + return ( +
+ + +
+ ); +}; + +Controlled.parameters = { + docs: { + description: { + story, + }, + }, +}; diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerCustomValidation.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerCustomValidation.stories.tsx new file mode 100644 index 00000000000000..25a5159bd61445 --- /dev/null +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerCustomValidation.stories.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { Field, FieldProps, makeStyles } from '@fluentui/react-components'; +import { TimePicker, TimePickerErrorType, TimePickerProps } from '@fluentui/react-timepicker-compat-preview'; + +const useStyles = makeStyles({ + control: { + maxWidth: '300px', + }, +}); + +const getErrorMessage = (error?: TimePickerErrorType): FieldProps['validationMessage'] => { + switch (error) { + case 'invalid-input': + return 'Invalid time format. Please use the 24-hour format HH:MM.'; + case 'out-of-bounds': + return 'Time out of the 10:00 to 19:59 range.'; + case 'required-input': + return 'Time is required.'; + default: + return ''; + } +}; + +export const CustomValidation = () => { + const styles = useStyles(); + + const [anchor] = React.useState(new Date('November 25, 2023')); + + const [errorType, setErrorType] = React.useState(); + const handleTimeSelect: TimePickerProps['onTimeSelect'] = (_ev, { error }) => { + setErrorType(error); + }; + + const validateFreeFormTime: TimePickerProps['validateFreeFormTime'] = (time: string | undefined) => { + if (!time) { + return { error: 'required-input', date: null }; + } + const timeRegex = /^([01]?\d|2[0-3]):([0-5]\d)$/; + if (!timeRegex.test(time)) { + return { error: 'invalid-input', date: null }; + } + + const [hours, minutes] = time.split(':').map(Number); + const date = new Date(anchor.getFullYear(), anchor.getMonth(), anchor.getDate(), hours, minutes); + + if (date.getHours() < 10 || date.getHours() >= 20) { + return { date, error: 'out-of-bounds' }; + } + + return { date }; + }; + + return ( + + + + ); +}; + +CustomValidation.parameters = { + docs: { + description: { + story: + 'Use `validateOption` to validate the time selected from the dropdown. Use `validateFreeFormTime` to add custom validation for input value submitted in a freeform TimePicker, and perform custom parsing from the input value to date object.', + }, + }, +}; diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerDescription.md b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerDescription.md index e69de29bb2d1d6..8fae7086f4e3dc 100644 --- a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerDescription.md +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerDescription.md @@ -0,0 +1,3 @@ +`TimePicker` offers a control that’s optimized for selecting a time from a drop-down list or using free-form input to enter a custom time. + +Note: TimePicker is a compat component - its internal architecture does not follow all the principles regular Fluent UI v9 components follow - it is not composed of atomic hooks and it might be more difficult to tweak its appearance and behavior. It however follows Fluent 2 design and uses design tokens, it is production ready and it is stable. diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.md b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.md new file mode 100644 index 00000000000000..a7014006ff1599 --- /dev/null +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.md @@ -0,0 +1,9 @@ +TimePicker supports the `freeform` prop, which allows freeform text input. +The selection behavior of freeform TimePicker aligns with the native `change` event behavior for text input: + +- When the value in the TimePicker input changes, and the TimePicker loses focus, the selected time is computed from the `input` value. +- When TimePicker input value has changed and Enter key is pressed on the `input`: + - if the dropdown is expanded and the `input` value is prefix of an option, the selected time is set to the matching option. + - if the dropdown is collapsed or the `input` value does not match any option, the selected time is computed from `input` value. + +The selected time is available in `onTimeSelect` callback. Use Field to display the error message based on the error type provided by `onTimeSelect`. diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx new file mode 100644 index 00000000000000..6112895f337f75 --- /dev/null +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { Field, FieldProps, makeStyles } from '@fluentui/react-components'; +import { TimePicker, TimePickerErrorType, TimePickerProps } from '@fluentui/react-timepicker-compat-preview'; +import story from './TimePickerFreeform.md'; + +const useStyles = makeStyles({ + control: { + maxWidth: '300px', + }, +}); + +const getErrorMessage = (error?: TimePickerErrorType): FieldProps['validationMessage'] => { + switch (error) { + case 'invalid-input': + return 'Invalid time format. Please use the 24-hour format HH:MM.'; + case 'out-of-bounds': + return 'Time out of the 10:00 to 19:59 range.'; + case 'required-input': + return 'Time is required.'; + default: + return ''; + } +}; + +export const FreeformWithErrorHandling = () => { + const styles = useStyles(); + + const [errorType, setErrorType] = React.useState(); + const handleTimeSelect: TimePickerProps['onTimeSelect'] = (_ev, { error }) => { + setErrorType(error); + }; + + return ( + + + + ); +}; + +FreeformWithErrorHandling.parameters = { + docs: { + description: { + story, + }, + }, +}; diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerWithDatePicker.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerWithDatePicker.stories.tsx new file mode 100644 index 00000000000000..405f88b9da9b99 --- /dev/null +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerWithDatePicker.stories.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { Field, makeStyles } from '@fluentui/react-components'; +import { DatePicker, DatePickerProps } from '@fluentui/react-datepicker-compat'; +import { TimePicker, TimePickerProps, formatDateToTimeString } from '@fluentui/react-timepicker-compat-preview'; + +const useStyles = makeStyles({ + root: { + display: 'grid', + columnGap: '20px', + gridTemplateColumns: 'repeat(2, 1fr)', + maxWidth: '600px', + marginBottom: '10px', + }, +}); + +export const TimePickerWithDatePicker = () => { + const styles = useStyles(); + + const [selectedDate, setSelectedDate] = React.useState(null); + + const [selectedTime, setSelectedTime] = React.useState(null); + const [timePickerValue, setTimePickerValue] = React.useState( + selectedTime ? formatDateToTimeString(selectedTime) : '', + ); + + const onSelectDate: DatePickerProps['onSelectDate'] = date => { + setSelectedDate(date); + if (date && selectedTime) { + setSelectedTime( + new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + selectedTime.getHours(), + selectedTime.getMinutes(), + ), + ); + } + }; + + const onTimeSelect: TimePickerProps['onTimeSelect'] = (_ev, data) => { + setSelectedTime(data.selectedTime); + setTimePickerValue(data.selectedTimeText ?? ''); + }; + const onTimePickerInput = (ev: React.ChangeEvent) => { + setTimePickerValue(ev.target.value); + }; + + return ( +
+
+ + + + + + +
+ + {selectedDate && ( +
Selected date time: {selectedTime ? selectedTime.toString() : selectedDate.toString()}
+ )} +
+ ); +}; diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/index.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/index.stories.tsx index a18ae1ea2969f8..55465acc49122b 100644 --- a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/index.stories.tsx +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/index.stories.tsx @@ -4,7 +4,11 @@ import descriptionMd from './TimePickerDescription.md'; import bestPracticesMd from './TimePickerBestPractices.md'; export { Default } from './TimePickerDefault.stories'; +export { Controlled } from './TimePickerControlled.stories'; export { CustomTimeString } from './TimePickerCustomTimeString.stories'; +export { FreeformWithErrorHandling } from './TimePickerFreeform.stories'; +export { CustomValidation } from './TimePickerCustomValidation.stories'; +export { TimePickerWithDatePicker } from './TimePickerWithDatePicker.stories'; export default { title: 'Preview Components/TimePicker', From 40952b9e097e0b5a2ae539c02871da8004994adb Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Fri, 27 Oct 2023 15:41:12 +0200 Subject: [PATCH 02/19] preview release --- apps/public-docsite-v9/package.json | 1 + ...ompat-preview-fb5e9428-5ce6-4664-b04d-d484656b25a7.json | 7 +++++++ .../react-timepicker-compat-preview/package.json | 1 - 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 change/@fluentui-react-timepicker-compat-preview-fb5e9428-5ce6-4664-b04d-d484656b25a7.json diff --git a/apps/public-docsite-v9/package.json b/apps/public-docsite-v9/package.json index e4fa526ecf7647..5389684deca9ec 100644 --- a/apps/public-docsite-v9/package.json +++ b/apps/public-docsite-v9/package.json @@ -37,6 +37,7 @@ "@fluentui/theme-designer": "*", "@fluentui/react-search-preview": "*", "@fluentui/react-motion-preview": "*", + "@fluentui/react-timepicker-compat-preview": "*", "@griffel/react": "^1.5.14", "@microsoft/applicationinsights-web": "^3", "react": "17.0.2", diff --git a/change/@fluentui-react-timepicker-compat-preview-fb5e9428-5ce6-4664-b04d-d484656b25a7.json b/change/@fluentui-react-timepicker-compat-preview-fb5e9428-5ce6-4664-b04d-d484656b25a7.json new file mode 100644 index 00000000000000..44516c92150b8b --- /dev/null +++ b/change/@fluentui-react-timepicker-compat-preview-fb5e9428-5ce6-4664-b04d-d484656b25a7.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: release preview package", + "packageName": "@fluentui/react-timepicker-compat-preview", + "email": "yuanboxue@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-timepicker-compat-preview/package.json b/packages/react-components/react-timepicker-compat-preview/package.json index 284f9567ed549c..2b3442a13ffeb2 100644 --- a/packages/react-components/react-timepicker-compat-preview/package.json +++ b/packages/react-components/react-timepicker-compat-preview/package.json @@ -1,7 +1,6 @@ { "name": "@fluentui/react-timepicker-compat-preview", "version": "0.0.0", - "private": true, "description": "Fluent UI TimePicker Compat Component", "main": "lib-commonjs/index.js", "module": "lib/index.js", From ca932d87bc8367f5151d00f3cd5f016a076f0ef4 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Fri, 27 Oct 2023 17:13:54 +0200 Subject: [PATCH 03/19] escape in ts comment --- .../src/components/TimePicker/timeMath.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/timeMath.ts b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/timeMath.ts index 398c09a1d9eee7..7292947ef61e8c 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/timeMath.ts +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/timeMath.ts @@ -41,8 +41,8 @@ export function keyToDate(key: string): Date | null { * @example * const date = new Date(2023, 9, 6, 23, 45, 12); * formatDateToTimeString(date); // Returns "23:45" - * formatDateToTimeString(date, { showSeconds: true }); // Returns "23:45:12" - * formatDateToTimeString(date, { hourCycle: 'h12', showSeconds: true }); // Returns "11:45:12 PM" + * formatDateToTimeString(date, \{ showSeconds: true \}); // Returns "23:45:12" + * formatDateToTimeString(date, \{ hourCycle: 'h12', showSeconds: true \}); // Returns "11:45:12 PM" */ export function formatDateToTimeString(date: Date, { hourCycle, showSeconds }: TimeFormatOptions = {}): string { return date.toLocaleTimeString([], { From 394b2bdad4b290e39967f376a127562db5c92392 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Fri, 27 Oct 2023 18:26:12 +0200 Subject: [PATCH 04/19] remove CustomValidation story because it makes more sense for later --- .../TimePickerCustomValidation.stories.tsx | 80 ------------------- .../stories/TimePicker/index.stories.tsx | 1 - 2 files changed, 81 deletions(-) delete mode 100644 packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerCustomValidation.stories.tsx diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerCustomValidation.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerCustomValidation.stories.tsx deleted file mode 100644 index 25a5159bd61445..00000000000000 --- a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerCustomValidation.stories.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import * as React from 'react'; -import { Field, FieldProps, makeStyles } from '@fluentui/react-components'; -import { TimePicker, TimePickerErrorType, TimePickerProps } from '@fluentui/react-timepicker-compat-preview'; - -const useStyles = makeStyles({ - control: { - maxWidth: '300px', - }, -}); - -const getErrorMessage = (error?: TimePickerErrorType): FieldProps['validationMessage'] => { - switch (error) { - case 'invalid-input': - return 'Invalid time format. Please use the 24-hour format HH:MM.'; - case 'out-of-bounds': - return 'Time out of the 10:00 to 19:59 range.'; - case 'required-input': - return 'Time is required.'; - default: - return ''; - } -}; - -export const CustomValidation = () => { - const styles = useStyles(); - - const [anchor] = React.useState(new Date('November 25, 2023')); - - const [errorType, setErrorType] = React.useState(); - const handleTimeSelect: TimePickerProps['onTimeSelect'] = (_ev, { error }) => { - setErrorType(error); - }; - - const validateFreeFormTime: TimePickerProps['validateFreeFormTime'] = (time: string | undefined) => { - if (!time) { - return { error: 'required-input', date: null }; - } - const timeRegex = /^([01]?\d|2[0-3]):([0-5]\d)$/; - if (!timeRegex.test(time)) { - return { error: 'invalid-input', date: null }; - } - - const [hours, minutes] = time.split(':').map(Number); - const date = new Date(anchor.getFullYear(), anchor.getMonth(), anchor.getDate(), hours, minutes); - - if (date.getHours() < 10 || date.getHours() >= 20) { - return { date, error: 'out-of-bounds' }; - } - - return { date }; - }; - - return ( - - - - ); -}; - -CustomValidation.parameters = { - docs: { - description: { - story: - 'Use `validateOption` to validate the time selected from the dropdown. Use `validateFreeFormTime` to add custom validation for input value submitted in a freeform TimePicker, and perform custom parsing from the input value to date object.', - }, - }, -}; diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/index.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/index.stories.tsx index 55465acc49122b..04a9ae51db0b88 100644 --- a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/index.stories.tsx +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/index.stories.tsx @@ -7,7 +7,6 @@ export { Default } from './TimePickerDefault.stories'; export { Controlled } from './TimePickerControlled.stories'; export { CustomTimeString } from './TimePickerCustomTimeString.stories'; export { FreeformWithErrorHandling } from './TimePickerFreeform.stories'; -export { CustomValidation } from './TimePickerCustomValidation.stories'; export { TimePickerWithDatePicker } from './TimePickerWithDatePicker.stories'; export default { From 6d3616fda676367883e57e8d895b9881b6183fee Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Mon, 30 Oct 2023 12:50:23 +0100 Subject: [PATCH 05/19] spec --- .../docs/Spec.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/react-components/react-timepicker-compat-preview/docs/Spec.md diff --git a/packages/react-components/react-timepicker-compat-preview/docs/Spec.md b/packages/react-components/react-timepicker-compat-preview/docs/Spec.md new file mode 100644 index 00000000000000..de6d15914090a0 --- /dev/null +++ b/packages/react-components/react-timepicker-compat-preview/docs/Spec.md @@ -0,0 +1,38 @@ +# @fluentui/react-timepicker-compat-preview Spec + +## Background + +Compat component for [V8 TimePicker](https://developer.microsoft.com/en-us/fluentui#/controls/web/timepicker). + +> ⚠️ A compat component is a component taken from v8 and partially updated with the v9 toolset while keeping its original functionality and most of the original API surface. The most noticeable change being the removal of all v8 dependencies and using only v9 dependencies. While this is a good first step, this is not the final v9 component. We are working on a fully fleshed v9 replacement that will follow all v9 patterns and conventions. + +TimePicker offers a control that’s optimized for selecting a time from a drop-down list or using free-form input to enter a custom time. + +**TimePicker is built on top of v9 Combobox. Combobox [Spec.md](../../react-combobox/docs/Spec.md) covers the variants, structure and accessibility of TimePicker. This spec highlights the TimePicker specifics.** + +## Prior Art + +- [26642](https://github.com/microsoft/fluentui/issues/26642) + +## Selection Behaviors + +When selecting a time, the time is validated, `onTimeSelect` callback is fired with the selected time and the error if the time is invalid. TimePicker has two variants that provides different selection behavior: + +1. **Basic TimePicker**: a v9 Combobox with predefined time options. + - Selecting an option from the dropdown invokes `onTimeSelect` callback. +2. **Freeform TimePicker**: a v9 Combobox with predefined time options that allows freeform input. + - Selecting an option from the dropdown invokes `onTimeSelect` callback. + - Time is selected from freeform input when its value has changed, and TimePicker loses focus or Enter key is pressed. `onTimeSelect` is triggered with the selected time from `input` value. This behavior aligns with the native `change` event for text input. + > freeform TimePicker's selection behavior is different from freeform Combobox. Combobox lacks the equivalent callback for native change event ([29494](https://github.com/microsoft/fluentui/issues/29494)) + +## API + +See API at [TimePicker.types.ts](../src/components/TimePicker/TimePicker.types.ts). + +TimePicker share slots, visual and positioning props with Combobox. Its own specific props are: + +- For selection: `defaultSelectedTime`, `selectedTime` and `onTimeSelect`. + - Selection validation: `validateFreeFormTime` +- For generating time options: + - `startHour`, `endHour` and `increment` props are used to generate the predefined time options. + - The options' format can be changed via `hourCycle` and `showSeconds` props. Further customization is available via `formatDateToTimeString`. From eea9ca14b4e15df61bd5e0e137ba1c3595abd3f6 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Mon, 30 Oct 2023 12:53:10 +0100 Subject: [PATCH 06/19] spec --- .../react-timepicker-compat-preview/docs/Migration.md | 2 +- .../react-timepicker-compat-preview/docs/Spec.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/docs/Migration.md b/packages/react-components/react-timepicker-compat-preview/docs/Migration.md index 1767a7ace98af0..46ec6d4e7832d0 100644 --- a/packages/react-components/react-timepicker-compat-preview/docs/Migration.md +++ b/packages/react-components/react-timepicker-compat-preview/docs/Migration.md @@ -29,6 +29,6 @@ V8 TimePicker is built on v8 Combobox, and v9 TimePicker compat on v9 Combobox. ### Validate selected time V8 TimePicker allows custom validation on freeform input via `onValidateUserInput`. There is no way to validate selected option from dropdown. -V9 TimePicker allows custom validation on freeform input via `validateFreeFormTime` and on selected option via `validateOption`. +V9 TimePicker allows custom validation on freeform input via `validateFreeFormTime` and on selected option via `validateOption` (TODO). v8 TimePicker has default error messages. v9 TimePicker has no default error message - it returns an error type from `onTimeSelect` that can be used to display a custom error message. diff --git a/packages/react-components/react-timepicker-compat-preview/docs/Spec.md b/packages/react-components/react-timepicker-compat-preview/docs/Spec.md index de6d15914090a0..92c0822f9c3c5a 100644 --- a/packages/react-components/react-timepicker-compat-preview/docs/Spec.md +++ b/packages/react-components/react-timepicker-compat-preview/docs/Spec.md @@ -32,7 +32,7 @@ See API at [TimePicker.types.ts](../src/components/TimePicker/TimePicker.types.t TimePicker share slots, visual and positioning props with Combobox. Its own specific props are: - For selection: `defaultSelectedTime`, `selectedTime` and `onTimeSelect`. - - Selection validation: `validateFreeFormTime` + - Selection validation: `validateFreeFormTime` and `validateOption` (TODO) - For generating time options: - `startHour`, `endHour` and `increment` props are used to generate the predefined time options. - The options' format can be changed via `hourCycle` and `showSeconds` props. Further customization is available via `formatDateToTimeString`. From bf076fa99236e81d5f44beea77abc38c890bcea3 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Mon, 30 Oct 2023 17:11:08 +0100 Subject: [PATCH 07/19] onTimeSelect => onTimeChange --- .../components/TimePicker/TimePicker.test.tsx | 24 +++++++++---------- .../components/TimePicker/TimePicker.types.ts | 2 +- .../components/TimePicker/useTimePicker.tsx | 10 ++++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.test.tsx b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.test.tsx index e441946b1d60d1..1b0a394f4da7a0 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.test.tsx @@ -49,9 +49,9 @@ describe('TimePicker', () => { it('shows controlled time correctly', () => { const TestExample = () => { const [selectedTime, setSelectedTime] = React.useState(dateAnchor); - const onTimeSelect: TimePickerProps['onTimeSelect'] = (_e, data) => setSelectedTime(data.selectedTime); + const onTimeChange: TimePickerProps['onTimeChange'] = (_e, data) => setSelectedTime(data.selectedTime); return ( - + ); }; @@ -70,7 +70,7 @@ describe('TimePicker', () => { const ControlledFreeFormExample = () => { const [selectedTime, setSelectedTime] = React.useState(dateAnchor); - const onTimeSelect: TimePickerProps['onTimeSelect'] = (e, data) => { + const onTimeChange: TimePickerProps['onTimeChange'] = (e, data) => { handleTimeSelect(e, data); setSelectedTime(data.selectedTime); }; @@ -80,13 +80,13 @@ describe('TimePicker', () => { dateAnchor={dateAnchor} startHour={10} selectedTime={selectedTime} - onTimeSelect={onTimeSelect} + onTimeChange={onTimeChange} /> ); }; const UnControlledFreeFormExample = () => ( - + ); beforeEach(() => { @@ -130,12 +130,12 @@ describe('TimePicker', () => { name | Component ${'uncontrolled'} | ${UnControlledFreeFormExample} ${'controlled'} | ${ControlledFreeFormExample} - `('$name - trigger onTimeSelect only when value change', ({ Component }) => { + `('$name - trigger onTimeChange only when value change', ({ Component }) => { const { getByRole, getAllByRole } = render(); const input = getByRole('combobox'); - // Call onTimeSelect when select an option + // Call onTimeChange when select an option userEvent.click(input); userEvent.click(getAllByRole('option')[1]); expect(handleTimeSelect).toHaveBeenCalledTimes(1); @@ -145,11 +145,11 @@ describe('TimePicker', () => { ); handleTimeSelect.mockClear(); - // Do not call onTimeSelect on Enter when the value remains the same + // Do not call onTimeChange on Enter when the value remains the same fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); expect(handleTimeSelect).toHaveBeenCalledTimes(0); - // Call onTimeSelect on Enter when the value changes + // Call onTimeChange on Enter when the value changes userEvent.type(input, '111{enter}'); expect(handleTimeSelect).toHaveBeenCalledTimes(1); expect(handleTimeSelect).toHaveBeenCalledWith( @@ -162,18 +162,18 @@ describe('TimePicker', () => { name | Component ${'uncontrolled'} | ${UnControlledFreeFormExample} ${'controlled'} | ${ControlledFreeFormExample} - `('$name - trigger onTimeSelect on blur when value change', ({ Component }) => { + `('$name - trigger onTimeChange on blur when value change', ({ Component }) => { const { getByRole } = render(); const input = getByRole('combobox'); const expandIcon = getByRole('button'); - // Do not call onTimeSelect when clicking dropdown icon + // Do not call onTimeChange when clicking dropdown icon userEvent.type(input, '111'); userEvent.click(expandIcon); expect(handleTimeSelect).toHaveBeenCalledTimes(0); - // Call onTimeSelect on focus lose + // Call onTimeChange on focus lose userEvent.tab(); expect(handleTimeSelect).toHaveBeenCalledWith( expect.anything(), diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts index 540efaf33f725f..4a6614d59cf9a9 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts @@ -138,7 +138,7 @@ export type TimePickerProps = Omit< /** * Callback for when a time selection is made. */ - onTimeSelect?: (event: TimeSelectionEvents, data: TimeSelectionData) => void; + onTimeChange?: (event: TimeSelectionEvents, data: TimeSelectionData) => void; /** * Custom the date strings displayed in dropdown options. diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx index 3b02d39e68abb7..825e65bae70de4 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx @@ -32,7 +32,7 @@ export const useTimePicker_unstable = (props: TimePickerProps, ref: React.Ref(undefined); - const selectTime: TimePickerProps['onTimeSelect'] = React.useCallback( + const selectTime: TimePickerProps['onTimeChange'] = React.useCallback( (e, data) => { setSelectedTime(data.selectedTime); setSubmittedText(data.selectedTimeText); - onTimeSelect?.(e, data); + onTimeChange?.(e, data); }, - [onTimeSelect, setSelectedTime], + [onTimeChange, setSelectedTime], ); const selectedOptions = React.useMemo(() => { @@ -163,7 +163,7 @@ const useStableDateAnchor = (providedDate: Date | undefined, startHour: Hour, en * - Enter/Tab key is pressed on the input. * - TimePicker loses focus, signifying a possible change. */ -const useSelectTimeFromValue = (state: TimePickerState, callback: TimePickerProps['onTimeSelect']) => { +const useSelectTimeFromValue = (state: TimePickerState, callback: TimePickerProps['onTimeChange']) => { const { activeOption, freeform, validateFreeFormTime, submittedText, setActiveOption, value } = state; // Base Combobox has activeOption default to first option in dropdown even if it doesn't match input value, and Enter key will select it. From 56329ec4d0d077a97134dce3a06a1f50f6d4160f Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Mon, 30 Oct 2023 17:33:27 +0100 Subject: [PATCH 08/19] pick from combobox props instead of omit --- .../components/TimePicker/TimePicker.types.ts | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts index 4a6614d59cf9a9..079ef996ff6418 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts @@ -94,15 +94,25 @@ export type TimeFormatOptions = { /** * TimePicker Props */ -export type TimePickerProps = Omit< +export type TimePickerProps = Pick< ComboboxProps, - // Omit children as TimePicker has predefined children - | 'children' - // Omit selection props as TimePicker has `selectedTime` props - | 'defaultSelectedOptions' - | 'multiselect' - | 'onOptionSelect' - | 'selectedOptions' + | 'root' + | 'expandIcon' + | 'input' + | 'listbox' + | 'as' + | 'appearance' + | 'defaultOpen' + | 'defaultValue' + | 'inlinePopup' + | 'onOpenChange' + | 'open' + | 'placeholder' + | 'positioning' + | 'size' + | 'value' + | 'mountNode' + | 'freeform' > & TimeFormatOptions & { /** @@ -148,14 +158,14 @@ export type TimePickerProps = Omit< /** * Custom validation for the input time string from user in freeform TimePicker. */ - validateFreeFormTime?: (time: string | undefined) => TimeStringValidationResult; + formatTimeStringToDate?: (time: string | undefined) => TimeStringValidationResult; }; /** * State used in rendering TimePicker */ export type TimePickerState = ComboboxState & - Required> & { + Required> & { /** * Submitted text from the input field. It is used to determine if the input value has changed when user submit a new value on Enter or blur from input. */ From 3ec168b59a05d32c959b92482749810b40b9e233 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Mon, 30 Oct 2023 17:45:53 +0100 Subject: [PATCH 09/19] validateFreeFormTime => formatTimeStringToDate --- .../components/TimePicker/useTimePicker.tsx | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx index 825e65bae70de4..ac1073baee43d5 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx @@ -29,14 +29,14 @@ export const useTimePicker_unstable = (props: TimePickerProps, ref: React.Ref - formatDateToTimeString - ? formatDateToTimeString(dateTime) - : defaultFormatDateToTimeString(dateTime, { showSeconds, hourCycle }), - [hourCycle, formatDateToTimeString, showSeconds], - ); const options: TimePickerOption[] = React.useMemo( () => getTimesBetween(dateStartAnchor, dateEndAnchor, increment).map(time => ({ date: time, key: dateToKey(time), - text: dateToText(time), + text: formatDateToTimeString(time, { showSeconds, hourCycle }), })), - [dateStartAnchor, dateEndAnchor, increment, dateToText], + [dateEndAnchor, dateStartAnchor, formatDateToTimeString, hourCycle, increment, showSeconds], ); const [selectedTime, setSelectedTime] = useControllableState({ @@ -82,7 +75,8 @@ export const useTimePicker_unstable = (props: TimePickerProps, ref: React.Ref { - const selectedOption = options.find(date => date.key === dateToKey(selectedTime)); + const selectedTimeKey = dateToKey(selectedTime); + const selectedOption = options.find(date => date.key === selectedTimeKey); return selectedOption ? [selectedOption.key] : []; }, [options, selectedTime]); @@ -116,7 +110,7 @@ export const useTimePicker_unstable = (props: TimePickerProps, ref: React.Ref getDateFromTimeString(time, dateStartAnchor, dateEndAnchor, { hourCycle, showSeconds }), [dateEndAnchor, dateStartAnchor, hourCycle, showSeconds], @@ -125,7 +119,7 @@ export const useTimePicker_unstable = (props: TimePickerProps, ref: React.Ref { - const { activeOption, freeform, validateFreeFormTime, submittedText, setActiveOption, value } = state; + const { activeOption, freeform, formatTimeStringToDate, submittedText, setActiveOption, value } = state; // Base Combobox has activeOption default to first option in dropdown even if it doesn't match input value, and Enter key will select it. // This effect ensures that the activeOption is cleared when the input doesn't match any option. @@ -186,14 +180,14 @@ const useSelectTimeFromValue = (state: TimePickerState, callback: TimePickerProp return; } - const { date: selectedTime, error } = validateFreeFormTime(value); + const { date: selectedTime, error } = formatTimeStringToDate(value); // Only triggers callback when the text in input has changed. if (submittedText !== value) { callback?.(e, { selectedTime, selectedTimeText: value, error }); } }, - [callback, freeform, submittedText, validateFreeFormTime, value], + [callback, freeform, submittedText, formatTimeStringToDate, value], ); const handleKeyDown: ComboboxProps['onKeyDown'] = React.useCallback( From eda4fdc9368f7de454473bfd191343082bf7ddf5 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Mon, 30 Oct 2023 17:50:16 +0100 Subject: [PATCH 10/19] api --- .../etc/react-timepicker-compat-preview.api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md b/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md index ffbb0b7da62923..df2c6568e8f2c1 100644 --- a/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md +++ b/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md @@ -19,23 +19,23 @@ export const TimePicker: ForwardRefComponent; export const timePickerClassNames: SlotClassNames; // @public -export type TimePickerProps = Omit & TimeFormatOptions & { +export type TimePickerProps = Pick & TimeFormatOptions & { startHour?: Hour; endHour?: Hour; increment?: number; dateAnchor?: Date; selectedTime?: Date | null; defaultSelectedTime?: Date; - onTimeSelect?: (event: TimeSelectionEvents, data: TimeSelectionData) => void; + onTimeChange?: (event: TimeSelectionEvents, data: TimeSelectionData) => void; formatDateToTimeString?: (date: Date) => string; - validateFreeFormTime?: (time: string | undefined) => TimeStringValidationResult; + formatTimeStringToDate?: (time: string | undefined) => TimeStringValidationResult; }; // @public (undocumented) export type TimePickerSlots = ComboboxSlots; // @public -export type TimePickerState = ComboboxState & Required> & { +export type TimePickerState = ComboboxState & Required> & { submittedText: string | undefined; }; From c26b914ee6c6b372258ff9d253642a997c9844c4 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Mon, 30 Oct 2023 17:56:48 +0100 Subject: [PATCH 11/19] memo --- .../components/TimePicker/useTimePicker.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx index ac1073baee43d5..0b59c4dd4a4772 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx @@ -135,20 +135,18 @@ export const useTimePicker_unstable = (props: TimePickerProps, ref: React.Ref { const [fallbackDateAnchor] = React.useState(() => new Date()); - // Convert the Date object to a stable key representation. This ensures that the memoization remains stable when a new Date object representing the same date is passed in. - const dateAnchorKey = dateToKey(providedDate ?? null); - const dateAnchor = React.useMemo( - () => keyToDate(dateAnchorKey) ?? fallbackDateAnchor, - [dateAnchorKey, fallbackDateAnchor], - ); + const providedDateKey = dateToKey(providedDate ?? null); - const dateStartAnchor = React.useMemo(() => getDateStartAnchor(dateAnchor, startHour), [dateAnchor, startHour]); - const dateEndAnchor = React.useMemo( - () => getDateEndAnchor(dateAnchor, startHour, endHour), - [dateAnchor, endHour, startHour], - ); + return React.useMemo(() => { + const dateAnchor = providedDate ?? fallbackDateAnchor; + + const dateStartAnchor = getDateStartAnchor(dateAnchor, startHour); + const dateEndAnchor = getDateEndAnchor(dateAnchor, startHour, endHour); - return { dateStartAnchor, dateEndAnchor }; + return { dateStartAnchor, dateEndAnchor }; + // `providedDate`'s stable key representation is used as dependency instead of the Date object. This ensures that the memoization remains stable when a new Date object representing the same date is passed in. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [endHour, fallbackDateAnchor, providedDateKey, startHour]); }; /** From b52235417a952dfde548ea6fd9fe577a1493d6cb Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Tue, 31 Oct 2023 10:19:25 +0100 Subject: [PATCH 12/19] type --- .../react-timepicker-compat-preview.api.md | 3 +- .../components/TimePicker/TimePicker.types.ts | 37 +++++++++---------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md b/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md index df2c6568e8f2c1..5a671c38dd3788 100644 --- a/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md +++ b/packages/react-components/react-timepicker-compat-preview/etc/react-timepicker-compat-preview.api.md @@ -7,6 +7,7 @@ import type { ComboboxProps } from '@fluentui/react-combobox'; import type { ComboboxSlots } from '@fluentui/react-combobox'; import type { ComboboxState } from '@fluentui/react-combobox'; +import { ComponentProps } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import * as React_2 from 'react'; import type { SelectionEvents } from '@fluentui/react-combobox'; @@ -19,7 +20,7 @@ export const TimePicker: ForwardRefComponent; export const timePickerClassNames: SlotClassNames; // @public -export type TimePickerProps = Pick & TimeFormatOptions & { +export type TimePickerProps = Omit, 'input'>, 'children' | 'size'> & Pick & TimeFormatOptions & { startHour?: Hour; endHour?: Hour; increment?: number; diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts index 079ef996ff6418..d27a3aa8135b52 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import type { ComboboxSlots, ComboboxState, ComboboxProps, SelectionEvents } from '@fluentui/react-combobox'; +import { ComponentProps } from '@fluentui/react-utilities'; export type Hour = | 0 @@ -94,26 +95,22 @@ export type TimeFormatOptions = { /** * TimePicker Props */ -export type TimePickerProps = Pick< - ComboboxProps, - | 'root' - | 'expandIcon' - | 'input' - | 'listbox' - | 'as' - | 'appearance' - | 'defaultOpen' - | 'defaultValue' - | 'inlinePopup' - | 'onOpenChange' - | 'open' - | 'placeholder' - | 'positioning' - | 'size' - | 'value' - | 'mountNode' - | 'freeform' -> & +export type TimePickerProps = Omit, 'input'>, 'children' | 'size'> & + Pick< + ComboboxProps, + | 'appearance' + | 'defaultOpen' + | 'defaultValue' + | 'inlinePopup' + | 'onOpenChange' + | 'open' + | 'placeholder' + | 'positioning' + | 'size' + | 'value' + | 'mountNode' + | 'freeform' + > & TimeFormatOptions & { /** * Start hour (inclusive) for the time range, 0-24. From d479e6e9b945cb4714e3a1dc55208bc6f51a87ba Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Tue, 31 Oct 2023 10:25:38 +0100 Subject: [PATCH 13/19] update comment --- .../src/components/TimePicker/TimePicker.types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts index d27a3aa8135b52..84893e206a6840 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.types.ts @@ -148,12 +148,12 @@ export type TimePickerProps = Omit, 'input onTimeChange?: (event: TimeSelectionEvents, data: TimeSelectionData) => void; /** - * Custom the date strings displayed in dropdown options. + * Customizes the formatting of date strings displayed in dropdown options. */ formatDateToTimeString?: (date: Date) => string; /** - * Custom validation for the input time string from user in freeform TimePicker. + * In the freeform TimePicker, customizes the parsing from the input time string into a Date and provides custom validation. */ formatTimeStringToDate?: (time: string | undefined) => TimeStringValidationResult; }; From 2916c4972373d49005d9f5e918a5e88b979d1071 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Tue, 31 Oct 2023 11:06:11 +0100 Subject: [PATCH 14/19] use freeform timepicker with datepicker --- .../stories/TimePicker/TimePickerWithDatePicker.stories.tsx | 1 + .../stories/TimePicker/index.stories.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerWithDatePicker.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerWithDatePicker.stories.tsx index 405f88b9da9b99..990b6460e582c1 100644 --- a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerWithDatePicker.stories.tsx +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerWithDatePicker.stories.tsx @@ -55,6 +55,7 @@ export const TimePickerWithDatePicker = () => { Date: Tue, 31 Oct 2023 14:16:46 +0100 Subject: [PATCH 15/19] small rename --- .../stories/TimePicker/TimePickerFreeform.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx index 1ad753cc0ba25c..4ed8d2d88186cf 100644 --- a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx @@ -26,7 +26,7 @@ export const FreeformWithErrorHandling = () => { const styles = useStyles(); const [errorType, setErrorType] = React.useState(); - const handleTimeSelect: TimePickerProps['onTimeChange'] = (_ev, { error }) => { + const handleTimeChange: TimePickerProps['onTimeChange'] = (_ev, { error }) => { setErrorType(error); }; @@ -39,7 +39,7 @@ export const FreeformWithErrorHandling = () => { } validationMessage={getErrorMessage(errorType)} > - + ); }; From 68bb6b6f63ae19482eb3d31fe70c0f757c24dfe6 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Tue, 31 Oct 2023 14:27:05 +0100 Subject: [PATCH 16/19] more docs --- .../react-timepicker-compat-preview/docs/Migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-timepicker-compat-preview/docs/Migration.md b/packages/react-components/react-timepicker-compat-preview/docs/Migration.md index b887fed5a5e569..89c5d91501c879 100644 --- a/packages/react-components/react-timepicker-compat-preview/docs/Migration.md +++ b/packages/react-components/react-timepicker-compat-preview/docs/Migration.md @@ -29,6 +29,6 @@ V8 TimePicker is built on v8 Combobox, and v9 TimePicker compat on v9 Combobox. ### Validate selected time V8 TimePicker allows custom validation on freeform input via `onValidateUserInput`. There is no way to validate selected option from dropdown. -V9 TimePicker allows custom validation on freeform input via `formatDateToTimeString` and on selected option via `validateOption` (TODO). +V9 TimePicker should be used together with `Field` component, and it provides more flexibility for custom validation. You can perform custom parsing and validation for freeform input using `formatDateToTimeString`. Validation of the selected time option from the dropdown can be achieved by validating the `selectedTime` within `onTimeChange` callback. v8 TimePicker has default error messages. v9 TimePicker has no default error message - it returns an error type from `onTimeChange` that can be used to display a custom error message. From 37c21a8e0e884a08471ef1812552faa226604b81 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Tue, 31 Oct 2023 16:33:23 +0100 Subject: [PATCH 17/19] fix merge issue --- .../VerticalStackedBarChart.base.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index 32da8a200e18e8..74115233fcd67e 100644 --- a/packages/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -532,7 +532,7 @@ export class VerticalStackedBarChartBase extends React.Component< const legend: ILegend = { title: point.legend, - color, + color: color, action: () => { this._onLegendClick(point.legend); }, @@ -774,7 +774,7 @@ export class VerticalStackedBarChartBase extends React.Component< const shouldHighlight = this._legendHighlighted(point.legend) || this._noLegendHighlighted() ? true : false; this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, - shouldHighlight, + shouldHighlight: shouldHighlight, href: this.props.href, }); const rectFocusProps = !shouldFocusWholeStack && { @@ -1068,4 +1068,4 @@ export class VerticalStackedBarChartBase extends React.Component< this.props.data.filter(item => item.chartData.length === 0).length === 0 ); } -} +} \ No newline at end of file From 3ee54f15c86d034af48ffa332a7d537910a9ad43 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Tue, 31 Oct 2023 16:34:03 +0100 Subject: [PATCH 18/19] fix merge issue --- .../VerticalStackedBarChart/VerticalStackedBarChart.base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index 74115233fcd67e..5984faffac9f0e 100644 --- a/packages/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -1068,4 +1068,4 @@ export class VerticalStackedBarChartBase extends React.Component< this.props.data.filter(item => item.chartData.length === 0).length === 0 ); } -} \ No newline at end of file +} From 3f9619fa3121397a2b0ca5e8365d7cf04fce0e25 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Tue, 31 Oct 2023 17:52:07 +0100 Subject: [PATCH 19/19] fix name after merge --- .../stories/TimePicker/TimePickerFreeform.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx index 4ed8d2d88186cf..755460e6890b61 100644 --- a/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx +++ b/packages/react-components/react-timepicker-compat-preview/stories/TimePicker/TimePickerFreeform.stories.tsx @@ -26,8 +26,8 @@ export const FreeformWithErrorHandling = () => { const styles = useStyles(); const [errorType, setErrorType] = React.useState(); - const handleTimeChange: TimePickerProps['onTimeChange'] = (_ev, { error }) => { - setErrorType(error); + const handleTimeChange: TimePickerProps['onTimeChange'] = (_ev, data) => { + setErrorType(data.errorType); }; return (