Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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