Skip to content

Commit f1deb30

Browse files
authored
fix(material-experimental/theming): Make M3 work with typography-hierarchy (#28540)
* fix(material-experimental/theming): Make M3 work with typography-hierarchy * test: Add tests for M3 typography hierarchy
1 parent 130afed commit f1deb30

File tree

4 files changed

+220
-9
lines changed

4 files changed

+220
-9
lines changed

src/dev-app/theme-m3.scss

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ html {
3939
// @include matx.popover-edit-theme($light-theme);
4040
}
4141

42-
// TODO(mmalerba): Support M3 for typography hierarchy.
43-
// @include mat.typography-hierarchy($light-theme);
42+
@include mat.typography-hierarchy($light-theme);
4443

4544
.demo-strong-focus {
4645
// Note: we can theme the indicators directly through `strong-focus-indicators` as well.
@@ -77,9 +76,10 @@ $density-scales: (-1, -2, -3, -4, minimum, maximum);
7776
}
7877
}
7978

80-
// Enable back-compat CSS for color="..." API.
79+
// Enable back-compat CSS for color="..." API & typography hierarchy.
8180
.demo-color-api-back-compat {
8281
@include matx.color-variants-back-compat($light-theme);
82+
@include mat.typography-hierarchy($light-theme, $back-compat: true);
8383

8484
&.demo-unicorn-dark-theme {
8585
@include matx.color-variants-back-compat($dark-theme);

src/material/core/theming/tests/BUILD.bazel

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,9 @@ build_test(
5555
ts_library(
5656
name = "unit_test_lib",
5757
testonly = True,
58-
srcs = [
59-
"theming-definition-api.spec.ts",
60-
"theming-inspection-api.spec.ts",
61-
"theming-mixin-api.spec.ts",
62-
],
58+
srcs = glob([
59+
"*.spec.ts",
60+
]),
6361
# TODO(ESM): remove this once the Bazel NodeJS rules can handle ESM with `nodejs_binary`.
6462
devmode_module = "commonjs",
6563
deps = [
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {compileString} from 'sass';
2+
import {runfiles} from '@bazel/runfiles';
3+
import * as path from 'path';
4+
5+
import {createLocalAngularPackageImporter} from '../../../../../tools/sass/local-sass-importer';
6+
import {pathToFileURL} from 'url';
7+
8+
// Note: For Windows compatibility, we need to resolve the directory paths through runfiles
9+
// which are guaranteed to reside in the source tree.
10+
const testDir = path.join(runfiles.resolvePackageRelative('../_all-theme.scss'), '../tests');
11+
const packagesDir = path.join(runfiles.resolveWorkspaceRelative('src/cdk/_index.scss'), '../..');
12+
13+
const localPackageSassImporter = createLocalAngularPackageImporter(packagesDir);
14+
15+
const mdcSassImporter = {
16+
findFileUrl: (url: string) => {
17+
if (url.toString().startsWith('@material')) {
18+
return pathToFileURL(
19+
path.join(runfiles.resolveWorkspaceRelative('./node_modules'), url),
20+
) as URL;
21+
}
22+
return null;
23+
},
24+
};
25+
26+
/** Transpiles given Sass content into CSS. */
27+
function transpile(content: string) {
28+
return compileString(
29+
`
30+
@use '../../../index' as mat;
31+
@use '../../../../material-experimental/index' as matx;
32+
33+
$internals: _mat-theming-internals-do-not-access;
34+
35+
$theme: matx.define-theme();
36+
37+
${content}
38+
`,
39+
{
40+
loadPaths: [testDir],
41+
importers: [localPackageSassImporter, mdcSassImporter],
42+
},
43+
).css.toString();
44+
}
45+
46+
function verifyFullSelector(css: string, selector: string) {
47+
expect(css).toMatch(
48+
new RegExp(String.raw`(^|\n)` + selector.replace(/\./g, String.raw`\.`) + String.raw` \{`),
49+
);
50+
}
51+
52+
describe('typography hierarchy', () => {
53+
describe('for M3', () => {
54+
it('should emit styles for h1', () => {
55+
const css = transpile('@include mat.typography-hierarchy($theme)');
56+
verifyFullSelector(
57+
css,
58+
'.mat-display-large, .mat-typography .mat-display-large, .mat-typography h1',
59+
);
60+
});
61+
62+
it('should emit default body styles', () => {
63+
const css = transpile('@include mat.typography-hierarchy($theme)');
64+
verifyFullSelector(css, '.mat-body-large, .mat-typography .mat-body-large, .mat-typography');
65+
});
66+
67+
it('should emit default body paragraph styles', () => {
68+
const css = transpile('@include mat.typography-hierarchy($theme)');
69+
verifyFullSelector(
70+
css,
71+
'.mat-body-large p, .mat-typography .mat-body-large p, .mat-typography p',
72+
);
73+
});
74+
75+
it('should emit m2 selectors when requested', () => {
76+
const css = transpile('@include mat.typography-hierarchy($theme, $back-compat: true)');
77+
verifyFullSelector(
78+
css,
79+
'.mat-display-large, .mat-typography .mat-display-large, .mat-typography h1, .mat-h1, .mat-typography .mat-h1, .mat-headline-1, .mat-typography .mat-headline-1',
80+
);
81+
});
82+
83+
it('should use custom selector prefix', () => {
84+
const css = transpile(`@include mat.typography-hierarchy($theme, $selector: '.special')`);
85+
verifyFullSelector(css, '.mat-display-large, .special .mat-display-large, .special h1');
86+
});
87+
});
88+
});

src/material/core/typography/_typography.scss

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
@use 'sass:list';
2+
@use 'sass:map';
3+
@use 'sass:string';
14
@use 'typography-utils';
25
@use '../theming/inspection';
36
@use './versioning';
@@ -8,11 +11,133 @@
811
@forward './definition';
912
@forward './versioning';
1013

14+
@mixin typography-hierarchy($theme, $selector: '.mat-typography', $back-compat: false) {
15+
@if inspection.get-theme-version($theme) == 1 {
16+
@include _m3-typography-hierarchy($theme, $selector, $back-compat);
17+
}
18+
@else {
19+
@include _m2-typography-hierarchy($theme, $selector);
20+
}
21+
}
22+
23+
@function _get-selector($selectors, $prefix) {
24+
$result: ();
25+
@each $selector in $selectors {
26+
// Don't add "naked" tag selectors, and don't nest prefix selector.
27+
@if string.index($selector, '.') == 1 {
28+
$result: list.append($result, $selector, $separator: comma);
29+
}
30+
// Don't nest the prefix selector in itself.
31+
@if $selector != $prefix {
32+
$result: list.append($result, '#{$prefix} #{$selector}', $separator: comma);
33+
}
34+
}
35+
@return $result;
36+
}
37+
38+
@mixin _m3-typography-level($theme, $selector-prefix, $level, $selectors, $margin: null) {
39+
#{_get-selector($selectors, $selector-prefix)} {
40+
// TODO(mmalerba): When we expose system tokens as CSS vars, we should change this to emit token
41+
// slots.
42+
font: inspection.get-theme-typography($theme, $level, font);
43+
letter-spacing: inspection.get-theme-typography($theme, $level, letter-spacing);
44+
@if $margin != null {
45+
margin: 0 0 $margin;
46+
}
47+
}
48+
}
49+
50+
@mixin _m3-typography-hierarchy($theme, $selector-prefix, $add-m2-selectors) {
51+
$levels: (
52+
display-large: (
53+
selectors: ('.mat-display-large', 'h1'),
54+
m2-selectors: ('.mat-h1', '.mat-headline-1'),
55+
margin: 0.5em
56+
),
57+
display-medium: (
58+
selectors: ('.mat-display-medium', 'h2'),
59+
m2-selectors: ('.mat-h2', '.mat-headline-2'),
60+
margin: 0.5em
61+
),
62+
display-small: (
63+
selectors: ('.mat-display-small', 'h3'),
64+
m2-selectors: ('.mat-h3', '.mat-headline-3'),
65+
margin: 0.5em
66+
),
67+
headline-large: (
68+
selectors: ('.mat-headline-large', 'h4'),
69+
m2-selectors: ('.mat-h4', '.mat-headline-4'),
70+
margin: 0.5em
71+
),
72+
headline-medium: (
73+
selectors: ('.mat-headline-medium', 'h5'),
74+
m2-selectors: ('.mat-h5', '.mat-headline-5'),
75+
margin: 0.5em
76+
),
77+
headline-small: (
78+
selectors: ('.mat-headline-small', 'h6'),
79+
m2-selectors: ('.mat-h6', '.mat-headline-6'),
80+
margin: 0.5em
81+
),
82+
title-large: (
83+
selectors: ('.mat-title-large'),
84+
m2-selectors: ('.mat-subtitle-1'),
85+
),
86+
title-medium: (
87+
selectors: ('.mat-title-medium'),
88+
m2-selectors: ('.mat-subtitle-2'),
89+
),
90+
title-small: (
91+
selectors: ('.mat-title-small')
92+
),
93+
body-large: (
94+
selectors: ('.mat-body-large', $selector-prefix),
95+
m2-selectors: ('.mat-body', '.mat-body-strong', '.mat-body-2'),
96+
),
97+
body-medium: (
98+
selectors: ('.mat-body-medium')
99+
),
100+
body-small: (
101+
selectors: ('.mat-body-small')
102+
),
103+
label-large: (
104+
selectors: ('.mat-label-large')
105+
),
106+
label-medium: (
107+
selectors: ('.mat-label-medium')
108+
),
109+
label-small: (
110+
selectors: ('.mat-label-small'),
111+
m2-selectors: ('.mat-small', '.mat-caption')
112+
),
113+
);
114+
115+
@each $level, $options in $levels {
116+
@if $add-m2-selectors {
117+
$options: map.set($options, selectors,
118+
list.join(map.get($options, selectors), map.get($options, m2-selectors) or ()));
119+
}
120+
$options: map.remove($options, m2-selectors);
121+
122+
// Apply styles for the level.
123+
@include _m3-typography-level($theme, $selector-prefix, $level, $options...);
124+
125+
// Also style <p> inside body-large.
126+
@if $level == body-large {
127+
#{_get-selector(map.get($options, selectors), $selector-prefix)} {
128+
p {
129+
margin: 0 0 0.75em;
130+
}
131+
}
132+
}
133+
}
134+
}
135+
11136
/// Emits baseline typographic styles based on a given config.
12137
/// @param {Map} $config-or-theme A typography config for an entire theme.
13138
/// @param {String} $selector Ancestor selector under which native elements, such as h1, will
14139
/// be styled.
15-
@mixin typography-hierarchy($theme, $selector: '.mat-typography') {
140+
@mixin _m2-typography-hierarchy($theme, $selector) {
16141
// Note that it seems redundant to prefix the class rules with the `$selector`, however it's
17142
// necessary if we want to allow people to overwrite the tag selectors. This is due to
18143
// selectors like `#{$selector} h1` being more specific than ones like `.mat-title`.

0 commit comments

Comments
 (0)