From 3e9cecf3c869b67146f1d6e35393bf0324aa0c36 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 27 Feb 2025 11:01:12 +0100 Subject: [PATCH] fix(material/timepicker): unable to reopen if closed by scroll strategy The timepicker wasn't updating its internal state when it gets closed through the overlay which meant that the user can't reopen it. Fixes #30558. --- src/material/timepicker/BUILD.bazel | 3 ++ src/material/timepicker/timepicker.spec.ts | 39 ++++++++++++++++++++-- src/material/timepicker/timepicker.ts | 8 ++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/material/timepicker/BUILD.bazel b/src/material/timepicker/BUILD.bazel index a46395de2a01..880e63549c0a 100644 --- a/src/material/timepicker/BUILD.bazel +++ b/src/material/timepicker/BUILD.bazel @@ -54,12 +54,15 @@ ng_test_library( deps = [ ":timepicker", "//src/cdk/keycodes", + "//src/cdk/overlay", + "//src/cdk/scrolling", "//src/cdk/testing/private", "//src/material/core", "//src/material/form-field", "//src/material/input", "@npm//@angular/forms", "@npm//@angular/platform-browser", + "@npm//rxjs", ], ) diff --git a/src/material/timepicker/timepicker.spec.ts b/src/material/timepicker/timepicker.spec.ts index da1bd81e5365..9182f2efed11 100644 --- a/src/material/timepicker/timepicker.spec.ts +++ b/src/material/timepicker/timepicker.spec.ts @@ -1,4 +1,4 @@ -import {Component, Provider, signal, ViewChild} from '@angular/core'; +import {Component, inject, Provider, signal, ViewChild} from '@angular/core'; import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {DateAdapter, provideNativeDateAdapter} from '@angular/material/core'; @@ -24,10 +24,13 @@ import { import {MatInput} from '@angular/material/input'; import {MatFormField, MatLabel, MatSuffix} from '@angular/material/form-field'; import {MatTimepickerInput} from './timepicker-input'; -import {MatTimepicker} from './timepicker'; +import {MAT_TIMEPICKER_SCROLL_STRATEGY, MatTimepicker} from './timepicker'; import {MatTimepickerToggle} from './timepicker-toggle'; import {MAT_TIMEPICKER_CONFIG, MatTimepickerOption} from './util'; import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms'; +import {ScrollDispatcher} from '@angular/cdk/scrolling'; +import {Overlay} from '@angular/cdk/overlay'; +import {Subject} from 'rxjs'; describe('MatTimepicker', () => { let adapter: DateAdapter; @@ -440,6 +443,38 @@ describe('MatTimepicker', () => { flush(); }).not.toThrow(); })); + + it('should be able to reopen the panel when closed by a scroll strategy', fakeAsync(() => { + const scrolledSubject = new Subject(); + + TestBed.resetTestingModule(); + configureTestingModule([ + { + provide: ScrollDispatcher, + useValue: {scrolled: () => scrolledSubject}, + }, + { + provide: MAT_TIMEPICKER_SCROLL_STRATEGY, + useFactory: () => { + const overlay = inject(Overlay); + return () => overlay.scrollStrategies.close(); + }, + }, + ]); + + const fixture = TestBed.createComponent(StandaloneTimepicker); + fixture.detectChanges(); + fixture.componentInstance.timepicker.open(); + fixture.detectChanges(); + expect(getPanel()).toBeTruthy(); + scrolledSubject.next(); + fixture.detectChanges(); + flush(); + expect(getPanel()).toBeFalsy(); + fixture.componentInstance.timepicker.open(); + fixture.detectChanges(); + expect(getPanel()).toBeTruthy(); + })); }); // Note: these tests intentionally don't cover the full option generation logic diff --git a/src/material/timepicker/timepicker.ts b/src/material/timepicker/timepicker.ts index c11b5cabc1d0..97df89a24d1a 100644 --- a/src/material/timepicker/timepicker.ts +++ b/src/material/timepicker/timepicker.ts @@ -70,7 +70,7 @@ export const MAT_TIMEPICKER_SCROLL_STRATEGY = new InjectionToken<() => ScrollStr providedIn: 'root', factory: () => { const overlay = inject(Overlay); - return () => overlay.scrollStrategies.reposition(); + return () => overlay.scrollStrategies.close(); }, }, ); @@ -340,10 +340,8 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { hasBackdrop: false, }); - this._overlayRef.keydownEvents().subscribe(event => { - this._handleKeydown(event); - }); - + this._overlayRef.detachments().subscribe(() => this.close()); + this._overlayRef.keydownEvents().subscribe(event => this._handleKeydown(event)); this._overlayRef.outsidePointerEvents().subscribe(event => { const target = _getEventTarget(event) as HTMLElement; const origin = this._input()?.getOverlayOrigin().nativeElement;