From 87e3ba7531b4ff59923d7b92a7356aa5de28a8aa Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 6 Feb 2024 00:01:00 +0000 Subject: [PATCH 1/2] fix(material-experimental/theming): Make M3 work with typography-hierarchy --- src/dev-app/theme-m3.scss | 6 +- src/material/core/typography/_typography.scss | 127 +++++++++++++++++- 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/src/dev-app/theme-m3.scss b/src/dev-app/theme-m3.scss index d882dfb5d968..1ec8f6edd4c4 100644 --- a/src/dev-app/theme-m3.scss +++ b/src/dev-app/theme-m3.scss @@ -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. @@ -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); diff --git a/src/material/core/typography/_typography.scss b/src/material/core/typography/_typography.scss index 93ba526a72cd..35089c1facc3 100644 --- a/src/material/core/typography/_typography.scss +++ b/src/material/core/typography/_typography.scss @@ -1,3 +1,6 @@ +@use 'sass:list'; +@use 'sass:map'; +@use 'sass:string'; @use 'typography-utils'; @use '../theming/inspection'; @use './versioning'; @@ -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

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`. From 7da1da04257a28fa4863511dd14e49320cc64aa7 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 6 Feb 2024 21:51:10 +0000 Subject: [PATCH 2/2] test: Add tests for M3 typography hierarchy --- src/material/core/theming/tests/BUILD.bazel | 8 +- .../theming-typography-hierarchy.spec.ts | 88 +++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 src/material/core/theming/tests/theming-typography-hierarchy.spec.ts diff --git a/src/material/core/theming/tests/BUILD.bazel b/src/material/core/theming/tests/BUILD.bazel index 4d2657c60c53..3d5f466d8f1d 100644 --- a/src/material/core/theming/tests/BUILD.bazel +++ b/src/material/core/theming/tests/BUILD.bazel @@ -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 = [ diff --git a/src/material/core/theming/tests/theming-typography-hierarchy.spec.ts b/src/material/core/theming/tests/theming-typography-hierarchy.spec.ts new file mode 100644 index 000000000000..a8ecbbf278a1 --- /dev/null +++ b/src/material/core/theming/tests/theming-typography-hierarchy.spec.ts @@ -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'); + }); + }); +});