Skip to content

feat(material/timepicker): add timepicker component #29806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Oct 4, 2024

Conversation

crisbeto
Copy link
Member

@crisbeto crisbeto commented Oct 1, 2024

Addresses a long-time feature request by adding a component that allows users to select a time. The new component uses a combination of an input and a dropdown to allow users to either type a time or select it from a pre-defined list. Example usage:

<mat-form-field>
  <mat-label>Pick a time</mat-label>
  <input matInput [matTimepicker]="picker"/>
  <mat-timepicker #picker/>
  <mat-timepicker-toggle [for]="picker"/>
</mat-form-field>

Features of the new component include:

  • Automatically parses the typed-in value to a date object using the current DateAdapter. Existing date adapters have been updated to add support for parsing times.
  • Time values can be generated either using the interval input (e.g. interval="45min") or provided directly through the options input.
  • Integrated into @angular/forms by providing itself as a ControlValueAccessor and Validator.
  • Offers built-in validation for minimum, maximum and time formatting.
  • Offers keyboard navigation support.
  • Accessibility implemented using the combobox + listbox pattern.
  • Can be used either with mat-form-field or on its own.
  • Can be combined with mat-datepicker (docs to follow, see the dev app for now).
  • Includes test harnesses for all directives.
  • Works with Material's theming system.
  • Can be configured globally through an injection token.
  • Can be used either as an NgModule or by importing the standalone directives.

FAQ

Why did it take so long?

One of the main reasons why we hadn't provided a timepicker component until now was that there's no
universally-accepted design for what a timepicker should look like.

Doesn't Material Design have a timepicker?

Material Design has had a specification for a timepicker for years, but we didn't want to implement it because:

  1. This design is primarily geared towards mobile users on Android. It would look out of place in the desktop-focused enterprise UIs that a lot of Angular developers build.
  2. The time dial UI is complicated and can be overwhelming, especially in the 24h variant.
  3. The accessibility pattern is unclear, users may have to fall back to using the inputs.
  4. It's unclear how the time selection would work on non-Westernized locales whose time formatting isn't some variation of HH:MM.
  5. The time dial requires very precise movements if the user wants to select a specific time between others (e.g. 6:52). This can be unusable for users with some disabilities.
  6. The non-dial functionality (inputs in a dropdown) don't add much to the user experience.

There are community implementations of the dial design that you can install if you want it for your app.

What are some alternate designs that you considered?

Some libraries like Kendo UI, Ignite UI or MUI, as well as Chrome's implementation of <input type="time"/> appear to have settled on a multi-column design for the dropdown. We didn't want to do something similar because:

  1. The selected state is only shown using one sensory characteristic (color) which is problematic for accessibility. While we could either add a second one (e.g. a checkbox) or adjust the design somehow, we felt that this would make it look sub-optimal.
  2. The UI only looks good on smaller sizes and when each column has roughly the same amount of text. Changing either for a particular column can throw off the whole UI's appearance.
  3. It requires the user to tab through several controls within the dialog.
  4. It's unclear how the time selection would work on non-Westernized locales whose time formatting isn't some variation of HH:MM.
  5. Each column requires a lot of filler whitespace in order to be able to align the selected states to each other which can look off on some selections.

Why this design?

We chose the current design, because:

  1. Users are familiar with it, e.g. Google Calendar uses something similar for their time selection.
  2. It reuses the design from existing Material Design components.
  3. It uses an established accessibility pattern (combobox + listbox) and it doesn't have the same concerns as the multi-column design around indicating the selected state.
  4. It allows us to support a wide range of locales.
  5. It's compact, allowing us to do some sort of unified control with mat-datepicker in the future.

Fixes #5648.

@crisbeto crisbeto added the target: major This PR is targeted for the next major release label Oct 1, 2024
@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: build & ci Related the build and CI infrastructure of the project labels Oct 1, 2024
@crisbeto crisbeto added the merge: preserve commits When the PR is merged, a rebase and merge should be performed label Oct 1, 2024
@angular-robot angular-robot bot added the area: docs Related to the documentation label Oct 1, 2024
@crisbeto crisbeto added the dev-app preview When applied, previews of the dev-app are deployed to Firebase label Oct 1, 2024
Copy link

github-actions bot commented Oct 1, 2024

Deployed dev-app for 3c27b91 to: https://ng-dev-previews-comp--pr-angular-components-29806-dev-fxh7qbvt.web.app

Note: As new commits are pushed to this pull request, this link is updated after the preview is rebuilt.

@crisbeto crisbeto force-pushed the timepicker branch 4 times, most recently from 3200fe9 to 3d04ba9 Compare October 1, 2024 11:02
@crisbeto crisbeto marked this pull request as ready for review October 1, 2024 11:18
@crisbeto crisbeto requested review from a team as code owners October 1, 2024 11:18
@crisbeto crisbeto requested review from amysorto, mmalerba, andrewseguin and wagnermaciel and removed request for a team October 1, 2024 11:18
@josephperrott josephperrott removed the request for review from a team October 1, 2024 14:48
Copy link
Contributor

@naaajii naaajii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a thought: setting minimum time to 12PM, maximum time to 3PM & interval to something like 40m doesn't show the last time 3PM (max) in dropdown.

@crisbeto
Copy link
Member Author

crisbeto commented Oct 2, 2024

That's a good point, although then the last option won't have the same interval as the rest. This can be worked around by using the options input instead or tweaking the interval.


if (event.keyCode === ESCAPE && !hasModifierKey(event) && this.value() !== null) {
event.preventDefault();
this.value.set(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part seems surprising to me. Why does ESCAPE clear the input? The datepicker doesnt do this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this off the W3C example:

Escape
If the listbox is displayed, closes it
If the listbox is not displayed, clears the textbox.


/** Sets up the code that watches for changes in the value and adjusts the input. */
private _respondToValueChanges(): void {
effect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if this function is called twice? Should we save the return result from effect (is there one?) to assert against if this is called more than once

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't put any safeguards, because it's a private function and the effect will throw if the function is called outside of the constructor.

effect(() => {
const timepicker = this.timepicker();
timepicker.registerInput(this);
timepicker.closed.subscribe(() => this._onTouched?.());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im rusty on my rxjs, but should these be unsubscribed at some point

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally we would, but outputs get completed automatically on destroy: https://github.com/angular/angular/blob/main/packages/core/src/authoring/output/output_emitter_ref.ts#L40

/** Sets up the logic that adjusts the input if the min/max changes. */
private _respondToMinMaxChanges(): void {
effect(() => {
// Read the min/max so the effect knows when to fire.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems clunky - seems like the effect API is missing a way for users to do this more naturally

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely clunky, I've raised it with the folks working on reactivity.

Sets up the boilerplate for the timepicker module.
Adds some more tests for the logic that compares times to ensure that they don't consider the date.
Fixes that there wasn't any side padding in the sidenav when using M3 in the dev app.
…ate options

Adds the following utilities to the timpicker:
* `parseInterval` - turns an interval value into a number of seconds.
* `generateOptions` - generates a list of timepicker options between a minimum and maximum, and with a specific interval.
Addresses a long-time feature request by adding a component that allows users to select a time.
The new component uses a combination of an `input` and a dropdown to allow users to either type
a time or select it from a pre-defined list. Example usage:

```html
<mat-form-field>
  <mat-label>Pick a time</mat-label>
  <input matInput [matTimepicker]="picker"/>
  <mat-timepicker #picker/>
  <mat-timepicker-toggle [for]="picker"/>
</mat-form-field>
```

Features of the new component include:
* Automatically parses the typed-in value to a date object using the current `DateAdapter`. Existing date adapters have been updated to add support for parsing times.
* Time values can be generated either using the `interval` input (e.g. `interval="45min"`) or provided directly through the `options` input.
* Integrated into `@angular/forms` by providing itself as a `ControlValueAccessor` and `Validator`.
* Offers built-in validation for minimum, maximum and time formatting.
* Offers keyboard navigation support.
* Accessibility implemented using the combobox + listbox pattern.
* Can be used either with `mat-form-field` or on its own.
* Can be combined with `mat-datepicker` (docs to follow, see the dev app for now).
* Includes test harnesses for all directives.
* Works with Material's theming system.
* Can be configured globally through an injection token.
* Can be used either as an `NgModule` or by importing the standalone directives.

One of the main reasons why we hadn't provided a timepicker component until now was that there's no
universally-accepted design for what a timepicker should look like.

Material Design has had a [specification for a timepicker](https://m3.material.io/components/time-pickers/overview) for years, but we didn't want to implement it because:
1. This design is primarily geared towards mobile users on Android. It would look out of place in the desktop-focused enterprise UIs that a lot of Angular developers build.
2. The time dial UI is complicated and can be overwhelming, especially in the 24h variant.
3. The accessibility pattern is unclear, users may have to fall back to using the inputs.
4. It's unclear how the time selection would work on non-Westernized locales whose time formatting isn't some variation of `HH:MM`.
5. The time dial requires very precise movements if the user wants to select a specific time between others (e.g. 6:52). This can be unusable for users with some disabilities.
6. The non-dial functionality (inputs in a dropdown) don't add much to the user experience.

There are [community implementations](https://dhutaryan.github.io/ngx-mat-timepicker) of the dial design that you can install if you want it for your app.

Some libraries like [Kendo UI](https://www.telerik.com/kendo-angular-ui/components/dateinputs/timepicker), [Ignite UI](https://www.infragistics.com/products/ignite-ui-angular/angular/components/time-picker) or [MUI](https://mui.com/x/react-date-pickers/time-picker/), as well as Chrome's implementation of `<input type="time"/>` appear to have settled on a multi-column design for the dropdown. We didn't want to do something similar because:
1. The selected state is only shown using one sensory characteristic (color) which is problematic for accessibility. While we could either add a second one (e.g. a checkbox) or adjust the design somehow, we felt that this would make it look sub-optimal.
2. The UI only looks good on smaller sizes and when each column has roughly the same amount of text. Changing either for a particular column can throw off the whole UI's appearance.
3. It requires the user to tab through several controls within the dialog.
4. It's unclear how the time selection would work on non-Westernized locales whose time formatting isn't some variation of `HH:MM`.
5. Each column requires a lot of filler whitespace in order to be able to align the selected states to each other which can look off on some selections.

We chose the current design, because:
1. Users are familiar with it, e.g. Google Calendar uses something similar for their time selection.
2. It reuses the design from existing Material Design components.
3. It uses an established accessibility pattern (combobox + listbox) and it doesn't have the same concerns as the multi-column design around indicating the selected state.
4. It allows us to support a wide range of locales.
5. It's compact, allowing us to do some sort of unified control with `mat-datepicker` in the future.

Fixes angular#5648.
Adds test harnesses for `MatTimepickerInput`, `MatTimepicker` and `MatTimepickerToggle`.
Adds a basic example to work around a CI failure.
Fixes that nothing was happening if the user clicks on the toggle while the timepicker is open.
Fixes that the timepicker toggle icon was being read out as an image.
Fixes that the `text` field in the `matTimepickerParse` error wasn't up-to-date with the element's value.
* Allows spaces between the interval value and unit.
* Adds support for `hour`, `hours`, `minute`, `min`, `minutes`, `second` and `seconds` units in the interval.
Previously we were trying to rely on the browser to parse time, but we couldn't rely on it fully because browsers are inconsistent in how the handle time strings. We had some fallback logic to try and patch over it, but it led to some bug.

These changes switch to relying fully on our own logic for parsing times.
Adds a target to extract the tokens from the timepicker.
@crisbeto crisbeto added the action: merge The PR is ready for merge by the caretaker label Oct 4, 2024
@crisbeto crisbeto merged commit f750300 into angular:main Oct 4, 2024
22 of 24 checks passed
@AntonGorelov
Copy link

Hello everyone! I believe I found a bug and thought I'd let you know about it.

In the deployed application at the link above, selecting a one-second interval is likely to cause a memory leak.

Steps for reproduce:

  1. select 1s interval
  2. Click on the timepicker button.

And this is an expected bug, most likely. The number of options in select increases a lot. I would suggest to change the input method for this option. It is up to the user to select or manually enter the hours/minutes/seconds in each input separately. For example, as specified in the Material spec.
In this case there is a separate input for entering hours/minutes/seconds.

@crisbeto
Copy link
Member Author

crisbeto commented Oct 6, 2024

The list of options is only there to make it easier for the user, it's not used for validation. If you wanted to allow the user to enter seconds, you'd just have to change the accepted time format to include seconds.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Nov 6, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: build & ci Related the build and CI infrastructure of the project area: docs Related to the documentation detected: feature PR contains a feature commit dev-app preview When applied, previews of the dev-app are deployed to Firebase merge: preserve commits When the PR is merged, a rebase and merge should be performed target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat(datepicker): add support for choosing time
4 participants