From 552581061e1760bd74acc6a09f6a49cf141eb109 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 19 Oct 2023 01:01:04 +0000 Subject: [PATCH] fix(material/schematics): Create a schematic to add the base theme dimension As of v17, users need to include the new "base" theme dimension for the components they use. For users that are using the "theme" mixins now, the base dimension will be pulled in automatically. However, for users using the "color", "typography", and "density" mixins only, they will need to include the "base" mixin as well. This schematic works by scanning all of the app's Sass to determine which components had their "color", "typography", or "density" mixins included, but did *not* have their "theme" mixin included. It then locates all of the calls to "mat.core()" and inserts calls to the "base" mixins for the identified compoennts, immediately following. It makes sense to use "mat.core()" as a signal for when to insert them, because the "base" mixins should be used once-per-app, like "mat.core()". We can't guarantee that a "$theme" will be available at the insertion point, so we create a "$dummy-theme" to pass to the base mixins. We also insert a comment above the new mixins explaining why they were added and a TODO to clean up the "$dummy-theme". Once we have the documentation updated to cover the base dimension, we should follow-up by linking to it from the inserted comment. --- src/material/_index.scss | 2 +- src/material/schematics/ng-update/index.ts | 3 +- .../migrations/theme-base-v17/index.ts | 130 +++++++ .../migrations/theme-base-v17/migration.ts | 316 +++++++++++++++++ .../ng-update/test-cases/theme-base.spec.ts | 323 ++++++++++++++++++ 5 files changed, 772 insertions(+), 2 deletions(-) create mode 100644 src/material/schematics/ng-update/migrations/theme-base-v17/index.ts create mode 100644 src/material/schematics/ng-update/migrations/theme-base-v17/migration.ts create mode 100644 src/material/schematics/ng-update/test-cases/theme-base.spec.ts diff --git a/src/material/_index.scss b/src/material/_index.scss index fececf4b899f..133fa5a4606e 100644 --- a/src/material/_index.scss +++ b/src/material/_index.scss @@ -47,7 +47,7 @@ @forward './core/style/elevation' show elevation, overridable-elevation, elevation-transition; // Theme bundles -@forward './core/theming/all-theme' show all-component-themes; +@forward './core/theming/all-theme' show all-component-themes, all-component-bases; @forward './core/color/all-color' show all-component-colors; @forward './core/typography/all-typography' show all-component-typographies; diff --git a/src/material/schematics/ng-update/index.ts b/src/material/schematics/ng-update/index.ts index ee4cd5b680b5..91d843c39012 100644 --- a/src/material/schematics/ng-update/index.ts +++ b/src/material/schematics/ng-update/index.ts @@ -14,8 +14,9 @@ import { } from '@angular/cdk/schematics'; import {materialUpgradeData} from './upgrade-data'; +import {ThemeBaseMigration} from './migrations/theme-base-v17'; -const materialMigrations: NullableDevkitMigration[] = []; +const materialMigrations: NullableDevkitMigration[] = [ThemeBaseMigration]; /** Entry point for the migration schematics with target of Angular Material v17 */ export function updateToV17(): Rule { diff --git a/src/material/schematics/ng-update/migrations/theme-base-v17/index.ts b/src/material/schematics/ng-update/migrations/theme-base-v17/index.ts new file mode 100644 index 000000000000..929ac29cfff5 --- /dev/null +++ b/src/material/schematics/ng-update/migrations/theme-base-v17/index.ts @@ -0,0 +1,130 @@ +/** + * @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 {extname} from '@angular-devkit/core'; +import {SchematicContext} from '@angular-devkit/schematics'; +import {DevkitMigration, ResolvedResource, TargetVersion} from '@angular/cdk/schematics'; +import {addThemeBaseMixins, checkThemeBaseMixins} from './migration'; + +/** Adds an @include for theme base mixins that aren't already included by the app. */ +export class ThemeBaseMigration extends DevkitMigration { + /** Number of files that have been migrated. */ + static migratedFileCount = 0; + + /** All base mixins that we have found an existing @include for. */ + static foundBaseMixins = new Set(); + + /** All base mixins that appear to be missing an @include. */ + static missingBaseMixins = new Set(); + + /** Whether to run this migration. */ + enabled = this.targetVersion === TargetVersion.V17; + + /** + * All Sass stylesheets visited. (We save a record, so we can go back through them in the + * `postAnalysis` phase). + */ + visitedSassStylesheets: ResolvedResource[] = []; + + /** + * Visit each stylesheet, noting which base mixins are accounted for (because the user is calling + * `mat.-theme()`), and which ones are missing (because the user is calling one of the + * theme-partial mixins: `mat.()`, `mat.-typography()`, + * or `mat.-density()`. + * + * We don't make any modifications at this point. Instead, the results of visiting each stylesheet + * are aggregated into a static variable which is used to determine which mixins to add in + * `postAnalysis` phase. + */ + override visitStylesheet(stylesheet: ResolvedResource): void { + if (extname(stylesheet.filePath) === '.scss') { + this.visitedSassStylesheets.push(stylesheet); + + const content = stylesheet.content; + const {found, missing} = checkThemeBaseMixins(content); + for (const mixin of found) { + ThemeBaseMigration.foundBaseMixins.add(mixin); + ThemeBaseMigration.missingBaseMixins.delete(mixin); + } + for (const mixin of missing) { + if (!ThemeBaseMigration.foundBaseMixins.has(mixin)) { + ThemeBaseMigration.missingBaseMixins.add(mixin); + } + } + } + } + + /** + * Perform the necessary updates detected while visiting the stylesheets. The + * `mat.-base()` mixins behave similarly to `mat.core()`, in that they needed to be + * included once globally. So we locate calls to `mat.core()` and add the missing mixins + * identified by earlier at these locations. + */ + override postAnalysis() { + // If we're not missing any mixins, there's nothing to migrate. + if (ThemeBaseMigration.missingBaseMixins.size === 0) { + return; + } + // If we have all-component-bases, we don't need any others and there is nothing to migrate. + if (ThemeBaseMigration.foundBaseMixins.has('all-component-bases')) { + return; + } + // If we're missing all-component-bases, we just need to add it, not the individual mixins. + if (ThemeBaseMigration.missingBaseMixins.has('all-component-bases')) { + ThemeBaseMigration.missingBaseMixins = new Set(['all-component-bases']); + } + for (const stylesheet of this.visitedSassStylesheets) { + const content = stylesheet.content; + const migratedContent = content + ? addThemeBaseMixins(content, ThemeBaseMigration.missingBaseMixins) + : content; + + if (migratedContent && migratedContent !== content) { + this.fileSystem + .edit(stylesheet.filePath) + .remove(0, stylesheet.content.length) + .insertLeft(0, migratedContent); + ThemeBaseMigration.migratedFileCount++; + } + } + if (ThemeBaseMigration.migratedFileCount === 0) { + const mixinsText = [...ThemeBaseMigration.missingBaseMixins] + .sort() + .map(m => `mat.${m}($theme)`) + .join('\n'); + this.failures.push({ + filePath: this.context.tree.root.path, + message: + `The following mixins could not be automatically added, please add them manually` + + ` if needed:\n${mixinsText}`, + }); + } + } + + /** Logs out the number of migrated files at the end of the migration. */ + static override globalPostMigration( + _tree: unknown, + _targetVersion: TargetVersion, + context: SchematicContext, + ): void { + const fileCount = ThemeBaseMigration.migratedFileCount; + const mixinCount = ThemeBaseMigration.missingBaseMixins.size; + + if (fileCount > 0 && mixinCount > 0) { + const fileCountText = fileCount === 1 ? '1 file' : `${fileCount} files`; + const mixinCountText = + mixinCount === 1 ? '1 theme base mixin' : `${mixinCount} theme base mixins`; + context.logger.info(`Added ${mixinCountText} to ${fileCountText}.`); + } + + // Reset to avoid leaking between tests. + ThemeBaseMigration.migratedFileCount = 0; + ThemeBaseMigration.missingBaseMixins = new Set(); + ThemeBaseMigration.foundBaseMixins = new Set(); + } +} diff --git a/src/material/schematics/ng-update/migrations/theme-base-v17/migration.ts b/src/material/schematics/ng-update/migrations/theme-base-v17/migration.ts new file mode 100644 index 000000000000..ef3d4d8b0238 --- /dev/null +++ b/src/material/schematics/ng-update/migrations/theme-base-v17/migration.ts @@ -0,0 +1,316 @@ +/** + * @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 + */ + +/** Preamble to insert before the missing mixins. */ +const MISSING_MIXIN_PREAMBLE_LINES = `\ +// The following mixins include base theme styles that are only needed once per application. These +// theme styles do not depend on the color, typography, or density settings in your theme. However, +// these styles may differ depending on the theme's design system. Currently all themes use the +// Material 2 design system, but in the future it may be possible to create theme based on other +// design systems, such as Material 3. +// +// Please note: you do not need to include the 'base' mixins here, if you include the corresponding +// 'theme' mixin elsewhere in your app. The full 'theme' mixins already include the base styles. +// +// TODO: Please move these @include statements to the preferred place in your Sass, and pass your +// real theme rather than the dummy theme. This will ensure the correct values for your app are +// used. +$dummy-theme: ();`.split('\n'); + +/** The sets of theme mixins to check for. */ +const THEME_MIXIN_SETS: { + theme: string; + color: string; + typography: string; + density: string; + base: string; +}[] = [ + { + theme: 'all-component-themes', + color: 'all-component-colors', + typography: 'all-component-typographies', + density: 'all-component-densities', + base: 'all-component-bases', + }, + ...[ + 'core', + 'card', + 'progress-bar', + 'tooltip', + 'form-field', + 'input', + 'select', + 'autocomplete', + 'dialog', + 'chips', + 'slide-toggle', + 'radio', + 'slider', + 'menu', + 'list', + 'paginator', + 'tabs', + 'checkbox', + 'button', + 'icon-button', + 'fab', + 'snack-bar', + 'table', + 'progress-spinner', + 'badge', + 'bottom-sheet', + 'button-toggle', + 'datepicker', + 'divider', + 'expansion', + 'grid-list', + 'icon', + 'sidenav', + 'stepper', + 'sort', + 'toolbar', + 'tree', + ].map(comp => ({ + theme: `${comp}-theme`, + color: `${comp}-color`, + typography: `${comp}-typography`, + density: `${comp}-density`, + base: `${comp}-base`, + })), +]; + +/** Possible pairs of comment characters in a Sass file. */ +const COMMENT_PAIRS = new Map([ + ['/*', '*/'], + ['//', '\n'], +]); + +/** Prefix for the placeholder that will be used to escape comments. */ +const COMMENT_PLACEHOLDER_START = '__<} { + const placeholders: Record = {}; + let commentCounter = 0; + let [openIndex, closeIndex] = findComment(content); + + while (openIndex > -1 && closeIndex > -1) { + const placeholder = COMMENT_PLACEHOLDER_START + commentCounter++ + COMMENT_PLACEHOLDER_END; + placeholders[placeholder] = content.slice(openIndex, closeIndex); + content = content.slice(0, openIndex) + placeholder + content.slice(closeIndex); + [openIndex, closeIndex] = findComment(content); + } + + return {content, placeholders}; +} + +/** Finds the start and end index of a comment in a file. */ +function findComment(content: string): [openIndex: number, closeIndex: number] { + // Add an extra new line at the end so that we can correctly capture single-line comments + // at the end of the file. It doesn't really matter that the end index will be out of bounds, + // because `String.prototype.slice` will clamp it to the string length. + content += '\n'; + + for (const [open, close] of COMMENT_PAIRS.entries()) { + const openIndex = content.indexOf(open); + + if (openIndex > -1) { + const closeIndex = content.indexOf(close, openIndex + 1); + return closeIndex > -1 ? [openIndex, closeIndex + close.length] : [-1, -1]; + } + } + + return [-1, -1]; +} + +/** Restores the comments that have been escaped by `escapeComments`. */ +function restoreComments(content: string, placeholders: Record): string { + Object.keys(placeholders).forEach(key => (content = content.replace(key, placeholders[key]))); + return content; +} + +/** Escapes special regex characters in a string. */ +function escapeRegExp(str: string): string { + return str.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +} + +/** Parses out the namespace from a Sass `@use` statement. */ +function extractNamespaceFromUseStatement(fullImport: string): string { + const closeQuoteIndex = Math.max(fullImport.lastIndexOf(`"`), fullImport.lastIndexOf(`'`)); + + if (closeQuoteIndex > -1) { + const asExpression = 'as '; + const asIndex = fullImport.indexOf(asExpression, closeQuoteIndex); + + // If we found an ` as ` expression, we consider the rest of the text as the namespace. + if (asIndex > -1) { + return fullImport + .slice(asIndex + asExpression.length) + .split(';')[0] + .trim(); + } + + // Otherwise the namespace is the name of the file that is being imported. + const lastSlashIndex = fullImport.lastIndexOf('/', closeQuoteIndex); + + if (lastSlashIndex > -1) { + const fileName = fullImport + .slice(lastSlashIndex + 1, closeQuoteIndex) + // Sass allows for leading underscores to be omitted and it technically supports .scss. + .replace(/^_|(\.import)?\.scss$|\.import$/g, ''); + + // Sass ignores `/index` and infers the namespace as the next segment in the path. + if (fileName === 'index') { + const nextSlashIndex = fullImport.lastIndexOf('/', lastSlashIndex - 1); + + if (nextSlashIndex > -1) { + return fullImport.slice(nextSlashIndex + 1, lastSlashIndex); + } + } else { + return fileName; + } + } + } + + throw Error(`Could not extract namespace from import "${fullImport}".`); +} + +/** Gets the set of namespaces that the given import path is aliased to by @use. */ +function getAtUseNamespaces(content: string, path: string) { + const namespaces = new Set(); + const pattern = new RegExp(`@use +['"]~?${escapeRegExp(path)}['"].*;?\n`, 'g'); + let match: RegExpExecArray | null = null; + + while ((match = pattern.exec(content))) { + namespaces.add(extractNamespaceFromUseStatement(match[0])); + } + + return namespaces; +} + +/** Gets a list of matches representing where the given mixin is included with `@include`. */ +function getAtIncludes(content: string, namespace: string, mixin: string): RegExpMatchArray[] { + // The ending checks what comes after the mixin name. We need to check that we don't see a word + // character or `-` immediately following the mixin name, as that would change the name. Beyond + // that character we can match anything, to the end of the line. + const ending = '([^\\n\\w-][^\\n]*)?($|\\n)'; + const pattern = new RegExp( + `@include\\s+${escapeRegExp(namespace)}\\.${escapeRegExp(mixin)}${ending}`, + 'g', + ); + return [...content.matchAll(pattern)]; +} + +/** Checks whether the given mixin is included with `@include`. */ +function isMixinAtIncluded(content: string, namespace: string, mixin: string) { + return !!getAtIncludes(content, namespace, mixin).length; +} + +/** Inserts the given lines after the match point. */ +function insertLinesAfterMatch(content: string, match: RegExpMatchArray, lines: string[]): string { + const insertionPoint = match.index! + match[0].length; + return ( + content.substring(0, insertionPoint) + + lines.join('\n') + + '\n' + + content.substring(insertionPoint) + ); +} + +/** Gets the indentation at the given line in the content. */ +function getIndentation(content: string, index: number) { + let indentationStart = 0; + let indentationEnd = index; + for (let i = index; i >= 0; i--) { + if (content[i] === '\n') { + indentationStart = i + 1; + break; + } + if (!/\s/.exec(content[i])) { + indentationEnd = i; + } + } + return content.slice(indentationStart, indentationEnd); +} + +/** Gets the lines to insert to address the missing mixins. */ +function getMissingMixinLines(namespace: string, mixins: Set, indentation: string) { + return [ + ...MISSING_MIXIN_PREAMBLE_LINES, + ...[...mixins].sort().map(mixin => `@include ${namespace}.${mixin}($dummy-theme);`), + ].map(line => indentation + line); +} + +/** + * Checks which theme bases are found in the file via the existing included mixins, + * and which ones may be missing. + */ +export function checkThemeBaseMixins(fileContent: string): { + found: Set; + missing: Set; +} { + const found = new Set(); + const missing = new Set(); + + // Strip out comments, so they don't confuse our migration. + const {content} = escapeComments(fileContent); + const materialNamespaces = getAtUseNamespaces(content, '@angular/material'); + + // Check over all namespaces for mixins of interest. + for (const namespace of materialNamespaces) { + for (const mixins of THEME_MIXIN_SETS) { + // If they include the theme mixin, that accounts for the base theme styles. + if (isMixinAtIncluded(content, namespace, mixins.theme)) { + found.add(mixins.base); + missing.delete(mixins.base); + continue; + } + // If they haven't called the theme mixin, but do call one of the partials, + // we assume they're missing the base styles. + if (!found.has(mixins.base)) { + if ( + isMixinAtIncluded(content, namespace, mixins.color) || + isMixinAtIncluded(content, namespace, mixins.typography) || + isMixinAtIncluded(content, namespace, mixins.density) + ) { + missing.add(mixins.base); + } + } + } + } + + return {found, missing}; +} + +/** Adds the given theme base mixins, after the call to `mat.core()`. */ +export function addThemeBaseMixins(fileContent: string, mixins: Set): string { + // Strip out comments, so they don't confuse our migration. + let {content, placeholders} = escapeComments(fileContent); + const materialNamespaces = getAtUseNamespaces(content, '@angular/material'); + + for (const namespace of materialNamespaces) { + // Update the @includes in reverse order, so our changes don't mess up the indices we found. + const coreIncludes = getAtIncludes(content, namespace, 'core').reverse(); + for (const coreInclude of coreIncludes) { + if (coreInclude.index === undefined) { + throw Error(`Cannot find location of mat.core() match: ${coreInclude}`); + } + const indentation = getIndentation(content, coreInclude.index); + const lines = getMissingMixinLines(namespace, mixins, indentation); + content = insertLinesAfterMatch(content, coreInclude, lines); + } + } + + return restoreComments(content, placeholders); +} diff --git a/src/material/schematics/ng-update/test-cases/theme-base.spec.ts b/src/material/schematics/ng-update/test-cases/theme-base.spec.ts new file mode 100644 index 000000000000..763f5e35f78a --- /dev/null +++ b/src/material/schematics/ng-update/test-cases/theme-base.spec.ts @@ -0,0 +1,323 @@ +import {createTestCaseSetup} from '@angular/cdk/schematics/testing'; +import {MIGRATION_PATH} from '../../paths'; + +function defineTest( + description: string, + inputs: {[filename: string]: string}, + expected: {[filename: string]: string}, +) { + it(description, async () => { + const PATH = 'projects/cdk-testing/'; + const {runFixers, writeFile, appTree} = await createTestCaseSetup( + 'migration-v17', + MIGRATION_PATH, + [], + ); + + for (const filename in inputs) { + writeFile(PATH + filename, inputs[filename]); + } + + await runFixers(); + + for (const filename in expected) { + const actual = appTree.readContent(PATH + filename); + // Jasmine's expect(...).toBe(...) doesn't show us the full output. + if (actual != expected[filename]) { + fail(['\nActual:', actual, 'Expected:', expected[filename]].join('\n')); + } + } + }); +} + +describe('theme base mixins migration', () => { + defineTest( + 'should add base if color found', + { + 'global.scss': ` + @use '@angular/material' as mat; + $theme: (); + @include mat.core(); + @include mat.button-color($theme); + `, + }, + { + 'global.scss': ` + @use '@angular/material' as mat; + $theme: (); + @include mat.core(); + // The following mixins include base theme styles that are only needed once per application. These + // theme styles do not depend on the color, typography, or density settings in your theme. However, + // these styles may differ depending on the theme's design system. Currently all themes use the + // Material 2 design system, but in the future it may be possible to create theme based on other + // design systems, such as Material 3. + // + // Please note: you do not need to include the 'base' mixins here, if you include the corresponding + // 'theme' mixin elsewhere in your app. The full 'theme' mixins already include the base styles. + // + // TODO: Please move these @include statements to the preferred place in your Sass, and pass your + // real theme rather than the dummy theme. This will ensure the correct values for your app are + // used. + $dummy-theme: (); + @include mat.button-base($dummy-theme); + @include mat.button-color($theme); + `, + }, + ); + + defineTest( + 'should add base if typography found', + { + 'global.scss': ` + @use '@angular/material'; + $theme: (); + @include material.all-component-typographies($theme); + @include material.core(); + `, + }, + { + 'global.scss': ` + @use '@angular/material'; + $theme: (); + @include material.all-component-typographies($theme); + @include material.core(); + // The following mixins include base theme styles that are only needed once per application. These + // theme styles do not depend on the color, typography, or density settings in your theme. However, + // these styles may differ depending on the theme's design system. Currently all themes use the + // Material 2 design system, but in the future it may be possible to create theme based on other + // design systems, such as Material 3. + // + // Please note: you do not need to include the 'base' mixins here, if you include the corresponding + // 'theme' mixin elsewhere in your app. The full 'theme' mixins already include the base styles. + // + // TODO: Please move these @include statements to the preferred place in your Sass, and pass your + // real theme rather than the dummy theme. This will ensure the correct values for your app are + // used. + $dummy-theme: (); + @include material.all-component-bases($dummy-theme); + `, + }, + ); + + defineTest( + 'should add base if density found', + { + 'global.scss': ` + @use "@angular/material" as mat; + @include mat.core ; + @include mat.checkbox-density(maximum); + @include mat.card-density(-3); + `, + }, + { + 'global.scss': ` + @use "@angular/material" as mat; + @include mat.core ; + // The following mixins include base theme styles that are only needed once per application. These + // theme styles do not depend on the color, typography, or density settings in your theme. However, + // these styles may differ depending on the theme's design system. Currently all themes use the + // Material 2 design system, but in the future it may be possible to create theme based on other + // design systems, such as Material 3. + // + // Please note: you do not need to include the 'base' mixins here, if you include the corresponding + // 'theme' mixin elsewhere in your app. The full 'theme' mixins already include the base styles. + // + // TODO: Please move these @include statements to the preferred place in your Sass, and pass your + // real theme rather than the dummy theme. This will ensure the correct values for your app are + // used. + $dummy-theme: (); + @include mat.card-base($dummy-theme); + @include mat.checkbox-base($dummy-theme); + @include mat.checkbox-density(maximum); + @include mat.card-density(-3); + `, + }, + ); + + defineTest( + 'should not add all-components-bases and individual bases', + { + 'global.scss': ` + @use '@angular/material' as mat; + $theme: (); + @include mat.core(); + @include mat.all-component-colors($theme); + @include mat.button-typography($theme); + `, + }, + { + 'global.scss': ` + @use '@angular/material' as mat; + $theme: (); + @include mat.core(); + // The following mixins include base theme styles that are only needed once per application. These + // theme styles do not depend on the color, typography, or density settings in your theme. However, + // these styles may differ depending on the theme's design system. Currently all themes use the + // Material 2 design system, but in the future it may be possible to create theme based on other + // design systems, such as Material 3. + // + // Please note: you do not need to include the 'base' mixins here, if you include the corresponding + // 'theme' mixin elsewhere in your app. The full 'theme' mixins already include the base styles. + // + // TODO: Please move these @include statements to the preferred place in your Sass, and pass your + // real theme rather than the dummy theme. This will ensure the correct values for your app are + // used. + $dummy-theme: (); + @include mat.all-component-bases($dummy-theme); + @include mat.all-component-colors($theme); + @include mat.button-typography($theme); + `, + }, + ); + + defineTest( + 'should not add individual bases if all-component-themes is present', + { + 'global.scss': ` + @use '@angular/material' as mat; + $theme: (); + @include mat.core(); + @include mat.all-component-themes($theme); + @include mat.tabs-density($theme); + `, + }, + { + 'global.scss': ` + @use '@angular/material' as mat; + $theme: (); + @include mat.core(); + @include mat.all-component-themes($theme); + @include mat.tabs-density($theme); + `, + }, + ); + + defineTest( + 'should update all instances of mat.core', + { + 'global.scss': ` + @use '@angular/material' as mat; + .dark-theme { + $dark-theme: (); + @include mat.core(); + @include mat.slider-color($dark-theme); + } + .light-theme { + $light-theme: (); + @include mat.core(); + @include mat.slider-color($light-theme); + } + `, + }, + { + 'global.scss': ` + @use '@angular/material' as mat; + .dark-theme { + $dark-theme: (); + @include mat.core(); + // The following mixins include base theme styles that are only needed once per application. These + // theme styles do not depend on the color, typography, or density settings in your theme. However, + // these styles may differ depending on the theme's design system. Currently all themes use the + // Material 2 design system, but in the future it may be possible to create theme based on other + // design systems, such as Material 3. + // + // Please note: you do not need to include the 'base' mixins here, if you include the corresponding + // 'theme' mixin elsewhere in your app. The full 'theme' mixins already include the base styles. + // + // TODO: Please move these @include statements to the preferred place in your Sass, and pass your + // real theme rather than the dummy theme. This will ensure the correct values for your app are + // used. + $dummy-theme: (); + @include mat.slider-base($dummy-theme); + @include mat.slider-color($dark-theme); + } + .light-theme { + $light-theme: (); + @include mat.core(); + // The following mixins include base theme styles that are only needed once per application. These + // theme styles do not depend on the color, typography, or density settings in your theme. However, + // these styles may differ depending on the theme's design system. Currently all themes use the + // Material 2 design system, but in the future it may be possible to create theme based on other + // design systems, such as Material 3. + // + // Please note: you do not need to include the 'base' mixins here, if you include the corresponding + // 'theme' mixin elsewhere in your app. The full 'theme' mixins already include the base styles. + // + // TODO: Please move these @include statements to the preferred place in your Sass, and pass your + // real theme rather than the dummy theme. This will ensure the correct values for your app are + // used. + $dummy-theme: (); + @include mat.slider-base($dummy-theme); + @include mat.slider-color($light-theme); + } + `, + }, + ); + + defineTest( + 'should work across multiple files', + { + 'global.scss': ` + @use '@angular/material' as mat; + @use './theme'; + @use './typography'; + @include mat.core(); + @include theme.app-colors(); + @include theme.app-typography(); + `, + '_theme.scss': ` + @use '@angular/material' as mat; + $theme: (); + @mixin app-colors() { + @include mat.form-field-color($theme); + } + `, + '_typography.scss': ` + @use '@angular/material' as mat; + $typography: mat.define-typography-config(); + @mixin app-typography() { + @include mat.select-typography($typography); + } + `, + }, + { + 'global.scss': ` + @use '@angular/material' as mat; + @use './theme'; + @use './typography'; + @include mat.core(); + // The following mixins include base theme styles that are only needed once per application. These + // theme styles do not depend on the color, typography, or density settings in your theme. However, + // these styles may differ depending on the theme's design system. Currently all themes use the + // Material 2 design system, but in the future it may be possible to create theme based on other + // design systems, such as Material 3. + // + // Please note: you do not need to include the 'base' mixins here, if you include the corresponding + // 'theme' mixin elsewhere in your app. The full 'theme' mixins already include the base styles. + // + // TODO: Please move these @include statements to the preferred place in your Sass, and pass your + // real theme rather than the dummy theme. This will ensure the correct values for your app are + // used. + $dummy-theme: (); + @include mat.form-field-base($dummy-theme); + @include mat.select-base($dummy-theme); + @include theme.app-colors(); + @include theme.app-typography(); + `, + '_theme.scss': ` + @use '@angular/material' as mat; + $theme: (); + @mixin app-colors() { + @include mat.form-field-color($theme); + } + `, + '_typography.scss': ` + @use '@angular/material' as mat; + $typography: mat.define-typography-config(); + @mixin app-typography() { + @include mat.select-typography($typography); + } + `, + }, + ); +});