Skip to content

Commit

Permalink
fix(material/timepicker): disable toggle if timepicker is disabled (#…
Browse files Browse the repository at this point in the history
…30137)

Fixes that the timepicker toggle wasn't considered as disabled automatically when the timepicker is disabled.

Fixes #30134.
  • Loading branch information
crisbeto authored Dec 9, 2024
1 parent 0c40595 commit db8f6c0
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 26 deletions.
8 changes: 5 additions & 3 deletions src/material/timepicker/timepicker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,9 @@ export class MatTimepickerInput<D> implements ControlValueAccessor, Validator, O

/** Handles clicks on the input or the containing form field. */
private _handleClick = (): void => {
this.timepicker().open();
if (!this.disabled()) {
this.timepicker().open();
}
};

/** Handles the `input` event. */
Expand Down Expand Up @@ -278,15 +280,15 @@ export class MatTimepickerInput<D> implements ControlValueAccessor, Validator, O
/** Handles the `keydown` event. */
protected _handleKeydown(event: KeyboardEvent) {
// All keyboard events while open are handled through the timepicker.
if (this.timepicker().isOpen()) {
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) && !this.disabled()) {
} else if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
event.preventDefault();
this.timepicker().open();
}
Expand Down
4 changes: 2 additions & 2 deletions src/material/timepicker/timepicker-toggle.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
aria-haspopup="listbox"
[attr.aria-label]="ariaLabel()"
[attr.aria-expanded]="timepicker().isOpen()"
[attr.tabindex]="disabled() ? -1 : tabIndex()"
[disabled]="disabled()"
[attr.tabindex]="_isDisabled() ? -1 : tabIndex()"
[disabled]="_isDisabled()"
[disableRipple]="disableRipple()">

<ng-content select="[matTimepickerToggleIcon]">
Expand Down
8 changes: 7 additions & 1 deletion src/material/timepicker/timepicker-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
HostAttributeToken,
inject,
input,
Expand Down Expand Up @@ -46,6 +47,11 @@ export class MatTimepickerToggle<D> {
return isNaN(parsed) ? null : parsed;
})();

protected _isDisabled = computed(() => {
const timepicker = this.timepicker();
return this.disabled() || timepicker.disabled();
});

/** Timepicker instance that the button will toggle. */
readonly timepicker: InputSignal<MatTimepicker<D>> = input.required<MatTimepicker<D>>({
alias: 'for',
Expand Down Expand Up @@ -73,7 +79,7 @@ export class MatTimepickerToggle<D> {

/** Opens the connected timepicker. */
protected _open(event: Event): void {
if (this.timepicker() && !this.disabled()) {
if (this.timepicker() && !this._isDisabled()) {
this.timepicker().open();
event.stopPropagation();
}
Expand Down
4 changes: 1 addition & 3 deletions src/material/timepicker/timepicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ mat-timepicker {
}
}

// stylelint-disable material/no-prefixes
.mat-timepicker-input:read-only {
.mat-timepicker-input[readonly] {
cursor: pointer;
}
// stylelint-enable material/no-prefixes

@include cdk.high-contrast {
.mat-timepicker-toggle-default-icon {
Expand Down
13 changes: 13 additions & 0 deletions src/material/timepicker/timepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,19 @@ describe('MatTimepicker', () => {
fixture.detectChanges();
expect(getPanel()).toBeFalsy();
});

it('should disable the toggle when the timepicker is disabled', () => {
const fixture = TestBed.createComponent(StandaloneTimepicker);
const toggle = getToggle(fixture);
fixture.detectChanges();
expect(toggle.disabled).toBe(false);
expect(toggle.getAttribute('tabindex')).toBe('0');

fixture.componentInstance.disabled.set(true);
fixture.detectChanges();
expect(toggle.disabled).toBe(true);
expect(toggle.getAttribute('tabindex')).toBe('-1');
});
});

describe('global defaults', () => {
Expand Down
43 changes: 26 additions & 17 deletions src/material/timepicker/timepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
effect,
ElementRef,
inject,
Expand Down Expand Up @@ -104,7 +105,7 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
private _isOpen = signal(false);
private _activeDescendant = signal<string | null>(null);

private _input: MatTimepickerInput<D>;
private _input = signal<MatTimepickerInput<D> | null>(null);
private _overlayRef: OverlayRef | null = null;
private _portal: TemplatePortal<unknown> | null = null;
private _optionsCacheKey: string | null = null;
Expand Down Expand Up @@ -174,6 +175,9 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
alias: 'aria-labelledby',
});

/** Whether the timepicker is currently disabled. */
readonly disabled: Signal<boolean> = computed(() => !!this._input()?.disabled());

constructor() {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
validateAdapter(this._dateAdapter, this._dateFormats);
Expand Down Expand Up @@ -204,14 +208,16 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {

/** Opens the timepicker. */
open(): void {
if (!this._input) {
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.
this._input.focus();
input.focus();

if (this._isOpen()) {
return;
Expand All @@ -220,14 +226,14 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
this._isOpen.set(true);
this._generateOptions();
const overlayRef = this._getOverlayRef();
overlayRef.updateSize({width: this._input.getOverlayOrigin().nativeElement.offsetWidth});
overlayRef.updateSize({width: input.getOverlayOrigin().nativeElement.offsetWidth});
this._portal ??= new TemplatePortal(this._panelTemplate(), this._viewContainerRef);
overlayRef.attach(this._portal);
this._onOpenRender?.destroy();
this._onOpenRender = afterNextRender(
() => {
const options = this._options();
this._syncSelectedState(this._input.value(), options, options[0]);
this._syncSelectedState(input.value(), options, options[0]);
this._onOpenRender = null;
},
{injector: this._injector},
Expand All @@ -247,11 +253,13 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {

/** Registers an input with the timepicker. */
registerInput(input: MatTimepickerInput<D>): void {
if (this._input && input !== this._input && (typeof ngDevMode === 'undefined' || ngDevMode)) {
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 = input;
this._input.set(input);
}

ngOnDestroy(): void {
Expand All @@ -265,15 +273,15 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
protected _selectValue(value: D) {
this.close();
this.selected.emit({value, source: this});
this._input.focus();
this._input()?.focus();
}

/** Gets the value of the `aria-labelledby` attribute. */
protected _getAriaLabelledby(): string | null {
if (this.ariaLabel()) {
return null;
}
return this.ariaLabelledby() || this._input?._getLabelId() || null;
return this.ariaLabelledby() || this._input()?._getLabelId() || null;
}

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

const positionStrategy = this._overlay
.position()
.flexibleConnectedTo(this._input.getOverlayOrigin())
.flexibleConnectedTo(this._input()!.getOverlayOrigin())
.withFlexibleDimensions(false)
.withPush(false)
.withTransformOriginOn('.mat-timepicker-panel')
Expand Down Expand Up @@ -317,9 +325,9 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {

this._overlayRef.outsidePointerEvents().subscribe(event => {
const target = _getEventTarget(event) as HTMLElement;
const origin = this._input.getOverlayOrigin().nativeElement;
const origin = this._input()?.getOverlayOrigin().nativeElement;

if (target && target !== origin && !origin.contains(target)) {
if (target && origin && target !== origin && !origin.contains(target)) {
this.close();
}
});
Expand All @@ -336,10 +344,11 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
if (options !== null) {
this._timeOptions = options;
} else {
const input = this._input();
const adapter = this._dateAdapter;
const timeFormat = this._dateFormats.display.timeInput;
const min = this._input.min() || adapter.setTime(adapter.today(), 0, 0, 0);
const max = this._input.max() || adapter.setTime(adapter.today(), 23, 59, 0);
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);

Expand Down Expand Up @@ -432,11 +441,11 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
*/
private _handleInputStateChanges(): void {
effect(() => {
const value = this._input?.value();
const input = this._input();
const options = this._options();

if (this._isOpen()) {
this._syncSelectedState(value, options, null);
if (this._isOpen() && input) {
this._syncSelectedState(input.value(), options, null);
}
});
}
Expand Down
3 changes: 3 additions & 0 deletions tools/public_api_guard/material/timepicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
readonly ariaLabelledby: InputSignal<string | null>;
close(): void;
readonly closed: OutputEmitterRef<void>;
readonly disabled: Signal<boolean>;
readonly disableRipple: InputSignalWithTransform<boolean, unknown>;
protected _getAriaLabelledby(): string | null;
readonly interval: InputSignalWithTransform<number | null, number | string | null>;
Expand Down Expand Up @@ -125,6 +126,8 @@ export class MatTimepickerToggle<D> {
readonly ariaLabel: InputSignal<string | undefined>;
readonly disabled: InputSignalWithTransform<boolean, unknown>;
readonly disableRipple: InputSignalWithTransform<boolean, unknown>;
// (undocumented)
protected _isDisabled: Signal<boolean>;
protected _open(event: Event): void;
readonly tabIndex: InputSignal<number | null>;
readonly timepicker: InputSignal<MatTimepicker<D>>;
Expand Down

0 comments on commit db8f6c0

Please sign in to comment.