diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index f160934b0194..1b556164f5ed 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -40,7 +40,6 @@ export class BlockScrollStrategy implements ScrollStrategy { // @public export class CdkConnectedOverlay implements OnDestroy, OnChanges { constructor(...args: unknown[]); - asPopover: boolean; readonly attach: EventEmitter; attachOverlay(): void; backdropClass: string | string[]; @@ -59,8 +58,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { minHeight: number | string; minWidth: number | string; // (undocumented) - static ngAcceptInputType_asPopover: unknown; - // (undocumented) static ngAcceptInputType_disposeOnNavigation: unknown; // (undocumented) static ngAcceptInputType_flexibleDimensions: unknown; @@ -73,6 +70,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { // (undocumented) static ngAcceptInputType_push: unknown; // (undocumented) + static ngAcceptInputType_usePopover: unknown; + // (undocumented) ngOnChanges(changes: SimpleChanges): void; // (undocumented) ngOnDestroy(): void; @@ -92,10 +91,11 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { push: boolean; scrollStrategy: ScrollStrategy; transformOriginSelector: string; + usePopover: boolean; viewportMargin: ViewportMargin; width: number | string; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -210,7 +210,7 @@ export function createCloseScrollStrategy(injector: Injector, config?: CloseScro export function createFlexibleConnectedPositionStrategy(injector: Injector, origin: FlexibleConnectedPositionStrategyOrigin): FlexibleConnectedPositionStrategy; // @public -export function createGlobalPositionStrategy(_injector: Injector): GlobalPositionStrategy; +export function createGlobalPositionStrategy(injector: Injector): GlobalPositionStrategy; // @public export function createNoopScrollStrategy(): NoopScrollStrategy; @@ -225,24 +225,17 @@ export function createRepositionScrollStrategy(injector: Injector, config?: Repo export class FlexibleConnectedPositionStrategy implements PositionStrategy { constructor(connectedTo: FlexibleConnectedPositionStrategyOrigin, _viewportRuler: ViewportRuler, _document: Document, _platform: Platform, _overlayContainer: OverlayContainer); apply(): void; - asPopover(isPopover: boolean): this; attach(overlayRef: OverlayRef): void; - attachBackdrop(backdrop: HTMLElement, host: HTMLElement): boolean; - attachHost(host: HTMLElement): boolean; - createStructure(): { - pane: HTMLDivElement; - host: HTMLDivElement; - } | null; // (undocumented) detach(): void; dispose(): void; + getPopoverInsertionPoint(): Element; _origin: FlexibleConnectedPositionStrategyOrigin; positionChanges: Observable; get positions(): ConnectionPositionPair[]; _preferredPositions: ConnectionPositionPair[]; reapplyLastPosition(): void; setOrigin(origin: FlexibleConnectedPositionStrategyOrigin): this; - updateStackingOrder(): boolean; withDefaultOffsetX(offset: number): this; withDefaultOffsetY(offset: number): this; withFlexibleDimensions(flexibleDimensions?: boolean): this; @@ -277,6 +270,7 @@ export class FullscreenOverlayContainer extends OverlayContainer implements OnDe // @public export class GlobalPositionStrategy implements PositionStrategy { + constructor(injector?: Injector); apply(): void; // (undocumented) attach(overlayRef: OverlayRef): void; @@ -285,6 +279,7 @@ export class GlobalPositionStrategy implements PositionStrategy { centerVertically(offset?: string): this; dispose(): void; end(value?: string): this; + getPopoverInsertionPoint(): Element; // @deprecated height(value?: string): this; left(value?: string): this; @@ -342,6 +337,7 @@ export class OverlayConfig { panelClass?: string | string[]; positionStrategy?: PositionStrategy; scrollStrategy?: ScrollStrategy; + usePopover?: boolean; width?: number | string; } @@ -470,15 +466,9 @@ export interface OverlaySizeConfig { export interface PositionStrategy { apply(): void; attach(overlayRef: OverlayRef): void; - attachBackdrop?(backdrop: HTMLElement, host: HTMLElement): boolean; - attachHost?(host: HTMLElement): boolean; - createStructure?(): { - pane: HTMLElement; - host: HTMLElement; - } | null; detach?(): void; dispose(): void; - updateStackingOrder?(host: HTMLElement): boolean; + getPopoverInsertionPoint?(): Element; } // @public diff --git a/goldens/material/select/index.api.md b/goldens/material/select/index.api.md index 2f02eff60d41..d0975c05859a 100644 --- a/goldens/material/select/index.api.md +++ b/goldens/material/select/index.api.md @@ -316,7 +316,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit ngOnInit(): void; _onBlur(): void; _onChange: (value: any) => void; - onContainerClick(): void; + onContainerClick(event: MouseEvent): void; // (undocumented) _onFocus(): void; _onTouched: () => void; diff --git a/goldens/material/timepicker/index.api.md b/goldens/material/timepicker/index.api.md index d97326892cd0..29b17a051d43 100644 --- a/goldens/material/timepicker/index.api.md +++ b/goldens/material/timepicker/index.api.md @@ -46,6 +46,8 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { readonly disabled: Signal; readonly disableRipple: InputSignalWithTransform; protected _getAriaLabelledby(): string | null; + // (undocumented) + _getOverlayHost(): HTMLElement | undefined; protected _handleAnimationEnd(event: AnimationEvent): void; readonly interval: InputSignalWithTransform; readonly isOpen: Signal; diff --git a/src/cdk/dialog/dialog.ts b/src/cdk/dialog/dialog.ts index 45ac68f5961c..93ea177750bf 100644 --- a/src/cdk/dialog/dialog.ts +++ b/src/cdk/dialog/dialog.ts @@ -401,7 +401,8 @@ export class Dialog implements OnDestroy { sibling !== overlayContainer && sibling.nodeName !== 'SCRIPT' && sibling.nodeName !== 'STYLE' && - !sibling.hasAttribute('aria-live') + !sibling.hasAttribute('aria-live') && + !sibling.hasAttribute('popover') ) { this._ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden')); sibling.setAttribute('aria-hidden', 'true'); diff --git a/src/cdk/overlay/overlay-config.ts b/src/cdk/overlay/overlay-config.ts index fa1d96551280..c382a1a5fe02 100644 --- a/src/cdk/overlay/overlay-config.ts +++ b/src/cdk/overlay/overlay-config.ts @@ -61,6 +61,12 @@ export class OverlayConfig { */ disposeOnNavigation?: boolean = false; + /** + * Whether the overlay should be rendered as a native popover element, + * rather than placing it inside of the overlay container. + */ + usePopover?: boolean = false; + constructor(config?: OverlayConfig) { if (config) { // Use `Iterable` instead of `Array` because TypeScript, as of 3.6.3, diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index 914d4c0aa8f1..62ed6d4ab868 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -222,8 +222,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { } /** Whether the connected overlay should be rendered inside a popover element or the overlay container. */ - @Input({alias: 'cdkConnectedOverlayAsPopover', transform: booleanAttribute}) - asPopover: boolean = false; + @Input({alias: 'cdkConnectedOverlayUsePopover', transform: booleanAttribute}) + usePopover: boolean = false; /** Event emitted when the backdrop is clicked. */ @Output() readonly backdropClick = new EventEmitter(); @@ -331,6 +331,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { scrollStrategy: this.scrollStrategy, hasBackdrop: this.hasBackdrop, disposeOnNavigation: this.disposeOnNavigation, + usePopover: this.usePopover, }); if (this.width || this.width === 0) { @@ -380,8 +381,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { .withGrowAfterOpen(this.growAfterOpen) .withViewportMargin(this.viewportMargin) .withLockedPosition(this.lockPosition) - .withTransformOriginOn(this.transformOriginSelector) - .asPopover(this.asPopover); + .withTransformOriginOn(this.transformOriginSelector); } /** Returns the position strategy of the overlay to be set on the overlay config */ diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index 8a976be76d87..c78012a5c9c8 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -117,23 +117,11 @@ export class OverlayRef implements PortalOutlet { attach(portal: Portal): any { // Insert the host into the DOM before attaching the portal, otherwise // the animations module will skip animations on repeat attachments. - if (!this._host.parentElement) { - const hasAttachedHost = this._positionStrategy?.attachHost?.(this._host); - - if (!hasAttachedHost && this._previousHostParent) { - this._previousHostParent.appendChild(this._host); - } - } + this._attachHost(); const attachResult = this._portalOutlet.attach(portal); this._positionStrategy?.attach(this); - - const hasUpdatedStackingOrder = this._positionStrategy?.updateStackingOrder?.(this._host); - - if (!hasUpdatedStackingOrder) { - this._updateStackingOrder(); - } - + this._updateStackingOrder(); this._updateElementSize(); this._updateElementDirection(); @@ -415,6 +403,20 @@ export class OverlayRef implements PortalOutlet { this._pane.style.pointerEvents = enablePointer ? '' : 'none'; } + private _attachHost() { + if (!this._host.parentElement) { + if (this._config.usePopover && this._positionStrategy?.getPopoverInsertionPoint) { + this._positionStrategy.getPopoverInsertionPoint().after(this._host); + } else { + this._previousHostParent?.appendChild(this._host); + } + } + + if (this._config.usePopover) { + this._host.showPopover(); + } + } + /** Attaches a backdrop for this overlay. */ private _attachBackdrop() { const showingClass = 'cdk-overlay-backdrop-showing'; @@ -432,12 +434,10 @@ export class OverlayRef implements PortalOutlet { this._toggleClasses(this._backdropRef.element, this._config.backdropClass, true); } - const strategyAttached = this._positionStrategy?.attachBackdrop?.( - this._backdropRef.element, - this._host, - ); - - if (!strategyAttached) { + if (this._config.usePopover) { + // When using popovers, the backdrop needs to be inside the popover. + this._host.prepend(this._backdropRef.element); + } else { // Insert the backdrop before the pane in the DOM order, // in order to handle stacked overlays properly. this._host.parentElement!.insertBefore(this._backdropRef.element, this._host); @@ -461,7 +461,7 @@ export class OverlayRef implements PortalOutlet { * in its original DOM position. */ private _updateStackingOrder() { - if (this._host.nextSibling) { + if (!this._config.usePopover && this._host.nextSibling) { this._host.parentNode!.appendChild(this._host); } } diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index 93ea0642c635..1e3f059a0ddd 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -47,34 +47,34 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov const idGenerator = injector.get(_IdGenerator); const appRef = injector.get(ApplicationRef); const directionality = injector.get(Directionality); - const overlayConfig = new OverlayConfig(config); - const customStructure = overlayConfig.positionStrategy?.createStructure?.(); + const renderer = + injector.get(Renderer2, null, {optional: true}) || + injector.get(RendererFactory2).createRenderer(null, null); - let pane: HTMLElement; - let host: HTMLElement; + const overlayConfig = new OverlayConfig(config); - if (customStructure) { - pane = customStructure.pane; - host = customStructure.host; - } else { - host = doc.createElement('div'); - pane = doc.createElement('div'); - host.appendChild(pane); - overlayContainer.getContainerElement().appendChild(host); - } + overlayConfig.direction = overlayConfig.direction || directionality.value; + overlayConfig.usePopover = !!overlayConfig?.usePopover && 'showPopover' in doc.body; + const pane = doc.createElement('div'); + const host = doc.createElement('div'); pane.id = idGenerator.getId('cdk-overlay-'); pane.classList.add('cdk-overlay-pane'); + host.appendChild(pane); - const portalOutlet = new DomPortalOutlet(pane, appRef, injector); - const renderer = - injector.get(Renderer2, null, {optional: true}) || - injector.get(RendererFactory2).createRenderer(null, null); + if (overlayConfig.usePopover) { + host.setAttribute('popover', 'manual'); + host.classList.add('cdk-overlay-popover'); + } - overlayConfig.direction = overlayConfig.direction || directionality.value; + if (overlayConfig.usePopover && overlayConfig.positionStrategy?.getPopoverInsertionPoint) { + overlayConfig.positionStrategy.getPopoverInsertionPoint().after(host); + } else { + overlayContainer.getContainerElement().appendChild(host); + } return new OverlayRef( - portalOutlet, + new DomPortalOutlet(pane, appRef, injector), host, pane, overlayConfig, diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts index 90e2b558b391..bfb472aff4d6 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -2982,7 +2982,7 @@ describe('FlexibleConnectedPositionStrategy', () => { }); it('should place the overlay inside the overlay container by default', () => { - attachOverlay({positionStrategy}); + attachOverlay({positionStrategy, usePopover: false}); expect(containerElement.contains(overlayRef.hostElement)).toBe(true); expect(overlayRef.hostElement.getAttribute('popover')).toBeFalsy(); }); @@ -2992,8 +2992,7 @@ describe('FlexibleConnectedPositionStrategy', () => { return; } - positionStrategy.asPopover(true); - attachOverlay({positionStrategy}); + attachOverlay({positionStrategy, usePopover: true}); expect(containerElement.contains(overlayRef.hostElement)).toBe(false); expect(originElement.nextElementSibling).toBe(overlayRef.hostElement); @@ -3005,8 +3004,7 @@ describe('FlexibleConnectedPositionStrategy', () => { return; } - positionStrategy.asPopover(true); - attachOverlay({positionStrategy}); + attachOverlay({positionStrategy, usePopover: true}); expect(originElement.nextElementSibling).toBe(overlayRef.hostElement); overlayRef.detach(); diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 16719c4cf43c..7816b5f0c86b 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -95,9 +95,6 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Whether the overlay position is locked. */ private _positionLocked = false; - /** Whether the overlay is using popovers for positioning. */ - private _popoverEnabled = false; - /** Cached origin dimensions */ private _originRect: Dimensions; @@ -514,71 +511,16 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { return this; } - /** - * Configures that the overlay should be rendered inside a native popover. This has the benefit - * if co-locating the overlay with the trigger and being better for accessibility. - * @param isPopover Whether the overlay should be a popover. - */ - asPopover(isPopover: boolean): this { - this._popoverEnabled = isPopover && 'showPopover' in this._document.body; - return this; - } - /** @docs-private */ - createStructure() { - if (!this._popoverEnabled) { - return null; - } - - const pane = this._document.createElement('div'); - const host = this._document.createElement('div'); - host.setAttribute('popover', 'manual'); - host.classList.add('cdk-overlay-popover'); - host.appendChild(pane); - this.attachHost(host); - return {pane, host}; - } - - /** @docs-private */ - attachHost(host: HTMLElement): boolean { - if (!this._popoverEnabled) { - return false; - } - - if (!host.parentNode) { - let originEl: Element | null; - - if (this._origin instanceof ElementRef) { - originEl = this._origin.nativeElement; - } else if (typeof Element !== 'undefined' && this._origin instanceof Element) { - originEl = this._origin; - } else { - originEl = null; - } - - if (originEl) { - originEl.after(host); - } else { - document.body.appendChild(host); - } - } - - host.showPopover(); - return true; - } + getPopoverInsertionPoint(): Element { + const origin = this._origin; - /** @docs-private */ - attachBackdrop(backdrop: HTMLElement, host: HTMLElement): boolean { - if (this._popoverEnabled) { - host.prepend(backdrop); + if (origin instanceof ElementRef) { + return origin.nativeElement; + } else if (origin instanceof Element) { + return origin; } - return this._popoverEnabled; - } - - /** @docs-private */ - updateStackingOrder(): boolean { - // We don't need to update the stacking order since popovers handle it for us. - return this._popoverEnabled; + return document.body.lastChild as Element; } /** diff --git a/src/cdk/overlay/position/global-position-strategy.spec.ts b/src/cdk/overlay/position/global-position-strategy.spec.ts index 7897d619beb2..550b12c271f5 100644 --- a/src/cdk/overlay/position/global-position-strategy.spec.ts +++ b/src/cdk/overlay/position/global-position-strategy.spec.ts @@ -7,11 +7,14 @@ import { OverlayRef, createOverlayRef, createGlobalPositionStrategy, + GlobalPositionStrategy, + OverlayContainer, } from '../index'; describe('GlobalPositonStrategy', () => { let overlayRef: OverlayRef; let injector: Injector; + let portal: ComponentPortal; beforeEach(() => { injector = TestBed.inject(Injector); @@ -25,7 +28,7 @@ describe('GlobalPositonStrategy', () => { }); function attachOverlay(config: OverlayConfig): OverlayRef { - const portal = new ComponentPortal(BlankPortal); + portal = new ComponentPortal(BlankPortal); overlayRef = createOverlayRef(injector, config); overlayRef.attach(portal); TestBed.inject(ApplicationRef).tick(); @@ -469,6 +472,50 @@ describe('GlobalPositonStrategy', () => { expect(elementStyle.marginRight).toBe(''); expect(parentStyle.justifyContent).toBe('flex-end'); }); + + describe('DOM location', () => { + let positionStrategy: GlobalPositionStrategy; + let containerElement: HTMLElement; + + beforeEach(() => { + containerElement = TestBed.inject(OverlayContainer).getContainerElement(); + positionStrategy = createGlobalPositionStrategy(injector); + }); + + it('should place the overlay inside the overlay container by default', () => { + attachOverlay({positionStrategy, usePopover: false}); + expect(containerElement.contains(overlayRef.hostElement)).toBe(true); + expect(overlayRef.hostElement.getAttribute('popover')).toBeFalsy(); + }); + + it('should be able to opt into placing the overlay inside a popover element', () => { + if (!('showPopover' in document.body)) { + return; + } + + attachOverlay({positionStrategy, usePopover: true}); + + expect(containerElement.contains(overlayRef.hostElement)).toBe(false); + expect(document.body.lastChild).toBe(overlayRef.hostElement); + expect(overlayRef.hostElement.getAttribute('popover')).toBe('manual'); + }); + + it('should re-attach the popover at the end of the body', () => { + if (!('showPopover' in document.body)) { + return; + } + + attachOverlay({positionStrategy, usePopover: true}); + expect(document.body.lastChild).toBe(overlayRef.hostElement); + + overlayRef.detach(); + TestBed.inject(ApplicationRef).tick(); + expect(overlayRef.hostElement.parentNode).toBeFalsy(); + + overlayRef.attach(portal); + expect(document.body.lastChild).toBe(overlayRef.hostElement); + }); + }); }); @Component({ diff --git a/src/cdk/overlay/position/global-position-strategy.ts b/src/cdk/overlay/position/global-position-strategy.ts index c174079b2b4a..a29af9b31651 100644 --- a/src/cdk/overlay/position/global-position-strategy.ts +++ b/src/cdk/overlay/position/global-position-strategy.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Injector} from '@angular/core'; +import {DOCUMENT, Injector} from '@angular/core'; import {OverlayRef} from '../overlay-ref'; import {PositionStrategy} from './position-strategy'; @@ -17,10 +17,8 @@ const wrapperClass = 'cdk-global-overlay-wrapper'; * Creates a global position strategy. * @param injector Injector used to resolve dependencies for the strategy. */ -export function createGlobalPositionStrategy(_injector: Injector): GlobalPositionStrategy { - // Note: `injector` is unused, but we may need it in - // the future which would introduce a breaking change. - return new GlobalPositionStrategy(); +export function createGlobalPositionStrategy(injector: Injector): GlobalPositionStrategy { + return new GlobalPositionStrategy(injector); } /** @@ -41,6 +39,13 @@ export class GlobalPositionStrategy implements PositionStrategy { private _width = ''; private _height = ''; private _isDisposed = false; + private _document: Document; + + constructor(injector?: Injector) { + // TODO(crisbeto): injector should be required, but some internal apps + // don't go through `createGlobalPositionStrategy` so they don't provide it. + this._document = injector?.get(DOCUMENT) || document; + } attach(overlayRef: OverlayRef): void { const config = overlayRef.getConfig(); @@ -269,4 +274,9 @@ export class GlobalPositionStrategy implements PositionStrategy { this._overlayRef = null!; this._isDisposed = true; } + + /** @docs-private */ + getPopoverInsertionPoint(): Element { + return this._document.body.lastChild as Element; + } } diff --git a/src/cdk/overlay/position/position-strategy.ts b/src/cdk/overlay/position/position-strategy.ts index 490116619f5a..de0e96626502 100644 --- a/src/cdk/overlay/position/position-strategy.ts +++ b/src/cdk/overlay/position/position-strategy.ts @@ -23,29 +23,8 @@ export interface PositionStrategy { dispose(): void; /** - * Creates the structure of the overlay. If not provided or if it returns null, - * structure will be created inside the overlay container. + * Gets the element in the DOM after which to insert + * the overlay when it is rendered out as a popover. */ - createStructure?(): {pane: HTMLElement; host: HTMLElement} | null; - - /** - * Attaches the host element to the DOM. The return value indicates whether the - * position strategy ended up attaching the host. If it didn't, it will fall back - * to the default behavior. - */ - attachHost?(host: HTMLElement): boolean; - - /** - * Attaches the backdrop element to the host. The return value indicates whether the - * position strategy ended up attaching the backdrop. If it didn't, it will fall back - * to the default behavior. - */ - attachBackdrop?(backdrop: HTMLElement, host: HTMLElement): boolean; - - /** - * Updates the stacking order of the overlay. The return value indicates whether the - * position strategy ended up updating the stacking order. If it hasn't, the overlay - * will fall back to the default stacking update logic. - */ - updateStackingOrder?(host: HTMLElement): boolean; + getPopoverInsertionPoint?(): Element; } diff --git a/src/material/select/select.ts b/src/material/select/select.ts index b05f7f8b6e3a..80bfea9f75a0 100644 --- a/src/material/select/select.ts +++ b/src/material/select/select.ts @@ -71,6 +71,7 @@ import { NgForm, Validators, } from '@angular/forms'; +import {_getEventTarget} from '@angular/cdk/platform'; import { _animationsDisabled, _countGroupLabelsBeforeOption, @@ -1461,10 +1462,12 @@ export class MatSelect * @docs-private */ setDescribedByIds(ids: string[]) { + const element = this._elementRef.nativeElement; + if (ids.length) { - this._elementRef.nativeElement.setAttribute('aria-describedby', ids.join(' ')); + element.setAttribute('aria-describedby', ids.join(' ')); } else { - this._elementRef.nativeElement.removeAttribute('aria-describedby'); + element.removeAttribute('aria-describedby'); } } @@ -1472,9 +1475,14 @@ export class MatSelect * Implemented as part of MatFormFieldControl. * @docs-private */ - onContainerClick() { - this.focus(); - this.open(); + onContainerClick(event: MouseEvent) { + const target = _getEventTarget(event) as Node | null; + const overlayHost = this._overlayDir.overlayRef?.hostElement; + + if (!target || !overlayHost || !overlayHost.contains(target)) { + this.focus(); + this.open(); + } } /** diff --git a/src/material/timepicker/timepicker-input.ts b/src/material/timepicker/timepicker-input.ts index a6fbe28430b9..656860228778 100644 --- a/src/material/timepicker/timepicker-input.ts +++ b/src/material/timepicker/timepicker-input.ts @@ -41,7 +41,7 @@ import {MAT_INPUT_VALUE_ACCESSOR} from '../input'; import {Subscription} from 'rxjs'; import {DOWN_ARROW, ESCAPE, hasModifierKey, UP_ARROW} from '@angular/cdk/keycodes'; import {validateAdapter} from './util'; -import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; +import {_getEventTarget, _getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; /** * Input that can be used to enter time and connect to a `mat-timepicker`. @@ -265,8 +265,15 @@ export class MatTimepickerInput } /** Handles clicks on the input or the containing form field. */ - private _handleClick = (): void => { - if (!this.disabled() && this.openOnClick()) { + private _handleClick = (event: MouseEvent): void => { + if (this.disabled() || !this.openOnClick()) { + return; + } + + const target = _getEventTarget(event) as Node | null; + const overlayHost = this.timepicker()._getOverlayHost(); + + if (!target || !overlayHost || !overlayHost.contains(target)) { this.timepicker().open(); } }; diff --git a/src/material/timepicker/timepicker.ts b/src/material/timepicker/timepicker.ts index 042e0524a321..768fef0df6ba 100644 --- a/src/material/timepicker/timepicker.ts +++ b/src/material/timepicker/timepicker.ts @@ -312,6 +312,10 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { this._overlayRef?.dispose(); } + _getOverlayHost() { + return this._overlayRef?.hostElement; + } + /** Selects a specific time value. */ protected _selectValue(option: MatOption) { this.close();