Skip to content

Commit

Permalink
chore(TimePicker-compat): preview release with added stories and docs (
Browse files Browse the repository at this point in the history
…microsoft#29677)

* doc and stories

* preview release

* escape in ts comment

* remove CustomValidation story because it makes more sense for later

* spec

* spec

* onTimeSelect => onTimeChange

* pick from combobox props instead of omit

* validateFreeFormTime => formatTimeStringToDate

* api

* memo

* type

* update comment

* use freeform timepicker with datepicker

* small rename

* more docs

* fix merge issue

* fix merge issue
  • Loading branch information
YuanboXue-Amber authored Oct 31, 2023
1 parent a6d0b97 commit d466462
Show file tree
Hide file tree
Showing 18 changed files with 351 additions and 5 deletions.
1 change: 1 addition & 0 deletions apps/public-docsite-v9/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: release preview package",
"packageName": "@fluentui/react-timepicker-compat-preview",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
<TimePicker />
```

# 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)`.
Original file line number Diff line number Diff line change
@@ -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` | `onTimeChange` |
| `onFormatDate` | `formatDateToTimeString` |
| `onValidateUserInput` | `formatDateToTimeString` |
| `onValidationResult` | `onTimeChange` 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 `<input>` element may be set on `<Combobox>`, including the `onChange` handler. Because of this, the v8 `onChange` selection callback has been updated to `onTimeChange`. The v9 TimePicker's `onChange` event behavior is the same as for an `<input>` 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 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.
Original file line number Diff line number Diff line change
@@ -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, `onTimeChange` 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 `onTimeChange` callback.
2. **Freeform TimePicker**: a v9 Combobox with predefined time options that allows freeform input.
- Selecting an option from the dropdown invokes `onTimeChange` callback.
- Time is selected from freeform input when its value has changed, and TimePicker loses focus or <kbd>Enter</kbd> key is pressed. `onTimeChange` 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 `onTimeChange`.
- parsing and validation of selected time text: `formatDateToTimeString`
- 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`.
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,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<TimePickerProps>;

// @public (undocumented)
export const timePickerClassNames: SlotClassNames<TimePickerSlots>;

// @public
export type TimePickerErrorType = 'invalid-input' | 'out-of-bounds' | 'required-input';

// @public
export type TimePickerProps = Omit<ComponentProps<Partial<ComboboxSlots>, 'input'>, 'children' | 'size'> & Pick<ComboboxProps, 'appearance' | 'defaultOpen' | 'defaultValue' | 'inlinePopup' | 'onOpenChange' | 'open' | 'placeholder' | 'positioning' | 'size' | 'value' | 'mountNode' | 'freeform'> & TimeFormatOptions & {
startHour?: Hour;
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ export type TimeFormatOptions = {
/**
* TimePicker Props
*/
export type TimePickerProps = Omit<ComponentProps<Partial<ComboboxSlots>, 'input'>, 'children' | 'size'> &
export type TimePickerProps = Omit<
ComponentProps<Partial<ComboboxSlots>, 'input'>,
| 'children' // TODO add children prop to allow custom children through render function
| 'size'
> &
Pick<
ComboboxProps,
| 'appearance'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './TimePicker';
export * from './TimePicker.types';
export * from './useTimePicker';
export * from './useTimePickerStyles.styles';
export { formatDateToTimeString } from './timeMath';
Original file line number Diff line number Diff line change
Expand Up @@ -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([], {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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 (
<Field label="Select a time (default Selection)">
<TimePicker
startHour={8}
endHour={20}
defaultSelectedTime={defaultSelectedTime}
defaultValue={formatDateToTimeString(defaultSelectedTime)}
/>
</Field>
);
};

const ControlledSelection = () => {
const [selectedTime, setSelectedTime] = React.useState<Date | null>(new Date('November 25, 2023 12:30:00'));
const [value, setValue] = React.useState<string>(selectedTime ? formatDateToTimeString(selectedTime) : '');

const onTimeChange: TimePickerProps['onTimeChange'] = (_ev, data) => {
setSelectedTime(data.selectedTime);
setValue(data.selectedTimeText ?? '');
};
const onInput = (ev: React.ChangeEvent<HTMLInputElement>) => {
setValue(ev.target.value);
};

return (
<Field label="Select a time (controlled Selection)">
<TimePicker
startHour={8}
endHour={20}
selectedTime={selectedTime}
onTimeChange={onTimeChange}
value={value}
onInput={onInput}
/>
</Field>
);
};

export const Controlled = () => {
const styles = useStyles();
return (
<div className={styles.root}>
<DefaultSelection />
<ControlledSelection />
</div>
);
};

Controlled.parameters = {
docs: {
description: {
story,
},
},
};
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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 `onTimeChange` callback. Use Field to display the error message based on the error type provided by `onTimeChange`.
Original file line number Diff line number Diff line change
@@ -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<TimePickerErrorType>();
const handleTimeChange: TimePickerProps['onTimeChange'] = (_ev, data) => {
setErrorType(data.errorType);
};

return (
<Field
required
label={
`Type a time outside of 10:00 to 19:59,` +
` type an invalid time, or leave the input empty and close the TimePicker.`
}
validationMessage={getErrorMessage(errorType)}
>
<TimePicker className={styles.control} freeform startHour={10} endHour={20} onTimeChange={handleTimeChange} />
</Field>
);
};

FreeformWithErrorHandling.parameters = {
docs: {
description: {
story,
},
},
};
Loading

0 comments on commit d466462

Please sign in to comment.