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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ng-dev/commit-message.mts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const commitMessage: CommitMessageConfig = {
'material/sort',
'material/stepper',
'material/testing',
'material/timepicker',
'material/theming',
'material/toolbar',
'material/tooltip',
Expand Down
1 change: 1 addition & 0 deletions src/components-examples/config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ALL_EXAMPLES = [
"//src/components-examples/material/bottom-sheet",
"//src/components-examples/material/badge",
"//src/components-examples/material/autocomplete",
"//src/components-examples/material/timepicker",
"//src/components-examples/material-experimental/column-resize",
"//src/components-examples/material-experimental/popover-edit",
"//src/components-examples/material-experimental/selection",
Expand Down
51 changes: 51 additions & 0 deletions src/components-examples/material/timepicker/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")

package(default_visibility = ["//visibility:public"])

ng_module(
name = "timepicker",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
assets = glob([
"**/*.html",
"**/*.css",
]),
deps = [
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/material/timepicker",
"//src/material/timepicker/testing",
"@npm//@angular/platform-browser",
"@npm//@types/jasmine",
],
)

filegroup(
name = "source-files",
srcs = glob([
"**/*.html",
"**/*.css",
"**/*.ts",
]),
)

ng_test_library(
name = "unit_tests_lib",
srcs = glob(["**/*.spec.ts"]),
deps = [
":timepicker",
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/material/core",
"//src/material/timepicker",
"//src/material/timepicker/testing",
"@npm//@angular/platform-browser",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_tests_lib"],
)
2 changes: 2 additions & 0 deletions src/components-examples/material/timepicker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {TimepickerOverviewExample} from './timepicker-overview/timepicker-overview-example';
export {TimepickerHarnessExample} from './timepicker-harness/timepicker-harness-example';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<input [matTimepicker]="picker" [(value)]="date"/>
<mat-timepicker #picker/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {MatTimepickerInputHarness} from '@angular/material/timepicker/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {TimepickerHarnessExample} from './timepicker-harness-example';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {DateAdapter, MatNativeDateModule} from '@angular/material/core';

describe('TimepickerHarnessExample', () => {
let fixture: ComponentFixture<TimepickerHarnessExample>;
let loader: HarnessLoader;

beforeEach(() => {
TestBed.configureTestingModule({imports: [NoopAnimationsModule, MatNativeDateModule]});
TestBed.inject(DateAdapter).setLocale('en-US'); // Set the locale to en-US to guarantee consistent tests.
fixture = TestBed.createComponent(TimepickerHarnessExample);
fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture);
});

it('should load all timepicker input harnesses', async () => {
const inputs = await loader.getAllHarnesses(MatTimepickerInputHarness);
expect(inputs.length).toBe(1);
});

it('should open and close a timepicker', async () => {
const input = await loader.getHarness(MatTimepickerInputHarness);
expect(await input.isTimepickerOpen()).toBe(false);

await input.openTimepicker();
expect(await input.isTimepickerOpen()).toBe(true);
});

it('should set the input value', async () => {
const input = await loader.getHarness(MatTimepickerInputHarness);
expect(await input.getValue()).toBe('11:45 AM');

await input.setValue('3:21 PM');
expect(await input.getValue()).toBe('3:21 PM');
});

it('should select an option from the timepicker', async () => {
const input = await loader.getHarness(MatTimepickerInputHarness);
const timepicker = await input.openTimepicker();
expect(await input.getValue()).toBe('11:45 AM');

await timepicker.selectOption({text: '1:00 PM'});
expect(await input.getValue()).toBe('1:00 PM');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {ChangeDetectionStrategy, Component, Signal, signal} from '@angular/core';
import {provideNativeDateAdapter} from '@angular/material/core';
import {MatTimepickerModule} from '@angular/material/timepicker';

/**
* @title Testing with MatTimepickerInputHarness
*/
@Component({
selector: 'timepicker-harness-example',
templateUrl: 'timepicker-harness-example.html',
standalone: true,
providers: [provideNativeDateAdapter()],
imports: [MatTimepickerModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TimepickerHarnessExample {
date: Signal<Date | null>;

constructor() {
const today = new Date();
this.date = signal(new Date(today.getFullYear(), today.getMonth(), today.getDate(), 11, 45));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<mat-form-field>
<mat-label>Pick a time</mat-label>
<input matInput [matTimepicker]="picker">
<mat-timepicker-toggle matIconSuffix [for]="picker"/>
<mat-timepicker #picker/>
</mat-form-field>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {MatTimepickerModule} from '@angular/material/timepicker';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';
import {provideNativeDateAdapter} from '@angular/material/core';

/** @title Basic timepicker */
@Component({
selector: 'timepicker-overview-example',
templateUrl: 'timepicker-overview-example.html',
standalone: true,
providers: [provideNativeDateAdapter()],
imports: [MatFormFieldModule, MatInputModule, MatTimepickerModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TimepickerOverviewExample {}
1 change: 1 addition & 0 deletions src/dev-app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ ng_module(
"//src/dev-app/table-scroll-container",
"//src/dev-app/tabs",
"//src/dev-app/theme",
"//src/dev-app/timepicker",
"//src/dev-app/toolbar",
"//src/dev-app/tooltip",
"//src/dev-app/tree",
Expand Down
2 changes: 1 addition & 1 deletion src/dev-app/dev-app/dev-app-layout.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<mat-sidenav-container class="demo-container">
<mat-sidenav #navigation role="navigation">
<mat-nav-list>
<mat-nav-list class="demo-nav-list">
@for (navItem of navItems; track navItem) {
<a
mat-list-item
Expand Down
1 change: 1 addition & 0 deletions src/dev-app/dev-app/dev-app-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export class DevAppLayout {
{name: 'Table', route: '/table'},
{name: 'Tabs', route: '/tabs'},
{name: 'Theme', route: '/theme'},
{name: 'Timepicker', route: '/timepicker'},
{name: 'Toolbar', route: '/toolbar'},
{name: 'Tooltip', route: '/tooltip'},
{name: 'Tree', route: '/tree'},
Expand Down
4 changes: 4 additions & 0 deletions src/dev-app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ export const DEV_APP_ROUTES: Routes = [
path: 'theme',
loadComponent: () => import('./theme/theme-demo').then(m => m.ThemeDemo),
},
{
path: 'timepicker',
loadComponent: () => import('./timepicker/timepicker-demo').then(m => m.TimepickerDemo),
},
{
path: 'toolbar',
loadComponent: () => import('./toolbar/toolbar-demo').then(m => m.ToolbarDemo),
Expand Down
5 changes: 5 additions & 0 deletions src/dev-app/theme-m3.scss
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@ $density-scales: (-1, -2, -3, -4, minimum, maximum);
.demo-config-buttons button {
margin: 4px;
}

// In M3 we need some spacing around the list in the sidenav.
mat-nav-list.demo-nav-list {
margin: 8px;
}
28 changes: 28 additions & 0 deletions src/dev-app/timepicker/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
load("//tools:defaults.bzl", "ng_module", "sass_binary")

package(default_visibility = ["//visibility:public"])

ng_module(
name = "timepicker",
srcs = glob(["**/*.ts"]),
assets = [
"timepicker-demo.html",
":timepicker_demo_scss",
],
deps = [
"//src/material/button",
"//src/material/card",
"//src/material/core",
"//src/material/datepicker",
"//src/material/form-field",
"//src/material/icon",
"//src/material/input",
"//src/material/select",
"//src/material/timepicker",
],
)

sass_binary(
name = "timepicker_demo_scss",
src = "timepicker-demo.scss",
)
99 changes: 99 additions & 0 deletions src/dev-app/timepicker/timepicker-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<div class="demo-row">
<div>
<div>
<h2>Basic timepicker</h2>
<mat-form-field>
<mat-label>Pick a time</mat-label>
<input
matInput
[matTimepicker]="basicPicker"
[matTimepickerMin]="minControl.value"
[matTimepickerMax]="maxControl.value"
[formControl]="control">
<mat-timepicker [interval]="intervalControl.value" #basicPicker/>
<mat-timepicker-toggle [for]="basicPicker" matSuffix/>
</mat-form-field>

<p>Value: {{control.value}}</p>
<p>Dirty: {{control.dirty}}</p>
<p>Touched: {{control.touched}}</p>
<p>Errors: {{control.errors | json}}</p>
<button mat-button (click)="randomizeValue()">Assign a random value</button>
</div>

<div>
<h2>Timepicker and datepicker</h2>
<mat-form-field>
<mat-label>Pick a date</mat-label>
<input
matInput
[matDatepicker]="combinedDatepicker"
[(ngModel)]="combinedValue">
<mat-datepicker #combinedDatepicker/>
<mat-datepicker-toggle [for]="combinedDatepicker" matSuffix/>
</mat-form-field>

<div>
<mat-form-field>
<mat-label>Pick a time</mat-label>
<input
matInput
[matTimepicker]="combinedTimepicker"
[matTimepickerMin]="minControl.value"
[matTimepickerMax]="maxControl.value"
[(ngModel)]="combinedValue"
[ngModelOptions]="{updateOn: 'blur'}">
<mat-timepicker [interval]="intervalControl.value" #combinedTimepicker/>
<mat-timepicker-toggle [for]="combinedTimepicker" matSuffix/>
</mat-form-field>
</div>

<p>Value: {{combinedValue}}</p>
</div>

<div>
<h2>Timepicker without form field</h2>
<input [matTimepicker]="nonFormFieldPicker">
<mat-timepicker aria-label="Standalone timepicker" #nonFormFieldPicker/>
</div>
</div>

<mat-card appearance="outlined" class="demo-card">
<mat-card-header>
<mat-card-title>State</mat-card-title>
</mat-card-header>

<mat-card-content>
<div class="demo-form-fields">
<mat-form-field>
<mat-label>Locale</mat-label>
<mat-select [formControl]="localeControl">
@for (locale of locales; track $index) {
<mat-option [value]="locale">{{locale}}</mat-option>
}
</mat-select>
</mat-form-field>

<mat-form-field>
<mat-label>Interval</mat-label>
<input matInput [formControl]="intervalControl"/>
</mat-form-field>

<mat-form-field>
<mat-label>Min time</mat-label>
<input matInput [matTimepicker]="minPicker" [formControl]="minControl">
<mat-timepicker #minPicker/>
<mat-timepicker-toggle [for]="minPicker" matSuffix/>
</mat-form-field>

<mat-form-field>
<mat-label>Max time</mat-label>
<input matInput [matTimepicker]="maxPicker" [formControl]="maxControl">
<mat-timepicker #maxPicker/>
<mat-timepicker-toggle [for]="maxPicker" matSuffix/>
</mat-form-field>
</div>
</mat-card-content>
</mat-card>
</div>

22 changes: 22 additions & 0 deletions src/dev-app/timepicker/timepicker-demo.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.demo-row {
display: flex;
align-items: flex-start;
gap: 100px;
}

.demo-card {
width: 600px;
max-width: 100%;
flex-shrink: 0;
}

.demo-form-fields {
display: flex;
flex-wrap: wrap;
gap: 0 2%;
margin-top: 16px;

mat-form-field {
flex-basis: 49%;
}
}
Loading
Loading