', 'Run tests for specified components', args =>
- args.positional('components', {type: 'array'}),
+ args.positional('components', {type: 'string', array: true}),
)
.option('debug', {
alias: 'local',
@@ -53,8 +57,9 @@ const {components, debug, firefox, watch} = yargs(args)
default: true,
description: 'Whether tests should be re-run automatically upon changes.',
})
- .strict()
- .parseSync();
+ .strict();
+
+const {components, debug, firefox, watch} = parser.parseSync() as unknown as CliArgs;
// Whether tests for all components should be run.
const all = components.length === 1 && components[0] === 'all';
@@ -84,51 +89,49 @@ if (all) {
console.warn(chalk.yellow('Unable to run all component tests in watch mode.'));
console.warn(chalk.yellow('Tests will be run in non-watch mode..'));
}
- shelljs.exec(
+ sh.exec(
`pnpm -s bazel test --test_tag_filters=-e2e,browser:${browserName} ` +
`--build_tag_filters=browser:${browserName} --build_tests_only //src/...`,
);
- return;
-}
+} else {
+ // Exit if no component has been specified.
+ if (!components.length) {
+ console.error(
+ chalk.red(
+ 'No component specified. Please either specify individual components, or pass "all" ' +
+ 'in order to run tests for all components.',
+ ),
+ );
+ console.info(chalk.yellow('Below are a few examples of how the script can be run:'));
+ console.info(chalk.yellow(` - pnpm test all`));
+ console.info(chalk.yellow(` - pnpm test cdk/overlay material/stepper`));
+ console.info(chalk.yellow(` - pnpm test button toolbar`));
+ process.exit(1);
+ }
-// Exit if no component has been specified.
-if (!components.length) {
- console.error(
- chalk.red(
- 'No component specified. Please either specify individual components, or pass "all" ' +
- 'in order to run tests for all components.',
- ),
+ const bazelAction = debug ? 'run' : 'test';
+ const testLabels = components.map(
+ t => `${getBazelPackageOfComponentName(t)}:${getTargetName(t)}`,
);
- console.info(chalk.yellow('Below are a few examples of how the script can be run:'));
- console.info(chalk.yellow(` - pnpm test all`));
- console.info(chalk.yellow(` - pnpm test cdk/overlay material/stepper`));
- console.info(chalk.yellow(` - pnpm test button toolbar`));
- process.exit(1);
-}
-
-const bazelAction = debug ? 'run' : 'test';
-const testLabels = components.map(t => `${getBazelPackageOfComponentName(t)}:${getTargetName(t)}`);
-
-// Runs Bazel for the determined test labels.
-shelljs.exec(`${bazelBinary} ${bazelAction} ${testLabels.join(' ')}`);
+ // Runs Bazel for the determined test labels.
+ sh.exec(`${bazelBinary} ${bazelAction} ${testLabels.join(' ')}`);
+}
/**
* Gets the Bazel package label for the specified component name. Throws if
* the component could not be resolved to a Bazel package.
*/
-function getBazelPackageOfComponentName(name) {
+function getBazelPackageOfComponentName(name: string) {
// Before guessing any Bazel package, we test if the name contains the
// package name already. If so, we just use that for Bazel package.
const targetName =
- convertPathToBazelLabel(name) || convertPathToBazelLabel(path.join(packagesDir, name));
+ convertPathToBazelLabel(name) || convertPathToBazelLabel(join(packagesDir, name));
if (targetName !== null) {
return targetName;
}
// If the name does not contain an explicit package name, try to guess it.
const guess = guessPackageName(name, packagesDir);
- const guessLabel = guess.result
- ? convertPathToBazelLabel(path.join(packagesDir, guess.result))
- : null;
+ const guessLabel = guess.result ? convertPathToBazelLabel(join(packagesDir, guess.result)) : null;
if (guessLabel) {
return guessLabel;
@@ -144,15 +147,15 @@ function getBazelPackageOfComponentName(name) {
}
/** Converts a path to a Bazel label. */
-function convertPathToBazelLabel(name) {
- if (shelljs.test('-d', name)) {
- return `//${convertPathToPosix(path.relative(projectDir, name))}`;
+function convertPathToBazelLabel(name: string) {
+ if (sh.test('-d', name)) {
+ return `//${convertPathToPosix(relative(process.cwd(), name))}`;
}
return null;
}
/** Gets the name of the target that should be run. */
-function getTargetName(packageName) {
+function getTargetName(packageName: string) {
// Schematics don't have _debug and browser targets.
if (packageName && packageName.endsWith('schematics')) {
return 'unit_tests';
diff --git a/scripts/util.js b/scripts/util.mts
similarity index 72%
rename from scripts/util.js
rename to scripts/util.mts
index 67e8d64f1fcf..f5031cf0b797 100644
--- a/scripts/util.js
+++ b/scripts/util.mts
@@ -1,5 +1,5 @@
-const path = require('path');
-const shelljs = require('shelljs');
+import {join} from 'path';
+import sh from 'shelljs';
/** Map of common typos in target names. The key is the typo, the value is the correct form. */
const commonTypos = new Map([['snackbar', 'snack-bar']]);
@@ -13,12 +13,12 @@ const orderedGuessPackages = ['material', 'cdk', 'material-experimental', 'cdk-e
* Tries to guess the full name of a package, based on a shorthand name.
* Returns an object with the result of the guess and the names that were attempted.
*/
-function guessPackageName(name, packagesDir) {
+export function guessPackageName(name: string, packagesDir: string) {
name = correctTypos(name);
// Build up a list of packages that we're going to try.
- const attempts = [name, ...orderedGuessPackages.map(package => path.join(package, name))];
- const result = attempts.find(guessName => shelljs.test('-d', path.join(packagesDir, guessName)));
+ const attempts = [name, ...orderedGuessPackages.map(guessPackage => join(guessPackage, name))];
+ const result = attempts.find(guessName => sh.test('-d', join(packagesDir, guessName)));
return {
result: result ? convertPathToPosix(result) : null,
@@ -27,12 +27,12 @@ function guessPackageName(name, packagesDir) {
}
/** Converts an arbitrary path to a Posix path. */
-function convertPathToPosix(pathName) {
+export function convertPathToPosix(pathName: string) {
return pathName.replace(/\\/g, '/');
}
/** Correct common typos in a target name */
-function correctTypos(target) {
+function correctTypos(target: string) {
let correctedTarget = target;
for (const [typo, correction] of commonTypos) {
correctedTarget = correctedTarget.replace(typo, correction);
@@ -40,8 +40,3 @@ function correctTypos(target) {
return correctedTarget;
}
-
-module.exports = {
- guessPackageName,
- convertPathToPosix,
-};
diff --git a/src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-form-example.html b/src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-reactive-form-example.html
similarity index 97%
rename from src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-form-example.html
rename to src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-reactive-form-example.html
index b7987183786b..d121016c525d 100644
--- a/src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-form-example.html
+++ b/src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-reactive-form-example.html
@@ -7,4 +7,4 @@
Option selected: {{shoesControl.value ? shoesControl.value[0] : 'None'}}
-
\ No newline at end of file
+
diff --git a/src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-reactive-form-example.ts b/src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-reactive-form-example.ts
index 792f785200a0..15638f509367 100644
--- a/src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-reactive-form-example.ts
+++ b/src/components-examples/material/list/list-single-selection-reactive-form/list-single-selection-reactive-form-example.ts
@@ -6,12 +6,13 @@ interface Shoes {
value: string;
name: string;
}
+
/**
* @title List with single selection using Reactive forms
*/
@Component({
selector: 'list-single-selection-reactive-form-example',
- templateUrl: 'list-single-selection-form-example.html',
+ templateUrl: 'list-single-selection-reactive-form-example.html',
imports: [MatListModule, FormsModule, ReactiveFormsModule],
})
export class ListSingleSelectionReactiveFormExample {
diff --git a/src/components-examples/material/list/list-single-selection/list-single-selection-example.html b/src/components-examples/material/list/list-single-selection/list-single-selection-example.html
index 03221a0f5159..0c653bd3b6da 100644
--- a/src/components-examples/material/list/list-single-selection/list-single-selection-example.html
+++ b/src/components-examples/material/list/list-single-selection/list-single-selection-example.html
@@ -1,10 +1,9 @@
-
\ No newline at end of file
+
+ @for (shoe of typesOfShoes; track shoe) {
+ {{shoe}}
+ }
+
+
+
+ Option selected: {{shoes.selectedOptions.hasValue() ? shoes.selectedOptions.selected[0].value : 'None'}}
+
diff --git a/src/components-examples/material/list/list-single-selection/list-single-selection-example.ts b/src/components-examples/material/list/list-single-selection/list-single-selection-example.ts
index 4f9fbd0f29bf..7027d7c290a7 100644
--- a/src/components-examples/material/list/list-single-selection/list-single-selection-example.ts
+++ b/src/components-examples/material/list/list-single-selection/list-single-selection-example.ts
@@ -1,32 +1,14 @@
import {Component} from '@angular/core';
-import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatListModule} from '@angular/material/list';
-interface Shoes {
- value: string;
- name: string;
-}
+
/**
- * @title List with single selection using Reactive Forms
+ * @title List with single selection
*/
@Component({
selector: 'list-single-selection-example',
templateUrl: 'list-single-selection-example.html',
- imports: [MatListModule, FormsModule, ReactiveFormsModule],
+ imports: [MatListModule],
})
export class ListSingleSelectionExample {
- form: FormGroup;
- shoes: Shoes[] = [
- {value: 'boots', name: 'Boots'},
- {value: 'clogs', name: 'Clogs'},
- {value: 'loafers', name: 'Loafers'},
- {value: 'moccasins', name: 'Moccasins'},
- {value: 'sneakers', name: 'Sneakers'},
- ];
- shoesControl = new FormControl();
-
- constructor() {
- this.form = new FormGroup({
- clothes: this.shoesControl,
- });
- }
+ typesOfShoes: string[] = ['Boots', 'Clogs', 'Loafers', 'Moccasins', 'Sneakers'];
}
diff --git a/src/material-date-fns-adapter/adapter/date-fns-adapter.spec.ts b/src/material-date-fns-adapter/adapter/date-fns-adapter.spec.ts
index d85b32e8341d..aa4973e3e419 100644
--- a/src/material-date-fns-adapter/adapter/date-fns-adapter.spec.ts
+++ b/src/material-date-fns-adapter/adapter/date-fns-adapter.spec.ts
@@ -535,6 +535,15 @@ describe('DateFnsAdapter', () => {
expect(adapter.getSeconds(result)).toBe(0);
});
+ it('should parse a time string containing only hours', () => {
+ const result = adapter.parseTime('11', 'HH')!;
+ expect(result).toBeTruthy();
+ expect(adapter.isValid(result)).toBe(true);
+ expect(adapter.getHours(result)).toBe(11);
+ expect(adapter.getMinutes(result)).toBe(0);
+ expect(adapter.getSeconds(result)).toBe(0);
+ });
+
it('should return an invalid date when parsing invalid time string', () => {
expect(adapter.isValid(adapter.parseTime('abc', 'p')!)).toBe(false);
expect(adapter.isValid(adapter.parseTime('123', 'p')!)).toBe(false);
diff --git a/src/material-date-fns-adapter/adapter/date-fns-adapter.ts b/src/material-date-fns-adapter/adapter/date-fns-adapter.ts
index 91e37fb1cc2d..3201880f33dc 100644
--- a/src/material-date-fns-adapter/adapter/date-fns-adapter.ts
+++ b/src/material-date-fns-adapter/adapter/date-fns-adapter.ts
@@ -162,35 +162,7 @@ export class DateFnsAdapter extends DateAdapter {
}
parse(value: unknown, parseFormat: string | string[]): Date | null {
- if (typeof value == 'string' && value.length > 0) {
- const iso8601Date = parseISO(value);
-
- if (this.isValid(iso8601Date)) {
- return iso8601Date;
- }
-
- const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat];
-
- if (!parseFormat.length) {
- throw Error('Formats array must not be empty.');
- }
-
- for (const currentFormat of formats) {
- const fromFormat = parse(value, currentFormat, new Date(), {locale: this.locale});
-
- if (this.isValid(fromFormat)) {
- return fromFormat;
- }
- }
-
- return this.invalid();
- } else if (typeof value === 'number') {
- return new Date(value);
- } else if (value instanceof Date) {
- return this.clone(value);
- }
-
- return null;
+ return this._parse(value, parseFormat);
}
format(date: Date, displayFormat: string): string {
@@ -278,10 +250,48 @@ export class DateFnsAdapter extends DateAdapter {
}
override parseTime(value: unknown, parseFormat: string | string[]): Date | null {
- return this.parse(value, parseFormat);
+ return this._parse(value, parseFormat, false);
}
override addSeconds(date: Date, amount: number): Date {
return addSeconds(date, amount);
}
+
+ private _parse(
+ value: unknown,
+ parseFormat: string | string[],
+ shouldTryParseIso = true,
+ ): Date | null {
+ if (typeof value == 'string' && value.length > 0) {
+ if (shouldTryParseIso) {
+ const iso8601Date = parseISO(value);
+
+ if (this.isValid(iso8601Date)) {
+ return iso8601Date;
+ }
+ }
+
+ const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat];
+
+ if (!parseFormat.length) {
+ throw Error('Formats array must not be empty.');
+ }
+
+ for (const currentFormat of formats) {
+ const fromFormat = parse(value, currentFormat, new Date(), {locale: this.locale});
+
+ if (this.isValid(fromFormat)) {
+ return fromFormat;
+ }
+ }
+
+ return this.invalid();
+ } else if (typeof value === 'number') {
+ return new Date(value);
+ } else if (value instanceof Date) {
+ return this.clone(value);
+ }
+
+ return null;
+ }
}
diff --git a/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts b/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts
index 5eed39517609..962ea66ee11f 100644
--- a/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts
+++ b/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts
@@ -633,6 +633,15 @@ describe('LuxonDateAdapter', () => {
expect(adapter.getSeconds(result)).toBe(0);
});
+ it('should parse a time string containing only hours', () => {
+ const result = adapter.parseTime('11', 'HH')!;
+ expect(result).toBeTruthy();
+ expect(adapter.isValid(result)).toBe(true);
+ expect(adapter.getHours(result)).toBe(11);
+ expect(adapter.getMinutes(result)).toBe(0);
+ expect(adapter.getSeconds(result)).toBe(0);
+ });
+
it('should parse a time string with characters around the time', () => {
adapter.setLocale('bg-BG');
const result = adapter.parseTime('14:52 ч.', 't')!;
diff --git a/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts b/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts
index dffc542d3c26..c53365209bb6 100644
--- a/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts
+++ b/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts
@@ -614,6 +614,15 @@ describe('MomentDateAdapter', () => {
expect(adapter.getSeconds(result)).toBe(0);
});
+ it('should parse a time string containing only hours', () => {
+ const result = adapter.parseTime('11', 'HH')!;
+ expect(result).toBeTruthy();
+ expect(adapter.isValid(result)).toBe(true);
+ expect(adapter.getHours(result)).toBe(11);
+ expect(adapter.getMinutes(result)).toBe(0);
+ expect(adapter.getSeconds(result)).toBe(0);
+ });
+
it('should parse a time string with characters around the time', () => {
adapter.setLocale('bg-BG');
const result = adapter.parseTime('14:52 ч.', 'LT')!;
diff --git a/src/material/datepicker/calendar-body.ts b/src/material/datepicker/calendar-body.ts
index 34af64d8faba..18a163974166 100644
--- a/src/material/datepicker/calendar-body.ts
+++ b/src/material/datepicker/calendar-body.ts
@@ -615,7 +615,9 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView
const col = cell.getAttribute('data-mat-col');
if (row && col) {
- return this.rows[parseInt(row)][parseInt(col)];
+ // We need the optional read here, because this can
+ // fire too late when the user is navigating quickly.
+ return this.rows[parseInt(row)]?.[parseInt(col)] || null;
}
}
diff --git a/src/material/datepicker/calendar.ts b/src/material/datepicker/calendar.ts
index 9f54fb28a703..436070604eae 100644
--- a/src/material/datepicker/calendar.ts
+++ b/src/material/datepicker/calendar.ts
@@ -327,7 +327,7 @@ export class MatCalendar implements AfterContentInit, AfterViewChecked, OnDes
private _maxDate: D | null;
/** Function used to filter which dates are selectable. */
- @Input() dateFilter: (date: D) => boolean;
+ @Input() dateFilter?: ((date: D) => boolean) | null;
/** Function that can be used to add custom CSS classes to dates. */
@Input() dateClass: MatCalendarCellClassFunction;
diff --git a/src/material/datepicker/date-range-input.ts b/src/material/datepicker/date-range-input.ts
index 0d507fd3c24a..86992cb617c4 100644
--- a/src/material/datepicker/date-range-input.ts
+++ b/src/material/datepicker/date-range-input.ts
@@ -154,7 +154,7 @@ export class MatDateRangeInput
get dateFilter() {
return this._dateFilter;
}
- set dateFilter(value: DateFilterFn) {
+ set dateFilter(value: DateFilterFn | null | undefined) {
const start = this._startInput;
const end = this._endInput;
const wasMatchingStart = start && start._matchesFilter(start.value);
@@ -169,7 +169,7 @@ export class MatDateRangeInput
end._validatorOnChange();
}
}
- private _dateFilter: DateFilterFn;
+ private _dateFilter?: DateFilterFn | null;
/** The minimum valid date. */
@Input()
diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts
index 35075750eaa9..7d3f8c1825f7 100644
--- a/src/material/datepicker/datepicker-base.ts
+++ b/src/material/datepicker/datepicker-base.ts
@@ -346,7 +346,7 @@ export interface MatDatepickerControl {
min: D | null;
max: D | null;
disabled: boolean;
- dateFilter: DateFilterFn;
+ dateFilter: DateFilterFn | null | undefined;
getConnectedOverlayOrigin(): ElementRef;
getOverlayLabelId(): string | null;
stateChanges: Observable;
@@ -545,7 +545,7 @@ export abstract class MatDatepickerBase<
return this.datepickerInput && this.datepickerInput.max;
}
- _getDateFilter(): DateFilterFn {
+ _getDateFilter(): DateFilterFn | null | undefined {
return this.datepickerInput && this.datepickerInput.dateFilter;
}
diff --git a/src/material/datepicker/datepicker-input-base.ts b/src/material/datepicker/datepicker-input-base.ts
index 632cec9d45a4..01de0feb77b5 100644
--- a/src/material/datepicker/datepicker-input-base.ts
+++ b/src/material/datepicker/datepicker-input-base.ts
@@ -203,7 +203,7 @@ export abstract class MatDatepickerInputBase | undefined;
+ protected abstract _getDateFilter(): DateFilterFn | null | undefined;
/** Registers a date selection model with the input. */
_registerModel(model: MatDateSelectionModel): void {
diff --git a/src/material/datepicker/datepicker-input.ts b/src/material/datepicker/datepicker-input.ts
index d555a1b5812e..dea01bd24252 100644
--- a/src/material/datepicker/datepicker-input.ts
+++ b/src/material/datepicker/datepicker-input.ts
@@ -119,7 +119,7 @@ export class MatDatepickerInput
get dateFilter() {
return this._dateFilter;
}
- set dateFilter(value: DateFilterFn) {
+ set dateFilter(value: DateFilterFn | null | undefined) {
const wasMatchingValue = this._matchesFilter(this.value);
this._dateFilter = value;
@@ -127,7 +127,7 @@ export class MatDatepickerInput
this._validatorOnChange();
}
}
- private _dateFilter: DateFilterFn;
+ private _dateFilter: DateFilterFn | null | undefined;
/** The combined form control validator for this input. */
protected _validator: ValidatorFn | null;
diff --git a/src/material/datepicker/month-view.html b/src/material/datepicker/month-view.html
index 538d85f91435..fa0c2750d94d 100644
--- a/src/material/datepicker/month-view.html
+++ b/src/material/datepicker/month-view.html
@@ -1,7 +1,7 @@
implements AfterContentInit, OnChanges, OnDestroy {
private _maxDate: D | null;
/** Function used to filter which dates are selectable. */
- @Input() dateFilter: (date: D) => boolean;
+ @Input() dateFilter: ((date: D) => boolean) | null | undefined;
/** Function that can be used to add custom CSS classes to dates. */
@Input() dateClass: MatCalendarCellClassFunction;
@@ -183,40 +184,40 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
@ViewChild(MatCalendarBody) _matCalendarBody: MatCalendarBody;
/** The label for this month (e.g. "January 2017"). */
- _monthLabel: string;
+ _monthLabel = signal('');
/** Grid of calendar cells representing the dates of the month. */
- _weeks: MatCalendarCell[][];
+ _weeks = signal([]);
/** The number of blank cells in the first row before the 1st of the month. */
- _firstWeekOffset: number;
+ _firstWeekOffset = signal(0);
/** Start value of the currently-shown date range. */
- _rangeStart: number | null;
+ _rangeStart = signal(null);
/** End value of the currently-shown date range. */
- _rangeEnd: number | null;
+ _rangeEnd = signal(null);
/** Start value of the currently-shown comparison date range. */
- _comparisonRangeStart: number | null;
+ _comparisonRangeStart = signal(null);
/** End value of the currently-shown comparison date range. */
- _comparisonRangeEnd: number | null;
+ _comparisonRangeEnd = signal(null);
/** Start of the preview range. */
- _previewStart: number | null;
+ _previewStart = signal(null);
/** End of the preview range. */
- _previewEnd: number | null;
+ _previewEnd = signal(null);
/** Whether the user is currently selecting a range of dates. */
- _isRange: boolean;
+ _isRange = signal(false);
/** The date of the month that today falls on. Null if today is in another month. */
- _todayDate: number | null;
+ _todayDate = signal(null);
/** The names of the weekdays. */
- _weekdays: {long: string; narrow: string; id: number}[];
+ _weekdays = signal<{long: string; narrow: string; id: number}[]>([]);
constructor(...args: unknown[]);
@@ -359,7 +360,7 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
return;
case ESCAPE:
// Abort the current range selection if the user presses escape mid-selection.
- if (this._previewEnd != null && !hasModifierKey(event)) {
+ if (this._previewEnd() != null && !hasModifierKey(event)) {
this._clearPreview();
// If a drag is in progress, cancel the drag without changing the
// current selection.
@@ -402,23 +403,26 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
/** Initializes this month view. */
_init() {
this._setRanges(this.selected);
- this._todayDate = this._getCellCompareValue(this._dateAdapter.today());
- this._monthLabel = this._dateFormats.display.monthLabel
- ? this._dateAdapter.format(this.activeDate, this._dateFormats.display.monthLabel)
- : this._dateAdapter
- .getMonthNames('short')
- [this._dateAdapter.getMonth(this.activeDate)].toLocaleUpperCase();
+ this._todayDate.set(this._getCellCompareValue(this._dateAdapter.today()));
+ this._monthLabel.set(
+ this._dateFormats.display.monthLabel
+ ? this._dateAdapter.format(this.activeDate, this._dateFormats.display.monthLabel)
+ : this._dateAdapter
+ .getMonthNames('short')
+ [this._dateAdapter.getMonth(this.activeDate)].toLocaleUpperCase(),
+ );
let firstOfMonth = this._dateAdapter.createDate(
this._dateAdapter.getYear(this.activeDate),
this._dateAdapter.getMonth(this.activeDate),
1,
);
- this._firstWeekOffset =
+ this._firstWeekOffset.set(
(DAYS_PER_WEEK +
this._dateAdapter.getDayOfWeek(firstOfMonth) -
this._dateAdapter.getFirstDayOfWeek()) %
- DAYS_PER_WEEK;
+ DAYS_PER_WEEK,
+ );
this._initWeekdays();
this._createWeekCells();
@@ -446,8 +450,8 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
this.selected as DateRange,
event,
);
- this._previewStart = this._getCellCompareValue(previewRange.start);
- this._previewEnd = this._getCellCompareValue(previewRange.end);
+ this._previewStart.set(this._getCellCompareValue(previewRange.start));
+ this._previewEnd.set(this._getCellCompareValue(previewRange.end));
if (this.activeDrag && value) {
const dragRange = this._rangeStrategy.createDrag?.(
@@ -458,16 +462,10 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
);
if (dragRange) {
- this._previewStart = this._getCellCompareValue(dragRange.start);
- this._previewEnd = this._getCellCompareValue(dragRange.end);
+ this._previewStart.set(this._getCellCompareValue(dragRange.start));
+ this._previewEnd.set(this._getCellCompareValue(dragRange.end));
}
}
-
- // Note that here we need to use `detectChanges`, rather than `markForCheck`, because
- // the way `_focusActiveCell` is set up at the moment makes it fire at the wrong time
- // when navigating one month back using the keyboard which will cause this handler
- // to throw a "changed after checked" error when updating the preview state.
- this._changeDetectorRef.detectChanges();
}
}
@@ -512,20 +510,20 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
const longWeekdays = this._dateAdapter.getDayOfWeekNames('long');
// Rotate the labels for days of the week based on the configured first day of the week.
- let weekdays = longWeekdays.map((long, i) => {
+ const weekdays = longWeekdays.map((long, i) => {
return {long, narrow: narrowWeekdays[i], id: uniqueIdCounter++};
});
- this._weekdays = weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek));
+ this._weekdays.set(weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek)));
}
/** Creates MatCalendarCells for the dates in this month. */
private _createWeekCells() {
const daysInMonth = this._dateAdapter.getNumDaysInMonth(this.activeDate);
const dateNames = this._dateAdapter.getDateNames();
- this._weeks = [[]];
- for (let i = 0, cell = this._firstWeekOffset; i < daysInMonth; i++, cell++) {
+ const weeks: MatCalendarCell[][] = [[]];
+ for (let i = 0, cell = this._firstWeekOffset(); i < daysInMonth; i++, cell++) {
if (cell == DAYS_PER_WEEK) {
- this._weeks.push([]);
+ weeks.push([]);
cell = 0;
}
const date = this._dateAdapter.createDate(
@@ -537,7 +535,7 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
const ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel);
const cellClasses = this.dateClass ? this.dateClass(date, 'month') : undefined;
- this._weeks[this._weeks.length - 1].push(
+ weeks[weeks.length - 1].push(
new MatCalendarCell(
i + 1,
dateNames[i],
@@ -549,6 +547,7 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
),
);
}
+ this._weeks.set(weeks);
}
/** Date filter for the month */
@@ -603,16 +602,17 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
/** Sets the current range based on a model value. */
private _setRanges(selectedValue: DateRange | D | null) {
if (selectedValue instanceof DateRange) {
- this._rangeStart = this._getCellCompareValue(selectedValue.start);
- this._rangeEnd = this._getCellCompareValue(selectedValue.end);
- this._isRange = true;
+ this._rangeStart.set(this._getCellCompareValue(selectedValue.start));
+ this._rangeEnd.set(this._getCellCompareValue(selectedValue.end));
+ this._isRange.set(true);
} else {
- this._rangeStart = this._rangeEnd = this._getCellCompareValue(selectedValue);
- this._isRange = false;
+ this._rangeStart.set(this._getCellCompareValue(selectedValue));
+ this._rangeEnd.set(this._rangeStart());
+ this._isRange.set(false);
}
- this._comparisonRangeStart = this._getCellCompareValue(this.comparisonStart);
- this._comparisonRangeEnd = this._getCellCompareValue(this.comparisonEnd);
+ this._comparisonRangeStart.set(this._getCellCompareValue(this.comparisonStart));
+ this._comparisonRangeEnd.set(this._getCellCompareValue(this.comparisonEnd));
}
/** Gets whether a date can be selected in the month view. */
@@ -622,6 +622,7 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
/** Clears out preview state. */
private _clearPreview() {
- this._previewStart = this._previewEnd = null;
+ this._previewStart.set(null);
+ this._previewEnd.set(null);
}
}
diff --git a/src/material/datepicker/multi-year-view.html b/src/material/datepicker/multi-year-view.html
index 84220c8fa95f..91a72ab51eb0 100644
--- a/src/material/datepicker/multi-year-view.html
+++ b/src/material/datepicker/multi-year-view.html
@@ -3,10 +3,10 @@
implements AfterContentInit, OnDestroy {
private _maxDate: D | null;
/** A function used to filter which dates are selectable. */
- @Input() dateFilter: (date: D) => boolean;
+ @Input() dateFilter: ((date: D) => boolean) | null | undefined;
/** Function that can be used to add custom CSS classes to date cells. */
@Input() dateClass: MatCalendarCellClassFunction;
@@ -150,13 +151,13 @@ export class MatMultiYearView implements AfterContentInit, OnDestroy {
@ViewChild(MatCalendarBody) _matCalendarBody: MatCalendarBody;
/** Grid of calendar cells representing the currently displayed years. */
- _years: MatCalendarCell[][];
+ _years = signal([]);
/** The year that today falls on. */
- _todayYear: number;
+ _todayYear = signal(0);
/** The year of the selected date. Null if the selected date is null. */
- _selectedYear: number | null;
+ _selectedYear = signal(null);
constructor(...args: unknown[]);
@@ -180,7 +181,7 @@ export class MatMultiYearView implements AfterContentInit, OnDestroy {
/** Initializes this multi-year view. */
_init() {
- this._todayYear = this._dateAdapter.getYear(this._dateAdapter.today());
+ this._todayYear.set(this._dateAdapter.getYear(this._dateAdapter.today()));
// We want a range years such that we maximize the number of
// enabled dates visible at once. This prevents issues where the minimum year
@@ -192,14 +193,15 @@ export class MatMultiYearView implements AfterContentInit, OnDestroy {
const minYearOfPage =
activeYear - getActiveOffset(this._dateAdapter, this.activeDate, this.minDate, this.maxDate);
- this._years = [];
+ const years: MatCalendarCell[][] = [];
for (let i = 0, row: number[] = []; i < yearsPerPage; i++) {
row.push(minYearOfPage + i);
if (row.length == yearsPerRow) {
- this._years.push(row.map(year => this._createCellForYear(year)));
+ years.push(row.map(year => this._createCellForYear(year)));
row = [];
}
}
+ this._years.set(years);
this._changeDetectorRef.markForCheck();
}
@@ -389,16 +391,16 @@ export class MatMultiYearView implements AfterContentInit, OnDestroy {
/** Sets the currently-highlighted year based on a model value. */
private _setSelectedYear(value: DateRange | D | null) {
- this._selectedYear = null;
+ this._selectedYear.set(null);
if (value instanceof DateRange) {
const displayValue = value.start || value.end;
if (displayValue) {
- this._selectedYear = this._dateAdapter.getYear(displayValue);
+ this._selectedYear.set(this._dateAdapter.getYear(displayValue));
}
} else if (value) {
- this._selectedYear = this._dateAdapter.getYear(value);
+ this._selectedYear.set(this._dateAdapter.getYear(value));
}
}
}
diff --git a/src/material/datepicker/year-view.html b/src/material/datepicker/year-view.html
index afb9821dd416..343cd814dfd6 100644
--- a/src/material/datepicker/year-view.html
+++ b/src/material/datepicker/year-view.html
@@ -3,11 +3,11 @@
implements AfterContentInit, OnDestroy {
private _maxDate: D | null;
/** A function used to filter which dates are selectable. */
- @Input() dateFilter: (date: D) => boolean;
+ @Input() dateFilter: ((date: D) => boolean) | null | undefined;
/** Function that can be used to add custom CSS classes to date cells. */
@Input() dateClass: MatCalendarCellClassFunction;
@@ -139,19 +140,19 @@ export class MatYearView implements AfterContentInit, OnDestroy {
@ViewChild(MatCalendarBody) _matCalendarBody: MatCalendarBody;
/** Grid of calendar cells representing the months of the year. */
- _months: MatCalendarCell[][];
+ _months = signal([]);
/** The label for this year (e.g. "2017"). */
- _yearLabel: string;
+ _yearLabel = signal('');
/** The month in this year that today falls on. Null if today is in a different year. */
- _todayMonth: number | null;
+ _todayMonth = signal(null);
/**
* The month in this year that the selected Date falls on.
* Null if the selected Date is in a different year.
*/
- _selectedMonth: number | null;
+ _selectedMonth = signal(null);
constructor(...args: unknown[]);
@@ -296,16 +297,18 @@ export class MatYearView implements AfterContentInit, OnDestroy {
/** Initializes this year view. */
_init() {
this._setSelectedMonth(this.selected);
- this._todayMonth = this._getMonthInCurrentYear(this._dateAdapter.today());
- this._yearLabel = this._dateAdapter.getYearName(this.activeDate);
+ this._todayMonth.set(this._getMonthInCurrentYear(this._dateAdapter.today()));
+ this._yearLabel.set(this._dateAdapter.getYearName(this.activeDate));
let monthNames = this._dateAdapter.getMonthNames('short');
// First row of months only contains 5 elements so we can fit the year label on the same row.
- this._months = [
- [0, 1, 2, 3],
- [4, 5, 6, 7],
- [8, 9, 10, 11],
- ].map(row => row.map(month => this._createCellForMonth(month, monthNames[month])));
+ this._months.set(
+ [
+ [0, 1, 2, 3],
+ [4, 5, 6, 7],
+ [8, 9, 10, 11],
+ ].map(row => row.map(month => this._createCellForMonth(month, monthNames[month]))),
+ );
this._changeDetectorRef.markForCheck();
}
@@ -435,10 +438,11 @@ export class MatYearView implements AfterContentInit, OnDestroy {
/** Sets the currently-selected month based on a model value. */
private _setSelectedMonth(value: DateRange | D | null) {
if (value instanceof DateRange) {
- this._selectedMonth =
- this._getMonthInCurrentYear(value.start) || this._getMonthInCurrentYear(value.end);
+ this._selectedMonth.set(
+ this._getMonthInCurrentYear(value.start) || this._getMonthInCurrentYear(value.end),
+ );
} else {
- this._selectedMonth = this._getMonthInCurrentYear(value);
+ this._selectedMonth.set(this._getMonthInCurrentYear(value));
}
}
}
diff --git a/src/material/radio/radio.scss b/src/material/radio/radio.scss
index d92b39e23ba6..2b294bb1cd81 100644
--- a/src/material/radio/radio.scss
+++ b/src/material/radio/radio.scss
@@ -11,6 +11,12 @@ $fallbacks: m3-radio.get-tokens();
@include radio-common.radio-structure(true);
@include radio-common.radio-noop-animations();
+ // Clicking the label toggles the radio, but MDC does not include any styles that inform the
+ // user of this. Therefore we add the pointer cursor on top of MDC's styles.
+ label {
+ cursor: pointer;
+ }
+
.mdc-radio__background::before {
background-color: token-utils.slot(radio-ripple-color, $fallbacks);
}
diff --git a/src/material/timepicker/timepicker-input.ts b/src/material/timepicker/timepicker-input.ts
index 93e695139fde..8e9d0106f55d 100644
--- a/src/material/timepicker/timepicker-input.ts
+++ b/src/material/timepicker/timepicker-input.ts
@@ -317,6 +317,14 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O
}
}
+ /** Called by the timepicker to sync up the user-selected value. */
+ _timepickerValueAssigned(value: D | null) {
+ if (!this._dateAdapter.sameTime(value, this.value())) {
+ this._assignUserSelection(value, true);
+ this._formatValue(value);
+ }
+ }
+
/** Sets up the code that watches for changes in the value and adjusts the input. */
private _respondToValueChanges(): void {
effect(() => {
@@ -346,12 +354,6 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O
const timepicker = this.timepicker();
timepicker.registerInput(this);
timepicker.closed.subscribe(() => this._onTouched?.());
- timepicker.selected.subscribe(({value}) => {
- if (!this._dateAdapter.sameTime(value, this.value())) {
- this._assignUserSelection(value, true);
- this._formatValue(value);
- }
- });
});
}
@@ -371,8 +373,10 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O
* @param propagateToAccessor Whether the value should be propagated to the ControlValueAccessor.
*/
private _assignUserSelection(selection: D | null, propagateToAccessor: boolean) {
+ let toAssign: D | null;
+
if (selection == null || !this._isValid(selection)) {
- this.value.set(selection);
+ toAssign = selection;
} else {
// If a datepicker and timepicker are writing to the same object and the user enters an
// invalid time into the timepicker, we may end up clearing their selection from the
@@ -384,12 +388,15 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O
const hours = adapter.getHours(selection);
const minutes = adapter.getMinutes(selection);
const seconds = adapter.getSeconds(selection);
- this.value.set(target ? adapter.setTime(target, hours, minutes, seconds) : selection);
+ toAssign = target ? adapter.setTime(target, hours, minutes, seconds) : selection;
}
+ // Propagate to the form control before emitting to `valueChange`.
if (propagateToAccessor) {
- this._onChange?.(this.value());
+ this._onChange?.(toAssign);
}
+
+ this.value.set(toAssign);
}
/** Formats the current value and assigns it to the input. */
diff --git a/src/material/timepicker/timepicker.spec.ts b/src/material/timepicker/timepicker.spec.ts
index 3fa080efb157..0953b748e50c 100644
--- a/src/material/timepicker/timepicker.spec.ts
+++ b/src/material/timepicker/timepicker.spec.ts
@@ -1156,6 +1156,75 @@ describe('MatTimepicker', () => {
expect(input.disabled).toBe(true);
expect(fixture.componentInstance.input.disabled()).toBe(true);
});
+
+ it('should emit to valueChange before assigning control value when typing', () => {
+ const fixture = TestBed.createComponent(TimepickerWithForms);
+ const control = fixture.componentInstance.control;
+ let eventValue: Date | null = null;
+ let controlValue: Date | null = null;
+ fixture.detectChanges();
+
+ const subscription = fixture.componentInstance.input.value.subscribe(value => {
+ eventValue = value;
+ controlValue = control.value;
+ });
+
+ typeInElement(getInput(fixture), '1:37 PM');
+ fixture.detectChanges();
+
+ expect(eventValue).toBeTruthy();
+ expect(controlValue).toBeTruthy();
+ expectSameTime(eventValue, controlValue);
+ subscription.unsubscribe();
+ });
+
+ it('should emit to valueChange before assigning control value when clicking an option', () => {
+ const fixture = TestBed.createComponent(TimepickerWithForms);
+ const control = fixture.componentInstance.control;
+ let eventValue: Date | null = null;
+ let controlValue: Date | null = null;
+ fixture.detectChanges();
+
+ const subscription = fixture.componentInstance.input.value.subscribe(value => {
+ eventValue = value;
+ controlValue = control.value;
+ });
+
+ getInput(fixture).click();
+ fixture.detectChanges();
+ getOptions()[5].click();
+ fixture.detectChanges();
+ fixture.detectChanges();
+
+ expect(eventValue).toBeTruthy();
+ expect(controlValue).toBeTruthy();
+ expectSameTime(eventValue, controlValue);
+ subscription.unsubscribe();
+ });
+
+ it('should emit to selected event before assigning control value when clicking an option', () => {
+ const fixture = TestBed.createComponent(TimepickerWithForms);
+ const control = fixture.componentInstance.control;
+ let eventValue: Date | null = null;
+ let controlValue: Date | null = null;
+ fixture.detectChanges();
+
+ const subscription = fixture.componentInstance.timepicker.selected.subscribe(event => {
+ eventValue = event.value;
+ controlValue = control.value;
+ });
+
+ getInput(fixture).click();
+ fixture.detectChanges();
+ getOptions()[5].click();
+ fixture.detectChanges();
+ fixture.detectChanges();
+
+ expect(eventValue).toBeTruthy();
+ expect(controlValue).toBeTruthy();
+ expectSameTime(eventValue, controlValue);
+ subscription.unsubscribe();
+ });
});
describe('timepicker toggle', () => {
@@ -1410,6 +1479,7 @@ class TimepickerTwoWayBinding {
})
class TimepickerWithForms {
@ViewChild(MatTimepickerInput) input: MatTimepickerInput;
+ @ViewChild(MatTimepicker) timepicker: MatTimepicker;
readonly control = new FormControl(null, [Validators.required]);
readonly min = signal(null);
readonly max = signal(null);
diff --git a/src/material/timepicker/timepicker.ts b/src/material/timepicker/timepicker.ts
index ab63291c9699..e5feb08a4854 100644
--- a/src/material/timepicker/timepicker.ts
+++ b/src/material/timepicker/timepicker.ts
@@ -296,6 +296,8 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent {
current.deselect(false);
}
});
+ // Notify the input first so it can sync up the form control before emitting to `selected`.
+ this._input()?._timepickerValueAssigned(option.value);
this.selected.emit({value: option.value, source: this});
this._input()?.focus();
}