diff --git a/src/material/schematics/ng-generate/theme-color/README.md b/src/material/schematics/ng-generate/theme-color/README.md index 1a9330d05e60..85c1243bd684 100644 --- a/src/material/schematics/ng-generate/theme-color/README.md +++ b/src/material/schematics/ng-generate/theme-color/README.md @@ -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 @@ -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. diff --git a/src/material/schematics/ng-generate/theme-color/index.spec.ts b/src/material/schematics/ng-generate/theme-color/index.spec.ts index 73096a151bed..82e5e672b486 100644 --- a/src/material/schematics/ng-generate/theme-color/index.spec.ts +++ b/src/material/schematics/ng-generate/theme-color/index.spec.ts @@ -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, { @@ -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`); + }); }); }); @@ -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, { @@ -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)`); + }); }); }); }); diff --git a/src/material/schematics/ng-generate/theme-color/index.ts b/src/material/schematics/ng-generate/theme-color/index.ts index ce722dc97c7e..5e8bd37ba912 100644 --- a/src/material/schematics/ng-generate/theme-color/index.ts +++ b/src/material/schematics/ng-generate/theme-color/index.ts @@ -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. @@ -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, @@ -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) { @@ -1018,6 +1032,12 @@ function getColorComment( if (neutralColor) { colorComment += ', neutral: ' + neutralColor; } + if (neutralVariantColor) { + colorComment += ', neutral variant: ' + neutralVariantColor; + } + if (errorColor) { + colorComment += ', error: ' + errorColor; + } return colorComment; } @@ -1028,6 +1048,8 @@ export default function (options: Schema): Rule { options.secondaryColor, options.tertiaryColor, options.neutralColor, + options.neutralVariantColor, + options.errorColor, ); const colorPalettes = getColorPalettes( @@ -1035,6 +1057,8 @@ export default function (options: Schema): Rule { options.secondaryColor, options.tertiaryColor, options.neutralColor, + options.neutralVariantColor, + options.errorColor, ); let lightHighContrastColorScheme: DynamicScheme; @@ -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) { @@ -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 diff --git a/src/material/schematics/ng-generate/theme-color/schema.d.ts b/src/material/schematics/ng-generate/theme-color/schema.d.ts index 8e26e2e0ab01..bab53f3c1fe5 100644 --- a/src/material/schematics/ng-generate/theme-color/schema.d.ts +++ b/src/material/schematics/ng-generate/theme-color/schema.d.ts @@ -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. */