Skip to content

perf(cdk/overlay): add tree-shakeable alternatives for overlay APIs #30904

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
Apr 20, 2025
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
21 changes: 21 additions & 0 deletions goldens/cdk/overlay/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 2 additions & 7 deletions src/cdk/overlay/fullscreen-overlay-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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();
Expand Down
130 changes: 52 additions & 78 deletions src/cdk/overlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
inject,
RendererFactory2,
DOCUMENT,
Renderer2,
} from '@angular/core';
import {_IdGenerator} from '../a11y';
import {_CdkPrivateStyleLoader} from '../private';
Expand All @@ -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,
Expand All @@ -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() {}
Expand All @@ -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);
}

/**
Expand All @@ -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>(ApplicationRef);
}

return new DomPortalOutlet(pane, this._appRef, this._injector);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -44,6 +44,24 @@ export type FlexibleConnectedPositionStrategyOrigin =
/** Equivalent of `DOMRect` without some of the properties we don't care about. */
type Dimensions = Omit<DOMRect, 'x' | 'y' | 'toJSON'>;

/**
* 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
Expand Down
11 changes: 11 additions & 0 deletions src/cdk/overlay/position/global-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 6 additions & 18 deletions src/cdk/overlay/position/overlay-position-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand All @@ -32,7 +26,7 @@ export class OverlayPositionBuilder {
* Creates a global position strategy.
*/
global(): GlobalPositionStrategy {
return new GlobalPositionStrategy();
return createGlobalPositionStrategy(this._injector);
}

/**
Expand All @@ -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);
}
}
8 changes: 6 additions & 2 deletions src/cdk/overlay/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
10 changes: 10 additions & 0 deletions src/cdk/overlay/scroll/block-scroll-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@
* 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';
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.
*/
Expand Down
Loading
Loading