Skip to content

Commit

Permalink
add validation to extend-theme
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalerba committed Jun 17, 2024
1 parent 55e5c65 commit ab52c43
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 23 deletions.
8 changes: 5 additions & 3 deletions src/material/checkbox/_checkbox-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@
&.mat-primary {
@include token-utils.create-token-values(
tokens-mdc-checkbox.$prefix,
tokens-mdc-checkbox.get-color-tokens($theme, primary));
tokens-mdc-checkbox.get-color-tokens($theme, primary)
);
}

&.mat-warn {
@include token-utils.create-token-values(
tokens-mdc-checkbox.$prefix,
tokens-mdc-checkbox.get-color-tokens($theme, warn));
tokens-mdc-checkbox.get-color-tokens($theme, warn)
);
}
}
}
Expand Down Expand Up @@ -123,7 +125,7 @@
/// @param {Map} $theme The material theme for an application.
/// @param {Map} $overrides The token values to override in the theme.
@function extend-theme($theme, $overrides: ()) {
@return token-utils.extend-theme($theme, checkbox, $overrides);
@return token-utils.extend-theme($theme, ((mdc, checkbox), (mat, checkbox)), $overrides);
}

/// Outputs all (base, color, typography, and density) theme styles for the mat-checkbox.
Expand Down
154 changes: 151 additions & 3 deletions src/material/core/theming/tests/m3-theme.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {parse, Rule} from 'postcss';
import {compileString} from 'sass';
import {runfiles} from '@bazel/runfiles';
import * as path from 'path';
import {parse, Rule} from 'postcss';
import {compileString} from 'sass';

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

// Note: For Windows compatibility, we need to resolve the directory paths through runfiles
// which are guaranteed to reside in the source tree.
Expand Down Expand Up @@ -128,4 +128,152 @@ describe('M3 theme', () => {
/Calls to Angular Material theme mixins with an M3 theme must be wrapped in a selector/,
);
});

describe('theme extension API', () => {
it('should allow overriding token value', () => {
const css = transpile(`
@use '../../tokens/token-utils';
$theme: token-utils.extend-theme($theme, ((mdc, checkbox), (mat, checkbox)), (
selected-checkmark-color: magenta
));
html {
@include mat.checkbox-theme($theme);
}
`);

expect(css).toContain('--mdc-checkbox-selected-checkmark-color: magenta');
});

it('should not override token value for other color variant', () => {
const css = transpile(`
@use '../../tokens/token-utils';
$theme: token-utils.extend-theme($theme, ((mdc, checkbox), (mat, checkbox)), (
selected-checkmark-color: magenta
));
html {
@include mat.checkbox-theme($theme, $color-variant: secondary);
}
`);

expect(css).not.toContain('--mdc-checkbox-selected-checkmark-color: magenta');
});

it('should allow overriding specific color variant separately', () => {
const css = transpile(`
@use '../../tokens/token-utils';
$theme: token-utils.extend-theme($theme, ((mdc, checkbox), (mat, checkbox)), (
selected-checkmark-color: magenta,
tertiary: (
selected-checkmark-color: cyan,
),
));
html {
@include mat.checkbox-theme($theme);
}
.tertiary {
@include mat.checkbox-color($theme, $color-variant: tertiary);
}
`);

expect(css).toContain('--mdc-checkbox-selected-checkmark-color: magenta');
expect(css).toContain('--mdc-checkbox-selected-checkmark-color: cyan');
});
});

it('should error if used on M2 theme', () => {
expect(() =>
transpile(`
@use '../../tokens/token-utils';
$theme: mat.m2-define-light-theme(mat.$m2-red-palette, mat.$m2-red-palette);
$theme: token-utils.extend-theme($theme, ((mdc, checkbox), (mat, checkbox)), (
selected-checkmark-color: magenta
));
html {
@include mat.checkbox-theme($theme);
}
`),
).toThrowError(/The `extend-theme` functions are only supported for M3 themes/);
});

it('should error on invalid namespace', () => {
expect(() =>
transpile(`
@use '../../tokens/token-utils';
$theme: token-utils.extend-theme($theme, ((mdc, checkbox), (mat, checkbocks)), (
selected-checkmark-color: magenta
));
html {
@include mat.checkbox-theme($theme);
}
`),
).toThrowError(
/Error extending theme: Theme does not have tokes for namespace `\(mat, checkbocks\)`/,
);
});

it('should error on ambiguous shorthand token name', () => {
expect(() =>
transpile(`
@use '../../tokens/token-utils';
$theme: token-utils.extend-theme($theme, ((mdc, checkbox), (mdc, radio)), (
selected-checkmark-color: magenta
));
html {
@include mat.checkbox-theme($theme);
}
`),
).toThrowError(
/Error extending theme: Ambiguous token name `.*` exists in multiple namespaces: `\(mdc, checkbox\)` and `\(mdc, radio\)`/,
);
});

it('should error on unknown variant', () => {
expect(() =>
transpile(`
@use '../../tokens/token-utils';
$theme: token-utils.extend-theme($theme, ((mdc, checkbox), (mat, checkbox)), (
accent: (
selected-checkmark-color: magenta
)
));
html {
@include mat.checkbox-theme($theme);
}
`),
).toThrowError(
/Error extending theme: Unrecognized color variant `accent`. Allowed variants are: primary, secondary, tertiary, error, surface/,
);
});

it('should error on unknown token', () => {
expect(() =>
transpile(`
@use '../../tokens/token-utils';
$theme: token-utils.extend-theme($theme, ((mdc, checkbox), (mat, checkbox)), (
fake-token: red
));
html {
@include mat.checkbox-theme($theme);
}
`),
).toThrowError(/Error extending theme: Unrecognized token `fake-token`. Allowed tokens are: /);
});
});
68 changes: 51 additions & 17 deletions src/material/core/tokens/_token-utils.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@use '../m2/palette' as m2-palette;
@use '../m2/theming' as m2-theming;
@use '../m2/typography' as m2-typography;
@use '../theming/inspection';

// Indicates whether we're building internally. Used for backwards compatibility.
$private-is-internal-build: false;
Expand Down Expand Up @@ -424,29 +425,62 @@ $_component-prefix: null;
/// Returns a theme config with the given tokens overridden.
/// @param {Map} $theme The material theme for an application.
/// @param {Map} $overrides The token values to override in the theme.
@function extend-theme($theme, $component, $overrides: ()) {
@function extend-theme($theme, $extend-namespaces, $overrides: ()) {
$internals: _mat-theming-internals-do-not-access;
$systems: (color-tokens, typography-tokens, density-tokens, base-tokens);
$variants: (primary, secondary, tertiary, error, surface);

@each $system in (color, typography, density, base) {
$system-name: $system + '-tokens';
$namespaces: map.get($theme, $internals, $system-name);
@each $namespace, $tokens in $namespaces {
@if (list.nth($namespace, 2) == $component) {
$namespace-overrides: if(
list.length($namespace) == 3,
map.get($overrides, list.nth($namespace, 3)),
$overrides
);

@each $name, $value in $tokens {
$token: map.get($namespace-overrides, $name);
@if $token != null {
$theme: map.set($theme, $internals, $system-name, $namespace, $name, $token);
}
@if (inspection.get-theme-version($theme) < 1) {
@error #{'The `extend-theme` functions are only supported for M3 themes'};
}

// Determine which system and namespace each shorthand token belongs to.
$seen-tokens: ();
@each $namespace in $extend-namespaces {
@each $system in $systems {
$tokens: map.get($theme, $internals, $system, $namespace);
@if $tokens == null {
@error #{'Error extending theme: Theme does not have tokes for namespace `('}#{$namespace}#{')`'};
}
@each $name, $value in $tokens {
@if map.has-key($seen-tokens, $name) {
@error #{'Error extending theme: Ambiguous token name `'}#{$name}#{'` exists in multiple namespaces: `('}#{list.nth(map.get($seen-tokens, $name), 1)}#{')` and `('}#{$namespace}#{')`'};
}
$seen-tokens: map.set($seen-tokens, $name, ($namespace, $system));
}
}
}

// Update internal tokens based on given overrides.
@each $token, $value in $overrides {
@if (meta.type-of($value) == 'map') {
$variant: $token;
$variant-overrides: $value;
@if (list.index($variants, $variant) == null) {
@error #{'Error extending theme: Unrecognized color variant `'}#{$variant}#{'`. Allowed variants are: '}#{$variants};
}
@each $token, $value in $variant-overrides {
$theme: _update-token($theme, $seen-tokens, $token, $value, $variant);
}
} @else {
$theme: _update-token($theme, $seen-tokens, $token, $value);
}
}

@return $theme;
}

// Update the given token in the given theme.
@function _update-token($theme, $seen-tokens, $token, $value, $variant: null) {
$internals: _mat-theming-internals-do-not-access;
$token-info: map.get($seen-tokens, $token);
@if $token-info == null {
@error #{'Error extending theme: Unrecognized token `'}#{$token}#{'`. Allowed tokens are: '}#{map.keys($seen-tokens)};
}
$namespace: list.nth($token-info, 1);
@if ($variant != null) {
$namespace: list.append($namespace, $variant);
}
$system: list.nth($token-info, 2);
@return map.set($theme, $internals, $system, $namespace, $token, $value);
}

0 comments on commit ab52c43

Please sign in to comment.