diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index b43323a18566..26f46ecd8661 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -197,6 +197,27 @@ export class ConnectionPositionPair { panelClass?: string | string[] | undefined; } +// @public +export function createBlockScrollStrategy(injector: Injector): BlockScrollStrategy; + +// @public +export function createCloseScrollStrategy(injector: Injector, config?: CloseScrollStrategyConfig): CloseScrollStrategy; + +// @public +export function createFlexibleConnectedPositionStrategy(injector: Injector, origin: FlexibleConnectedPositionStrategyOrigin): FlexibleConnectedPositionStrategy; + +// @public +export function createGlobalPositionStrategy(_injector: Injector): GlobalPositionStrategy; + +// @public +export function createNoopScrollStrategy(): NoopScrollStrategy; + +// @public +export function createOverlayRef(injector: Injector, config?: OverlayConfig): OverlayRef; + +// @public +export function createRepositionScrollStrategy(injector: Injector, config?: RepositionScrollStrategyConfig): RepositionScrollStrategy; + // @public export class FlexibleConnectedPositionStrategy implements PositionStrategy { constructor(connectedTo: FlexibleConnectedPositionStrategyOrigin, _viewportRuler: ViewportRuler, _document: Document, _platform: Platform, _overlayContainer: OverlayContainer); diff --git a/src/cdk/overlay/fullscreen-overlay-container.spec.ts b/src/cdk/overlay/fullscreen-overlay-container.spec.ts index 77615ae877b7..3a9942acc111 100644 --- a/src/cdk/overlay/fullscreen-overlay-container.spec.ts +++ b/src/cdk/overlay/fullscreen-overlay-container.spec.ts @@ -23,7 +23,7 @@ describe('FullscreenOverlayContainer', () => { // stubs here, we should reconsider whether to use a Proxy instead. Avoiding a proxy for // now since it isn't supported on IE. See: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy - fakeDocument = { + return { body: document.body, head: document.head, fullscreenElement: document.createElement('div'), @@ -51,20 +51,15 @@ describe('FullscreenOverlayContainer', () => { createTextNode: (...args: [string]) => document.createTextNode(...args), createComment: (...args: [string]) => document.createComment(...args), }; - - return fakeDocument; }, }, ], }); overlay = TestBed.inject(Overlay); + fakeDocument = TestBed.inject(DOCUMENT); })); - afterEach(() => { - fakeDocument = null; - }); - it('should open an overlay inside a fullscreen element and move it to the body', () => { const fixture = TestBed.createComponent(TestComponentWithTemplatePortals); fixture.detectChanges(); diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index 2ec785b52568..935196bd2c3e 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -19,6 +19,7 @@ import { inject, RendererFactory2, DOCUMENT, + Renderer2, } from '@angular/core'; import {_IdGenerator} from '../a11y'; import {_CdkPrivateStyleLoader} from '../private'; @@ -30,6 +31,56 @@ import {OverlayRef} from './overlay-ref'; import {OverlayPositionBuilder} from './position/overlay-position-builder'; import {ScrollStrategyOptions} from './scroll/index'; +/** + * Creates an overlay. + * @param injector Injector to use when resolving the overlay's dependencies. + * @param config Configuration applied to the overlay. + * @returns Reference to the created overlay. + */ +export function createOverlayRef(injector: Injector, config?: OverlayConfig): OverlayRef { + // This is done in the overlay container as well, but we have it here + // since it's common to mock out the overlay container in tests. + injector.get(_CdkPrivateStyleLoader).load(_CdkOverlayStyleLoader); + + const overlayContainer = injector.get(OverlayContainer); + const doc = injector.get(DOCUMENT); + const idGenerator = injector.get(_IdGenerator); + const appRef = injector.get(ApplicationRef); + const directionality = injector.get(Directionality); + + const host = doc.createElement('div'); + const pane = doc.createElement('div'); + + pane.id = idGenerator.getId('cdk-overlay-'); + pane.classList.add('cdk-overlay-pane'); + host.appendChild(pane); + overlayContainer.getContainerElement().appendChild(host); + + const portalOutlet = new DomPortalOutlet(pane, appRef, injector); + const overlayConfig = new OverlayConfig(config); + const renderer = + injector.get(Renderer2, null, {optional: true}) || + injector.get(RendererFactory2).createRenderer(null, null); + + overlayConfig.direction = overlayConfig.direction || directionality.value; + + return new OverlayRef( + portalOutlet, + host, + pane, + overlayConfig, + injector.get(NgZone), + injector.get(OverlayKeyboardDispatcher), + doc, + injector.get(Location), + injector.get(OverlayOutsideClickDispatcher), + config?.disableAnimations ?? + injector.get(ANIMATION_MODULE_TYPE, null, {optional: true}) === 'NoopAnimations', + injector.get(EnvironmentInjector), + renderer, + ); +} + /** * Service to create Overlays. Overlays are dynamically added pieces of floating UI, meant to be * used as a low-level building block for other components. Dialogs, tooltips, menus, @@ -41,21 +92,8 @@ import {ScrollStrategyOptions} from './scroll/index'; @Injectable({providedIn: 'root'}) export class Overlay { scrollStrategies = inject(ScrollStrategyOptions); - private _overlayContainer = inject(OverlayContainer); private _positionBuilder = inject(OverlayPositionBuilder); - private _keyboardDispatcher = inject(OverlayKeyboardDispatcher); private _injector = inject(Injector); - private _ngZone = inject(NgZone); - private _document = inject(DOCUMENT); - private _directionality = inject(Directionality); - private _location = inject(Location); - private _outsideClickDispatcher = inject(OverlayOutsideClickDispatcher); - private _animationsModuleType = inject(ANIMATION_MODULE_TYPE, {optional: true}); - private _idGenerator = inject(_IdGenerator); - private _renderer = inject(RendererFactory2).createRenderer(null, null); - - private _appRef: ApplicationRef; - private _styleLoader = inject(_CdkPrivateStyleLoader); constructor(...args: unknown[]); constructor() {} @@ -66,31 +104,7 @@ export class Overlay { * @returns Reference to the created overlay. */ create(config?: OverlayConfig): OverlayRef { - // This is done in the overlay container as well, but we have it here - // since it's common to mock out the overlay container in tests. - this._styleLoader.load(_CdkOverlayStyleLoader); - - const host = this._createHostElement(); - const pane = this._createPaneElement(host); - const portalOutlet = this._createPortalOutlet(pane); - const overlayConfig = new OverlayConfig(config); - - overlayConfig.direction = overlayConfig.direction || this._directionality.value; - - return new OverlayRef( - portalOutlet, - host, - pane, - overlayConfig, - this._ngZone, - this._keyboardDispatcher, - this._document, - this._location, - this._outsideClickDispatcher, - config?.disableAnimations ?? this._animationsModuleType === 'NoopAnimations', - this._injector.get(EnvironmentInjector), - this._renderer, - ); + return createOverlayRef(this._injector, config); } /** @@ -101,44 +115,4 @@ export class Overlay { position(): OverlayPositionBuilder { return this._positionBuilder; } - - /** - * Creates the DOM element for an overlay and appends it to the overlay container. - * @returns Newly-created pane element - */ - private _createPaneElement(host: HTMLElement): HTMLElement { - const pane = this._document.createElement('div'); - - pane.id = this._idGenerator.getId('cdk-overlay-'); - pane.classList.add('cdk-overlay-pane'); - host.appendChild(pane); - - return pane; - } - - /** - * Creates the host element that wraps around an overlay - * and can be used for advanced positioning. - * @returns Newly-create host element. - */ - private _createHostElement(): HTMLElement { - const host = this._document.createElement('div'); - this._overlayContainer.getContainerElement().appendChild(host); - return host; - } - - /** - * Create a DomPortalOutlet into which the overlay content can be loaded. - * @param pane The DOM element to turn into a portal outlet. - * @returns A portal outlet for the given DOM element. - */ - private _createPortalOutlet(pane: HTMLElement): DomPortalOutlet { - // We have to resolve the ApplicationRef later in order to allow people - // to use overlay-based providers during app initialization. - if (!this._appRef) { - this._appRef = this._injector.get(ApplicationRef); - } - - return new DomPortalOutlet(pane, this._appRef, this._injector); - } } diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 7e14097ac4cd..21932086e888 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -7,7 +7,7 @@ */ import {PositionStrategy} from './position-strategy'; -import {ElementRef} from '@angular/core'; +import {DOCUMENT, ElementRef, Injector} from '@angular/core'; import {ViewportRuler, CdkScrollable, ViewportScrollPosition} from '../../scrolling'; import { ConnectedOverlayPositionChange, @@ -44,6 +44,24 @@ export type FlexibleConnectedPositionStrategyOrigin = /** Equivalent of `DOMRect` without some of the properties we don't care about. */ type Dimensions = Omit; +/** + * Creates a flexible position strategy. + * @param injector Injector used to resolve dependnecies for the position strategy. + * @param origin Origin relative to which to position the overlay. + */ +export function createFlexibleConnectedPositionStrategy( + injector: Injector, + origin: FlexibleConnectedPositionStrategyOrigin, +): FlexibleConnectedPositionStrategy { + return new FlexibleConnectedPositionStrategy( + origin, + injector.get(ViewportRuler), + injector.get(DOCUMENT), + injector.get(Platform), + injector.get(OverlayContainer), + ); +} + /** * A strategy for positioning overlays. Using this strategy, an overlay is given an * implicit position relative some origin element. The relative position is defined in terms of diff --git a/src/cdk/overlay/position/global-position-strategy.ts b/src/cdk/overlay/position/global-position-strategy.ts index 94481f034cff..c174079b2b4a 100644 --- a/src/cdk/overlay/position/global-position-strategy.ts +++ b/src/cdk/overlay/position/global-position-strategy.ts @@ -6,12 +6,23 @@ * found in the LICENSE file at https://angular.dev/license */ +import {Injector} from '@angular/core'; import {OverlayRef} from '../overlay-ref'; import {PositionStrategy} from './position-strategy'; /** Class to be added to the overlay pane wrapper. */ 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(); +} + /** * A strategy for positioning overlays. Using this strategy, an overlay is given an * explicit position relative to the browser's viewport. We use flexbox, instead of diff --git a/src/cdk/overlay/position/overlay-position-builder.ts b/src/cdk/overlay/position/overlay-position-builder.ts index 266dea042fab..671bbe2953da 100644 --- a/src/cdk/overlay/position/overlay-position-builder.ts +++ b/src/cdk/overlay/position/overlay-position-builder.ts @@ -6,24 +6,18 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Platform} from '../../platform'; -import {ViewportRuler} from '../../scrolling'; - -import {Injectable, inject, DOCUMENT} from '@angular/core'; -import {OverlayContainer} from '../overlay-container'; +import {Injectable, Injector, inject} from '@angular/core'; import { + createFlexibleConnectedPositionStrategy, FlexibleConnectedPositionStrategy, FlexibleConnectedPositionStrategyOrigin, } from './flexible-connected-position-strategy'; -import {GlobalPositionStrategy} from './global-position-strategy'; +import {createGlobalPositionStrategy, GlobalPositionStrategy} from './global-position-strategy'; /** Builder for overlay position strategy. */ @Injectable({providedIn: 'root'}) export class OverlayPositionBuilder { - private _viewportRuler = inject(ViewportRuler); - private _document = inject(DOCUMENT); - private _platform = inject(Platform); - private _overlayContainer = inject(OverlayContainer); + private _injector = inject(Injector); constructor(...args: unknown[]); constructor() {} @@ -32,7 +26,7 @@ export class OverlayPositionBuilder { * Creates a global position strategy. */ global(): GlobalPositionStrategy { - return new GlobalPositionStrategy(); + return createGlobalPositionStrategy(this._injector); } /** @@ -42,12 +36,6 @@ export class OverlayPositionBuilder { flexibleConnectedTo( origin: FlexibleConnectedPositionStrategyOrigin, ): FlexibleConnectedPositionStrategy { - return new FlexibleConnectedPositionStrategy( - origin, - this._viewportRuler, - this._document, - this._platform, - this._overlayContainer, - ); + return createFlexibleConnectedPositionStrategy(this._injector, origin); } } diff --git a/src/cdk/overlay/public-api.ts b/src/cdk/overlay/public-api.ts index 82367446c882..43a89dfdffea 100644 --- a/src/cdk/overlay/public-api.ts +++ b/src/cdk/overlay/public-api.ts @@ -11,7 +11,7 @@ export * from './position/connected-position'; export * from './scroll/index'; export * from './overlay-module'; export * from './dispatchers/index'; -export {Overlay} from './overlay'; +export {Overlay, createOverlayRef} from './overlay'; export {OverlayContainer} from './overlay-container'; export {CdkOverlayOrigin, CdkConnectedOverlay} from './overlay-directives'; export {FullscreenOverlayContainer} from './fullscreen-overlay-container'; @@ -22,11 +22,15 @@ export {OverlayPositionBuilder} from './position/overlay-position-builder'; // Export pre-defined position strategies and interface to build custom ones. export {PositionStrategy} from './position/position-strategy'; -export {GlobalPositionStrategy} from './position/global-position-strategy'; +export { + GlobalPositionStrategy, + createGlobalPositionStrategy, +} from './position/global-position-strategy'; export { ConnectedPosition, FlexibleConnectedPositionStrategy, FlexibleConnectedPositionStrategyOrigin, STANDARD_DROPDOWN_ADJACENT_POSITIONS, STANDARD_DROPDOWN_BELOW_POSITIONS, + createFlexibleConnectedPositionStrategy, } from './position/flexible-connected-position-strategy'; diff --git a/src/cdk/overlay/scroll/block-scroll-strategy.ts b/src/cdk/overlay/scroll/block-scroll-strategy.ts index 5ebff3d04a5d..a481f1b81f9a 100644 --- a/src/cdk/overlay/scroll/block-scroll-strategy.ts +++ b/src/cdk/overlay/scroll/block-scroll-strategy.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import {DOCUMENT, Injector} from '@angular/core'; import {ScrollStrategy} from './scroll-strategy'; import {ViewportRuler} from '../../scrolling'; import {coerceCssPixelValue} from '../../coercion'; @@ -13,6 +14,15 @@ import {supportsScrollBehavior} from '../../platform'; const scrollBehaviorSupported = supportsScrollBehavior(); +/** + * Creates a scroll strategy that prevents the user from scrolling while the overlay is open. + * @param injector Injector used to resolve dependencies of the scroll strategy. + * @param config Configuration options for the scroll strategy. + */ +export function createBlockScrollStrategy(injector: Injector): BlockScrollStrategy { + return new BlockScrollStrategy(injector.get(ViewportRuler), injector.get(DOCUMENT)); +} + /** * Strategy that will prevent the user from scrolling while the overlay is visible. */ diff --git a/src/cdk/overlay/scroll/close-scroll-strategy.ts b/src/cdk/overlay/scroll/close-scroll-strategy.ts index 027a53526c32..2703e386352d 100644 --- a/src/cdk/overlay/scroll/close-scroll-strategy.ts +++ b/src/cdk/overlay/scroll/close-scroll-strategy.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -import {NgZone} from '@angular/core'; +import {Injector, NgZone} from '@angular/core'; import {ScrollStrategy, getMatScrollStrategyAlreadyAttachedError} from './scroll-strategy'; import {Subscription} from 'rxjs'; import {ScrollDispatcher, ViewportRuler} from '../../scrolling'; @@ -20,6 +20,23 @@ export interface CloseScrollStrategyConfig { threshold?: number; } +/** + * Creates a scroll strategy that closes the overlay when the user starts to scroll. + * @param injector Injector used to resolve dependencies of the scroll strategy. + * @param config Configuration options for the scroll strategy. + */ +export function createCloseScrollStrategy( + injector: Injector, + config?: CloseScrollStrategyConfig, +): CloseScrollStrategy { + return new CloseScrollStrategy( + injector.get(ScrollDispatcher), + injector.get(NgZone), + injector.get(ViewportRuler), + config, + ); +} + /** * Strategy that will close the overlay as soon as the user starts scrolling. */ diff --git a/src/cdk/overlay/scroll/index.ts b/src/cdk/overlay/scroll/index.ts index 82b4ca51aa2e..47a3d846beb6 100644 --- a/src/cdk/overlay/scroll/index.ts +++ b/src/cdk/overlay/scroll/index.ts @@ -14,7 +14,8 @@ export {ScrollStrategyOptions} from './scroll-strategy-options'; export { RepositionScrollStrategy, RepositionScrollStrategyConfig, + createRepositionScrollStrategy, } from './reposition-scroll-strategy'; -export {CloseScrollStrategy} from './close-scroll-strategy'; -export {NoopScrollStrategy} from './noop-scroll-strategy'; -export {BlockScrollStrategy} from './block-scroll-strategy'; +export {CloseScrollStrategy, createCloseScrollStrategy} from './close-scroll-strategy'; +export {NoopScrollStrategy, createNoopScrollStrategy} from './noop-scroll-strategy'; +export {BlockScrollStrategy, createBlockScrollStrategy} from './block-scroll-strategy'; diff --git a/src/cdk/overlay/scroll/noop-scroll-strategy.ts b/src/cdk/overlay/scroll/noop-scroll-strategy.ts index 3ed8f3ef8981..583b6468938f 100644 --- a/src/cdk/overlay/scroll/noop-scroll-strategy.ts +++ b/src/cdk/overlay/scroll/noop-scroll-strategy.ts @@ -8,6 +8,11 @@ import {ScrollStrategy} from './scroll-strategy'; +/** Creates a scroll strategy that does nothing. */ +export function createNoopScrollStrategy(): NoopScrollStrategy { + return new NoopScrollStrategy(); +} + /** Scroll strategy that doesn't do anything. */ export class NoopScrollStrategy implements ScrollStrategy { /** Does nothing, as this scroll strategy is a no-op. */ diff --git a/src/cdk/overlay/scroll/reposition-scroll-strategy.ts b/src/cdk/overlay/scroll/reposition-scroll-strategy.ts index 73596477e3a7..1c30c1c6e8b9 100644 --- a/src/cdk/overlay/scroll/reposition-scroll-strategy.ts +++ b/src/cdk/overlay/scroll/reposition-scroll-strategy.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {NgZone} from '@angular/core'; +import {Injector, NgZone} from '@angular/core'; import {Subscription} from 'rxjs'; import {ScrollStrategy, getMatScrollStrategyAlreadyAttachedError} from './scroll-strategy'; import {ScrollDispatcher, ViewportRuler} from '../../scrolling'; @@ -24,6 +24,23 @@ export interface RepositionScrollStrategyConfig { autoClose?: boolean; } +/** + * Creates a scroll strategy that updates the overlay's position when the user scrolls. + * @param injector Injector used to resolve dependencies of the scroll strategy. + * @param config Configuration options for the scroll strategy. + */ +export function createRepositionScrollStrategy( + injector: Injector, + config?: RepositionScrollStrategyConfig, +): RepositionScrollStrategy { + return new RepositionScrollStrategy( + injector.get(ScrollDispatcher), + injector.get(ViewportRuler), + injector.get(NgZone), + config, + ); +} + /** * Strategy that will update the element position as the user is scrolling. */ diff --git a/src/cdk/overlay/scroll/scroll-strategy-options.ts b/src/cdk/overlay/scroll/scroll-strategy-options.ts index 3f49b7a0d568..d57971387339 100644 --- a/src/cdk/overlay/scroll/scroll-strategy-options.ts +++ b/src/cdk/overlay/scroll/scroll-strategy-options.ts @@ -6,14 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ -import {ScrollDispatcher, ViewportRuler} from '../../scrolling'; - -import {Injectable, NgZone, inject, DOCUMENT} from '@angular/core'; -import {BlockScrollStrategy} from './block-scroll-strategy'; -import {CloseScrollStrategy, CloseScrollStrategyConfig} from './close-scroll-strategy'; +import {Injectable, Injector, inject} from '@angular/core'; +import {createBlockScrollStrategy} from './block-scroll-strategy'; +import {CloseScrollStrategyConfig, createCloseScrollStrategy} from './close-scroll-strategy'; import {NoopScrollStrategy} from './noop-scroll-strategy'; import { - RepositionScrollStrategy, + createRepositionScrollStrategy, RepositionScrollStrategyConfig, } from './reposition-scroll-strategy'; @@ -25,11 +23,7 @@ import { */ @Injectable({providedIn: 'root'}) export class ScrollStrategyOptions { - private _scrollDispatcher = inject(ScrollDispatcher); - private _viewportRuler = inject(ViewportRuler); - private _ngZone = inject(NgZone); - - private _document = inject(DOCUMENT); + private _injector = inject(Injector); constructor(...args: unknown[]); constructor() {} @@ -41,11 +35,10 @@ export class ScrollStrategyOptions { * Close the overlay as soon as the user scrolls. * @param config Configuration to be used inside the scroll strategy. */ - close = (config?: CloseScrollStrategyConfig) => - new CloseScrollStrategy(this._scrollDispatcher, this._ngZone, this._viewportRuler, config); + close = (config?: CloseScrollStrategyConfig) => createCloseScrollStrategy(this._injector, config); /** Block scrolling. */ - block = () => new BlockScrollStrategy(this._viewportRuler, this._document); + block = () => createBlockScrollStrategy(this._injector); /** * Update the overlay's position on scroll. @@ -53,5 +46,5 @@ export class ScrollStrategyOptions { * Allows debouncing the reposition calls. */ reposition = (config?: RepositionScrollStrategyConfig) => - new RepositionScrollStrategy(this._scrollDispatcher, this._viewportRuler, this._ngZone, config); + createRepositionScrollStrategy(this._injector, config); }