Skip to content

Commit 9c75771

Browse files
committed
feat(material/schematics): add migration to switch to the new theming 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.
1 parent 4316787 commit 9c75771

File tree

7 files changed

+663
-2
lines changed

7 files changed

+663
-2
lines changed

src/cdk/_index.scss

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
@forward './overlay/overlay' show overlay;
1+
@forward './overlay/overlay' show overlay, $z-index-overlay-container, $z-index-overlay,
2+
$z-index-overlay-backdrop, $dark-backdrop-background;
23
@forward './a11y/a11y' show a11y-visually-hidden, high-contrast;
34
@forward './text-field/text-field' show text-field-autosize, text-field-autofill,
4-
text-field-autofill-color;
5+
text-field-autofill-color, text-field;

src/material/schematics/collection.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
"factory": "./ng-generate/address-form/index",
4343
"schema": "./ng-generate/address-form/schema.json",
4444
"aliases": ["address-form", "material-address-form", "material-addressForm"]
45+
},
46+
"themingApi": {
47+
"description": "Switch the project to the new @use-based Material theming API",
48+
"factory": "./ng-generate/theming-api/index",
49+
"schema": "./ng-generate/theming-api/schema.json",
50+
"aliases": ["theming-api", "sass-api"]
4551
}
4652
}
4753
}
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
2+
import {createTestApp, getFileContent} from '@angular/cdk/schematics/testing';
3+
import {COLLECTION_PATH} from '../../paths';
4+
import {Schema} from './schema';
5+
6+
describe('Material theming API schematic', () => {
7+
const options: Schema = {};
8+
let runner: SchematicTestRunner;
9+
10+
beforeEach(() => {
11+
runner = new SchematicTestRunner('schematics', COLLECTION_PATH);
12+
});
13+
14+
it('should migrate a theme based on the theming API', async () => {
15+
const app = await createTestApp(runner);
16+
app.create('/theme.scss', [
17+
`@import '~@angular/material/theming';`,
18+
19+
`@include mat-core();`,
20+
21+
`$candy-app-primary: mat-palette($mat-indigo);`,
22+
`$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);`,
23+
`$candy-app-theme: mat-light-theme((`,
24+
`color: (`,
25+
`primary: $candy-app-primary,`,
26+
`accent: $candy-app-accent,`,
27+
`)`,
28+
`));`,
29+
30+
`@include angular-material-theme($candy-app-theme);`,
31+
32+
`$dark-primary: mat-palette($mat-blue-grey);`,
33+
`$dark-accent: mat-palette($mat-amber, A200, A100, A400);`,
34+
`$dark-warn: mat-palette($mat-deep-orange);`,
35+
`$dark-theme: mat-dark-theme((`,
36+
`color: (`,
37+
`primary: $dark-primary,`,
38+
`accent: $dark-accent,`,
39+
`warn: $dark-warn,`,
40+
`)`,
41+
`));`,
42+
43+
`.unicorn-dark-theme {`,
44+
`@include angular-material-color($dark-theme);`,
45+
`}`
46+
].join('\n'));
47+
48+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
49+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
50+
`@use '~@angular/material' as mat;`,
51+
52+
`@include mat.core();`,
53+
54+
`$candy-app-primary: mat.define-palette(mat.$indigo-palette);`,
55+
`$candy-app-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);`,
56+
`$candy-app-theme: mat.define-light-theme((`,
57+
`color: (`,
58+
`primary: $candy-app-primary,`,
59+
`accent: $candy-app-accent,`,
60+
`)`,
61+
`));`,
62+
63+
`@include mat.all-component-themes($candy-app-theme);`,
64+
65+
`$dark-primary: mat.define-palette(mat.$blue-grey-palette);`,
66+
`$dark-accent: mat.define-palette(mat.$amber-palette, A200, A100, A400);`,
67+
`$dark-warn: mat.define-palette(mat.$deep-orange-palette);`,
68+
`$dark-theme: mat.define-dark-theme((`,
69+
`color: (`,
70+
`primary: $dark-primary,`,
71+
`accent: $dark-accent,`,
72+
`warn: $dark-warn,`,
73+
`)`,
74+
`));`,
75+
76+
`.unicorn-dark-theme {`,
77+
`@include mat.all-component-colors($dark-theme);`,
78+
`}`
79+
]);
80+
});
81+
82+
it('should migrate files using CDK APIs through the theming import', async () => {
83+
const app = await createTestApp(runner);
84+
app.create('/theme.scss', [
85+
`@import '~@angular/material/theming';`,
86+
``,
87+
`@include cdk-overlay();`,
88+
``,
89+
90+
`.my-dialog {`,
91+
`z-index: $cdk-z-index-overlay-container + 1;`,
92+
`}`,
93+
``,
94+
`@include cdk-high-contrast(active, off) {`,
95+
`button {`,
96+
`outline: solid 1px;`,
97+
`}`,
98+
`}`
99+
].join('\n'));
100+
101+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
102+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
103+
`@use '~@angular/cdk' as cdk;`,
104+
`@include cdk.overlay();`,
105+
``,
106+
`.my-dialog {`,
107+
`z-index: cdk.$z-index-overlay-container + 1;`,
108+
`}`,
109+
``,
110+
`@include cdk.high-contrast(active, off) {`,
111+
`button {`,
112+
`outline: solid 1px;`,
113+
`}`,
114+
`}`
115+
]);
116+
});
117+
118+
it('should migrate files using both Material and CDK APIs', async () => {
119+
const app = await createTestApp(runner);
120+
app.create('/theme.scss', [
121+
`@import './foo'`,
122+
`@import '~@angular/material/theming';`,
123+
``,
124+
`@include cdk-overlay();`,
125+
`@include mat-core();`,
126+
127+
`$candy-app-primary: mat-palette($mat-indigo);`,
128+
`$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);`,
129+
`$candy-app-theme: mat-light-theme((`,
130+
`color: (`,
131+
`primary: $candy-app-primary,`,
132+
`accent: $candy-app-accent,`,
133+
`)`,
134+
`));`,
135+
136+
`@include angular-material-theme($candy-app-theme);`,
137+
138+
`.my-dialog {`,
139+
`z-index: $cdk-z-index-overlay-container + 1;`,
140+
`}`,
141+
].join('\n'));
142+
143+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
144+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
145+
`@use '~@angular/cdk' as cdk;`,
146+
`@use '~@angular/material' as mat;`,
147+
`@import './foo'`,
148+
``,
149+
`@include cdk.overlay();`,
150+
`@include mat.core();`,
151+
152+
`$candy-app-primary: mat.define-palette(mat.$indigo-palette);`,
153+
`$candy-app-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);`,
154+
`$candy-app-theme: mat.define-light-theme((`,
155+
`color: (`,
156+
`primary: $candy-app-primary,`,
157+
`accent: $candy-app-accent,`,
158+
`)`,
159+
`));`,
160+
161+
`@include mat.all-component-themes($candy-app-theme);`,
162+
163+
`.my-dialog {`,
164+
`z-index: cdk.$z-index-overlay-container + 1;`,
165+
`}`
166+
]);
167+
});
168+
169+
it('should detect imports using double quotes', async () => {
170+
const app = await createTestApp(runner);
171+
app.create('/theme.scss', [
172+
`@import "~@angular/material/theming";`,
173+
`@include mat-core();`,
174+
].join('\n'));
175+
176+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
177+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
178+
`@use '~@angular/material' as mat;`,
179+
`@include mat.core();`,
180+
]);
181+
});
182+
183+
it('should migrate mixins that are invoked without parentheses', async () => {
184+
const app = await createTestApp(runner);
185+
app.create('/theme.scss', [
186+
`@import '~@angular/material/theming';`,
187+
`@include mat-base-typography;`,
188+
].join('\n'));
189+
190+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
191+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
192+
`@use '~@angular/material' as mat;`,
193+
`@include mat.typography-hierarchy;`,
194+
]);
195+
});
196+
197+
it('should allow an arbitrary number of spaces after @include and @import', async () => {
198+
const app = await createTestApp(runner);
199+
app.create('/theme.scss', [
200+
`@import '~@angular/material/theming';`,
201+
`@include mat-core;`,
202+
].join('\n'));
203+
204+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
205+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
206+
`@use '~@angular/material' as mat;`,
207+
`@include mat.core;`,
208+
]);
209+
});
210+
211+
it('should insert the new @use statement above other @import statements', async () => {
212+
const app = await createTestApp(runner);
213+
app.create('/theme.scss', [
214+
`@import './foo'`,
215+
`@import "~@angular/material/theming";`,
216+
`@import './bar'`,
217+
`@include mat-core();`,
218+
].join('\n'));
219+
220+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
221+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
222+
`@use '~@angular/material' as mat;`,
223+
`@import './foo'`,
224+
`@import './bar'`,
225+
`@include mat.core();`,
226+
]);
227+
});
228+
229+
it('should account for other @use statements when inserting the new Material @use', async () => {
230+
const app = await createTestApp(runner);
231+
app.create('/theme.scss', [
232+
`@use './foo'`,
233+
`@import './bar'`,
234+
`@import "~@angular/material/theming";`,
235+
`@include mat-core();`,
236+
].join('\n'));
237+
238+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
239+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
240+
`@use './foo'`,
241+
`@use '~@angular/material' as mat;`,
242+
`@import './bar'`,
243+
`@include mat.core();`,
244+
]);
245+
});
246+
247+
it('should account for file headers placed aboved the @import statements', async () => {
248+
const app = await createTestApp(runner);
249+
app.create('/theme.scss', [
250+
`/** This is a license. */`,
251+
`@import './foo'`,
252+
`@import '~@angular/material/theming';`,
253+
`@include mat-core();`,
254+
].join('\n'));
255+
256+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
257+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
258+
`/** This is a license. */`,
259+
`@use '~@angular/material' as mat;`,
260+
`@import './foo'`,
261+
`@include mat.core();`,
262+
]);
263+
});
264+
265+
it('should migrate multiple files within the same project', async () => {
266+
const app = await createTestApp(runner);
267+
app.create('/theme.scss', [
268+
`@import '~@angular/material/theming';`,
269+
`@include angular-material-theme();`,
270+
].join('\n'));
271+
272+
app.create('/components/dialog.scss', [
273+
`@import '~@angular/material/theming';`,
274+
`.my-dialog {`,
275+
`z-index: $cdk-z-index-overlay-container + 1;`,
276+
`}`,
277+
].join('\n'));
278+
279+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
280+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
281+
`@use '~@angular/material' as mat;`,
282+
`@include mat.all-component-themes();`,
283+
]);
284+
expect(getFileContent(tree, '/components/dialog.scss').split('\n')).toEqual([
285+
`@use '~@angular/cdk' as cdk;`,
286+
`.my-dialog {`,
287+
`z-index: cdk.$z-index-overlay-container + 1;`,
288+
`}`,
289+
]);
290+
});
291+
292+
it('should handle variables whose names overlap', async () => {
293+
const app = await createTestApp(runner);
294+
app.create('/theme.scss', [
295+
`@import '~@angular/material/theming';`,
296+
`$one: $mat-blue-grey;`,
297+
`$two: $mat-blue;`,
298+
'$three: $mat-blue',
299+
'$four: $mat-blue-gray',
300+
].join('\n'));
301+
302+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
303+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
304+
`@use '~@angular/material' as mat;`,
305+
`$one: mat.$blue-grey-palette;`,
306+
`$two: mat.$blue-palette;`,
307+
'$three: mat.$blue-palette',
308+
'$four: mat.$blue-gray-palette',
309+
]);
310+
});
311+
312+
it('should migrate individual component themes', async () => {
313+
const app = await createTestApp(runner);
314+
app.create('/theme.scss', [
315+
`@import '~@angular/material/theming';`,
316+
317+
`@include mat-core();`,
318+
319+
`$candy-app-primary: mat-palette($mat-indigo);`,
320+
`$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);`,
321+
`$candy-app-theme: mat-light-theme((`,
322+
`color: (`,
323+
`primary: $candy-app-primary,`,
324+
`accent: $candy-app-accent,`,
325+
`)`,
326+
`));`,
327+
328+
`@include mat-button-theme($candy-app-theme);`,
329+
`@include mat-table-theme($candy-app-theme);`,
330+
`@include mat-expansion-panel-theme($candy-app-theme);`,
331+
`@include mat-datepicker-theme($candy-app-theme);`,
332+
`@include mat-option-theme($candy-app-theme);`,
333+
].join('\n'));
334+
335+
const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise();
336+
expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([
337+
`@use '~@angular/material' as mat;`,
338+
339+
`@include mat.core();`,
340+
341+
`$candy-app-primary: mat.define-palette(mat.$indigo-palette);`,
342+
`$candy-app-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);`,
343+
`$candy-app-theme: mat.define-light-theme((`,
344+
`color: (`,
345+
`primary: $candy-app-primary,`,
346+
`accent: $candy-app-accent,`,
347+
`)`,
348+
`));`,
349+
350+
`@include mat.button-theme($candy-app-theme);`,
351+
`@include mat.table-theme($candy-app-theme);`,
352+
// This one is a special case, because the migration also fixes an incorrect name.
353+
`@include mat.expansion-theme($candy-app-theme);`,
354+
`@include mat.datepicker-theme($candy-app-theme);`,
355+
`@include mat.option-theme($candy-app-theme);`,
356+
]);
357+
});
358+
359+
});

0 commit comments

Comments
 (0)