@angular/material
Version:
Angular Material
892 lines (885 loc) • 48.7 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, ViewContainerRef, Injector, ANIMATION_MODULE_TYPE, signal, viewChild, viewChildren, input, output, booleanAttribute, computed, effect, ElementRef, afterNextRender, untracked, Component, ChangeDetectionStrategy, ViewEncapsulation, model, Renderer2, Directive, HostAttributeToken, NgModule } from '@angular/core';
import { Directionality } from '@angular/cdk/bidi';
import { Overlay } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { _getEventTarget, _getFocusedElementPierceShadowDom } from '@angular/cdk/platform';
import { TAB, ESCAPE, hasModifierKey, ENTER, DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
import { ActiveDescendantKeyManager, _IdGenerator } from '@angular/cdk/a11y';
import { D as DateAdapter, a as MAT_DATE_FORMATS } from './date-formats-9cbc3057.mjs';
import { a as MatOption, M as MAT_OPTION_PARENT_COMPONENT } from './option-acd4abcc.mjs';
import { Validators, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
import { M as MAT_FORM_FIELD } from './form-field-7b8fd54e.mjs';
import { M as MAT_INPUT_VALUE_ACCESSOR } from './input-value-accessor-16c2d528.mjs';
import { a as MatIconButton } from './icon-button-fac35010.mjs';
import { CdkScrollableModule } from '@angular/cdk/scrolling';
import 'rxjs';
import './ripple-acd53c76.mjs';
import '@angular/cdk/coercion';
import '@angular/cdk/private';
import './pseudo-checkbox-b981dcda.mjs';
import './structural-styles-7c66c8fc.mjs';
import '@angular/common';
import 'rxjs/operators';
import '@angular/cdk/observers/private';
import './ripple-loader-77b972ac.mjs';
/** Pattern that interval strings have to match. */
const INTERVAL_PATTERN = /^(\d*\.?\d+)\s*(h|hour|hours|m|min|minute|minutes|s|second|seconds)?$/i;
/**
* Injection token that can be used to configure the default options for the timepicker component.
*/
const MAT_TIMEPICKER_CONFIG = new InjectionToken('MAT_TIMEPICKER_CONFIG');
/** Parses an interval value into seconds. */
function parseInterval(value) {
let result;
if (value === null) {
return null;
}
else if (typeof value === 'number') {
result = value;
}
else {
if (value.trim().length === 0) {
return null;
}
const parsed = value.match(INTERVAL_PATTERN);
const amount = parsed ? parseFloat(parsed[1]) : null;
const unit = parsed?.[2]?.toLowerCase() || null;
if (!parsed || amount === null || isNaN(amount)) {
return null;
}
if (unit === 'h' || unit === 'hour' || unit === 'hours') {
result = amount * 3600;
}
else if (unit === 'm' || unit === 'min' || unit === 'minute' || unit === 'minutes') {
result = amount * 60;
}
else {
result = amount;
}
}
return result;
}
/**
* Generates the options to show in a timepicker.
* @param adapter Date adapter to be used to generate the options.
* @param formats Formatting config to use when displaying the options.
* @param min Time from which to start generating the options.
* @param max Time at which to stop generating the options.
* @param interval Amount of seconds between each option.
*/
function generateOptions(adapter, formats, min, max, interval) {
const options = [];
let current = adapter.compareTime(min, max) < 1 ? min : max;
while (adapter.sameDate(current, min) &&
adapter.compareTime(current, max) < 1 &&
adapter.isValid(current)) {
options.push({ value: current, label: adapter.format(current, formats.display.timeOptionLabel) });
current = adapter.addSeconds(current, interval);
}
return options;
}
/** Checks whether a date adapter is set up correctly for use with the timepicker. */
function validateAdapter(adapter, formats) {
function missingAdapterError(provider) {
return Error(`MatTimepicker: No provider found for ${provider}. You must add one of the following ` +
`to your app config: provideNativeDateAdapter, provideDateFnsAdapter, ` +
`provideLuxonDateAdapter, provideMomentDateAdapter, or provide a custom implementation.`);
}
if (!adapter) {
throw missingAdapterError('DateAdapter');
}
if (!formats) {
throw missingAdapterError('MAT_DATE_FORMATS');
}
if (formats.display.timeInput === undefined ||
formats.display.timeOptionLabel === undefined ||
formats.parse.timeInput === undefined) {
throw new Error('MatTimepicker: Incomplete `MAT_DATE_FORMATS` has been provided. ' +
'`MAT_DATE_FORMATS` must provide `display.timeInput`, `display.timeOptionLabel` ' +
'and `parse.timeInput` formats in order to be compatible with MatTimepicker.');
}
}
/** Injection token used to configure the behavior of the timepicker dropdown while scrolling. */
const MAT_TIMEPICKER_SCROLL_STRATEGY = new InjectionToken('MAT_TIMEPICKER_SCROLL_STRATEGY', {
providedIn: 'root',
factory: () => {
const overlay = inject(Overlay);
return () => overlay.scrollStrategies.reposition();
},
});
/**
* Renders out a listbox that can be used to select a time of day.
* Intended to be used together with `MatTimepickerInput`.
*/
class MatTimepicker {
_overlay = inject(Overlay);
_dir = inject(Directionality, { optional: true });
_viewContainerRef = inject(ViewContainerRef);
_injector = inject(Injector);
_defaultConfig = inject(MAT_TIMEPICKER_CONFIG, { optional: true });
_dateAdapter = inject(DateAdapter, { optional: true });
_dateFormats = inject(MAT_DATE_FORMATS, { optional: true });
_scrollStrategyFactory = inject(MAT_TIMEPICKER_SCROLL_STRATEGY);
_animationsDisabled = inject(ANIMATION_MODULE_TYPE, { optional: true }) === 'NoopAnimations';
_isOpen = signal(false);
_activeDescendant = signal(null);
_input = signal(null);
_overlayRef = null;
_portal = null;
_optionsCacheKey = null;
_localeChanges;
_onOpenRender = null;
_panelTemplate = viewChild.required('panelTemplate');
_timeOptions = [];
_options = viewChildren(MatOption);
_keyManager = new ActiveDescendantKeyManager(this._options, this._injector)
.withHomeAndEnd(true)
.withPageUpDown(true)
.withVerticalOrientation(true);
/**
* Interval between each option in the timepicker. The value can either be an amount of
* seconds (e.g. 90) or a number with a unit (e.g. 45m). Supported units are `s` for seconds,
* `m` for minutes or `h` for hours.
*/
interval = input(parseInterval(this._defaultConfig?.interval || null), { transform: parseInterval });
/**
* Array of pre-defined options that the user can select from, as an alternative to using the
* `interval` input. An error will be thrown if both `options` and `interval` are specified.
*/
options = input(null);
/** Whether the timepicker is open. */
isOpen = this._isOpen.asReadonly();
/** Emits when the user selects a time. */
selected = output();
/** Emits when the timepicker is opened. */
opened = output();
/** Emits when the timepicker is closed. */
closed = output();
/** ID of the active descendant option. */
activeDescendant = this._activeDescendant.asReadonly();
/** Unique ID of the timepicker's panel */
panelId = inject(_IdGenerator).getId('mat-timepicker-panel-');
/** Whether ripples within the timepicker should be disabled. */
disableRipple = input(this._defaultConfig?.disableRipple ?? false, {
transform: booleanAttribute,
});
/** ARIA label for the timepicker panel. */
ariaLabel = input(null, {
alias: 'aria-label',
});
/** ID of the label element for the timepicker panel. */
ariaLabelledby = input(null, {
alias: 'aria-labelledby',
});
/** Whether the timepicker is currently disabled. */
disabled = computed(() => !!this._input()?.disabled());
constructor() {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
validateAdapter(this._dateAdapter, this._dateFormats);
effect(() => {
const options = this.options();
const interval = this.interval();
if (options !== null && interval !== null) {
throw new Error('Cannot specify both the `options` and `interval` inputs at the same time');
}
else if (options?.length === 0) {
throw new Error('Value of `options` input cannot be an empty array');
}
});
}
// Since the panel ID is static, we can set it once without having to maintain a host binding.
const element = inject(ElementRef);
element.nativeElement.setAttribute('mat-timepicker-panel-id', this.panelId);
this._handleLocaleChanges();
this._handleInputStateChanges();
this._keyManager.change.subscribe(() => this._activeDescendant.set(this._keyManager.activeItem?.id || null));
}
/** Opens the timepicker. */
open() {
const input = this._input();
if (!input) {
return;
}
// Focus should already be on the input, but this call is in case the timepicker is opened
// programmatically. We need to call this even if the timepicker is already open, because
// the user might be clicking the toggle.
input.focus();
if (this._isOpen()) {
return;
}
this._isOpen.set(true);
this._generateOptions();
const overlayRef = this._getOverlayRef();
overlayRef.updateSize({ width: input.getOverlayOrigin().nativeElement.offsetWidth });
this._portal ??= new TemplatePortal(this._panelTemplate(), this._viewContainerRef);
// We need to check this in case `isOpen` was flipped, but change detection hasn't
// had a chance to run yet. See https://github.com/angular/components/issues/30637
if (!overlayRef.hasAttached()) {
overlayRef.attach(this._portal);
}
this._onOpenRender?.destroy();
this._onOpenRender = afterNextRender(() => {
const options = this._options();
this._syncSelectedState(input.value(), options, options[0]);
this._onOpenRender = null;
}, { injector: this._injector });
this.opened.emit();
}
/** Closes the timepicker. */
close() {
if (this._isOpen()) {
this._isOpen.set(false);
this.closed.emit();
if (this._animationsDisabled) {
this._overlayRef?.detach();
}
}
}
/** Registers an input with the timepicker. */
registerInput(input) {
const currentInput = this._input();
if (currentInput && input !== currentInput && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw new Error('MatTimepicker can only be registered with one input at a time');
}
this._input.set(input);
}
ngOnDestroy() {
this._keyManager.destroy();
this._localeChanges.unsubscribe();
this._onOpenRender?.destroy();
this._overlayRef?.dispose();
}
/** Selects a specific time value. */
_selectValue(option) {
this.close();
this._keyManager.setActiveItem(option);
this._options().forEach(current => {
// This is primarily here so we don't show two selected options while animating away.
if (current !== option) {
current.deselect(false);
}
});
this.selected.emit({ value: option.value, source: this });
this._input()?.focus();
}
/** Gets the value of the `aria-labelledby` attribute. */
_getAriaLabelledby() {
if (this.ariaLabel()) {
return null;
}
return this.ariaLabelledby() || this._input()?._getLabelId() || null;
}
/** Handles animation events coming from the panel. */
_handleAnimationEnd(event) {
if (event.animationName === '_mat-timepicker-exit') {
this._overlayRef?.detach();
}
}
/** Creates an overlay reference for the timepicker panel. */
_getOverlayRef() {
if (this._overlayRef) {
return this._overlayRef;
}
const positionStrategy = this._overlay
.position()
.flexibleConnectedTo(this._input().getOverlayOrigin())
.withFlexibleDimensions(false)
.withPush(false)
.withTransformOriginOn('.mat-timepicker-panel')
.withPositions([
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top',
},
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom',
panelClass: 'mat-timepicker-above',
},
]);
this._overlayRef = this._overlay.create({
positionStrategy,
scrollStrategy: this._scrollStrategyFactory(),
direction: this._dir || 'ltr',
hasBackdrop: false,
});
this._overlayRef.detachments().subscribe(() => this.close());
this._overlayRef.keydownEvents().subscribe(event => this._handleKeydown(event));
this._overlayRef.outsidePointerEvents().subscribe(event => {
const target = _getEventTarget(event);
const origin = this._input()?.getOverlayOrigin().nativeElement;
if (target && origin && target !== origin && !origin.contains(target)) {
this.close();
}
});
return this._overlayRef;
}
/** Generates the list of options from which the user can select.. */
_generateOptions() {
// Default the interval to 30 minutes.
const interval = this.interval() ?? 30 * 60;
const options = this.options();
if (options !== null) {
this._timeOptions = options;
}
else {
const input = this._input();
const adapter = this._dateAdapter;
const timeFormat = this._dateFormats.display.timeInput;
const min = input?.min() || adapter.setTime(adapter.today(), 0, 0, 0);
const max = input?.max() || adapter.setTime(adapter.today(), 23, 59, 0);
const cacheKey = interval + '/' + adapter.format(min, timeFormat) + '/' + adapter.format(max, timeFormat);
// Don't re-generate the options if the inputs haven't changed.
if (cacheKey !== this._optionsCacheKey) {
this._optionsCacheKey = cacheKey;
this._timeOptions = generateOptions(adapter, this._dateFormats, min, max, interval);
}
}
}
/**
* Synchronizes the internal state of the component based on a specific selected date.
* @param value Currently selected date.
* @param options Options rendered out in the timepicker.
* @param fallback Option to set as active if no option is selected.
*/
_syncSelectedState(value, options, fallback) {
let hasSelected = false;
for (const option of options) {
if (value && this._dateAdapter.sameTime(option.value, value)) {
option.select(false);
scrollOptionIntoView(option, 'center');
untracked(() => this._keyManager.setActiveItem(option));
hasSelected = true;
}
else {
option.deselect(false);
}
}
// If no option was selected, we need to reset the key manager since
// it might be holding onto an option that no longer exists.
if (!hasSelected) {
if (fallback) {
untracked(() => this._keyManager.setActiveItem(fallback));
scrollOptionIntoView(fallback, 'center');
}
else {
untracked(() => this._keyManager.setActiveItem(-1));
}
}
}
/** Handles keyboard events while the overlay is open. */
_handleKeydown(event) {
const keyCode = event.keyCode;
if (keyCode === TAB) {
this.close();
}
else if (keyCode === ESCAPE && !hasModifierKey(event)) {
event.preventDefault();
this.close();
}
else if (keyCode === ENTER) {
event.preventDefault();
if (this._keyManager.activeItem) {
this._selectValue(this._keyManager.activeItem);
}
else {
this.close();
}
}
else {
const previousActive = this._keyManager.activeItem;
this._keyManager.onKeydown(event);
const currentActive = this._keyManager.activeItem;
if (currentActive && currentActive !== previousActive) {
scrollOptionIntoView(currentActive, 'nearest');
}
}
}
/** Sets up the logic that updates the timepicker when the locale changes. */
_handleLocaleChanges() {
// Re-generate the options list if the locale changes.
this._localeChanges = this._dateAdapter.localeChanges.subscribe(() => {
this._optionsCacheKey = null;
if (this.isOpen()) {
this._generateOptions();
}
});
}
/**
* Sets up the logic that updates the timepicker when the state of the connected input changes.
*/
_handleInputStateChanges() {
effect(() => {
const input = this._input();
const options = this._options();
if (this._isOpen() && input) {
this._syncSelectedState(input.value(), options, null);
}
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatTimepicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: MatTimepicker, isStandalone: true, selector: "mat-timepicker", inputs: { interval: { classPropertyName: "interval", publicName: "interval", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, disableRipple: { classPropertyName: "disableRipple", publicName: "disableRipple", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected", opened: "opened", closed: "closed" }, providers: [
{
provide: MAT_OPTION_PARENT_COMPONENT,
useExisting: MatTimepicker,
},
], viewQueries: [{ propertyName: "_panelTemplate", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }, { propertyName: "_options", predicate: MatOption, descendants: true, isSignal: true }], exportAs: ["matTimepicker"], ngImport: i0, template: "<ng-template #panelTemplate>\n <div\n role=\"listbox\"\n class=\"mat-timepicker-panel\"\n [class.mat-timepicker-panel-animations-enabled]=\"!_animationsDisabled\"\n [class.mat-timepicker-panel-exit]=\"!isOpen()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"_getAriaLabelledby()\"\n [id]=\"panelId\"\n (animationend)=\"_handleAnimationEnd($event)\">\n @for (option of _timeOptions; track option.value) {\n <mat-option\n [value]=\"option.value\"\n (onSelectionChange)=\"_selectValue($event.source)\">{{option.label}}</mat-option>\n }\n </div>\n</ng-template>\n", styles: ["@keyframes _mat-timepicker-enter{from{opacity:0;transform:scaleY(0.8)}to{opacity:1;transform:none}}@keyframes _mat-timepicker-exit{from{opacity:1}to{opacity:0}}mat-timepicker{display:none}.mat-timepicker-panel{width:100%;max-height:256px;transform-origin:center top;overflow:auto;padding:8px 0;box-sizing:border-box;border-bottom-left-radius:var(--mat-timepicker-container-shape, var(--mat-sys-corner-extra-small));border-bottom-right-radius:var(--mat-timepicker-container-shape, var(--mat-sys-corner-extra-small));box-shadow:var(--mat-timepicker-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12));background-color:var(--mat-timepicker-container-background-color, var(--mat-sys-surface-container))}@media(forced-colors: active){.mat-timepicker-panel{outline:solid 1px}}.mat-timepicker-above .mat-timepicker-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--mat-timepicker-container-shape, var(--mat-sys-corner-extra-small));border-top-right-radius:var(--mat-timepicker-container-shape, var(--mat-sys-corner-extra-small))}.mat-timepicker-panel-animations-enabled{animation:_mat-timepicker-enter 120ms cubic-bezier(0, 0, 0.2, 1)}.mat-timepicker-panel-animations-enabled.mat-timepicker-panel-exit{animation:_mat-timepicker-exit 100ms linear}.mat-timepicker-input[readonly]{cursor:pointer}@media(forced-colors: active){.mat-timepicker-toggle-default-icon{color:CanvasText}}"], dependencies: [{ kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatTimepicker, decorators: [{
type: Component,
args: [{ selector: 'mat-timepicker', exportAs: 'matTimepicker', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [MatOption], providers: [
{
provide: MAT_OPTION_PARENT_COMPONENT,
useExisting: MatTimepicker,
},
], template: "<ng-template #panelTemplate>\n <div\n role=\"listbox\"\n class=\"mat-timepicker-panel\"\n [class.mat-timepicker-panel-animations-enabled]=\"!_animationsDisabled\"\n [class.mat-timepicker-panel-exit]=\"!isOpen()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"_getAriaLabelledby()\"\n [id]=\"panelId\"\n (animationend)=\"_handleAnimationEnd($event)\">\n @for (option of _timeOptions; track option.value) {\n <mat-option\n [value]=\"option.value\"\n (onSelectionChange)=\"_selectValue($event.source)\">{{option.label}}</mat-option>\n }\n </div>\n</ng-template>\n", styles: ["@keyframes _mat-timepicker-enter{from{opacity:0;transform:scaleY(0.8)}to{opacity:1;transform:none}}@keyframes _mat-timepicker-exit{from{opacity:1}to{opacity:0}}mat-timepicker{display:none}.mat-timepicker-panel{width:100%;max-height:256px;transform-origin:center top;overflow:auto;padding:8px 0;box-sizing:border-box;border-bottom-left-radius:var(--mat-timepicker-container-shape, var(--mat-sys-corner-extra-small));border-bottom-right-radius:var(--mat-timepicker-container-shape, var(--mat-sys-corner-extra-small));box-shadow:var(--mat-timepicker-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12));background-color:var(--mat-timepicker-container-background-color, var(--mat-sys-surface-container))}@media(forced-colors: active){.mat-timepicker-panel{outline:solid 1px}}.mat-timepicker-above .mat-timepicker-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--mat-timepicker-container-shape, var(--mat-sys-corner-extra-small));border-top-right-radius:var(--mat-timepicker-container-shape, var(--mat-sys-corner-extra-small))}.mat-timepicker-panel-animations-enabled{animation:_mat-timepicker-enter 120ms cubic-bezier(0, 0, 0.2, 1)}.mat-timepicker-panel-animations-enabled.mat-timepicker-panel-exit{animation:_mat-timepicker-exit 100ms linear}.mat-timepicker-input[readonly]{cursor:pointer}@media(forced-colors: active){.mat-timepicker-toggle-default-icon{color:CanvasText}}"] }]
}], ctorParameters: () => [] });
/**
* Scrolls an option into view.
* @param option Option to be scrolled into view.
* @param position Position to which to align the option relative to the scrollable container.
*/
function scrollOptionIntoView(option, position) {
option._getHostElement().scrollIntoView({ block: position, inline: position });
}
/**
* Input that can be used to enter time and connect to a `mat-timepicker`.
*/
class MatTimepickerInput {
_elementRef = inject(ElementRef);
_dateAdapter = inject(DateAdapter, { optional: true });
_dateFormats = inject(MAT_DATE_FORMATS, { optional: true });
_formField = inject(MAT_FORM_FIELD, { optional: true });
_onChange;
_onTouched;
_validatorOnChange;
_cleanupClick;
_accessorDisabled = signal(false);
_localeSubscription;
_timepickerSubscription;
_validator;
_lastValueValid = true;
_lastValidDate = null;
/** Value of the `aria-activedescendant` attribute. */
_ariaActiveDescendant = computed(() => {
const timepicker = this.timepicker();
const isOpen = timepicker.isOpen();
const activeDescendant = timepicker.activeDescendant();
return isOpen && activeDescendant ? activeDescendant : null;
});
/** Value of the `aria-expanded` attribute. */
_ariaExpanded = computed(() => this.timepicker().isOpen() + '');
/** Value of the `aria-controls` attribute. */
_ariaControls = computed(() => {
const timepicker = this.timepicker();
return timepicker.isOpen() ? timepicker.panelId : null;
});
/** Current value of the input. */
value = model(null);
/** Timepicker that the input is associated with. */
timepicker = input.required({
alias: 'matTimepicker',
});
/**
* Minimum time that can be selected or typed in. Can be either
* a date object (only time will be used) or a valid time string.
*/
min = input(null, {
alias: 'matTimepickerMin',
transform: (value) => this._transformDateInput(value),
});
/**
* Maximum time that can be selected or typed in. Can be either
* a date object (only time will be used) or a valid time string.
*/
max = input(null, {
alias: 'matTimepickerMax',
transform: (value) => this._transformDateInput(value),
});
/** Whether the input is disabled. */
disabled = computed(() => this.disabledInput() || this._accessorDisabled());
/**
* Whether the input should be disabled through the template.
* @docs-private
*/
disabledInput = input(false, {
transform: booleanAttribute,
alias: 'disabled',
});
constructor() {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
validateAdapter(this._dateAdapter, this._dateFormats);
}
const renderer = inject(Renderer2);
this._validator = this._getValidator();
this._respondToValueChanges();
this._respondToMinMaxChanges();
this._registerTimepicker();
this._localeSubscription = this._dateAdapter.localeChanges.subscribe(() => {
if (!this._hasFocus()) {
this._formatValue(this.value());
}
});
// Bind the click listener manually to the overlay origin, because we want the entire
// form field to be clickable, if the timepicker is used in `mat-form-field`.
this._cleanupClick = renderer.listen(this.getOverlayOrigin().nativeElement, 'click', this._handleClick);
}
/**
* Implemented as a part of `ControlValueAccessor`.
* @docs-private
*/
writeValue(value) {
// Note that we need to deserialize here, rather than depend on the value change effect,
// because `getValidDateOrNull` will clobber the value if it's parseable, but not created by
// the current adapter (see #30140).
const deserialized = this._dateAdapter.deserialize(value);
this.value.set(this._dateAdapter.getValidDateOrNull(deserialized));
}
/**
* Implemented as a part of `ControlValueAccessor`.
* @docs-private
*/
registerOnChange(fn) {
this._onChange = fn;
}
/**
* Implemented as a part of `ControlValueAccessor`.
* @docs-private
*/
registerOnTouched(fn) {
this._onTouched = fn;
}
/**
* Implemented as a part of `ControlValueAccessor`.
* @docs-private
*/
setDisabledState(isDisabled) {
this._accessorDisabled.set(isDisabled);
}
/**
* Implemented as a part of `Validator`.
* @docs-private
*/
validate(control) {
return this._validator(control);
}
/**
* Implemented as a part of `Validator`.
* @docs-private
*/
registerOnValidatorChange(fn) {
this._validatorOnChange = fn;
}
/** Gets the element to which the timepicker popup should be attached. */
getOverlayOrigin() {
return this._formField?.getConnectedOverlayOrigin() || this._elementRef;
}
/** Focuses the input. */
focus() {
this._elementRef.nativeElement.focus();
}
ngOnDestroy() {
this._cleanupClick();
this._timepickerSubscription?.unsubscribe();
this._localeSubscription.unsubscribe();
}
/** Gets the ID of the input's label. */
_getLabelId() {
return this._formField?.getLabelId() || null;
}
/** Handles clicks on the input or the containing form field. */
_handleClick = () => {
if (!this.disabled()) {
this.timepicker().open();
}
};
/** Handles the `input` event. */
_handleInput(value) {
const currentValue = this.value();
const date = this._dateAdapter.parseTime(value, this._dateFormats.parse.timeInput);
const hasChanged = !this._dateAdapter.sameTime(date, currentValue);
if (!date || hasChanged || !!(value && !currentValue)) {
// We need to fire the CVA change event for all nulls, otherwise the validators won't run.
this._assignUserSelection(date, true);
}
else {
// Call the validator even if the value hasn't changed since
// some fields change depending on what the user has entered.
this._validatorOnChange?.();
}
}
/** Handles the `blur` event. */
_handleBlur() {
const value = this.value();
// Only reformat on blur so the value doesn't change while the user is interacting.
if (value && this._isValid(value)) {
this._formatValue(value);
}
if (!this.timepicker().isOpen()) {
this._onTouched?.();
}
}
/** Handles the `keydown` event. */
_handleKeydown(event) {
// All keyboard events while open are handled through the timepicker.
if (this.timepicker().isOpen() || this.disabled()) {
return;
}
if (event.keyCode === ESCAPE && !hasModifierKey(event) && this.value() !== null) {
event.preventDefault();
this.value.set(null);
this._formatValue(null);
}
else if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
event.preventDefault();
this.timepicker().open();
}
}
/** Sets up the code that watches for changes in the value and adjusts the input. */
_respondToValueChanges() {
effect(() => {
const value = this._dateAdapter.deserialize(this.value());
const wasValid = this._lastValueValid;
this._lastValueValid = this._isValid(value);
// Reformat the value if it changes while the user isn't interacting.
if (!this._hasFocus()) {
this._formatValue(value);
}
if (value && this._lastValueValid) {
this._lastValidDate = value;
}
// Trigger the validator if the state changed.
if (wasValid !== this._lastValueValid) {
this._validatorOnChange?.();
}
});
}
/** Sets up the logic that registers the input with the timepicker. */
_registerTimepicker() {
effect(() => {
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);
}
});
});
}
/** Sets up the logic that adjusts the input if the min/max changes. */
_respondToMinMaxChanges() {
effect(() => {
// Read the min/max so the effect knows when to fire.
this.min();
this.max();
this._validatorOnChange?.();
});
}
/**
* Assigns a value set by the user to the input's model.
* @param selection Time selected by the user that should be assigned.
* @param propagateToAccessor Whether the value should be propagated to the ControlValueAccessor.
*/
_assignUserSelection(selection, propagateToAccessor) {
if (selection == null || !this._isValid(selection)) {
this.value.set(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
// datepicker. If the user enters a valid time afterwards, the datepicker's selection will
// have been lost. This logic restores the previously-valid date and sets its time to
// the newly-selected time.
const adapter = this._dateAdapter;
const target = adapter.getValidDateOrNull(this._lastValidDate || this.value());
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);
}
if (propagateToAccessor) {
this._onChange?.(this.value());
}
}
/** Formats the current value and assigns it to the input. */
_formatValue(value) {
value = this._dateAdapter.getValidDateOrNull(value);
this._elementRef.nativeElement.value =
value == null ? '' : this._dateAdapter.format(value, this._dateFormats.display.timeInput);
}
/** Checks whether a value is valid. */
_isValid(value) {
return !value || this._dateAdapter.isValid(value);
}
/** Transforms an arbitrary value into a value that can be assigned to a date-based input. */
_transformDateInput(value) {
const date = typeof value === 'string'
? this._dateAdapter.parseTime(value, this._dateFormats.parse.timeInput)
: this._dateAdapter.deserialize(value);
return date && this._dateAdapter.isValid(date) ? date : null;
}
/** Whether the input is currently focused. */
_hasFocus() {
return _getFocusedElementPierceShadowDom() === this._elementRef.nativeElement;
}
/** Gets a function that can be used to validate the input. */
_getValidator() {
return Validators.compose([
() => this._lastValueValid
? null
: { 'matTimepickerParse': { 'text': this._elementRef.nativeElement.value } },
control => {
const controlValue = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(control.value));
const min = this.min();
return !min || !controlValue || this._dateAdapter.compareTime(min, controlValue) <= 0
? null
: { 'matTimepickerMin': { 'min': min, 'actual': controlValue } };
},
control => {
const controlValue = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(control.value));
const max = this.max();
return !max || !controlValue || this._dateAdapter.compareTime(max, controlValue) >= 0
? null
: { 'matTimepickerMax': { 'max': max, 'actual': controlValue } };
},
]);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatTimepickerInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.0", type: MatTimepickerInput, isStandalone: true, selector: "input[matTimepicker]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, timepicker: { classPropertyName: "timepicker", publicName: "matTimepicker", isSignal: true, isRequired: true, transformFunction: null }, min: { classPropertyName: "min", publicName: "matTimepickerMin", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "matTimepickerMax", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "role": "combobox", "type": "text", "aria-haspopup": "listbox" }, listeners: { "blur": "_handleBlur()", "input": "_handleInput($event.target.value)", "keydown": "_handleKeydown($event)" }, properties: { "attr.aria-activedescendant": "_ariaActiveDescendant()", "attr.aria-expanded": "_ariaExpanded()", "attr.aria-controls": "_ariaControls()", "attr.mat-timepicker-id": "timepicker()?.panelId", "disabled": "disabled()" }, classAttribute: "mat-timepicker-input" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: MatTimepickerInput,
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: MatTimepickerInput,
multi: true,
},
{
provide: MAT_INPUT_VALUE_ACCESSOR,
useExisting: MatTimepickerInput,
},
], exportAs: ["matTimepickerInput"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatTimepickerInput, decorators: [{
type: Directive,
args: [{
selector: 'input[matTimepicker]',
exportAs: 'matTimepickerInput',
host: {
'class': 'mat-timepicker-input',
'role': 'combobox',
'type': 'text',
'aria-haspopup': 'listbox',
'[attr.aria-activedescendant]': '_ariaActiveDescendant()',
'[attr.aria-expanded]': '_ariaExpanded()',
'[attr.aria-controls]': '_ariaControls()',
'[attr.mat-timepicker-id]': 'timepicker()?.panelId',
'[disabled]': 'disabled()',
'(blur)': '_handleBlur()',
'(input)': '_handleInput($event.target.value)',
'(keydown)': '_handleKeydown($event)',
},
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: MatTimepickerInput,
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: MatTimepickerInput,
multi: true,
},
{
provide: MAT_INPUT_VALUE_ACCESSOR,
useExisting: MatTimepickerInput,
},
],
}]
}], ctorParameters: () => [] });
/** Button that can be used to open a `mat-timepicker`. */
class MatTimepickerToggle {
_defaultConfig = inject(MAT_TIMEPICKER_CONFIG, { optional: true });
_defaultTabIndex = (() => {
const value = inject(new HostAttributeToken('tabindex'), { optional: true });
const parsed = Number(value);
return isNaN(parsed) ? null : parsed;
})();
_isDisabled = computed(() => {
const timepicker = this.timepicker();
return this.disabled() || timepicker.disabled();
});
/** Timepicker instance that the button will toggle. */
timepicker = input.required({
alias: 'for',
});
/** Screen-reader label for the button. */
ariaLabel = input(undefined, {
alias: 'aria-label',
});
/** Screen-reader labelled by id for the button. */
ariaLabelledby = input(undefined, {
alias: 'aria-labelledby',
});
/** Default aria-label for the toggle if none is provided. */
_defaultAriaLabel = 'Open timepicker options';
/** Whether the toggle button is disabled. */
disabled = input(false, {
transform: booleanAttribute,
alias: 'disabled',
});
/** Tabindex for the toggle. */
tabIndex = input(this._defaultTabIndex);
/** Whether ripples on the toggle should be disabled. */
disableRipple = input(this._defaultConfig?.disableRipple ?? false, { transform: booleanAttribute });
/** Opens the connected timepicker. */
_open(event) {
if (this.timepicker() && !this._isDisabled()) {
this.timepicker().open();
event.stopPropagation();
}
}
/**
* Checks for ariaLabelledby and if empty uses custom
* aria-label or defaultAriaLabel if neither is provided.
*/
getAriaLabel() {
return this.ariaLabelledby() ? null : this.ariaLabel() || this._defaultAriaLabel;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatTimepickerToggle, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.0", type: MatTimepickerToggle, isStandalone: true, selector: "mat-timepicker-toggle", inputs: { timepicker: { classPropertyName: "timepicker", publicName: "for", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, disableRipple: { classPropertyName: "disableRipple", publicName: "disableRipple", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "_open($event)" }, properties: { "attr.tabindex": "null" }, classAttribute: "mat-timepicker-toggle" }, exportAs: ["matTimepickerToggle"], ngImport: i0, template: "<button\n mat-icon-button\n type=\"button\"\n aria-haspopup=\"listbox\"\n [attr.aria-label]=\"getAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n [attr.aria-expanded]=\"timepicker().isOpen()\"\n [attr.tabindex]=\"_isDisabled() ? -1 : tabIndex()\"\n [disabled]=\"_isDisabled()\"\n [disableRipple]=\"disableRipple()\">\n\n <ng-content select=\"[matTimepickerToggleIcon]\">\n <svg\n class=\"mat-timepicker-toggle-default-icon\"\n height=\"24px\"\n width=\"24px\"\n viewBox=\"0 -960 960 960\"\n fill=\"currentColor\"\n focusable=\"false\"\n aria-hidden=\"true\">\n <path d=\"m612-292 56-56-148-148v-184h-80v216l172 172ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z\"/>\n </svg>\n </ng-content>\n</button>\n", dependencies: [{ kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatTimepickerToggle, decorators: [{
type: Component,
args: [{ selector: 'mat-timepicker-toggle', host: {
'class': 'mat-timepicker-toggle',
'[attr.tabindex]': 'null',
// Bind the `click` on the host, rather than the inner `button`, so that we can call
// `stopPropagation` on it without affecting the user's `click` handlers. We need to stop
// it so that the input doesn't get focused automatically by the form field (See #21836).
'(click)': '_open($event)',
}, exportAs: 'matTimepickerToggle', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatIconButton], template: "<button\n mat-icon-button\n type=\"button\"\n aria-haspopup=\"listbox\"\n [attr.aria-label]=\"getAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n [attr.aria-expanded]=\"timepicker().isOpen()\"\n [attr.tabindex]=\"_isDisabled() ? -1 : tabIndex()\"\n [disabled]=\"_isDisabled()\"\n [disableRipple]=\"disableRipple()\">\n\n <ng-content select=\"[matTimepickerToggleIcon]\">\n <svg\n class=\"mat-timepicker-toggle-default-icon\"\n height=\"24px\"\n width=\"24px\"\n viewBox=\"0 -960 960 960\"\n fill=\"currentColor\"\n focusable=\"false\"\n aria-hidden=\"true\">\n <path d=\"m612-292 56-56-148-148v-184h-80v216l172 172ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z\"/>\n </svg>\n </ng-content>\n</button>\n" }]
}] });
class MatTimepickerModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatTimepickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.0", ngImport: i0, type: MatTimepickerModule, imports: [MatTimepicker, MatTimepickerInput, MatTimepickerToggle], exports: [CdkScrollableModule, MatTimepicker, MatTimepickerInput, MatTimepickerToggle] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatTimepickerModule, imports: [MatTimepicker, MatTimepickerToggle, CdkScrollableModule] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatTimepickerModule, decorators: [{
type: NgModule,
args: [{
imports: [MatTimepicker, MatTimepickerInput, MatTimepickerToggle],
exports: [CdkScrollableModule, MatTimepicker, MatTimepickerInput, MatTimepickerToggle],
}]
}] });
export { MAT_TIMEPICKER_CONFIG, MAT_TIMEPICKER_SCROLL_STRATEGY, MatTimepicker, MatTimepickerInput, MatTimepickerModule, MatTimepickerToggle };
//# sourceMappingURL=timepicker.mjs.map