Skip to content

Commit db8f6c0

Browse files
authored
fix(material/timepicker): disable toggle if timepicker is disabled (#30137)
Fixes that the timepicker toggle wasn't considered as disabled automatically when the timepicker is disabled. Fixes #30134.
1 parent 0c40595 commit db8f6c0

File tree

7 files changed

+57
-26
lines changed

7 files changed

+57
-26
lines changed

src/material/timepicker/timepicker-input.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,9 @@ export class MatTimepickerInput<D> implements ControlValueAccessor, Validator, O
244244

245245
/** Handles clicks on the input or the containing form field. */
246246
private _handleClick = (): void => {
247-
this.timepicker().open();
247+
if (!this.disabled()) {
248+
this.timepicker().open();
249+
}
248250
};
249251

250252
/** Handles the `input` event. */
@@ -278,15 +280,15 @@ export class MatTimepickerInput<D> implements ControlValueAccessor, Validator, O
278280
/** Handles the `keydown` event. */
279281
protected _handleKeydown(event: KeyboardEvent) {
280282
// All keyboard events while open are handled through the timepicker.
281-
if (this.timepicker().isOpen()) {
283+
if (this.timepicker().isOpen() || this.disabled()) {
282284
return;
283285
}
284286

285287
if (event.keyCode === ESCAPE && !hasModifierKey(event) && this.value() !== null) {
286288
event.preventDefault();
287289
this.value.set(null);
288290
this._formatValue(null);
289-
} else if ((event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) && !this.disabled()) {
291+
} else if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
290292
event.preventDefault();
291293
this.timepicker().open();
292294
}

src/material/timepicker/timepicker-toggle.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
aria-haspopup="listbox"
55
[attr.aria-label]="ariaLabel()"
66
[attr.aria-expanded]="timepicker().isOpen()"
7-
[attr.tabindex]="disabled() ? -1 : tabIndex()"
8-
[disabled]="disabled()"
7+
[attr.tabindex]="_isDisabled() ? -1 : tabIndex()"
8+
[disabled]="_isDisabled()"
99
[disableRipple]="disableRipple()">
1010

1111
<ng-content select="[matTimepickerToggleIcon]">

src/material/timepicker/timepicker-toggle.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
booleanAttribute,
1111
ChangeDetectionStrategy,
1212
Component,
13+
computed,
1314
HostAttributeToken,
1415
inject,
1516
input,
@@ -46,6 +47,11 @@ export class MatTimepickerToggle<D> {
4647
return isNaN(parsed) ? null : parsed;
4748
})();
4849

50+
protected _isDisabled = computed(() => {
51+
const timepicker = this.timepicker();
52+
return this.disabled() || timepicker.disabled();
53+
});
54+
4955
/** Timepicker instance that the button will toggle. */
5056
readonly timepicker: InputSignal<MatTimepicker<D>> = input.required<MatTimepicker<D>>({
5157
alias: 'for',
@@ -73,7 +79,7 @@ export class MatTimepickerToggle<D> {
7379

7480
/** Opens the connected timepicker. */
7581
protected _open(event: Event): void {
76-
if (this.timepicker() && !this.disabled()) {
82+
if (this.timepicker() && !this._isDisabled()) {
7783
this.timepicker().open();
7884
event.stopPropagation();
7985
}

src/material/timepicker/timepicker.scss

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,9 @@ mat-timepicker {
3838
}
3939
}
4040

41-
// stylelint-disable material/no-prefixes
42-
.mat-timepicker-input:read-only {
41+
.mat-timepicker-input[readonly] {
4342
cursor: pointer;
4443
}
45-
// stylelint-enable material/no-prefixes
4644

4745
@include cdk.high-contrast {
4846
.mat-timepicker-toggle-default-icon {

src/material/timepicker/timepicker.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,19 @@ describe('MatTimepicker', () => {
11641164
fixture.detectChanges();
11651165
expect(getPanel()).toBeFalsy();
11661166
});
1167+
1168+
it('should disable the toggle when the timepicker is disabled', () => {
1169+
const fixture = TestBed.createComponent(StandaloneTimepicker);
1170+
const toggle = getToggle(fixture);
1171+
fixture.detectChanges();
1172+
expect(toggle.disabled).toBe(false);
1173+
expect(toggle.getAttribute('tabindex')).toBe('0');
1174+
1175+
fixture.componentInstance.disabled.set(true);
1176+
fixture.detectChanges();
1177+
expect(toggle.disabled).toBe(true);
1178+
expect(toggle.getAttribute('tabindex')).toBe('-1');
1179+
});
11671180
});
11681181

11691182
describe('global defaults', () => {

src/material/timepicker/timepicker.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
booleanAttribute,
1313
ChangeDetectionStrategy,
1414
Component,
15+
computed,
1516
effect,
1617
ElementRef,
1718
inject,
@@ -104,7 +105,7 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
104105
private _isOpen = signal(false);
105106
private _activeDescendant = signal<string | null>(null);
106107

107-
private _input: MatTimepickerInput<D>;
108+
private _input = signal<MatTimepickerInput<D> | null>(null);
108109
private _overlayRef: OverlayRef | null = null;
109110
private _portal: TemplatePortal<unknown> | null = null;
110111
private _optionsCacheKey: string | null = null;
@@ -174,6 +175,9 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
174175
alias: 'aria-labelledby',
175176
});
176177

178+
/** Whether the timepicker is currently disabled. */
179+
readonly disabled: Signal<boolean> = computed(() => !!this._input()?.disabled());
180+
177181
constructor() {
178182
if (typeof ngDevMode === 'undefined' || ngDevMode) {
179183
validateAdapter(this._dateAdapter, this._dateFormats);
@@ -204,14 +208,16 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
204208

205209
/** Opens the timepicker. */
206210
open(): void {
207-
if (!this._input) {
211+
const input = this._input();
212+
213+
if (!input) {
208214
return;
209215
}
210216

211217
// Focus should already be on the input, but this call is in case the timepicker is opened
212218
// programmatically. We need to call this even if the timepicker is already open, because
213219
// the user might be clicking the toggle.
214-
this._input.focus();
220+
input.focus();
215221

216222
if (this._isOpen()) {
217223
return;
@@ -220,14 +226,14 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
220226
this._isOpen.set(true);
221227
this._generateOptions();
222228
const overlayRef = this._getOverlayRef();
223-
overlayRef.updateSize({width: this._input.getOverlayOrigin().nativeElement.offsetWidth});
229+
overlayRef.updateSize({width: input.getOverlayOrigin().nativeElement.offsetWidth});
224230
this._portal ??= new TemplatePortal(this._panelTemplate(), this._viewContainerRef);
225231
overlayRef.attach(this._portal);
226232
this._onOpenRender?.destroy();
227233
this._onOpenRender = afterNextRender(
228234
() => {
229235
const options = this._options();
230-
this._syncSelectedState(this._input.value(), options, options[0]);
236+
this._syncSelectedState(input.value(), options, options[0]);
231237
this._onOpenRender = null;
232238
},
233239
{injector: this._injector},
@@ -247,11 +253,13 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
247253

248254
/** Registers an input with the timepicker. */
249255
registerInput(input: MatTimepickerInput<D>): void {
250-
if (this._input && input !== this._input && (typeof ngDevMode === 'undefined' || ngDevMode)) {
256+
const currentInput = this._input();
257+
258+
if (currentInput && input !== currentInput && (typeof ngDevMode === 'undefined' || ngDevMode)) {
251259
throw new Error('MatTimepicker can only be registered with one input at a time');
252260
}
253261

254-
this._input = input;
262+
this._input.set(input);
255263
}
256264

257265
ngOnDestroy(): void {
@@ -265,15 +273,15 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
265273
protected _selectValue(value: D) {
266274
this.close();
267275
this.selected.emit({value, source: this});
268-
this._input.focus();
276+
this._input()?.focus();
269277
}
270278

271279
/** Gets the value of the `aria-labelledby` attribute. */
272280
protected _getAriaLabelledby(): string | null {
273281
if (this.ariaLabel()) {
274282
return null;
275283
}
276-
return this.ariaLabelledby() || this._input?._getLabelId() || null;
284+
return this.ariaLabelledby() || this._input()?._getLabelId() || null;
277285
}
278286

279287
/** Creates an overlay reference for the timepicker panel. */
@@ -284,7 +292,7 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
284292

285293
const positionStrategy = this._overlay
286294
.position()
287-
.flexibleConnectedTo(this._input.getOverlayOrigin())
295+
.flexibleConnectedTo(this._input()!.getOverlayOrigin())
288296
.withFlexibleDimensions(false)
289297
.withPush(false)
290298
.withTransformOriginOn('.mat-timepicker-panel')
@@ -317,9 +325,9 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
317325

318326
this._overlayRef.outsidePointerEvents().subscribe(event => {
319327
const target = _getEventTarget(event) as HTMLElement;
320-
const origin = this._input.getOverlayOrigin().nativeElement;
328+
const origin = this._input()?.getOverlayOrigin().nativeElement;
321329

322-
if (target && target !== origin && !origin.contains(target)) {
330+
if (target && origin && target !== origin && !origin.contains(target)) {
323331
this.close();
324332
}
325333
});
@@ -336,10 +344,11 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
336344
if (options !== null) {
337345
this._timeOptions = options;
338346
} else {
347+
const input = this._input();
339348
const adapter = this._dateAdapter;
340349
const timeFormat = this._dateFormats.display.timeInput;
341-
const min = this._input.min() || adapter.setTime(adapter.today(), 0, 0, 0);
342-
const max = this._input.max() || adapter.setTime(adapter.today(), 23, 59, 0);
350+
const min = input?.min() || adapter.setTime(adapter.today(), 0, 0, 0);
351+
const max = input?.max() || adapter.setTime(adapter.today(), 23, 59, 0);
343352
const cacheKey =
344353
interval + '/' + adapter.format(min, timeFormat) + '/' + adapter.format(max, timeFormat);
345354

@@ -432,11 +441,11 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
432441
*/
433442
private _handleInputStateChanges(): void {
434443
effect(() => {
435-
const value = this._input?.value();
444+
const input = this._input();
436445
const options = this._options();
437446

438-
if (this._isOpen()) {
439-
this._syncSelectedState(value, options, null);
447+
if (this._isOpen() && input) {
448+
this._syncSelectedState(input.value(), options, null);
440449
}
441450
});
442451
}

tools/public_api_guard/material/timepicker.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
3333
readonly ariaLabelledby: InputSignal<string | null>;
3434
close(): void;
3535
readonly closed: OutputEmitterRef<void>;
36+
readonly disabled: Signal<boolean>;
3637
readonly disableRipple: InputSignalWithTransform<boolean, unknown>;
3738
protected _getAriaLabelledby(): string | null;
3839
readonly interval: InputSignalWithTransform<number | null, number | string | null>;
@@ -125,6 +126,8 @@ export class MatTimepickerToggle<D> {
125126
readonly ariaLabel: InputSignal<string | undefined>;
126127
readonly disabled: InputSignalWithTransform<boolean, unknown>;
127128
readonly disableRipple: InputSignalWithTransform<boolean, unknown>;
129+
// (undocumented)
130+
protected _isDisabled: Signal<boolean>;
128131
protected _open(event: Event): void;
129132
readonly tabIndex: InputSignal<number | null>;
130133
readonly timepicker: InputSignal<MatTimepicker<D>>;

0 commit comments

Comments
 (0)