Skip to content

Commit

Permalink
fix(material-experimental/theming): Make M3 work with typography-hier…
Browse files Browse the repository at this point in the history
…archy (#28540)

* fix(material-experimental/theming): Make M3 work with typography-hierarchy

* test: Add tests for M3 typography hierarchy
  • Loading branch information
mmalerba authored Feb 6, 2024
1 parent 130afed commit f1deb30
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 9 deletions.
6 changes: 3 additions & 3 deletions src/dev-app/theme-m3.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ html {
// @include matx.popover-edit-theme($light-theme);
}

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

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

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

&.demo-unicorn-dark-theme {
@include matx.color-variants-back-compat($dark-theme);
Expand Down
8 changes: 3 additions & 5 deletions src/material/core/theming/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,9 @@ build_test(
ts_library(
name = "unit_test_lib",
testonly = True,
srcs = [
"theming-definition-api.spec.ts",
"theming-inspection-api.spec.ts",
"theming-mixin-api.spec.ts",
],
srcs = glob([
"*.spec.ts",
]),
# TODO(ESM): remove this once the Bazel NodeJS rules can handle ESM with `nodejs_binary`.
devmode_module = "commonjs",
deps = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {compileString} from 'sass';
import {runfiles} from '@bazel/runfiles';
import * as path from 'path';

import {createLocalAngularPackageImporter} from '../../../../../tools/sass/local-sass-importer';
import {pathToFileURL} from 'url';

// Note: For Windows compatibility, we need to resolve the directory paths through runfiles
// which are guaranteed to reside in the source tree.
const testDir = path.join(runfiles.resolvePackageRelative('../_all-theme.scss'), '../tests');
const packagesDir = path.join(runfiles.resolveWorkspaceRelative('src/cdk/_index.scss'), '../..');

const localPackageSassImporter = createLocalAngularPackageImporter(packagesDir);

const mdcSassImporter = {
findFileUrl: (url: string) => {
if (url.toString().startsWith('@material')) {
return pathToFileURL(
path.join(runfiles.resolveWorkspaceRelative('./node_modules'), url),
) as URL;
}
return null;
},
};

/** Transpiles given Sass content into CSS. */
function transpile(content: string) {
return compileString(
`
@use '../../../index' as mat;
@use '../../../../material-experimental/index' as matx;
$internals: _mat-theming-internals-do-not-access;
$theme: matx.define-theme();
${content}
`,
{
loadPaths: [testDir],
importers: [localPackageSassImporter, mdcSassImporter],
},
).css.toString();
}

function verifyFullSelector(css: string, selector: string) {
expect(css).toMatch(
new RegExp(String.raw`(^|\n)` + selector.replace(/\./g, String.raw`\.`) + String.raw` \{`),
);
}

describe('typography hierarchy', () => {
describe('for M3', () => {
it('should emit styles for h1', () => {
const css = transpile('@include mat.typography-hierarchy($theme)');
verifyFullSelector(
css,
'.mat-display-large, .mat-typography .mat-display-large, .mat-typography h1',
);
});

it('should emit default body styles', () => {
const css = transpile('@include mat.typography-hierarchy($theme)');
verifyFullSelector(css, '.mat-body-large, .mat-typography .mat-body-large, .mat-typography');
});

it('should emit default body paragraph styles', () => {
const css = transpile('@include mat.typography-hierarchy($theme)');
verifyFullSelector(
css,
'.mat-body-large p, .mat-typography .mat-body-large p, .mat-typography p',
);
});

it('should emit m2 selectors when requested', () => {
const css = transpile('@include mat.typography-hierarchy($theme, $back-compat: true)');
verifyFullSelector(
css,
'.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',
);
});

it('should use custom selector prefix', () => {
const css = transpile(`@include mat.typography-hierarchy($theme, $selector: '.special')`);
verifyFullSelector(css, '.mat-display-large, .special .mat-display-large, .special h1');
});
});
});
127 changes: 126 additions & 1 deletion src/material/core/typography/_typography.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
@use 'sass:list';
@use 'sass:map';
@use 'sass:string';
@use 'typography-utils';
@use '../theming/inspection';
@use './versioning';
Expand All @@ -8,11 +11,133 @@
@forward './definition';
@forward './versioning';

@mixin typography-hierarchy($theme, $selector: '.mat-typography', $back-compat: false) {
@if inspection.get-theme-version($theme) == 1 {
@include _m3-typography-hierarchy($theme, $selector, $back-compat);
}
@else {
@include _m2-typography-hierarchy($theme, $selector);
}
}

@function _get-selector($selectors, $prefix) {
$result: ();
@each $selector in $selectors {
// Don't add "naked" tag selectors, and don't nest prefix selector.
@if string.index($selector, '.') == 1 {
$result: list.append($result, $selector, $separator: comma);
}
// Don't nest the prefix selector in itself.
@if $selector != $prefix {
$result: list.append($result, '#{$prefix} #{$selector}', $separator: comma);
}
}
@return $result;
}

@mixin _m3-typography-level($theme, $selector-prefix, $level, $selectors, $margin: null) {
#{_get-selector($selectors, $selector-prefix)} {
// TODO(mmalerba): When we expose system tokens as CSS vars, we should change this to emit token
// slots.
font: inspection.get-theme-typography($theme, $level, font);
letter-spacing: inspection.get-theme-typography($theme, $level, letter-spacing);
@if $margin != null {
margin: 0 0 $margin;
}
}
}

@mixin _m3-typography-hierarchy($theme, $selector-prefix, $add-m2-selectors) {
$levels: (
display-large: (
selectors: ('.mat-display-large', 'h1'),
m2-selectors: ('.mat-h1', '.mat-headline-1'),
margin: 0.5em
),
display-medium: (
selectors: ('.mat-display-medium', 'h2'),
m2-selectors: ('.mat-h2', '.mat-headline-2'),
margin: 0.5em
),
display-small: (
selectors: ('.mat-display-small', 'h3'),
m2-selectors: ('.mat-h3', '.mat-headline-3'),
margin: 0.5em
),
headline-large: (
selectors: ('.mat-headline-large', 'h4'),
m2-selectors: ('.mat-h4', '.mat-headline-4'),
margin: 0.5em
),
headline-medium: (
selectors: ('.mat-headline-medium', 'h5'),
m2-selectors: ('.mat-h5', '.mat-headline-5'),
margin: 0.5em
),
headline-small: (
selectors: ('.mat-headline-small', 'h6'),
m2-selectors: ('.mat-h6', '.mat-headline-6'),
margin: 0.5em
),
title-large: (
selectors: ('.mat-title-large'),
m2-selectors: ('.mat-subtitle-1'),
),
title-medium: (
selectors: ('.mat-title-medium'),
m2-selectors: ('.mat-subtitle-2'),
),
title-small: (
selectors: ('.mat-title-small')
),
body-large: (
selectors: ('.mat-body-large', $selector-prefix),
m2-selectors: ('.mat-body', '.mat-body-strong', '.mat-body-2'),
),
body-medium: (
selectors: ('.mat-body-medium')
),
body-small: (
selectors: ('.mat-body-small')
),
label-large: (
selectors: ('.mat-label-large')
),
label-medium: (
selectors: ('.mat-label-medium')
),
label-small: (
selectors: ('.mat-label-small'),
m2-selectors: ('.mat-small', '.mat-caption')
),
);

@each $level, $options in $levels {
@if $add-m2-selectors {
$options: map.set($options, selectors,
list.join(map.get($options, selectors), map.get($options, m2-selectors) or ()));
}
$options: map.remove($options, m2-selectors);

// Apply styles for the level.
@include _m3-typography-level($theme, $selector-prefix, $level, $options...);

// Also style <p> inside body-large.
@if $level == body-large {
#{_get-selector(map.get($options, selectors), $selector-prefix)} {
p {
margin: 0 0 0.75em;
}
}
}
}
}

/// Emits baseline typographic styles based on a given config.
/// @param {Map} $config-or-theme A typography config for an entire theme.
/// @param {String} $selector Ancestor selector under which native elements, such as h1, will
/// be styled.
@mixin typography-hierarchy($theme, $selector: '.mat-typography') {
@mixin _m2-typography-hierarchy($theme, $selector) {
// Note that it seems redundant to prefix the class rules with the `$selector`, however it's
// necessary if we want to allow people to overwrite the tag selectors. This is due to
// selectors like `#{$selector} h1` being more specific than ones like `.mat-title`.
Expand Down

0 comments on commit f1deb30

Please sign in to comment.