From c2c57c832ba97ba8fc4411b9a545e8f7a1433b64 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Mon, 11 Feb 2019 20:42:03 +0100 Subject: [PATCH] fix: errors when building theme using CSS variables * Adds an extra theme to the build that will allow us to verify that everything still works if somebody passes in the palette colors as CSS variables. * Fixes a handful of places where Sass would either throw an error or would produce invalid CSS. Fixes #15107. --- src/cdk/a11y/_a11y.scss | 13 +- src/lib/badge/_badge-theme.scss | 20 ++- src/lib/core/BUILD.bazel | 8 + src/lib/core/ripple/_ripple.scss | 10 +- src/lib/core/style/_elevation.scss | 156 +++++++++--------- src/lib/core/theming/_theming.scss | 10 +- .../theming/test-css-variables-theme.scss | 24 +++ src/lib/datepicker/_datepicker-theme.scss | 24 ++- src/lib/sidenav/_sidenav-theme.scss | 20 ++- src/lib/sort/_sort-theme.scss | 20 ++- .../package-tools/gulp/build-scss-pipeline.ts | 2 +- 11 files changed, 202 insertions(+), 105 deletions(-) create mode 100644 src/lib/core/theming/test-css-variables-theme.scss diff --git a/src/cdk/a11y/_a11y.scss b/src/cdk/a11y/_a11y.scss index f8f7b96a6bb7..faa18eda44f7 100644 --- a/src/cdk/a11y/_a11y.scss +++ b/src/cdk/a11y/_a11y.scss @@ -18,13 +18,12 @@ } } -/** - * Applies styles for users in high contrast mode. Note that this only applies - * to Microsoft browsers. Chrome can be included by checking for the `html[hc]` - * attribute, however Chrome handles high contrast differently. - * @param target Which kind of high contrast setting to target. Defaults to `active`, can be - * `white-on-black` or `black-on-white`. - */ +// Applies styles for users in high contrast mode. Note that this only applies +// to Microsoft browsers. Chrome can be included by checking for the `html[hc]` +// attribute, however Chrome handles high contrast differently. +// +// @param target Which kind of high contrast setting to target. Defaults to `active`, can be +// `white-on-black` or `black-on-white`. @mixin cdk-high-contrast($target: active) { @media (-ms-high-contrast: $target) { @content; diff --git a/src/lib/badge/_badge-theme.scss b/src/lib/badge/_badge-theme.scss index cce6cc99de97..7b7dcd38e4b1 100644 --- a/src/lib/badge/_badge-theme.scss +++ b/src/lib/badge/_badge-theme.scss @@ -130,14 +130,22 @@ $mat-badge-large-size: $mat-badge-default-size + 6; .mat-badge-disabled { .mat-badge-content { - // The disabled color usually has some kind of opacity, but because the badge is overlayed - // on top of something else, it won't look good if it's opaque. We convert it into a solid - // color by taking the opacity from the rgba value and using the value to determine the - // percentage of the background to put into foreground when mixing the colors together. $app-background: mat-color($background, 'background'); $badge-color: mat-color($foreground, disabled-button); - $badge-opacity: opacity($badge-color); - background: mix($app-background, rgba($badge-color, 1), (1 - $badge-opacity) * 100%); + + // The disabled color usually has some kind of opacity, but because the badge is overlayed + // on top of something else, it won't look good if it's opaque. If it is a color *type*, + // we convert it into a solid color by taking the opacity from the rgba value and using + // the value to determine the percentage of the background to put into foreground when + // mixing the colors together. + @if (type-of($badge-color) == color and type-of($app-background) == color) { + $badge-opacity: opacity($badge-color); + background: mix($app-background, rgba($badge-color, 1), (1 - $badge-opacity) * 100%); + } + @else { + background: $badge-color; + } + color: mat-color($foreground, disabled-text); } } diff --git a/src/lib/core/BUILD.bazel b/src/lib/core/BUILD.bazel index d39f8cbaea4d..7340d5e4ba66 100644 --- a/src/lib/core/BUILD.bazel +++ b/src/lib/core/BUILD.bazel @@ -125,3 +125,11 @@ filegroup( name = "source-files", srcs = glob(["**/*.ts"]), ) + +# Test theme used to ensure that our themes will compile using CSS variables in the palettes. +sass_binary( + name = "test-css-variables-theme", + src = "theming/test-css-variables-theme.scss", + deps = [":all_themes"], + testonly = True +) diff --git a/src/lib/core/ripple/_ripple.scss b/src/lib/core/ripple/_ripple.scss index 084bb547bbb4..3f94c6109f44 100644 --- a/src/lib/core/ripple/_ripple.scss +++ b/src/lib/core/ripple/_ripple.scss @@ -40,6 +40,14 @@ $mat-ripple-color-opacity: 0.1; $foreground-base: map_get($foreground, base); .mat-ripple-element { - background-color: rgba($foreground-base, $mat-ripple-color-opacity); + // If the ripple color is resolves to a color *type*, we can use it directly, otherwise + // (e.g. it resolves to a CSS variable) we fall back to using the color and setting an opacity. + @if (type-of($foreground-base) == color) { + background-color: rgba($foreground-base, $mat-ripple-color-opacity); + } + @else { + background-color: $foreground-base; + opacity: $mat-ripple-color-opacity; + } } } diff --git a/src/lib/core/style/_elevation.scss b/src/lib/core/style/_elevation.scss index 420ab7a76ba2..29d82c090e86 100644 --- a/src/lib/core/style/_elevation.scss +++ b/src/lib/core/style/_elevation.scss @@ -32,92 +32,98 @@ // all of the values between them. @function _get-umbra-map($color, $opacity) { + $shadow-color: if(type-of($color) == color, rgba($color, $opacity * 0.2), $color); + @return ( - 0: '0px 0px 0px 0px #{rgba($color, $opacity * 0.2)}', - 1: '0px 2px 1px -1px #{rgba($color, $opacity * 0.2)}', - 2: '0px 3px 1px -2px #{rgba($color, $opacity * 0.2)}', - 3: '0px 3px 3px -2px #{rgba($color, $opacity * 0.2)}', - 4: '0px 2px 4px -1px #{rgba($color, $opacity * 0.2)}', - 5: '0px 3px 5px -1px #{rgba($color, $opacity * 0.2)}', - 6: '0px 3px 5px -1px #{rgba($color, $opacity * 0.2)}', - 7: '0px 4px 5px -2px #{rgba($color, $opacity * 0.2)}', - 8: '0px 5px 5px -3px #{rgba($color, $opacity * 0.2)}', - 9: '0px 5px 6px -3px #{rgba($color, $opacity * 0.2)}', - 10: '0px 6px 6px -3px #{rgba($color, $opacity * 0.2)}', - 11: '0px 6px 7px -4px #{rgba($color, $opacity * 0.2)}', - 12: '0px 7px 8px -4px #{rgba($color, $opacity * 0.2)}', - 13: '0px 7px 8px -4px #{rgba($color, $opacity * 0.2)}', - 14: '0px 7px 9px -4px #{rgba($color, $opacity * 0.2)}', - 15: '0px 8px 9px -5px #{rgba($color, $opacity * 0.2)}', - 16: '0px 8px 10px -5px #{rgba($color, $opacity * 0.2)}', - 17: '0px 8px 11px -5px #{rgba($color, $opacity * 0.2)}', - 18: '0px 9px 11px -5px #{rgba($color, $opacity * 0.2)}', - 19: '0px 9px 12px -6px #{rgba($color, $opacity * 0.2)}', - 20: '0px 10px 13px -6px #{rgba($color, $opacity * 0.2)}', - 21: '0px 10px 13px -6px #{rgba($color, $opacity * 0.2)}', - 22: '0px 10px 14px -6px #{rgba($color, $opacity * 0.2)}', - 23: '0px 11px 14px -7px #{rgba($color, $opacity * 0.2)}', - 24: '0px 11px 15px -7px #{rgba($color, $opacity * 0.2)}' + 0: '0px 0px 0px 0px #{$shadow-color}', + 1: '0px 2px 1px -1px #{$shadow-color}', + 2: '0px 3px 1px -2px #{$shadow-color}', + 3: '0px 3px 3px -2px #{$shadow-color}', + 4: '0px 2px 4px -1px #{$shadow-color}', + 5: '0px 3px 5px -1px #{$shadow-color}', + 6: '0px 3px 5px -1px #{$shadow-color}', + 7: '0px 4px 5px -2px #{$shadow-color}', + 8: '0px 5px 5px -3px #{$shadow-color}', + 9: '0px 5px 6px -3px #{$shadow-color}', + 10: '0px 6px 6px -3px #{$shadow-color}', + 11: '0px 6px 7px -4px #{$shadow-color}', + 12: '0px 7px 8px -4px #{$shadow-color}', + 13: '0px 7px 8px -4px #{$shadow-color}', + 14: '0px 7px 9px -4px #{$shadow-color}', + 15: '0px 8px 9px -5px #{$shadow-color}', + 16: '0px 8px 10px -5px #{$shadow-color}', + 17: '0px 8px 11px -5px #{$shadow-color}', + 18: '0px 9px 11px -5px #{$shadow-color}', + 19: '0px 9px 12px -6px #{$shadow-color}', + 20: '0px 10px 13px -6px #{$shadow-color}', + 21: '0px 10px 13px -6px #{$shadow-color}', + 22: '0px 10px 14px -6px #{$shadow-color}', + 23: '0px 11px 14px -7px #{$shadow-color}', + 24: '0px 11px 15px -7px #{$shadow-color}' ); } @function _get-penumbra-map($color, $opacity) { + $shadow-color: if(type-of($color) == color, rgba($color, $opacity * 0.14), $color); + @return ( - 0: '0px 0px 0px 0px #{rgba($color, $opacity * 0.14)}', - 1: '0px 1px 1px 0px #{rgba($color, $opacity * 0.14)}', - 2: '0px 2px 2px 0px #{rgba($color, $opacity * 0.14)}', - 3: '0px 3px 4px 0px #{rgba($color, $opacity * 0.14)}', - 4: '0px 4px 5px 0px #{rgba($color, $opacity * 0.14)}', - 5: '0px 5px 8px 0px #{rgba($color, $opacity * 0.14)}', - 6: '0px 6px 10px 0px #{rgba($color, $opacity * 0.14)}', - 7: '0px 7px 10px 1px #{rgba($color, $opacity * 0.14)}', - 8: '0px 8px 10px 1px #{rgba($color, $opacity * 0.14)}', - 9: '0px 9px 12px 1px #{rgba($color, $opacity * 0.14)}', - 10: '0px 10px 14px 1px #{rgba($color, $opacity * 0.14)}', - 11: '0px 11px 15px 1px #{rgba($color, $opacity * 0.14)}', - 12: '0px 12px 17px 2px #{rgba($color, $opacity * 0.14)}', - 13: '0px 13px 19px 2px #{rgba($color, $opacity * 0.14)}', - 14: '0px 14px 21px 2px #{rgba($color, $opacity * 0.14)}', - 15: '0px 15px 22px 2px #{rgba($color, $opacity * 0.14)}', - 16: '0px 16px 24px 2px #{rgba($color, $opacity * 0.14)}', - 17: '0px 17px 26px 2px #{rgba($color, $opacity * 0.14)}', - 18: '0px 18px 28px 2px #{rgba($color, $opacity * 0.14)}', - 19: '0px 19px 29px 2px #{rgba($color, $opacity * 0.14)}', - 20: '0px 20px 31px 3px #{rgba($color, $opacity * 0.14)}', - 21: '0px 21px 33px 3px #{rgba($color, $opacity * 0.14)}', - 22: '0px 22px 35px 3px #{rgba($color, $opacity * 0.14)}', - 23: '0px 23px 36px 3px #{rgba($color, $opacity * 0.14)}', - 24: '0px 24px 38px 3px #{rgba($color, $opacity * 0.14)}' + 0: '0px 0px 0px 0px #{$shadow-color}', + 1: '0px 1px 1px 0px #{$shadow-color}', + 2: '0px 2px 2px 0px #{$shadow-color}', + 3: '0px 3px 4px 0px #{$shadow-color}', + 4: '0px 4px 5px 0px #{$shadow-color}', + 5: '0px 5px 8px 0px #{$shadow-color}', + 6: '0px 6px 10px 0px #{$shadow-color}', + 7: '0px 7px 10px 1px #{$shadow-color}', + 8: '0px 8px 10px 1px #{$shadow-color}', + 9: '0px 9px 12px 1px #{$shadow-color}', + 10: '0px 10px 14px 1px #{$shadow-color}', + 11: '0px 11px 15px 1px #{$shadow-color}', + 12: '0px 12px 17px 2px #{$shadow-color}', + 13: '0px 13px 19px 2px #{$shadow-color}', + 14: '0px 14px 21px 2px #{$shadow-color}', + 15: '0px 15px 22px 2px #{$shadow-color}', + 16: '0px 16px 24px 2px #{$shadow-color}', + 17: '0px 17px 26px 2px #{$shadow-color}', + 18: '0px 18px 28px 2px #{$shadow-color}', + 19: '0px 19px 29px 2px #{$shadow-color}', + 20: '0px 20px 31px 3px #{$shadow-color}', + 21: '0px 21px 33px 3px #{$shadow-color}', + 22: '0px 22px 35px 3px #{$shadow-color}', + 23: '0px 23px 36px 3px #{$shadow-color}', + 24: '0px 24px 38px 3px #{$shadow-color}' ); } @function _get-ambient-map($color, $opacity) { + $shadow-color: if(type-of($color) == color, rgba($color, $opacity * 0.12), $color); + @return ( - 0: '0px 0px 0px 0px #{rgba($color, $opacity * 0.12)}', - 1: '0px 1px 3px 0px #{rgba($color, $opacity * 0.12)}', - 2: '0px 1px 5px 0px #{rgba($color, $opacity * 0.12)}', - 3: '0px 1px 8px 0px #{rgba($color, $opacity * 0.12)}', - 4: '0px 1px 10px 0px #{rgba($color, $opacity * 0.12)}', - 5: '0px 1px 14px 0px #{rgba($color, $opacity * 0.12)}', - 6: '0px 1px 18px 0px #{rgba($color, $opacity * 0.12)}', - 7: '0px 2px 16px 1px #{rgba($color, $opacity * 0.12)}', - 8: '0px 3px 14px 2px #{rgba($color, $opacity * 0.12)}', - 9: '0px 3px 16px 2px #{rgba($color, $opacity * 0.12)}', - 10: '0px 4px 18px 3px #{rgba($color, $opacity * 0.12)}', - 11: '0px 4px 20px 3px #{rgba($color, $opacity * 0.12)}', - 12: '0px 5px 22px 4px #{rgba($color, $opacity * 0.12)}', - 13: '0px 5px 24px 4px #{rgba($color, $opacity * 0.12)}', - 14: '0px 5px 26px 4px #{rgba($color, $opacity * 0.12)}', - 15: '0px 6px 28px 5px #{rgba($color, $opacity * 0.12)}', - 16: '0px 6px 30px 5px #{rgba($color, $opacity * 0.12)}', - 17: '0px 6px 32px 5px #{rgba($color, $opacity * 0.12)}', - 18: '0px 7px 34px 6px #{rgba($color, $opacity * 0.12)}', - 19: '0px 7px 36px 6px #{rgba($color, $opacity * 0.12)}', - 20: '0px 8px 38px 7px #{rgba($color, $opacity * 0.12)}', - 21: '0px 8px 40px 7px #{rgba($color, $opacity * 0.12)}', - 22: '0px 8px 42px 7px #{rgba($color, $opacity * 0.12)}', - 23: '0px 9px 44px 8px #{rgba($color, $opacity * 0.12)}', - 24: '0px 9px 46px 8px #{rgba($color, $opacity * 0.12)}' + 0: '0px 0px 0px 0px #{$shadow-color}', + 1: '0px 1px 3px 0px #{$shadow-color}', + 2: '0px 1px 5px 0px #{$shadow-color}', + 3: '0px 1px 8px 0px #{$shadow-color}', + 4: '0px 1px 10px 0px #{$shadow-color}', + 5: '0px 1px 14px 0px #{$shadow-color}', + 6: '0px 1px 18px 0px #{$shadow-color}', + 7: '0px 2px 16px 1px #{$shadow-color}', + 8: '0px 3px 14px 2px #{$shadow-color}', + 9: '0px 3px 16px 2px #{$shadow-color}', + 10: '0px 4px 18px 3px #{$shadow-color}', + 11: '0px 4px 20px 3px #{$shadow-color}', + 12: '0px 5px 22px 4px #{$shadow-color}', + 13: '0px 5px 24px 4px #{$shadow-color}', + 14: '0px 5px 26px 4px #{$shadow-color}', + 15: '0px 6px 28px 5px #{$shadow-color}', + 16: '0px 6px 30px 5px #{$shadow-color}', + 17: '0px 6px 32px 5px #{$shadow-color}', + 18: '0px 7px 34px 6px #{$shadow-color}', + 19: '0px 7px 36px 6px #{$shadow-color}', + 20: '0px 8px 38px 7px #{$shadow-color}', + 21: '0px 8px 40px 7px #{$shadow-color}', + 22: '0px 8px 42px 7px #{$shadow-color}', + 23: '0px 9px 44px 8px #{$shadow-color}', + 24: '0px 9px 46px 8px #{$shadow-color}' ); } diff --git a/src/lib/core/theming/_theming.scss b/src/lib/core/theming/_theming.scss index 86c4538c58f0..e1e3336129a2 100644 --- a/src/lib/core/theming/_theming.scss +++ b/src/lib/core/theming/_theming.scss @@ -52,9 +52,15 @@ } $color: map-get($palette, $hue); - $opacity: if($opacity == null, opacity($color), $opacity); - @return rgba($color, $opacity); + @if (type-of($color) != color) { + // If the $color resolved to something different from a color (e.g. a CSS variable), + // we can't apply the opacity anyway so we return the value as is, otherwise Sass can + // throw an error or output something invalid. + @return $color; + } + + @return rgba($color, if($opacity == null, opacity($color), $opacity)); } diff --git a/src/lib/core/theming/test-css-variables-theme.scss b/src/lib/core/theming/test-css-variables-theme.scss new file mode 100644 index 000000000000..cb2f90b73678 --- /dev/null +++ b/src/lib/core/theming/test-css-variables-theme.scss @@ -0,0 +1,24 @@ +@import './all-theme'; + +// Recursively replaces all of the values inside a Sass map with a different value. +@function replace-all-values($palette, $replacement) { + $output: (); + + @each $key, $value in $palette { + @if (type-of($value) == 'map') { + $output: map-merge(($key: replace-all-values($value, $replacement)), $output); + } + @else { + $output: map-merge(($key: $replacement), $output); + } + } + + @return $output; +} + +// Theme used to test that our themes would compile if the colors were specified as CSS variables. +._demo-css-variables-theme { + $palette: mat-palette($mat-blue-grey); + $theme: mat-dark-theme($palette, $palette, $palette); + @include angular-material-theme(replace-all-values($theme, var(--test-var))); +} diff --git a/src/lib/datepicker/_datepicker-theme.scss b/src/lib/datepicker/_datepicker-theme.scss index d338be532aa6..e4cf88e03d25 100644 --- a/src/lib/datepicker/_datepicker-theme.scss +++ b/src/lib/datepicker/_datepicker-theme.scss @@ -17,7 +17,16 @@ $mat-calendar-weekday-table-font-size: 11px !default; } .mat-calendar-body-disabled > .mat-calendar-body-selected { - background-color: fade-out(mat-color($palette), $mat-datepicker-selected-fade-amount); + $background: mat-color($palette); + + @if (type-of($background) == color) { + background-color: fade-out($background, $mat-datepicker-selected-fade-amount); + } + @else { + // If we couldn't resolve to background to a color (e.g. it's a CSS variable), + // fall back to fading the content out via `opacity`. + opacity: $mat-datepicker-today-fade-amount; + } } .mat-calendar-body-today.mat-calendar-body-selected { @@ -78,7 +87,18 @@ $mat-calendar-weekday-table-font-size: 11px !default; } .mat-calendar-body-disabled > .mat-calendar-body-today:not(.mat-calendar-body-selected) { - border-color: fade-out(mat-color($foreground, hint-text), $mat-datepicker-today-fade-amount); + $color: mat-color($foreground, hint-text); + + @if (type-of($color) == color) { + border-color: fade-out($color, $mat-datepicker-today-fade-amount); + } + @else { + // If the color didn't resolve to a color value, but something like a CSS variable, we can't + // fade it out so we fall back to reducing the element opacity. Note that we don't use the + // $mat-datepicker-today-fade-amount, because hint text usually has some opacity applied + // to it already and we don't want them to stack on top of each other. + opacity: 0.5; + } } @include _mat-datepicker-color(map-get($theme, primary)); diff --git a/src/lib/sidenav/_sidenav-theme.scss b/src/lib/sidenav/_sidenav-theme.scss index de794c38796f..850b53ad8716 100644 --- a/src/lib/sidenav/_sidenav-theme.scss +++ b/src/lib/sidenav/_sidenav-theme.scss @@ -10,10 +10,6 @@ $background: map-get($theme, background); $foreground: map-get($theme, foreground); - // We use invert() here to have the darken the background color expected to be used. If the - // background is light, we use a dark backdrop. If the background is dark, - // we use a light backdrop. - $drawer-backdrop-color: invert(mat-color($background, card, 0.6)); $drawer-background-color: mat-color($background, dialog); $drawer-container-background-color: mat-color($background, background); $drawer-push-background-color: mat-color($background, dialog); @@ -59,7 +55,21 @@ } .mat-drawer-backdrop.mat-drawer-shown { - background-color: $drawer-backdrop-color; + $opacity: 0.6; + $backdrop-color: mat-color($background, card, $opacity); + + @if (type-of($backdrop-color) == color) { + // We use invert() here to have the darken the background color expected to be used. If the + // background is light, we use a dark backdrop. If the background is dark, + // we use a light backdrop. + background-color: invert($backdrop-color); + } + @else { + // If we couldn't resolve the backdrop color to a color value, fall back to using + // `opacity` to make it opaque since its end value could be a solid color. + background-color: $backdrop-color; + opacity: $opacity; + } } } diff --git a/src/lib/sort/_sort-theme.scss b/src/lib/sort/_sort-theme.scss index 667100201c09..4ed3e1e4aaf8 100644 --- a/src/lib/sort/_sort-theme.scss +++ b/src/lib/sort/_sort-theme.scss @@ -3,14 +3,22 @@ $foreground: map-get($theme, foreground); .mat-sort-header-arrow { - // Because the arrow is made up of multiple elements that are stacked on top of each other, - // we can't use the semi-trasparent color from the theme directly. We convert it into a solid - // color by taking the opacity from the rgba value and using the value to determine the - // percentage of the background to put into foreground when mixing the colors together. $table-background: mat-color($background, 'card'); $text-color: mat-color($foreground, secondary-text); - $text-opacity: opacity($text-color); - color: mix($table-background, rgba($text-color, 1), (1 - $text-opacity) * 100%); + + // Because the arrow is made up of multiple elements that are stacked on top of each other, + // we can't use the semi-trasparent color from the theme directly. If the value is a color + // *type*, we convert it into a solid color by taking the opacity from the rgba value and + // using the value to determine the percentage of the background to put into foreground + // when mixing the colors together. Otherwise, if it resolves to something different + // (e.g. it resolves to a CSS variable), we use the color directly. + @if (type-of($table-background) == color and type-of($text-color) == color) { + $text-opacity: opacity($text-color); + color: mix($table-background, rgba($text-color, 1), (1 - $text-opacity) * 100%); + } + @else { + color: $text-color; + } } } diff --git a/tools/package-tools/gulp/build-scss-pipeline.ts b/tools/package-tools/gulp/build-scss-pipeline.ts index 035a9f209374..eed5860095ae 100644 --- a/tools/package-tools/gulp/build-scss-pipeline.ts +++ b/tools/package-tools/gulp/build-scss-pipeline.ts @@ -17,7 +17,7 @@ gulpSass.compiler = nodeSass; /** Create a gulp task that builds SCSS files. */ export function buildScssPipeline(sourceDir: string, minifyOutput = false) { - return src(join(sourceDir, '**/*.scss')) + return src(join(sourceDir, '**/!(test-).scss')) .pipe(gulpSass({includePaths: sassIncludePaths}).on('error', gulpSass.logError)) .pipe(gulpIf(minifyOutput, gulpCleanCss())); }