diff --git a/guides/theming.md b/guides/theming.md index c9610de48f1b..038f87981c2f 100644 --- a/guides/theming.md +++ b/guides/theming.md @@ -48,6 +48,73 @@ html { } ``` +### CSS cascade layers + +Angular Material ships all component CSS inside the `angular-material` +[cascade layer](https://www.w3.org/TR/css-cascade-5/#layering). This lets +applications control where Material sits in the cascade relative to resets, +CDK styles, and utility frameworks such as Tailwind — without resorting to +extra specificity or `!important`. See also +[MDN: Cascade layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer). + +Component styles are layered automatically at build time — no action is +needed for component CSS. Prebuilt themes are also shipped pre-layered. + +**Layer order.** In Sass, `@use` must come before any other rules. After your +`@use` lines, declare the order of named layers: + +```scss +@use '@angular/material' as mat; + +@layer base, cdk-resets, cdk-overlay, angular-material, components, utilities; +``` + +If your app uses CDK overlay or drag-drop resets, include their published +layer names (`cdk-overlay`, `cdk-resets`) so their priority is predictable. + +**Tailwind CSS.** Placing `angular-material` before `utilities` allows +Tailwind utility classes to override Material styles at equal specificity. +Put Material `@use` and your layer prelude before Tailwind’s `@tailwind` +directives (Tailwind’s own docs describe layer setup for your stack): + +```scss +@use '@angular/material' as mat; + +@layer base, cdk-resets, cdk-overlay, angular-material, components, utilities; + +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +**Theme output.** Wrap `mat.theme` in `mat.theme-layer` so the generated +CSS custom properties land in the same `angular-material` layer as the +component styles. You can also use the same mixin around other Angular +Material theme output such as `all-component-themes` or individual +component theme mixins: + +```scss +@use '@angular/material' as mat; + +@layer base, cdk-resets, cdk-overlay, angular-material, components, utilities; + +@include mat.theme-layer { + html { + @include mat.theme(( + color: mat.$violet-palette, + typography: Roboto, + density: 0, + )); + } +} +``` + +**Cascade note.** In the CSS Cascade Level 5 model, unlayered author +styles have higher priority than any named layer. Plain CSS rules can +therefore override layered Material at equal specificity. Overrides that +live inside named layers must appear after `angular-material` in the +`@layer` order list, or use higher specificity / `!important` as usual. + You can use the following styles to apply the theme’s surface background and on-surface text colors as a default across your application: diff --git a/src/e2e-app/BUILD.bazel b/src/e2e-app/BUILD.bazel index 51e091c66d12..75637beae19f 100644 --- a/src/e2e-app/BUILD.bazel +++ b/src/e2e-app/BUILD.bazel @@ -43,6 +43,7 @@ ng_project( "//src/cdk/scrolling", "//src/cdk/testing/tests:test_components", "//src/components-examples/private", + "//src/material/button", "//src/material/core", "//src/material/slider", ], diff --git a/src/e2e-app/components/e2e-app/e2e-app.ts b/src/e2e-app/components/e2e-app/e2e-app.ts index 581b9550607b..79c49b98a9f7 100644 --- a/src/e2e-app/components/e2e-app/e2e-app.ts +++ b/src/e2e-app/components/e2e-app/e2e-app.ts @@ -16,6 +16,7 @@ export class E2eApp { {path: 'block-scroll-strategy', title: 'Block Scroll Strategy'}, {path: 'component-harness', title: 'Component Harness'}, {path: 'slider', title: 'Slider'}, + {path: 'tailwind-layer', title: 'Tailwind Layer'}, {path: 'virtual-scroll', title: 'Virtual Scroll'}, ]; } diff --git a/src/e2e-app/components/tailwind-layer-e2e.ts b/src/e2e-app/components/tailwind-layer-e2e.ts new file mode 100644 index 000000000000..1618e9e12fab --- /dev/null +++ b/src/e2e-app/components/tailwind-layer-e2e.ts @@ -0,0 +1,31 @@ +/** + * @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.dev/license + */ + +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {MatButtonModule} from '@angular/material/button'; + +@Component({ + selector: 'tailwind-layer-e2e', + template: ` + + + `, + imports: [MatButtonModule], + changeDetection: ChangeDetectionStrategy.Eager, +}) +export class TailwindLayerE2e {} diff --git a/src/e2e-app/main.ts b/src/e2e-app/main.ts index ee0d1e790bc5..dbd41ae89adb 100644 --- a/src/e2e-app/main.ts +++ b/src/e2e-app/main.ts @@ -7,6 +7,7 @@ import {ComponentHarnessE2e} from './components/component-harness-e2e'; import {E2eApp} from './components/e2e-app/e2e-app'; import {Home} from './components/home'; import {SliderE2e} from './components/slider-e2e'; +import {TailwindLayerE2e} from './components/tailwind-layer-e2e'; import {VirtualScrollE2E} from './components/virtual-scroll/virtual-scroll-e2e'; enableProdMode(); @@ -19,6 +20,7 @@ bootstrapApplication(E2eApp, { {path: 'block-scroll-strategy', component: BlockScrollStrategyE2E}, {path: 'component-harness', component: ComponentHarnessE2e}, {path: 'slider', component: SliderE2e}, + {path: 'tailwind-layer', component: TailwindLayerE2e}, {path: 'virtual-scroll', component: VirtualScrollE2E}, ]), provideZoneChangeDetection(), diff --git a/src/e2e-app/theme.scss b/src/e2e-app/theme.scss index d794f4750e68..f22617945611 100644 --- a/src/e2e-app/theme.scss +++ b/src/e2e-app/theme.scss @@ -1,5 +1,7 @@ @use '@angular/material' as mat; +@layer base, angular-material, utilities; + $theme: mat.define-theme(( color: ( theme-type: light, @@ -11,8 +13,21 @@ $theme: mat.define-theme(( ) )); -html { - @include mat.all-component-themes($theme); +@include mat.theme-layer { + html { + @include mat.all-component-themes($theme); + } + + @include mat.typography-hierarchy($theme); } -@include mat.typography-hierarchy($theme); +@layer utilities { + .tw-bg-lime-500 { + background-color: rgb(132, 204, 22); + } +} + +// Control utility that is intentionally unlayered. +.tw-bg-fuchsia-unlayered { + background-color: rgb(217, 70, 239); +} diff --git a/src/material-experimental/menubar/BUILD.bazel b/src/material-experimental/menubar/BUILD.bazel index 8556339b26a2..6a90bd59f45f 100644 --- a/src/material-experimental/menubar/BUILD.bazel +++ b/src/material-experimental/menubar/BUILD.bazel @@ -32,11 +32,13 @@ sass_library( sass_binary( name = "menubar_scss", src = "menubar.scss", + layer = "angular-material", ) sass_binary( name = "menubar_item_scss", src = "menubar-item.scss", + layer = "angular-material", ) ng_project( diff --git a/src/material-experimental/selection/BUILD.bazel b/src/material-experimental/selection/BUILD.bazel index 2cc414f9276d..15cec99036e3 100644 --- a/src/material-experimental/selection/BUILD.bazel +++ b/src/material-experimental/selection/BUILD.bazel @@ -27,4 +27,5 @@ sass_library( sass_binary( name = "selection_column_scss", src = "selection-column.scss", + layer = "angular-material", ) diff --git a/src/material/BUILD.bazel b/src/material/BUILD.bazel index 4640784669a3..22bea3c42e8f 100644 --- a/src/material/BUILD.bazel +++ b/src/material/BUILD.bazel @@ -54,6 +54,7 @@ sass_library( "//src/material/core/selection/pseudo-checkbox:_pseudo_checkbox_common", "//src/material/core/selection/pseudo-checkbox:sass_theme", "//src/material/core/style:_validation", + "//src/material/core/style:cascade_layers", "//src/material/core/style:elevation", "//src/material/core/style:private", "//src/material/core/style:sass_utils", @@ -130,6 +131,7 @@ ng_package( "//src/material/core/selection/pseudo-checkbox:_pseudo_checkbox_common", "//src/material/core/selection/pseudo-checkbox:sass_theme", "//src/material/core/style:_validation", + "//src/material/core/style:cascade_layers", "//src/material/core/style:elevation", "//src/material/core/style:private", "//src/material/core/style:sass_utils", diff --git a/src/material/_index.scss b/src/material/_index.scss index dfdf3738c594..76fa9ab9061e 100644 --- a/src/material/_index.scss +++ b/src/material/_index.scss @@ -33,6 +33,7 @@ $private-ease-in-out-curve-function, $private-swift-ease-out-duration, $private-xsmall; @forward './core/style/sass-utils' as private-*; @forward './core/style/validation' as private-*; +@forward './core/style/cascade-layers' show theme-layer; // Structural @forward './core/core' show core, app-background, elevation-classes; diff --git a/src/material/autocomplete/BUILD.bazel b/src/material/autocomplete/BUILD.bazel index 44dea63a861f..482d8f6cebc9 100644 --- a/src/material/autocomplete/BUILD.bazel +++ b/src/material/autocomplete/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "autocomplete.scss", + layer = "angular-material", deps = [ ":m2", ":m3", diff --git a/src/material/badge/BUILD.bazel b/src/material/badge/BUILD.bazel index 2943c37a634a..0a9cef7d097f 100644 --- a/src/material/badge/BUILD.bazel +++ b/src/material/badge/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "badge_css", src = "badge.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/bottom-sheet/BUILD.bazel b/src/material/bottom-sheet/BUILD.bazel index e7c5e6a32b10..14b69fe72d09 100644 --- a/src/material/bottom-sheet/BUILD.bazel +++ b/src/material/bottom-sheet/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "bottom-sheet-container.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/button-toggle/BUILD.bazel b/src/material/button-toggle/BUILD.bazel index ef5093e44c5a..0f0beb307cc9 100644 --- a/src/material/button-toggle/BUILD.bazel +++ b/src/material/button-toggle/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "button-toggle.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/button/BUILD.bazel b/src/material/button/BUILD.bazel index 9671cabd541c..3bb1e6bf510b 100644 --- a/src/material/button/BUILD.bazel +++ b/src/material/button/BUILD.bazel @@ -84,6 +84,7 @@ sass_library( sass_binary( name = "icon_button_css", src = "icon-button.scss", + layer = "angular-material", deps = [ ":base_lib", ":m2", @@ -96,6 +97,7 @@ sass_binary( sass_binary( name = "fab_css", src = "fab.scss", + layer = "angular-material", deps = [ ":base_lib", ":m2", @@ -109,6 +111,7 @@ sass_binary( sass_binary( name = "button_high_contrast", src = "button-high-contrast.scss", + layer = "angular-material", deps = [ "//src/cdk:sass_lib", ], @@ -128,6 +131,7 @@ sass_library( sass_binary( name = "css", src = "button.scss", + layer = "angular-material", deps = [ ":base_lib", ":m2", diff --git a/src/material/card/BUILD.bazel b/src/material/card/BUILD.bazel index de75a8684e6a..20d2793c5954 100644 --- a/src/material/card/BUILD.bazel +++ b/src/material/card/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "card.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", diff --git a/src/material/checkbox/BUILD.bazel b/src/material/checkbox/BUILD.bazel index 5eb1f4c0f456..8476190c1abd 100644 --- a/src/material/checkbox/BUILD.bazel +++ b/src/material/checkbox/BUILD.bazel @@ -64,6 +64,7 @@ sass_library( sass_binary( name = "css", src = "checkbox.scss", + layer = "angular-material", deps = [ ":checkbox_common", ":m2", diff --git a/src/material/chips/BUILD.bazel b/src/material/chips/BUILD.bazel index 0146ffcfaa3e..b4e9553979b5 100644 --- a/src/material/chips/BUILD.bazel +++ b/src/material/chips/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "chip_css", src = "chip.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -65,6 +66,7 @@ sass_binary( sass_binary( name = "chip_set_css", src = "chip-set.scss", + layer = "angular-material", deps = ["//src/material/core/style:vendor_prefixes"], ) diff --git a/src/material/core/BUILD.bazel b/src/material/core/BUILD.bazel index 87aaf7d74341..2d5b9e44b727 100644 --- a/src/material/core/BUILD.bazel +++ b/src/material/core/BUILD.bazel @@ -203,6 +203,7 @@ sass_library( sass_binary( name = "ripple_structure", src = "ripple/ripple-structure.scss", + layer = "angular-material", deps = [ ":ripple_sass", ], diff --git a/src/material/core/focus-indicators/BUILD.bazel b/src/material/core/focus-indicators/BUILD.bazel index 17495d4bbdaf..c8ddaa5b997c 100644 --- a/src/material/core/focus-indicators/BUILD.bazel +++ b/src/material/core/focus-indicators/BUILD.bazel @@ -25,5 +25,6 @@ ng_project( sass_binary( name = "structural_styles_css", src = "structural-styles.scss", + layer = "angular-material", deps = [":focus-indicators"], ) diff --git a/src/material/core/internal-form-field/BUILD.bazel b/src/material/core/internal-form-field/BUILD.bazel index 032b2f748a53..cab3abc55c8d 100644 --- a/src/material/core/internal-form-field/BUILD.bazel +++ b/src/material/core/internal-form-field/BUILD.bazel @@ -16,5 +16,6 @@ ng_project( sass_binary( name = "internal_form_field_css", src = "internal-form-field.scss", + layer = "angular-material", deps = ["//src/material/core/style:vendor_prefixes"], ) diff --git a/src/material/core/option/BUILD.bazel b/src/material/core/option/BUILD.bazel index db193878f52d..ffd610e153b3 100644 --- a/src/material/core/option/BUILD.bazel +++ b/src/material/core/option/BUILD.bazel @@ -77,6 +77,7 @@ ng_project( sass_binary( name = "option_css", src = "option.scss", + layer = "angular-material", deps = [ "//src/cdk:sass_lib", "//src/material/core/style:layout_common", @@ -88,6 +89,7 @@ sass_binary( sass_binary( name = "optgroup_css", src = "optgroup.scss", + layer = "angular-material", deps = [ "//src/material/core/tokens:token_utils", ], diff --git a/src/material/core/selection/pseudo-checkbox/BUILD.bazel b/src/material/core/selection/pseudo-checkbox/BUILD.bazel index 744a69374472..cc3fe7923ab5 100644 --- a/src/material/core/selection/pseudo-checkbox/BUILD.bazel +++ b/src/material/core/selection/pseudo-checkbox/BUILD.bazel @@ -66,6 +66,7 @@ sass_library( sass_binary( name = "css", src = "pseudo-checkbox.scss", + layer = "angular-material", deps = [ ":_pseudo_checkbox_common", "//src/material/core/style:checkbox_common", diff --git a/src/material/core/style/BUILD.bazel b/src/material/core/style/BUILD.bazel index 959dc5a7c22f..895ca3083288 100644 --- a/src/material/core/style/BUILD.bazel +++ b/src/material/core/style/BUILD.bazel @@ -67,6 +67,11 @@ sass_library( srcs = ["_sass-utils.scss"], ) +sass_library( + name = "cascade_layers", + srcs = ["_cascade-layers.scss"], +) + sass_library( name = "_validation", srcs = ["_validation.scss"], diff --git a/src/material/core/style/_cascade-layers.scss b/src/material/core/style/_cascade-layers.scss new file mode 100644 index 000000000000..7b21a6f9f4ba --- /dev/null +++ b/src/material/core/style/_cascade-layers.scss @@ -0,0 +1,14 @@ +// Angular Material uses a fixed layer name so shipped component CSS and Sass-emitted theme +// output can participate in the same app-authored @layer ordering. +$private-layer-name: angular-material; + +/// Wraps emitted CSS in Angular Material's cascade layer. +/// +/// Use this around Angular Material theme mixins such as `mat.theme`, +/// `mat.all-component-themes`, or individual component theme mixins when you want their +/// output to align with the layer used by Angular Material's shipped component CSS. +@mixin theme-layer { + @layer #{$private-layer-name} { + @content; + } +} diff --git a/src/material/core/theming/prebuilt/BUILD.bazel b/src/material/core/theming/prebuilt/BUILD.bazel index f631700a2031..eb3158f6061e 100644 --- a/src/material/core/theming/prebuilt/BUILD.bazel +++ b/src/material/core/theming/prebuilt/BUILD.bazel @@ -5,6 +5,7 @@ package(default_visibility = ["//visibility:public"]) sass_binary( name = "indigo-pink-theme", src = "indigo-pink.scss", + layer = "angular-material", deps = [ "//src/material/core:core_sass", "//src/material/core/m2:m2_sass", @@ -16,6 +17,7 @@ sass_binary( sass_binary( name = "deeppurple-amber-theme", src = "deeppurple-amber.scss", + layer = "angular-material", deps = [ "//src/material/core:core_sass", "//src/material/core/m2:m2_sass", @@ -27,6 +29,7 @@ sass_binary( sass_binary( name = "pink-bluegrey-theme", src = "pink-bluegrey.scss", + layer = "angular-material", deps = [ "//src/material/core:core_sass", "//src/material/core/m2:m2_sass", @@ -38,6 +41,7 @@ sass_binary( sass_binary( name = "purple-green-theme", src = "purple-green.scss", + layer = "angular-material", deps = [ "//src/material/core:core_sass", "//src/material/core/m2:m2_sass", @@ -49,6 +53,7 @@ sass_binary( sass_binary( name = "rose_red", src = "rose-red.scss", + layer = "angular-material", deps = [ "//src/material/core/theming:_palettes", "//src/material/core/tokens:system", @@ -58,6 +63,7 @@ sass_binary( sass_binary( name = "azure_blue", src = "azure-blue.scss", + layer = "angular-material", deps = [ "//src/material/core/theming:_palettes", "//src/material/core/tokens:system", @@ -67,6 +73,7 @@ sass_binary( sass_binary( name = "cyan_orange", src = "cyan-orange.scss", + layer = "angular-material", deps = [ "//src/material/core/theming:_palettes", "//src/material/core/tokens:system", @@ -76,6 +83,7 @@ sass_binary( sass_binary( name = "magenta_violet", src = "magenta-violet.scss", + layer = "angular-material", deps = [ "//src/material/core/theming:_palettes", "//src/material/core/tokens:system", diff --git a/src/material/core/theming/tests/BUILD.bazel b/src/material/core/theming/tests/BUILD.bazel index 49dd08d74b0b..1b52996817ba 100644 --- a/src/material/core/theming/tests/BUILD.bazel +++ b/src/material/core/theming/tests/BUILD.bazel @@ -52,6 +52,13 @@ sass_binary( deps = ["//src/material:sass_lib"], ) +sass_binary( + name = "test-cascade-layers", + testonly = True, + src = "test-cascade-layers.scss", + deps = ["//src/material:sass_lib"], +) + build_test( name = "sass_compile_tests", targets = [ @@ -59,6 +66,7 @@ build_test( ":test-theming-api", ":test-theming-bundle", ":test-typography-font-family", + ":test-cascade-layers", ], ) @@ -83,6 +91,8 @@ jasmine_test( ":unit_test_lib", "//src/material:sass_lib", "//src/material-experimental:sass_lib", + "//src/material/button:css", + "//src/material/prebuilt-themes:azure-blue", ], no_copy_to_bin = [ "//src/material:sass_lib", diff --git a/src/material/core/theming/tests/cascade-layers.spec.ts b/src/material/core/theming/tests/cascade-layers.spec.ts new file mode 100644 index 000000000000..935cd2172469 --- /dev/null +++ b/src/material/core/theming/tests/cascade-layers.spec.ts @@ -0,0 +1,79 @@ +import {runfiles} from '@bazel/runfiles'; +import * as fs from 'fs'; +import * as path from 'path'; +import {compileString} from 'sass'; + +import {createLocalAngularPackageImporter} from '../../../../../tools/sass/local-sass-importer.js'; + +const testDir = path.join(runfiles.resolvePackageRelative('../_all-theme.scss'), '../tests'); +const packagesDir = path.join(runfiles.resolveWorkspaceRelative('src/cdk/_index.scss'), '../..'); +const localPackageSassImporter = createLocalAngularPackageImporter(packagesDir); + +function transpile(content: string) { + return compileString( + ` + @use '../../../index' as mat; + + $theme: mat.define-theme(); + + ${content} + `, + { + loadPaths: [testDir], + importers: [localPackageSassImporter], + }, + ).css.toString(); +} + +describe('CSS cascade layers', () => { + it('wraps mat.theme with mat.theme-layer', () => { + const css = transpile(` + @include mat.theme-layer { + html { + @include mat.theme(( + color: ( + theme-type: light, + primary: mat.$violet-palette, + ), + typography: Roboto, + density: 0, + )); + } + } + `); + expect(css).toContain('@layer angular-material'); + }); + + it('does not emit @layer when theme-layer is omitted', () => { + const css = transpile(` + html { + @include mat.theme(( + color: ( + theme-type: light, + primary: mat.$violet-palette, + ), + typography: Roboto, + density: 0, + )); + } + `); + expect(css).not.toContain('@layer angular-material'); + }); + + it('wraps compiled component CSS in the angular-material layer', () => { + const css = fs.readFileSync( + runfiles.resolveWorkspaceRelative('src/material/button/button.css'), + 'utf8', + ); + expect(css).toMatch(/^@layer angular-material \{/); + expect(css).toContain('.mat-mdc-button-base'); + }); + + it('wraps prebuilt theme CSS in the angular-material layer', () => { + const css = fs.readFileSync( + runfiles.resolveWorkspaceRelative('src/material/prebuilt-themes/azure-blue.css'), + 'utf8', + ); + expect(css).toMatch(/^@layer angular-material \{/); + }); +}); diff --git a/src/material/core/theming/tests/test-cascade-layers.scss b/src/material/core/theming/tests/test-cascade-layers.scss new file mode 100644 index 000000000000..4cd6fe7b4975 --- /dev/null +++ b/src/material/core/theming/tests/test-cascade-layers.scss @@ -0,0 +1,16 @@ +@use '../../../index' as mat; + +$theme: mat.define-theme(); + +@include mat.theme-layer { + html { + @include mat.theme(( + color: ( + theme-type: light, + primary: mat.$violet-palette, + ), + typography: Roboto, + density: 0, + )); + } +} diff --git a/src/material/datepicker/BUILD.bazel b/src/material/datepicker/BUILD.bazel index af9de2eed2e7..62da920a6365 100644 --- a/src/material/datepicker/BUILD.bazel +++ b/src/material/datepicker/BUILD.bazel @@ -63,6 +63,7 @@ sass_library( sass_binary( name = "datepicker_content_css", src = "datepicker-content.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", @@ -72,6 +73,7 @@ sass_binary( sass_binary( name = "calendar_css", src = "calendar.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -83,6 +85,7 @@ sass_binary( sass_binary( name = "calendar_body_css", src = "calendar-body.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -94,6 +97,7 @@ sass_binary( sass_binary( name = "datepicker_toggle_css", src = "datepicker-toggle.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -104,11 +108,13 @@ sass_binary( sass_binary( name = "datepicker_actions_css", src = "datepicker-actions.scss", + layer = "angular-material", ) sass_binary( name = "date_range_input_css", src = "date-range-input.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/dialog/BUILD.bazel b/src/material/dialog/BUILD.bazel index fdeb092c536a..5a2c1e6fc8bc 100644 --- a/src/material/dialog/BUILD.bazel +++ b/src/material/dialog/BUILD.bazel @@ -60,6 +60,7 @@ sass_library( sass_binary( name = "css", src = "dialog.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/divider/BUILD.bazel b/src/material/divider/BUILD.bazel index 30ef52c68430..4c7d8009ea50 100644 --- a/src/material/divider/BUILD.bazel +++ b/src/material/divider/BUILD.bazel @@ -55,6 +55,7 @@ sass_library( sass_binary( name = "css", src = "divider.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", diff --git a/src/material/expansion/BUILD.bazel b/src/material/expansion/BUILD.bazel index 7c3ad0c29e93..f092626f0c01 100644 --- a/src/material/expansion/BUILD.bazel +++ b/src/material/expansion/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "expansion-panel.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -65,6 +66,7 @@ sass_binary( sass_binary( name = "header_css", src = "expansion-panel-header.scss", + layer = "angular-material", deps = [ ":m2", ":variables", diff --git a/src/material/form-field/BUILD.bazel b/src/material/form-field/BUILD.bazel index 9d2bbecd71f1..69e494e19fdd 100644 --- a/src/material/form-field/BUILD.bazel +++ b/src/material/form-field/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "form-field.scss", + layer = "angular-material", deps = [ ":form_field_partials", "//src/material/core/style:vendor_prefixes", diff --git a/src/material/grid-list/BUILD.bazel b/src/material/grid-list/BUILD.bazel index 7e32331c766d..cd285ba24405 100644 --- a/src/material/grid-list/BUILD.bazel +++ b/src/material/grid-list/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "grid-list.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/style:layout_common", diff --git a/src/material/icon/BUILD.bazel b/src/material/icon/BUILD.bazel index 200496244769..c571a00d94af 100644 --- a/src/material/icon/BUILD.bazel +++ b/src/material/icon/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "icon.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/style:vendor_prefixes", diff --git a/src/material/list/BUILD.bazel b/src/material/list/BUILD.bazel index 7f574e3e9b66..b18f8fe99a0b 100644 --- a/src/material/list/BUILD.bazel +++ b/src/material/list/BUILD.bazel @@ -55,6 +55,7 @@ sass_library( sass_binary( name = "css", src = "list.scss", + layer = "angular-material", deps = [ ":list_inherited_structure", ":list_item_hcm_indicator", @@ -83,6 +84,7 @@ sass_library( sass_binary( name = "option_css", src = "list-option.scss", + layer = "angular-material", deps = [ ":list_item_hcm_indicator", "//src/material/checkbox:checkbox_common", diff --git a/src/material/menu/BUILD.bazel b/src/material/menu/BUILD.bazel index 9b8ab63420f1..039b139fb79b 100644 --- a/src/material/menu/BUILD.bazel +++ b/src/material/menu/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "menu.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/paginator/BUILD.bazel b/src/material/paginator/BUILD.bazel index a685da54f71e..9185c5511a3f 100644 --- a/src/material/paginator/BUILD.bazel +++ b/src/material/paginator/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "paginator.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/progress-bar/BUILD.bazel b/src/material/progress-bar/BUILD.bazel index 7cb4f1c4b4e0..3f43b0dd1860 100644 --- a/src/material/progress-bar/BUILD.bazel +++ b/src/material/progress-bar/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "progress-bar.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/progress-spinner/BUILD.bazel b/src/material/progress-spinner/BUILD.bazel index d079b0679711..495609ceb435 100644 --- a/src/material/progress-spinner/BUILD.bazel +++ b/src/material/progress-spinner/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "progress-spinner.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/radio/BUILD.bazel b/src/material/radio/BUILD.bazel index 72a82b87f39e..f280f3e9248c 100644 --- a/src/material/radio/BUILD.bazel +++ b/src/material/radio/BUILD.bazel @@ -63,6 +63,7 @@ sass_library( sass_binary( name = "css", src = "radio.scss", + layer = "angular-material", deps = [ ":m2", ":radio_common", diff --git a/src/material/schematics/ng-add/index.spec.ts b/src/material/schematics/ng-add/index.spec.ts index 58977ad01a32..0db6f472183f 100644 --- a/src/material/schematics/ng-add/index.spec.ts +++ b/src/material/schematics/ng-add/index.spec.ts @@ -107,6 +107,8 @@ describe('ng-add schematic', () => { expect(themeContent).toContain(`@use '@angular/material' as mat;`); expect(themeContent).toContain(`@include mat.theme((`); + expect(themeContent).toContain(`@layer base, cdk-resets, cdk-overlay, angular-material`); + expect(themeContent).toContain(`@include mat.theme-layer`); }); it('should create a custom theme file if no SCSS file could be found', async () => { diff --git a/src/material/schematics/ng-add/theming/create-theme.ts b/src/material/schematics/ng-add/theming/create-theme.ts index fb25eb8eb010..70555a145f92 100644 --- a/src/material/schematics/ng-add/theming/create-theme.ts +++ b/src/material/schematics/ng-add/theming/create-theme.ts @@ -16,22 +16,29 @@ export function createTheme(userPaletteChoice: string): string { ]); return ` // Include theming for Angular Material with \`mat.theme()\`. -// This Sass mixin will define CSS variables that are used for styling Angular Material -// components according to the Material 3 design spec. -// Learn more about theming and how to use it for your application's -// custom components at https://material.angular.dev/guide/theming +// \`@use\` must come before any other rules (including \`@layer\`). @use '@angular/material' as mat; -html { - height: 100%; - @include mat.theme(( - color: ( - primary: mat.$${colorPalettes.get(userPaletteChoice)!.primary}-palette, - tertiary: mat.$${colorPalettes.get(userPaletteChoice)!.tertiary}-palette, - ), - typography: Roboto, - density: 0, - )); +// Cascade layer ordering. Angular Material component styles ship in the +// \`angular-material\` layer. Declaring layer order here makes overrides +// predictable alongside CDK styles and utility frameworks. +// Learn more: https://material.angular.dev/guide/theming#css-cascade-layers +@layer base, cdk-resets, cdk-overlay, angular-material, components, utilities; + +// Wrapping \`mat.theme()\` in \`mat.theme-layer\` places generated CSS +// custom properties in the same \`angular-material\` layer as component styles. +@include mat.theme-layer { + html { + height: 100%; + @include mat.theme(( + color: ( + primary: mat.$${colorPalettes.get(userPaletteChoice)!.primary}-palette, + tertiary: mat.$${colorPalettes.get(userPaletteChoice)!.tertiary}-palette, + ), + typography: Roboto, + density: 0, + )); + } } body { diff --git a/src/material/select/BUILD.bazel b/src/material/select/BUILD.bazel index a0322c99128f..6930fd7daad7 100644 --- a/src/material/select/BUILD.bazel +++ b/src/material/select/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "select.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/sidenav/BUILD.bazel b/src/material/sidenav/BUILD.bazel index 12b246015cc9..7d12c7587bca 100644 --- a/src/material/sidenav/BUILD.bazel +++ b/src/material/sidenav/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "drawer.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/slide-toggle/BUILD.bazel b/src/material/slide-toggle/BUILD.bazel index 670051f1243c..8a762d192ba3 100644 --- a/src/material/slide-toggle/BUILD.bazel +++ b/src/material/slide-toggle/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "slide-toggle.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/slider/BUILD.bazel b/src/material/slider/BUILD.bazel index e8cefdaac781..67b8cfbb0ee2 100644 --- a/src/material/slider/BUILD.bazel +++ b/src/material/slider/BUILD.bazel @@ -55,6 +55,7 @@ sass_library( sass_binary( name = "slider-css", src = "slider.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -66,6 +67,7 @@ sass_binary( sass_binary( name = "slider-thumb-css", src = "slider-thumb.scss", + layer = "angular-material", ) ng_project( diff --git a/src/material/slider/tailwind-layer.e2e.spec.ts b/src/material/slider/tailwind-layer.e2e.spec.ts new file mode 100644 index 000000000000..0c7b64c2231c --- /dev/null +++ b/src/material/slider/tailwind-layer.e2e.spec.ts @@ -0,0 +1,35 @@ +/** + * @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.dev/license + */ + +import {browser, by, element} from 'protractor'; + +describe('Tailwind layer + Angular Material E2E', () => { + beforeEach(async () => await browser.get('/tailwind-layer')); + + it('allows utility layer styles to override Angular Material styles', async () => { + const button = element(by.id('tailwind-utility-button')); + const backgroundColor = await browser.executeScript( + 'return getComputedStyle(arguments[0]).backgroundColor;', + button.getWebElement(), + ); + + // Tailwind's lime-500 utility value. + expect(backgroundColor).toBe('rgb(132, 204, 22)'); + }); + + it('also allows unlayered utility styles to override layered Material styles', async () => { + const button = element(by.id('unlayered-utility-button')); + const backgroundColor = await browser.executeScript( + 'return getComputedStyle(arguments[0]).backgroundColor;', + button.getWebElement(), + ); + + // Unlayered author styles outrank named layers in Cascade 5. + expect(backgroundColor).toBe('rgb(217, 70, 239)'); + }); +}); diff --git a/src/material/snack-bar/BUILD.bazel b/src/material/snack-bar/BUILD.bazel index 9908896fdc62..87307ff005cd 100644 --- a/src/material/snack-bar/BUILD.bazel +++ b/src/material/snack-bar/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "container_css", src = "snack-bar-container.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -64,6 +65,7 @@ sass_binary( sass_binary( name = "css", src = "simple-snack-bar.scss", + layer = "angular-material", ) ng_project( diff --git a/src/material/sort/BUILD.bazel b/src/material/sort/BUILD.bazel index f7508bf87e58..e3a568b6a49a 100644 --- a/src/material/sort/BUILD.bazel +++ b/src/material/sort/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "sort-header.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/focus-indicators", diff --git a/src/material/stepper/BUILD.bazel b/src/material/stepper/BUILD.bazel index 362a7aa08934..777ea52765de 100644 --- a/src/material/stepper/BUILD.bazel +++ b/src/material/stepper/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "stepper.scss", + layer = "angular-material", deps = [ ":m2", ":variables", @@ -64,6 +65,7 @@ sass_binary( sass_binary( name = "header_css", src = "step-header.scss", + layer = "angular-material", deps = [ ":m2", ":variables", diff --git a/src/material/table/BUILD.bazel b/src/material/table/BUILD.bazel index 67b096982285..c2c2577c94ed 100644 --- a/src/material/table/BUILD.bazel +++ b/src/material/table/BUILD.bazel @@ -60,6 +60,7 @@ sass_library( sass_binary( name = "css", src = "table.scss", + layer = "angular-material", deps = [ ":flex_sass", ":m2", diff --git a/src/material/tabs/BUILD.bazel b/src/material/tabs/BUILD.bazel index 8096c7c9d398..271f84b7e565 100644 --- a/src/material/tabs/BUILD.bazel +++ b/src/material/tabs/BUILD.bazel @@ -65,6 +65,7 @@ sass_library( sass_binary( name = "tab_group_css", src = "tab-group.scss", + layer = "angular-material", deps = [ ":m2", ":sass", @@ -76,6 +77,7 @@ sass_binary( sass_binary( name = "tab_nav_bar_css", src = "tab-nav-bar/tab-nav-bar.scss", + layer = "angular-material", deps = [ ":sass", ], @@ -84,6 +86,7 @@ sass_binary( sass_binary( name = "tab_link_css", src = "tab-nav-bar/tab-link.scss", + layer = "angular-material", deps = [ ":sass", "//src/material/core/style:variables", @@ -93,6 +96,7 @@ sass_binary( sass_binary( name = "tab_header_css", src = "tab-header.scss", + layer = "angular-material", deps = [ ":sass", "//src/cdk:sass_lib", @@ -102,6 +106,7 @@ sass_binary( sass_binary( name = "tab_body_css", src = "tab-body.scss", + layer = "angular-material", deps = ["//src/material/core/style:layout_common"], ) diff --git a/src/material/timepicker/BUILD.bazel b/src/material/timepicker/BUILD.bazel index 1e8bcf2457ad..3e4a157f898d 100644 --- a/src/material/timepicker/BUILD.bazel +++ b/src/material/timepicker/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "timepicker_css", src = "timepicker.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/toolbar/BUILD.bazel b/src/material/toolbar/BUILD.bazel index d52db6a50a6b..d5340b6cb7df 100644 --- a/src/material/toolbar/BUILD.bazel +++ b/src/material/toolbar/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "toolbar.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/tooltip/BUILD.bazel b/src/material/tooltip/BUILD.bazel index 5ff2539e1993..de4daf8af6b9 100644 --- a/src/material/tooltip/BUILD.bazel +++ b/src/material/tooltip/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "tooltip.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", diff --git a/src/material/tree/BUILD.bazel b/src/material/tree/BUILD.bazel index d727de28c820..b599d88379fa 100644 --- a/src/material/tree/BUILD.bazel +++ b/src/material/tree/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "tree.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 232d63d84443..87215a851ff3 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -29,17 +29,61 @@ spec_bundle = _spec_bundle http_server = _http_server ng_examples_db = _ng_examples_db -def sass_binary(sourcemap = False, include_paths = [], **kwargs): - _sass_binary( - sourcemap = sourcemap, - include_paths = include_paths, - module_mappings = { - "@angular/cdk": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/cdk", - "@angular/material": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/material", - "@angular/material-experimental": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/material-experimental", - }, - **kwargs - ) +def sass_binary(sourcemap = False, include_paths = [], layer = None, **kwargs): + _module_mappings = { + "@angular/cdk": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/cdk", + "@angular/material": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/material", + "@angular/material-experimental": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/material-experimental", + } + + if layer: + if sourcemap: + fail("sass_binary(layer=...) does not support sourcemap because wrapping the CSS in @layer invalidates the generated mappings.") + if kwargs.get("output_name") or kwargs.get("output_dir"): + fail("sass_binary(layer=...) does not support output_name or output_dir.") + + name = kwargs.pop("name") + visibility = kwargs.pop("visibility", None) + testonly = kwargs.get("testonly", False) + src = kwargs.get("src", "") + out_css = src.rsplit(".", 1)[0] + ".css" + + # Prefix only the filename to avoid creating spurious directories. + if "/" in out_css: + out_dir, out_file = out_css.rsplit("/", 1) + raw_css = out_dir + "/_layer_" + out_file + else: + raw_css = "_layer_" + out_css + + # Compile to an intermediate CSS file, then wrap the compiled output in a named @layer + # while preserving the final filename expected by component `styleUrl`s. + _sass_binary( + name = name + "_pre_layer", + sourcemap = False, + include_paths = include_paths, + output_name = raw_css, + module_mappings = _module_mappings, + visibility = ["//visibility:private"], + **kwargs + ) + + native.genrule( + name = name, + srcs = [":" + name + "_pre_layer"], + outs = [out_css], + cmd = ("printf '@layer %s {\\n' > $@ " + + "&& cat $< >> $@ " + + "&& printf '\\n}\\n' >> $@") % layer, + testonly = testonly, + visibility = visibility, + ) + else: + _sass_binary( + sourcemap = sourcemap, + include_paths = include_paths, + module_mappings = _module_mappings, + **kwargs + ) def sass_library(**kwargs): _sass_library(**kwargs)