From 50e8f943707a768030d9ac351905f66600e907ca Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 22 Aug 2024 08:29:50 +0200 Subject: [PATCH 1/4] fix(material/core): avoid having to manually load ripple styles Makes it so the ripple loads the necessary styles itself, instead of requiring the user to do it. BREAKING CHANGE: * The ripples styles are now loaded slightly later than before which can change their specificity. You may have to update any ripple style overrides. --- src/material/core/BUILD.bazel | 8 ++++++ src/material/core/_core.scss | 2 -- src/material/core/private/ripple-loader.ts | 1 + src/material/core/ripple/index.ts | 2 +- src/material/core/ripple/ripple-renderer.ts | 25 ++++++++++++++++++- .../core/ripple/ripple-structure.scss | 3 +++ src/material/core/ripple/ripple.ts | 7 +++++- src/material/list/BUILD.bazel | 1 + src/material/list/list-base.ts | 3 +++ tools/public_api_guard/material/core.md | 7 +++--- 10 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 src/material/core/ripple/ripple-structure.scss diff --git a/src/material/core/BUILD.bazel b/src/material/core/BUILD.bazel index a5b126bb209e..322a2ec9fe24 100644 --- a/src/material/core/BUILD.bazel +++ b/src/material/core/BUILD.bazel @@ -24,6 +24,7 @@ ng_module( ":option/option.css", ":option/optgroup.css", ":internal-form-field/internal-form-field.css", + ":ripple/ripple-structure.css", ] + glob(["**/*.html"]), deps = [ "//src:dev_mode_types", @@ -33,6 +34,7 @@ ng_module( "//src/cdk/coercion", "//src/cdk/keycodes", "//src/cdk/platform", + "//src/cdk/private", "@npm//@angular/animations", "@npm//@angular/core", "@npm//@angular/forms", @@ -94,6 +96,12 @@ sass_binary( deps = [":core_scss_lib"], ) +sass_binary( + name = "ripple_structure_scss", + src = "ripple/ripple-structure.scss", + deps = [":core_scss_lib"], +) + # M3 themes sass_binary( name = "azure_blue_prebuilt", diff --git a/src/material/core/_core.scss b/src/material/core/_core.scss index acd07c39755d..77f732b3bebd 100644 --- a/src/material/core/_core.scss +++ b/src/material/core/_core.scss @@ -1,7 +1,6 @@ @use '@angular/cdk'; @use './tokens/m2/mat/app' as tokens-mat-app; @use './tokens/token-utils'; -@use './ripple/ripple'; @use './style/elevation'; @use './focus-indicators/private'; @@ -15,7 +14,6 @@ --mat-app-on-surface: initial; } - @include ripple.ripple(); @include cdk.a11y-visually-hidden(); @include cdk.overlay(); @include cdk.text-field-autosize(); diff --git a/src/material/core/private/ripple-loader.ts b/src/material/core/private/ripple-loader.ts index 63aec3a26897..7cf0003cd7a4 100644 --- a/src/material/core/private/ripple-loader.ts +++ b/src/material/core/private/ripple-loader.ts @@ -15,6 +15,7 @@ import { defaultRippleAnimationConfig, } from '../ripple'; import {Platform, _getEventTarget} from '@angular/cdk/platform'; +import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; /** The options for the MatRippleLoader's event listeners. */ const eventListenerOptions = {capture: true}; diff --git a/src/material/core/ripple/index.ts b/src/material/core/ripple/index.ts index c116f5251acc..704621a4832f 100644 --- a/src/material/core/ripple/index.ts +++ b/src/material/core/ripple/index.ts @@ -12,7 +12,7 @@ import {MatRipple} from './ripple'; export * from './ripple'; export * from './ripple-ref'; -export * from './ripple-renderer'; +export {RippleRenderer, RippleTarget, defaultRippleAnimationConfig} from './ripple-renderer'; @NgModule({ imports: [MatCommonModule, MatRipple], diff --git a/src/material/core/ripple/ripple-renderer.ts b/src/material/core/ripple/ripple-renderer.ts index 89309484a8a7..0b13d4bca3e3 100644 --- a/src/material/core/ripple/ripple-renderer.ts +++ b/src/material/core/ripple/ripple-renderer.ts @@ -5,10 +5,18 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {ElementRef, NgZone} from '@angular/core'; +import { + ElementRef, + NgZone, + Component, + ChangeDetectionStrategy, + ViewEncapsulation, + Injector, +} from '@angular/core'; import {Platform, normalizePassiveListenerOptions, _getEventTarget} from '@angular/cdk/platform'; import {isFakeMousedownFromScreenReader, isFakeTouchstartFromScreenReader} from '@angular/cdk/a11y'; import {coerceElement} from '@angular/cdk/coercion'; +import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {RippleRef, RippleState, RippleConfig} from './ripple-ref'; import {RippleEventManager} from './ripple-event-manager'; @@ -58,6 +66,16 @@ const pointerDownEvents = ['mousedown', 'touchstart']; /** Events that signal that the pointer is up. */ const pointerUpEvents = ['mouseup', 'mouseleave', 'touchend', 'touchcancel']; +@Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + standalone: true, + styleUrl: 'ripple-structure.css', + host: {'mat-ripple-style-loader': ''}, +}) +export class _MatRippleStylesLoader {} + /** * Helper service that performs DOM manipulations. Not intended to be used outside this module. * The constructor takes a reference to the ripple directive's host element and a map of DOM @@ -105,11 +123,16 @@ export class RippleRenderer implements EventListenerObject { private _ngZone: NgZone, elementOrElementRef: HTMLElement | ElementRef, private _platform: Platform, + injector?: Injector, ) { // Only do anything if we're on the browser. if (_platform.isBrowser) { this._containerElement = coerceElement(elementOrElementRef); } + + if (injector) { + injector.get(_CdkPrivateStyleLoader).load(_MatRippleStylesLoader); + } } /** diff --git a/src/material/core/ripple/ripple-structure.scss b/src/material/core/ripple/ripple-structure.scss new file mode 100644 index 000000000000..1ef209c2644c --- /dev/null +++ b/src/material/core/ripple/ripple-structure.scss @@ -0,0 +1,3 @@ +@use './ripple'; + +@include ripple.ripple; diff --git a/src/material/core/ripple/ripple.ts b/src/material/core/ripple/ripple.ts index 72ca76189368..dce9aa124a86 100644 --- a/src/material/core/ripple/ripple.ts +++ b/src/material/core/ripple/ripple.ts @@ -18,7 +18,9 @@ import { OnInit, Optional, ANIMATION_MODULE_TYPE, + Injector, } from '@angular/core'; +import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {RippleAnimationConfig, RippleConfig, RippleRef} from './ripple-ref'; import {RippleRenderer, RippleTarget} from './ripple-renderer'; @@ -136,9 +138,12 @@ export class MatRipple implements OnInit, OnDestroy, RippleTarget { platform: Platform, @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions?: RippleGlobalOptions, @Optional() @Inject(ANIMATION_MODULE_TYPE) private _animationMode?: string, + injector?: Injector, ) { + // Note: cannot use `inject()` here, because this class + // gets instantiated manually in the ripple loader. this._globalOptions = globalOptions || {}; - this._rippleRenderer = new RippleRenderer(this, ngZone, _elementRef, platform); + this._rippleRenderer = new RippleRenderer(this, ngZone, _elementRef, platform, injector); } ngOnInit() { diff --git a/src/material/list/BUILD.bazel b/src/material/list/BUILD.bazel index 854552d0f760..834bcde15678 100644 --- a/src/material/list/BUILD.bazel +++ b/src/material/list/BUILD.bazel @@ -27,6 +27,7 @@ ng_module( "//src/cdk/coercion", "//src/cdk/collections", "//src/cdk/observers", + "//src/cdk/private", "//src/material/core", "//src/material/divider", "@npm//@angular/core", diff --git a/src/material/list/list-base.ts b/src/material/list/list-base.ts index 85d316290464..2f42bbfb05e9 100644 --- a/src/material/list/list-base.ts +++ b/src/material/list/list-base.ts @@ -21,6 +21,7 @@ import { Optional, QueryList, ANIMATION_MODULE_TYPE, + Injector, } from '@angular/core'; import { MAT_RIPPLE_GLOBAL_OPTIONS, @@ -29,6 +30,7 @@ import { RippleRenderer, RippleTarget, } from '@angular/material/core'; +import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {Subscription, merge} from 'rxjs'; import { MatListItemLine, @@ -223,6 +225,7 @@ export abstract class MatListItemBase implements AfterViewInit, OnDestroy, Rippl this._ngZone, this._hostElement, this._platform, + inject(Injector), ); this._rippleRenderer.setupTriggerEvents(this._hostElement); } diff --git a/tools/public_api_guard/material/core.md b/tools/public_api_guard/material/core.md index aa5296e9fd1b..b3e4be1c0c30 100644 --- a/tools/public_api_guard/material/core.md +++ b/tools/public_api_guard/material/core.md @@ -16,6 +16,7 @@ import { HighContrastModeDetector } from '@angular/cdk/a11y'; import * as i0 from '@angular/core'; import * as i1 from '@angular/cdk/bidi'; import { InjectionToken } from '@angular/core'; +import { Injector } from '@angular/core'; import { NgControl } from '@angular/forms'; import { NgForm } from '@angular/forms'; import { NgZone } from '@angular/core'; @@ -372,7 +373,7 @@ export type MatPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate'; // @public (undocumented) export class MatRipple implements OnInit, OnDestroy, RippleTarget { - constructor(_elementRef: ElementRef, ngZone: NgZone, platform: Platform, globalOptions?: RippleGlobalOptions, _animationMode?: string | undefined); + constructor(_elementRef: ElementRef, ngZone: NgZone, platform: Platform, globalOptions?: RippleGlobalOptions, _animationMode?: string | undefined, injector?: Injector); animation: RippleAnimationConfig; centered: boolean; color: string; @@ -396,7 +397,7 @@ export class MatRipple implements OnInit, OnDestroy, RippleTarget { // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; } // @public @@ -557,7 +558,7 @@ export class RippleRef { // @public export class RippleRenderer implements EventListenerObject { - constructor(_target: RippleTarget, _ngZone: NgZone, elementOrElementRef: HTMLElement | ElementRef, _platform: Platform); + constructor(_target: RippleTarget, _ngZone: NgZone, elementOrElementRef: HTMLElement | ElementRef, _platform: Platform, injector?: Injector); fadeInRipple(x: number, y: number, config?: RippleConfig): RippleRef; fadeOutAll(): void; fadeOutAllNonPersistent(): void; From 25966fe370982bf27b16ee87f13a0b91f7f0d319 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 22 Aug 2024 08:37:20 +0200 Subject: [PATCH 2/4] fix(cdk/overlay): avoid having to manually load structural styles Changes the overlay so it loads its structural styles automatically, instead of requiring the user to do it. BREAKING CHANGE: * The overlay stays are now loaded slightly later than before which can change their specificity. You may have to update any overlay style overrides. --- src/cdk/overlay/BUILD.bazel | 4 +++ src/cdk/overlay/_index.scss | 28 +++++++++--------- .../fullscreen-overlay-container.spec.ts | 1 + src/cdk/overlay/overlay-container.ts | 29 ++++++++++++++++++- src/cdk/overlay/overlay.ts | 9 +++++- .../block-scroll-strategy-e2e.ts | 7 ++++- src/material/core/_core.scss | 1 - tools/public_api_guard/cdk/overlay.md | 4 +++ 8 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/cdk/overlay/BUILD.bazel b/src/cdk/overlay/BUILD.bazel index 55713dc87df7..a7bc728efa0a 100644 --- a/src/cdk/overlay/BUILD.bazel +++ b/src/cdk/overlay/BUILD.bazel @@ -18,6 +18,9 @@ ng_module( ["**/*.ts"], exclude = ["**/*.spec.ts"], ), + assets = [ + ":overlay-prebuilt.css", + ], deps = [ "//src:dev_mode_types", "//src/cdk/bidi", @@ -25,6 +28,7 @@ ng_module( "//src/cdk/keycodes", "//src/cdk/platform", "//src/cdk/portal", + "//src/cdk/private", "//src/cdk/scrolling", "@npm//@angular/common", "@npm//@angular/core", diff --git a/src/cdk/overlay/_index.scss b/src/cdk/overlay/_index.scss index ec3eb99e8379..2da3f6020dad 100644 --- a/src/cdk/overlay/_index.scss +++ b/src/cdk/overlay/_index.scss @@ -76,23 +76,24 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default; -webkit-tap-highlight-color: transparent; transition: opacity $backdrop-animation-duration $backdrop-animation-timing-function; opacity: 0; + } + + .cdk-overlay-backdrop-showing { + opacity: 1; - &.cdk-overlay-backdrop-showing { - opacity: 1; - - // Note that we can't import and use the `high-contrast` mixin from `_a11y.scss`, because - // this file will be copied to the top-level `cdk` package when putting together the files - // for npm. Any relative import paths we use here will become invalid once the file is copied. - .cdk-high-contrast-active & { - // In high contrast mode the rgba background will become solid - // so we need to fall back to making it opaque using `opacity`. - opacity: 0.6; - } + // Note that we can't import and use the `high-contrast` mixin from `_a11y.scss`, because + // this file will be copied to the top-level `cdk` package when putting together the files + // for npm. Any relative import paths we use here will become invalid once the file is copied. + .cdk-high-contrast-active & { + // In high contrast mode the rgba background will become solid + // so we need to fall back to making it opaque using `opacity`. + opacity: 0.6; } } .cdk-overlay-dark-backdrop { - background: $overlay-backdrop-color; + // Add a CSS variable to make this easier to override. + background: var(--cdk-overlay-backdrop-dark-color, $overlay-backdrop-color); } .cdk-overlay-transparent-backdrop { @@ -105,7 +106,8 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default; // capturing the user's mouse scroll events. Since we also can't use something like // `rgba(0, 0, 0, 0)`, we work around the inconsistency by not setting the background at // all and using `opacity` to make the element transparent. - &.cdk-overlay-backdrop-showing { + &.cdk-overlay-backdrop-showing, + .cdk-high-contrast-active & { opacity: 0; visibility: visible; } diff --git a/src/cdk/overlay/fullscreen-overlay-container.spec.ts b/src/cdk/overlay/fullscreen-overlay-container.spec.ts index 02500dfb91c6..63b704193ae5 100644 --- a/src/cdk/overlay/fullscreen-overlay-container.spec.ts +++ b/src/cdk/overlay/fullscreen-overlay-container.spec.ts @@ -26,6 +26,7 @@ describe('FullscreenOverlayContainer', () => { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy fakeDocument = { body: document.body, + head: document.head, fullscreenElement: document.createElement('div'), fullscreenEnabled: true, addEventListener: (eventName: string, listener: EventListener) => { diff --git a/src/cdk/overlay/overlay-container.ts b/src/cdk/overlay/overlay-container.ts index d1b3fd2789df..5c27341638fb 100644 --- a/src/cdk/overlay/overlay-container.ts +++ b/src/cdk/overlay/overlay-container.ts @@ -7,14 +7,34 @@ */ import {DOCUMENT} from '@angular/common'; -import {Inject, Injectable, OnDestroy} from '@angular/core'; +import { + Inject, + Injectable, + OnDestroy, + Component, + ChangeDetectionStrategy, + ViewEncapsulation, + inject, +} from '@angular/core'; +import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {Platform, _isTestEnvironment} from '@angular/cdk/platform'; +@Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + standalone: true, + styleUrl: 'overlay-prebuilt.css', + host: {'cdk-overlay-style-loader': ''}, +}) +export class _CdkOverlayStyleLoader {} + /** Container inside which all overlays will render. */ @Injectable({providedIn: 'root'}) export class OverlayContainer implements OnDestroy { protected _containerElement: HTMLElement; protected _document: Document; + protected _styleLoader = inject(_CdkPrivateStyleLoader); constructor( @Inject(DOCUMENT) document: any, @@ -34,6 +54,8 @@ export class OverlayContainer implements OnDestroy { * @returns the container element */ getContainerElement(): HTMLElement { + this._loadStyles(); + if (!this._containerElement) { this._createContainer(); } @@ -84,4 +106,9 @@ export class OverlayContainer implements OnDestroy { this._document.body.appendChild(container); this._containerElement = container; } + + /** Loads the structural styles necessary for the overlay to work. */ + protected _loadStyles(): void { + this._styleLoader.load(_CdkOverlayStyleLoader); + } } diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index be660242b437..687b5b358f76 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -19,11 +19,13 @@ import { ANIMATION_MODULE_TYPE, Optional, EnvironmentInjector, + inject, } from '@angular/core'; +import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {OverlayKeyboardDispatcher} from './dispatchers/overlay-keyboard-dispatcher'; import {OverlayOutsideClickDispatcher} from './dispatchers/overlay-outside-click-dispatcher'; import {OverlayConfig} from './overlay-config'; -import {OverlayContainer} from './overlay-container'; +import {_CdkOverlayStyleLoader, OverlayContainer} from './overlay-container'; import {OverlayRef} from './overlay-ref'; import {OverlayPositionBuilder} from './position/overlay-position-builder'; import {ScrollStrategyOptions} from './scroll/index'; @@ -45,6 +47,7 @@ let nextUniqueId = 0; @Injectable({providedIn: 'root'}) export class Overlay { private _appRef: ApplicationRef; + private _styleLoader = inject(_CdkPrivateStyleLoader); constructor( /** Scrolling strategies that can be used when creating an overlay. */ @@ -68,6 +71,10 @@ 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); diff --git a/src/e2e-app/components/block-scroll-strategy/block-scroll-strategy-e2e.ts b/src/e2e-app/components/block-scroll-strategy/block-scroll-strategy-e2e.ts index ce4f93da1530..703bb9bc0eda 100644 --- a/src/e2e-app/components/block-scroll-strategy/block-scroll-strategy-e2e.ts +++ b/src/e2e-app/components/block-scroll-strategy/block-scroll-strategy-e2e.ts @@ -1,5 +1,5 @@ import {Component, inject} from '@angular/core'; -import {Overlay} from '@angular/cdk/overlay'; +import {Overlay, OverlayContainer} from '@angular/cdk/overlay'; import {ScrollingModule} from '@angular/cdk/scrolling'; @Component({ @@ -11,4 +11,9 @@ import {ScrollingModule} from '@angular/cdk/scrolling'; }) export class BlockScrollStrategyE2E { scrollStrategy = inject(Overlay).scrollStrategies.block(); + + constructor() { + // This loads the structural styles for the test. + inject(OverlayContainer).getContainerElement(); + } } diff --git a/src/material/core/_core.scss b/src/material/core/_core.scss index 77f732b3bebd..91a7cd41cc8c 100644 --- a/src/material/core/_core.scss +++ b/src/material/core/_core.scss @@ -15,7 +15,6 @@ } @include cdk.a11y-visually-hidden(); - @include cdk.overlay(); @include cdk.text-field-autosize(); @include cdk.text-field-autofill(); @include private.structural-styling('mat'); diff --git a/tools/public_api_guard/cdk/overlay.md b/tools/public_api_guard/cdk/overlay.md index 727db310e756..90a6652adfb8 100644 --- a/tools/public_api_guard/cdk/overlay.md +++ b/tools/public_api_guard/cdk/overlay.md @@ -4,6 +4,7 @@ ```ts +import { _CdkPrivateStyleLoader } from '@angular/cdk/private'; import { CdkScrollable } from '@angular/cdk/scrolling'; import { ComponentFactoryResolver } from '@angular/core'; import { ComponentPortal } from '@angular/cdk/portal'; @@ -303,11 +304,14 @@ export class OverlayContainer implements OnDestroy { // (undocumented) protected _document: Document; getContainerElement(): HTMLElement; + protected _loadStyles(): void; // (undocumented) ngOnDestroy(): void; // (undocumented) protected _platform: Platform; // (undocumented) + protected _styleLoader: _CdkPrivateStyleLoader; + // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; // (undocumented) static ɵprov: i0.ɵɵInjectableDeclaration; From 5688dd3fa33fd473e3e4432bbe26edcf35b556f2 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 22 Aug 2024 09:19:35 +0200 Subject: [PATCH 3/4] fix(cdk/text-field): avoid having to manually load text field styles Reworks the text field so that users don't have to manually load its structural styles. --- src/cdk/text-field/BUILD.bazel | 4 ++++ src/cdk/text-field/autofill.ts | 6 ++++++ src/cdk/text-field/autosize.ts | 8 ++++++-- src/cdk/text-field/text-field-style-loader.ts | 20 +++++++++++++++++++ src/material/core/_core.scss | 2 -- 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/cdk/text-field/text-field-style-loader.ts diff --git a/src/cdk/text-field/BUILD.bazel b/src/cdk/text-field/BUILD.bazel index 7c88c2889835..edbeab8b7623 100644 --- a/src/cdk/text-field/BUILD.bazel +++ b/src/cdk/text-field/BUILD.bazel @@ -16,9 +16,13 @@ ng_module( ["**/*.ts"], exclude = ["**/*.spec.ts"], ), + assets = [ + ":text-field-prebuilt.css", + ], deps = [ "//src/cdk/coercion", "//src/cdk/platform", + "//src/cdk/private", "@npm//@angular/core", "@npm//rxjs", ], diff --git a/src/cdk/text-field/autofill.ts b/src/cdk/text-field/autofill.ts index 2ad49e5f52e7..7717dd0d167e 100644 --- a/src/cdk/text-field/autofill.ts +++ b/src/cdk/text-field/autofill.ts @@ -11,14 +11,17 @@ import { Directive, ElementRef, EventEmitter, + inject, Injectable, NgZone, OnDestroy, OnInit, Output, } from '@angular/core'; +import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {coerceElement} from '@angular/cdk/coercion'; import {EMPTY, Observable, Subject} from 'rxjs'; +import {_CdkTextFieldStyleLoader} from './text-field-style-loader'; /** An event that is emitted when the autofill state of an input changes. */ export type AutofillEvent = { @@ -44,6 +47,7 @@ const listenerOptions = normalizePassiveListenerOptions({passive: true}); */ @Injectable({providedIn: 'root'}) export class AutofillMonitor implements OnDestroy { + private _styleLoader = inject(_CdkPrivateStyleLoader); private _monitoredElements = new Map(); constructor( @@ -70,6 +74,8 @@ export class AutofillMonitor implements OnDestroy { return EMPTY; } + this._styleLoader.load(_CdkTextFieldStyleLoader); + const element = coerceElement(elementOrRef); const info = this._monitoredElements.get(element); diff --git a/src/cdk/text-field/autosize.ts b/src/cdk/text-field/autosize.ts index 69d1c1dec8bd..c3b784851796 100644 --- a/src/cdk/text-field/autosize.ts +++ b/src/cdk/text-field/autosize.ts @@ -18,11 +18,14 @@ import { Optional, Inject, booleanAttribute, + inject, } from '@angular/core'; +import {DOCUMENT} from '@angular/common'; import {Platform} from '@angular/cdk/platform'; +import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {auditTime, takeUntil} from 'rxjs/operators'; import {fromEvent, Subject} from 'rxjs'; -import {DOCUMENT} from '@angular/common'; +import {_CdkTextFieldStyleLoader} from './text-field-style-loader'; /** Directive to automatically resize a textarea to fit its content. */ @Directive({ @@ -124,8 +127,9 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { /** @breaking-change 11.0.0 make document required */ @Optional() @Inject(DOCUMENT) document?: any, ) { + const styleLoader = inject(_CdkPrivateStyleLoader); + styleLoader.load(_CdkTextFieldStyleLoader); this._document = document; - this._textareaElement = this._elementRef.nativeElement as HTMLTextAreaElement; } diff --git a/src/cdk/text-field/text-field-style-loader.ts b/src/cdk/text-field/text-field-style-loader.ts new file mode 100644 index 000000000000..a859d3494c3a --- /dev/null +++ b/src/cdk/text-field/text-field-style-loader.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; + +/** Component used to load the structural styles of the text field. */ +@Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrl: 'text-field-prebuilt.css', + standalone: true, + host: {'cdk-text-field-style-loader': ''}, +}) +export class _CdkTextFieldStyleLoader {} diff --git a/src/material/core/_core.scss b/src/material/core/_core.scss index 91a7cd41cc8c..6ddcedfd18fb 100644 --- a/src/material/core/_core.scss +++ b/src/material/core/_core.scss @@ -15,8 +15,6 @@ } @include cdk.a11y-visually-hidden(); - @include cdk.text-field-autosize(); - @include cdk.text-field-autofill(); @include private.structural-styling('mat'); @include private.structural-styling('mat-mdc'); From 2cc4c65130013af903d78fa2e1d849b6a74e92fb Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 22 Aug 2024 14:51:24 +0200 Subject: [PATCH 4/4] refactor(cdk/private): avoid circular dependency errors in style loader Reworks the style loader to avoid errors if it gets invoked too early. --- src/cdk/private/style-loader.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/cdk/private/style-loader.ts b/src/cdk/private/style-loader.ts index 851b0957d1cd..5201aaf5c011 100644 --- a/src/cdk/private/style-loader.ts +++ b/src/cdk/private/style-loader.ts @@ -13,6 +13,7 @@ import { EnvironmentInjector, inject, Injectable, + Injector, Type, } from '@angular/core'; @@ -34,7 +35,8 @@ const appsWithLoaders = new WeakMap< */ @Injectable({providedIn: 'root'}) export class _CdkPrivateStyleLoader { - private _appRef = inject(ApplicationRef); + private _appRef: ApplicationRef | undefined; + private _injector = inject(Injector); private _environmentInjector = inject(EnvironmentInjector); /** @@ -42,17 +44,19 @@ export class _CdkPrivateStyleLoader { * @param loader Component which will be instantiated to load the styles. */ load(loader: Type): void { - let data = appsWithLoaders.get(this._appRef); + // Resolve the app ref lazily to avoid circular dependency errors if this is called too early. + const appRef = (this._appRef = this._appRef || this._injector.get(ApplicationRef)); + let data = appsWithLoaders.get(appRef); // If we haven't loaded for this app before, we have to initialize it. if (!data) { data = {loaders: new Set(), refs: []}; - appsWithLoaders.set(this._appRef, data); + appsWithLoaders.set(appRef, data); // When the app is destroyed, we need to clean up all the related loaders. - this._appRef.onDestroy(() => { - appsWithLoaders.get(this._appRef)?.refs.forEach(ref => ref.destroy()); - appsWithLoaders.delete(this._appRef); + appRef.onDestroy(() => { + appsWithLoaders.get(appRef)?.refs.forEach(ref => ref.destroy()); + appsWithLoaders.delete(appRef); }); }