Skip to content

fix(material/timepicker): disable toggle if timepicker is disabled #30137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading