Skip to content
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

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

Merged
merged 1 commit into from
Dec 9, 2024
Merged
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
fix(material/timepicker): disable toggle if timepicker is disabled
Fixes that the timepicker toggle wasn't considered as disabled automatically when the timepicker is disabled.

Fixes #30134.
  • Loading branch information
crisbeto committed Dec 6, 2024
commit 6d9a912595798e3f27b85d7005424a76e8bf61ef
8 changes: 5 additions & 3 deletions src/material/timepicker/timepicker-input.ts
Original file line number Diff line number Diff line change
@@ -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. */
@@ -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();
}
4 changes: 2 additions & 2 deletions src/material/timepicker/timepicker-toggle.html
Original file line number Diff line number Diff line change
@@ -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]">
8 changes: 7 additions & 1 deletion src/material/timepicker/timepicker-toggle.ts
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
HostAttributeToken,
inject,
input,
@@ -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',
@@ -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();
}
4 changes: 1 addition & 3 deletions src/material/timepicker/timepicker.scss
Original file line number Diff line number Diff line change
@@ -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 {
13 changes: 13 additions & 0 deletions src/material/timepicker/timepicker.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
43 changes: 26 additions & 17 deletions src/material/timepicker/timepicker.ts
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
effect,
ElementRef,
inject,
@@ -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;
@@ -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);
@@ -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;
@@ -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},
@@ -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 {
@@ -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. */
@@ -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')
@@ -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();
}
});
@@ -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);

@@ -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);
}
});
}
3 changes: 3 additions & 0 deletions tools/public_api_guard/material/timepicker.md
Original file line number Diff line number Diff line change
@@ -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>;
@@ -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>>;
Loading