Skip to content

Commit

Permalink
feat(material/schematics): Add option to customize colors for neutral…
Browse files Browse the repository at this point in the history
… variant and error palettes (#30321)
  • Loading branch information
amysorto authored Jan 14, 2025
1 parent 5f238ab commit 44c7320
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 16 deletions.
8 changes: 6 additions & 2 deletions src/material/schematics/ng-generate/theme-color/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ optimized to have enough contrast to be more accessible. See [Science of Color D
for more information about Material's color design.

For more customization, custom colors can be also be provided for the
secondary, tertiary, and neutral palette colors. It is recommended to choose colors that
are contrastful. Material has more detailed guidance for [accessible design](https://m3.material.io/foundations/accessible-design/patterns).
secondary, tertiary, neutral, neutral variant, and error palette colors. It is recommended to choose
colors that are contrastful. Material has more detailed guidance for [accessible design](https://m3.material.io/foundations/accessible-design/patterns).

## Options

Expand All @@ -30,6 +30,10 @@ secondary color generated from Material based on the primary.
tertiary color generated from Material based on the primary.
* `neutralColor` - Color to use for app's neutral color palette. Defaults to
neutral color generated from Material based on the primary.
* `neutralVariantColor` - Color to use for app's neutral variant color palette. Defaults to
neutral variant color generated from Material based on the primary.
* `errorColor` - Color to use for app's error color palette. Defaults to
error color generated from Material based on the other palettes.
* `includeHighContrast` - Whether to define high contrast values for the custom colors in the
generated file. For Sass files a mixin is defined, see the [high contrast override mixins section](#high-contrast-override-mixins)
for more information. Defaults to false.
Expand Down
202 changes: 202 additions & 0 deletions src/material/schematics/ng-generate/theme-color/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,64 @@ describe('material-theme-color-schematic', () => {
expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS));
});

it('should generate themes when provided a primary, secondary, tertiary, neutral, and neutral variant colors', async () => {
const tree = await runM3ThemeSchematic(runner, {
primaryColor: '#984061',
secondaryColor: '#984061',
tertiaryColor: '#984061',
neutralColor: '#984061',
neutralVariantColor: '#984061',
});

const generatedSCSS = tree.readText('_theme-colors.scss');

// Change test theme palette so that secondary, tertiary, and neutral are
// the same source color as primary to match schematic inputs
let testPalettes = testM3ColorPalettes;
testPalettes.secondary = testPalettes.primary;
testPalettes.tertiary = testPalettes.primary;
testPalettes.neutral = testPalettes.primary;
testPalettes.neutralVariant = testPalettes.primary;

const testSCSS = generateSCSSTheme(
testPalettes,
'Color palettes are generated from primary: #984061, secondary: #984061, tertiary: #984061, neutral: #984061, neutral variant: #984061',
);

expect(generatedSCSS).toBe(testSCSS);
expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS));
});

it('should generate themes when provided a primary, secondary, tertiary, neutral, neutral variant, and error colors', async () => {
const tree = await runM3ThemeSchematic(runner, {
primaryColor: '#984061',
secondaryColor: '#984061',
tertiaryColor: '#984061',
neutralColor: '#984061',
neutralVariantColor: '#984061',
errorColor: '#984061',
});

const generatedSCSS = tree.readText('_theme-colors.scss');

// Change test theme palette so that secondary, tertiary, and neutral are
// the same source color as primary to match schematic inputs
let testPalettes = testM3ColorPalettes;
testPalettes.secondary = testPalettes.primary;
testPalettes.tertiary = testPalettes.primary;
testPalettes.neutral = testPalettes.primary;
testPalettes.neutralVariant = testPalettes.primary;
testPalettes.error = testPalettes.primary;

const testSCSS = generateSCSSTheme(
testPalettes,
'Color palettes are generated from primary: #984061, secondary: #984061, tertiary: #984061, neutral: #984061, neutral variant: #984061, error: #984061',
);

expect(generatedSCSS).toBe(testSCSS);
expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS));
});

describe('and with high contrast overrides', () => {
it('should be able to generate high contrast overrides mixin', async () => {
const tree = await runM3ThemeSchematic(runner, {
Expand Down Expand Up @@ -300,6 +358,63 @@ describe('material-theme-color-schematic', () => {
expect(generatedCSS).toContain(`--mat-sys-tertiary: #ffebef`);
expect(generatedCSS).toContain(`--mat-sys-surface-bright: #4f5051`);
});

it('should be able to generate high contrast themes overrides when provided primary, secondary, tertiary, neutral, and neutral variant color', async () => {
const tree = await runM3ThemeSchematic(runner, {
primaryColor: '#984061',
secondaryColor: '#984061',
tertiaryColor: '#984061',
neutralColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette
neutralVariantColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette
includeHighContrast: true,
});

const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss'));

// Check a system variable from each color palette for their high contrast light theme value
expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`);
expect(generatedCSS).toContain(`--mat-sys-secondary: #580b2f`);
expect(generatedCSS).toContain(`--mat-sys-tertiary: #580b2f`);
expect(generatedCSS).toContain(`--mat-sys-surface-bright: #f9f9f9`);
expect(generatedCSS).toContain(`--mat-sys-surface-variant: #e2e2e2`);

// Check a system variable from each color palette for their high contrast dark theme value
expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`);
expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`);
expect(generatedCSS).toContain(`--mat-sys-tertiary: #ffebef`);
expect(generatedCSS).toContain(`--mat-sys-surface-bright: #4f5051`);
expect(generatedCSS).toContain(`--mat-sys-surface-variant: #454747`);
});

it('should be able to generate high contrast themes overrides when provided primary, secondary, tertiary, neutral, neutral variant, and error color', async () => {
const tree = await runM3ThemeSchematic(runner, {
primaryColor: '#984061',
secondaryColor: '#984061',
tertiaryColor: '#984061',
neutralColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette
neutralVariantColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette
errorColor: '#984061',
includeHighContrast: true,
});

const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss'));

// Check a system variable from each color palette for their high contrast light theme value
expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`);
expect(generatedCSS).toContain(`--mat-sys-secondary: #580b2f`);
expect(generatedCSS).toContain(`--mat-sys-tertiary: #580b2f`);
expect(generatedCSS).toContain(`--mat-sys-surface-bright: #f9f9f9`);
expect(generatedCSS).toContain(`--mat-sys-surface-variant: #e2e2e2`);
expect(generatedCSS).toContain(`--mat-sys-error: #580b2f`);

// Check a system variable from each color palette for their high contrast dark theme value
expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`);
expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`);
expect(generatedCSS).toContain(`--mat-sys-tertiary: #ffebef`);
expect(generatedCSS).toContain(`--mat-sys-surface-bright: #4f5051`);
expect(generatedCSS).toContain(`--mat-sys-surface-variant: #454747`);
expect(generatedCSS).toContain(`--mat-sys-error: #ffebef`);
});
});
});

Expand Down Expand Up @@ -405,6 +520,49 @@ describe('material-theme-color-schematic', () => {
expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#f6dce2, #534247)`);
});

it('should generate CSS system variables when provided a primary, secondary, tertiary, neutral, and neutral variant colors', async () => {
const tree = await runM3ThemeSchematic(runner, {
primaryColor: '#984061',
secondaryColor: '#984061',
tertiaryColor: '#984061',
neutralColor: '#984061',
neutralVariantColor: '#984061',
isScss: false,
});

const generatedCSS = tree.readText('theme.css');

// Check a system variable from each color palette for their light dark value
expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#984061, #ffb0c8)`);
expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#984061, #ffb0c8)`);
expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#984061, #ffb0c8)`);
expect(generatedCSS).toContain(`--mat-sys-error: light-dark(#ba1a1a, #ffb4ab)`);
expect(generatedCSS).toContain(`--mat-sys-surface: light-dark(#fff8f8, #2f0015);`);
expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#ffd9e2, #7b2949)`);
});

it('should generate CSS system variables when provided a primary, secondary, tertiary, neutral, neutral variant, and error colors', async () => {
const tree = await runM3ThemeSchematic(runner, {
primaryColor: '#984061',
secondaryColor: '#984061',
tertiaryColor: '#984061',
neutralColor: '#984061',
neutralVariantColor: '#984061',
errorColor: '#984061',
isScss: false,
});

const generatedCSS = tree.readText('theme.css');

// Check a system variable from each color palette for their light dark value
expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#984061, #ffb0c8)`);
expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#984061, #ffb0c8)`);
expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#984061, #ffb0c8)`);
expect(generatedCSS).toContain(`--mat-sys-error: light-dark(#984061, #ffb0c8)`);
expect(generatedCSS).toContain(`--mat-sys-surface: light-dark(#fff8f8, #2f0015);`);
expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#ffd9e2, #7b2949)`);
});

describe('and with high contrast overrides', () => {
it('should generate high contrast system variables', async () => {
const tree = await runM3ThemeSchematic(runner, {
Expand Down Expand Up @@ -485,6 +643,50 @@ describe('material-theme-color-schematic', () => {
expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#580b2f, #ffebef)`);
expect(generatedCSS).toContain(`--mat-sys-surface-bright: light-dark(#f9f9f9, #4f5051)`);
});

it('should generate high contrast system variables when provided primary, secondary, tertiary, neutral, and neutral variant color', async () => {
const tree = await runM3ThemeSchematic(runner, {
primaryColor: '#984061',
secondaryColor: '#984061',
tertiaryColor: '#984061',
neutralColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette
neutralVariantColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette
isScss: false,
includeHighContrast: true,
});

const generatedCSS = tree.readText('theme.css');

// Check a system variable from each color palette for their high contrast light dark value
expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#580b2f, #ffebef)`);
expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#580b2f, #ffebef)`);
expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#580b2f, #ffebef)`);
expect(generatedCSS).toContain(`--mat-sys-surface-bright: light-dark(#f9f9f9, #4f5051)`);
expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#e2e2e2, #454747);`);
});

it('should generate high contrast system variables when provided primary, secondary, tertiary, neutral, neutral variant, and error color', async () => {
const tree = await runM3ThemeSchematic(runner, {
primaryColor: '#984061',
secondaryColor: '#984061',
tertiaryColor: '#984061',
neutralColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette
neutralVariantColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette
errorColor: '#984061',
isScss: false,
includeHighContrast: true,
});

const generatedCSS = tree.readText('theme.css');

// Check a system variable from each color palette for their high contrast light dark value
expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#580b2f, #ffebef)`);
expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#580b2f, #ffebef)`);
expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#580b2f, #ffebef)`);
expect(generatedCSS).toContain(`--mat-sys-surface-bright: light-dark(#f9f9f9, #4f5051)`);
expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#e2e2e2, #454747);`);
expect(generatedCSS).toContain(`--mat-sys-error: light-dark(#580b2f, #ffebef)`);
});
});
});
});
Expand Down
66 changes: 52 additions & 14 deletions src/material/schematics/ng-generate/theme-color/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export function getColorPalettes(
secondaryColor?: string,
tertiaryColor?: string,
neutralColor?: string,
neutralVariantColor?: string,
errorColor?: string,
): ColorPalettes {
// Create tonal palettes for each color and custom color overrides if applicable. Used for both
// standard contrast and high contrast schemes since they share the same tonal palettes.
Expand Down Expand Up @@ -157,21 +159,31 @@ export function getColorPalettes(
);
}

const neutralVariantPalette = TonalPalette.fromHueAndChroma(
primaryColorHct.hue,
primaryColorHct.chroma / 8.0 + 4.0,
);
let neutralVariantPalette;
if (neutralVariantColor) {
neutralVariantPalette = TonalPalette.fromHct(getHctFromHex(neutralVariantColor));
} else {
neutralVariantPalette = TonalPalette.fromHueAndChroma(
primaryColorHct.hue,
primaryColorHct.chroma / 8.0 + 4.0,
);
}

// Need to create color scheme to get generated error tonal palette.
const errorPalette = getMaterialDynamicScheme(
primaryPalette,
secondaryPalette,
tertiaryPalette,
neutralPalette,
neutralVariantPalette,
/* isDark */ false,
/* contrastLevel */ 0,
).errorPalette;
let errorPalette;
if (errorColor) {
errorPalette = TonalPalette.fromHct(getHctFromHex(errorColor));
} else {
// Need to create color scheme to get generated error tonal palette.
errorPalette = getMaterialDynamicScheme(
primaryPalette,
secondaryPalette,
tertiaryPalette,
neutralPalette,
neutralVariantPalette,
/* isDark */ false,
/* contrastLevel */ 0,
).errorPalette;
}

return {
primary: primaryPalette,
Expand Down Expand Up @@ -1007,6 +1019,8 @@ function getColorComment(
secondaryColor?: string,
tertiaryColor?: string,
neutralColor?: string,
neutralVariantColor?: string,
errorColor?: string,
) {
let colorComment = 'Color palettes are generated from primary: ' + primaryColor;
if (secondaryColor) {
Expand All @@ -1018,6 +1032,12 @@ function getColorComment(
if (neutralColor) {
colorComment += ', neutral: ' + neutralColor;
}
if (neutralVariantColor) {
colorComment += ', neutral variant: ' + neutralVariantColor;
}
if (errorColor) {
colorComment += ', error: ' + errorColor;
}
return colorComment;
}

Expand All @@ -1028,13 +1048,17 @@ export default function (options: Schema): Rule {
options.secondaryColor,
options.tertiaryColor,
options.neutralColor,
options.neutralVariantColor,
options.errorColor,
);

const colorPalettes = getColorPalettes(
options.primaryColor,
options.secondaryColor,
options.tertiaryColor,
options.neutralColor,
options.neutralVariantColor,
options.errorColor,
);

let lightHighContrastColorScheme: DynamicScheme;
Expand All @@ -1059,6 +1083,13 @@ export default function (options: Schema): Rule {
/* isDark */ true,
/* contrastLevel */ 1.0,
);

// Error palettes get generated by the color scheme's other palettes. Override the generated
// error palette with the custom one if applicable.
if (options.errorColor) {
lightHighContrastColorScheme.errorPalette = colorPalettes.error;
darkHighContrastColorScheme.errorPalette = colorPalettes.error;
}
}

if (options.isScss) {
Expand Down Expand Up @@ -1098,6 +1129,13 @@ export default function (options: Schema): Rule {
/* contrastLevel */ 0,
);

// Error palettes get generated by the color scheme's other palettes. Override the generated
// error palette with the custom one if applicable.
if (options.errorColor) {
lightColorScheme.errorPalette = colorPalettes.error;
darkColorScheme.errorPalette = colorPalettes.error;
}

themeCss += getAllSysVariablesCSS(lightColorScheme, darkColorScheme);

// Add high contrast media query to overwrite the color values when the user specifies
Expand Down
8 changes: 8 additions & 0 deletions src/material/schematics/ng-generate/theme-color/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export interface Schema {
* Color to override the neutral color palette.
*/
neutralColor?: string;
/**
* Color to override the neutral variant color palette.
*/
neutralVariantColor?: string;
/**
* Color to override the error color palette.
*/
errorColor?: string;
/**
* Whether to create high contrast override theme mixins.
*/
Expand Down

0 comments on commit 44c7320

Please sign in to comment.