Skip to content

Commit

Permalink
feat(material/schematics): add migration to switch to the new theming…
Browse files Browse the repository at this point in the history
… API

Adds an `ng-generate` schematic that will switch over existing stylesheets to the new `@use`-based API. Furthermore, the migration code is set up in a way that should allow us to run it in g3 if necessary.
  • Loading branch information
crisbeto committed Mar 21, 2021
1 parent 4316787 commit 9c75771
Show file tree
Hide file tree
Showing 7 changed files with 663 additions and 2 deletions.
5 changes: 3 additions & 2 deletions src/cdk/_index.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@forward './overlay/overlay' show overlay;
@forward './overlay/overlay' show overlay, $z-index-overlay-container, $z-index-overlay,
$z-index-overlay-backdrop, $dark-backdrop-background;
@forward './a11y/a11y' show a11y-visually-hidden, high-contrast;
@forward './text-field/text-field' show text-field-autosize, text-field-autofill,
text-field-autofill-color;
text-field-autofill-color, text-field;
6 changes: 6 additions & 0 deletions src/material/schematics/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
"factory": "./ng-generate/address-form/index",
"schema": "./ng-generate/address-form/schema.json",
"aliases": ["address-form", "material-address-form", "material-addressForm"]
},
"themingApi": {
"description": "Switch the project to the new @use-based Material theming API",
"factory": "./ng-generate/theming-api/index",
"schema": "./ng-generate/theming-api/schema.json",
"aliases": ["theming-api", "sass-api"]
}
}
}
359 changes: 359 additions & 0 deletions src/material/schematics/ng-generate/theming-api/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
import {createTestApp, getFileContent} from '@angular/cdk/schematics/testing';
import {COLLECTION_PATH} from '../../paths';
import {Schema} from './schema';

describe('Material theming API schematic', () => {
const options: Schema = {};
let runner: SchematicTestRunner;

beforeEach(() => {
runner = new SchematicTestRunner('schematics', COLLECTION_PATH);
});

it('should migrate a theme based on the theming API', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import '~@angular/material/theming';`,

`@include mat-core();`,

`$candy-app-primary: mat-palette($mat-indigo);`,
`$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);`,
`$candy-app-theme: mat-light-theme((`,
`color: (`,
`primary: $candy-app-primary,`,
`accent: $candy-app-accent,`,
`)`,
`));`,

`@include angular-material-theme($candy-app-theme);`,

`$dark-primary: mat-palette($mat-blue-grey);`,
`$dark-accent: mat-palette($mat-amber, A200, A100, A400);`,
`$dark-warn: mat-palette($mat-deep-orange);`,
`$dark-theme: mat-dark-theme((`,
`color: (`,
`primary: $dark-primary,`,
`accent: $dark-accent,`,
`warn: $dark-warn,`,
`)`,
`));`,

`.unicorn-dark-theme {`,
`@include angular-material-color($dark-theme);`,
`}`
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/material' as mat;`,

`@include mat.core();`,

`$candy-app-primary: mat.define-palette(mat.$indigo-palette);`,
`$candy-app-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);`,
`$candy-app-theme: mat.define-light-theme((`,
`color: (`,
`primary: $candy-app-primary,`,
`accent: $candy-app-accent,`,
`)`,
`));`,

`@include mat.all-component-themes($candy-app-theme);`,

`$dark-primary: mat.define-palette(mat.$blue-grey-palette);`,
`$dark-accent: mat.define-palette(mat.$amber-palette, A200, A100, A400);`,
`$dark-warn: mat.define-palette(mat.$deep-orange-palette);`,
`$dark-theme: mat.define-dark-theme((`,
`color: (`,
`primary: $dark-primary,`,
`accent: $dark-accent,`,
`warn: $dark-warn,`,
`)`,
`));`,

`.unicorn-dark-theme {`,
`@include mat.all-component-colors($dark-theme);`,
`}`
]);
});

it('should migrate files using CDK APIs through the theming import', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import '~@angular/material/theming';`,
``,
`@include cdk-overlay();`,
``,

`.my-dialog {`,
`z-index: $cdk-z-index-overlay-container + 1;`,
`}`,
``,
`@include cdk-high-contrast(active, off) {`,
`button {`,
`outline: solid 1px;`,
`}`,
`}`
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/cdk' as cdk;`,
`@include cdk.overlay();`,
``,
`.my-dialog {`,
`z-index: cdk.$z-index-overlay-container + 1;`,
`}`,
``,
`@include cdk.high-contrast(active, off) {`,
`button {`,
`outline: solid 1px;`,
`}`,
`}`
]);
});

it('should migrate files using both Material and CDK APIs', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import './foo'`,
`@import '~@angular/material/theming';`,
``,
`@include cdk-overlay();`,
`@include mat-core();`,

`$candy-app-primary: mat-palette($mat-indigo);`,
`$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);`,
`$candy-app-theme: mat-light-theme((`,
`color: (`,
`primary: $candy-app-primary,`,
`accent: $candy-app-accent,`,
`)`,
`));`,

`@include angular-material-theme($candy-app-theme);`,

`.my-dialog {`,
`z-index: $cdk-z-index-overlay-container + 1;`,
`}`,
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/cdk' as cdk;`,
`@use '~@angular/material' as mat;`,
`@import './foo'`,
``,
`@include cdk.overlay();`,
`@include mat.core();`,

`$candy-app-primary: mat.define-palette(mat.$indigo-palette);`,
`$candy-app-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);`,
`$candy-app-theme: mat.define-light-theme((`,
`color: (`,
`primary: $candy-app-primary,`,
`accent: $candy-app-accent,`,
`)`,
`));`,

`@include mat.all-component-themes($candy-app-theme);`,

`.my-dialog {`,
`z-index: cdk.$z-index-overlay-container + 1;`,
`}`
]);
});

it('should detect imports using double quotes', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import "~@angular/material/theming";`,
`@include mat-core();`,
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/material' as mat;`,
`@include mat.core();`,
]);
});

it('should migrate mixins that are invoked without parentheses', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import '~@angular/material/theming';`,
`@include mat-base-typography;`,
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/material' as mat;`,
`@include mat.typography-hierarchy;`,
]);
});

it('should allow an arbitrary number of spaces after @include and @import', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import '~@angular/material/theming';`,
`@include mat-core;`,
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/material' as mat;`,
`@include mat.core;`,
]);
});

it('should insert the new @use statement above other @import statements', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import './foo'`,
`@import "~@angular/material/theming";`,
`@import './bar'`,
`@include mat-core();`,
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/material' as mat;`,
`@import './foo'`,
`@import './bar'`,
`@include mat.core();`,
]);
});

it('should account for other @use statements when inserting the new Material @use', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@use './foo'`,
`@import './bar'`,
`@import "~@angular/material/theming";`,
`@include mat-core();`,
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use './foo'`,
`@use '~@angular/material' as mat;`,
`@import './bar'`,
`@include mat.core();`,
]);
});

it('should account for file headers placed aboved the @import statements', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`/** This is a license. */`,
`@import './foo'`,
`@import '~@angular/material/theming';`,
`@include mat-core();`,
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`/** This is a license. */`,
`@use '~@angular/material' as mat;`,
`@import './foo'`,
`@include mat.core();`,
]);
});

it('should migrate multiple files within the same project', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import '~@angular/material/theming';`,
`@include angular-material-theme();`,
].join('\n'));

app.create('/components/dialog.scss', [
`@import '~@angular/material/theming';`,
`.my-dialog {`,
`z-index: $cdk-z-index-overlay-container + 1;`,
`}`,
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/material' as mat;`,
`@include mat.all-component-themes();`,
]);
expect(getFileContent(tree, '/components/dialog.scss').split('\n')).toEqual([
`@use '~@angular/cdk' as cdk;`,
`.my-dialog {`,
`z-index: cdk.$z-index-overlay-container + 1;`,
`}`,
]);
});

it('should handle variables whose names overlap', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import '~@angular/material/theming';`,
`$one: $mat-blue-grey;`,
`$two: $mat-blue;`,
'$three: $mat-blue',
'$four: $mat-blue-gray',
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/material' as mat;`,
`$one: mat.$blue-grey-palette;`,
`$two: mat.$blue-palette;`,
'$three: mat.$blue-palette',
'$four: mat.$blue-gray-palette',
]);
});

it('should migrate individual component themes', async () => {
const app = await createTestApp(runner);
app.create('/theme.scss', [
`@import '~@angular/material/theming';`,

`@include mat-core();`,

`$candy-app-primary: mat-palette($mat-indigo);`,
`$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);`,
`$candy-app-theme: mat-light-theme((`,
`color: (`,
`primary: $candy-app-primary,`,
`accent: $candy-app-accent,`,
`)`,
`));`,

`@include mat-button-theme($candy-app-theme);`,
`@include mat-table-theme($candy-app-theme);`,
`@include mat-expansion-panel-theme($candy-app-theme);`,
`@include mat-datepicker-theme($candy-app-theme);`,
`@include mat-option-theme($candy-app-theme);`,
].join('\n'));

const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
`@use '~@angular/material' as mat;`,

`@include mat.core();`,

`$candy-app-primary: mat.define-palette(mat.$indigo-palette);`,
`$candy-app-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);`,
`$candy-app-theme: mat.define-light-theme((`,
`color: (`,
`primary: $candy-app-primary,`,
`accent: $candy-app-accent,`,
`)`,
`));`,

`@include mat.button-theme($candy-app-theme);`,
`@include mat.table-theme($candy-app-theme);`,
// This one is a special case, because the migration also fixes an incorrect name.
`@include mat.expansion-theme($candy-app-theme);`,
`@include mat.datepicker-theme($candy-app-theme);`,
`@include mat.option-theme($candy-app-theme);`,
]);
});

});
Loading

0 comments on commit 9c75771

Please sign in to comment.