Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

feat(theme): add contrast opacity values for all color types and hues #8872

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions docs/guides/THEMES_IMPL_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ the `$mdTheming` service and tacked into the document head.
* Instead of using hard-coded color or a SCSS variable, the colors are defined with a mini-DSL
(described deblow).
* The build process takes all of those `-theme.scss` files and globs them up into one enourmous
string.
string.
* The build process wraps that string with code to set it an angular module constant:
``` angular.module('material.core').constant('$MD_THEME_CSS', 'HUGE_THEME_STRING'); ```
* That code gets dumped at the end of `angular-material.js`
Expand All @@ -24,15 +24,21 @@ mini-DSL, applies the colors for the theme, and appends the resulting CSS into t


### The mini-DSL
* Each color is written in the form `'{{palette-hue-opacity}}'`, where opacity is optional.
* Each color is written in the form `'{{palette-hue-contrast-opacity}}'`, where `hue`, `contrast`,
and opacity are optional.
* For example, `'{{primary-500}}'`
* Palettes are `primary`, `accent`, `warn`, `background`, `foreground`
* The hues for each type except `foreground` use the Material Design hues.
* The `forground` palette is a number from one to four:
* `foreground-1`: text
* `foreground-2`: secondary text, icons
* `foreground-3`: disabled text, hint text
* `foreground-4`: dividers
* There is also a special hue called `contrast` that will give a contrast color (for text).
For example, `accent-contrast` will be a contrast color for the accent color, for use as a text
color on an accent-colored background.
* Palettes are `primary`, `accent`, `warn`, `background`
* The hues for each type use the Material Design hues. When not specified, each palette defaults
`hue` to `500` with the exception of `background`
* The `opacity` value can be a decimal between 0 and 1 or one of the following values based on the
hue's contrast type (dark, light, or strongLight):
* `icon`: icon (0.54 / 0.87 / 1.0)
* `secondary`: secondary text (0.54 / 0.87)
* `disabled`: disabled text or icon (0.38 / 0.54)
* `hint`: hint text (0.38 / 0.50)
* `divider`: divider (0.12)
* `contrast` will give a contrast color (for text) and can be mixed with `opacity`.
For example, `accent-contrast` will be a contrast color for the accent color, for use as a text
color on an accent-colored background. Adding an `opacity` value as in `accent-contrast-icon` will
apply the Material Design icon opacity. Using a decimal opacity value as in `accent-contrast-0.25`
will apply the contrast color for the accent color at 25% opacity.
178 changes: 137 additions & 41 deletions src/core/services/theming/theming.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,9 @@ function detectDisabledThemes($mdThemingProvider) {
* {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules
* {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue
* {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules
*
* Foreground expansion: Applies rgba to black/white foreground text
*
* {{foreground-1}} - used for primary text
* {{foreground-2}} - used for secondary text/divider
* {{foreground-3}} - used for disabled text
* {{foreground-4}} - used for dividers
* {{primary-contrast-divider}} - Apply divider opacity to contrast color
* {{background-default-contrast}} - Apply primary text color for contrasting with default background
* {{background-50-contrast-icon}} - Apply contrast color for icon on background's shade 50 hue
*
*/

Expand All @@ -184,21 +180,14 @@ var GENERATED = { };
// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
var PALETTES;

// Text Colors on light and dark backgrounds
// Text colors are automatically generated based on background color when not specified
// Custom palettes can provide override colors
// @see https://www.google.com/design/spec/style/color.html#color-text-background-colors
var DARK_FOREGROUND = {
name: 'dark',
'1': 'rgba(0,0,0,0.87)',
'2': 'rgba(0,0,0,0.54)',
'3': 'rgba(0,0,0,0.38)',
'4': 'rgba(0,0,0,0.12)'
};
var LIGHT_FOREGROUND = {
name: 'light',
'1': 'rgba(255,255,255,1.0)',
'2': 'rgba(255,255,255,0.7)',
'3': 'rgba(255,255,255,0.5)',
'4': 'rgba(255,255,255,0.12)'
};

var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)';
Expand Down Expand Up @@ -235,6 +224,34 @@ var DARK_DEFAULT_HUES = {
'hue-3': 'A200'
}
};

// use inactive icon opacity from https://material.google.com/style/color.html#color-text-background-colors
// not inactive icon opacity from https://material.google.com/style/icons.html#icons-system-icons

var DARK_CONTRAST_OPACITY = {
'icon': 0.54,
'secondary': 0.54,
'disabled': 0.38,
'hint': 0.38,
'divider': 0.12,
};

var LIGHT_CONTRAST_OPACITY = {
'icon': 0.87,
'secondary': 0.7,
'disabled': 0.5,
'hint': 0.5,
'divider': 0.12
};

var STRONG_LIGHT_CONTRAST_OPACITY = {
'icon': 1.0,
'secondary': 0.7,
'disabled': 0.5,
'hint': 0.5,
'divider': 0.12
};

THEME_COLOR_TYPES.forEach(function(colorType) {
// Color types with unspecified default hues will use these default hue values
var defaultDefaultHues = {
Expand Down Expand Up @@ -861,20 +878,44 @@ function parseRules(theme, colorType, rules) {

var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g');
// Matches '{{ primary-color }}', etc
var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g');
var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g;
var hueRegex = new RegExp('(?:\'|")?{{\\s*(' + colorType + ')-?(color|default)?-?(contrast)?-?((?:\\d\\.?\\d*)|(?:[a-zA-Z]+))?\\s*}}(\"|\')?','g');
var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(contrast)?-?((?:\d\.?\d*)|(?:[a-zA-Z]+))?\s*\}\}'?"?/g;
var palette = PALETTES[color.name];
var defaultBgHue = theme.colors['background'].hues['default'];
var defaultBgContrastType = PALETTES[theme.colors['background'].name][defaultBgHue].contrastType;

// find and replace simple variables where we use a specific hue, not an entire palette
// eg. "{{primary-100}}"
//\(' + THEME_COLOR_TYPES.join('\|') + '\)'
rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) {
rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, contrast, opacity) {
Copy link
Contributor Author

@clshortfuse clshortfuse Jul 28, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it looks like this would be a breaking change, and it looks like the order groups in the regex were swapped, that's not the case. Before, either opacity or contrast could be used, but not both. Now both can be used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way that we could extract some of this logic into a service (or something) so that we can write some tests? I'd love some tests to make sure that it parses everything correctly in case we make more changes in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore that comment; didn't see all of the tests at the bottom 😆

var regexColorType = colorType;
if (colorType === 'foreground') {
if (hue == 'shadow') {
return theme.foregroundShadow;
} else {
return theme.foregroundPalette[hue] || theme.foregroundPalette['1'];
} else if (theme.foregroundPalette[hue]) {
// Use user defined palette number (ie: foreground-2)
return rgba( colorToRgbaArray( theme.foregroundPalette[hue] ) );
} else if (theme.foregroundPalette['1']){
return rgba( colorToRgbaArray( theme.foregroundPalette['1'] ) );
}
// Default to background-default-contrast-{opacity}
colorType = 'background';
contrast = 'contrast';
if (!opacity && hue) {
// Convert references to legacy hues to opacities (ie: foreground-4 to *-divider)
switch(hue) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, why do this not include a case for foreground-1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first ifchecks if the user has a custom foreground color that matches the hue specified in the CSS. The second if uses the user specified foreground-1 as a fallback. (ie: CSS wants foreground-4 but user only specified foreground-1) . I'm not seeing a case for a switch statement here without having an if statement inside the case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, look down from this comment. You have:

switch (hue) {
  case '2': ...
  case '3': ...
  case '4': ...
}

My question was why there was no case '1'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, because foreground-1 uses default opacity. I was thinking of adding a primary opacity for clarity, but that's redundant. I'll add a comment in the switch case to explain why we don't need a check for foreground-1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, comment was added below the line notes. That's why GitHub isn't marking these comments as outdated.

// hue-1 uses default opacity
case '2':
opacity = 'secondary';
break;
case '3':
opacity = 'disabled';
break;
case '4':
opacity = 'divider';
}
}
hue = 'default';
}

// `default` is also accepted as a hue-value, because the background palettes are
Expand All @@ -883,13 +924,51 @@ function parseRules(theme, colorType, rules) {
hue = theme.colors[colorType].hues[hue];
}

return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity );
var colorDetails = (PALETTES[ theme.colors[colorType].name ][hue] || '');

// If user has specified a foreground color, use those
if (colorType === 'background' && contrast && regexColorType !== 'foreground' && colorDetails.contrastType == defaultBgContrastType) {
// Don't process if colorType was changed
switch (opacity) {
case 'secondary':
case 'icon':
if (theme.foregroundPalette['2']) {
return rgba(colorToRgbaArray(theme.foregroundPalette['2']));
}
break;
case 'disabled':
case 'hint':
if (theme.foregroundPalette['3']) {
return rgba(colorToRgbaArray(theme.foregroundPalette['3']));
}
break;
case 'divider':
if (theme.foregroundPalette['4']) {
return rgba(colorToRgbaArray(theme.foregroundPalette['4']));
}
break;
default:
if (theme.foregroundPalette['1']) {
return rgba(colorToRgbaArray(theme.foregroundPalette['1']));
}
break;
}
}

if (contrast && opacity) {
opacity = colorDetails.opacity[opacity] || opacity;
}

return rgba( colorDetails[contrast ? 'contrast' : 'value'], opacity );
});

// For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
angular.forEach(color.hues, function(hueValue, hueName) {
var newRule = rules
.replace(hueRegex, function(match, _, colorType, hueType, opacity) {
.replace(hueRegex, function(match, colorType, hueType, contrast, opacity) {
if (contrast && opacity) {
opacity = palette[hueValue].opacity[opacity] || opacity;
}
return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
});
if (hueName !== 'default') {
Expand Down Expand Up @@ -1001,6 +1080,37 @@ function generateAllThemes($injector, $mdTheming) {
delete palette.contrastStrongLightColors;
delete palette.contrastDarkColors;

function getContrastType(hueName) {
if (defaultContrast === 'light' ? darkColors.indexOf(hueName) !== -1 : lightColors.indexOf(hueName) === -1) {
return 'dark';
}
if (strongLightColors.indexOf(hueName) !== -1) {
return 'strongLight';
}
return 'light';
}
function getContrastColor(contrastType) {
switch(contrastType) {
default:
case 'strongLight':
return STRONG_LIGHT_CONTRAST_COLOR;
case 'light':
return LIGHT_CONTRAST_COLOR;
case 'dark':
return DARK_CONTRAST_COLOR;
}
}
function getOpacityValues(contrastType) {
switch(contrastType) {
default:
case 'strongLight':
return STRONG_LIGHT_CONTRAST_OPACITY;
case 'light':
return LIGHT_CONTRAST_OPACITY;
case 'dark':
return DARK_CONTRAST_OPACITY;
}
}
// Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR }
angular.forEach(palette, function(hueValue, hueName) {
if (angular.isObject(hueValue)) return; // Already converted
Expand All @@ -1013,28 +1123,14 @@ function generateAllThemes($injector, $mdTheming) {
.replace('%3', hueName));
}

var contrastType = getContrastType(hueName);
palette[hueName] = {
hex: palette[hueName],
value: rgbValue,
contrast: getContrastColor()
contrastType: contrastType,
contrast: getContrastColor(contrastType),
opacity: getOpacityValues(contrastType)
};
function getContrastColor() {
if (defaultContrast === 'light') {
if (darkColors.indexOf(hueName) > -1) {
return DARK_CONTRAST_COLOR;
} else {
return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
: LIGHT_CONTRAST_COLOR;
}
} else {
if (lightColors.indexOf(hueName) > -1) {
return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
: LIGHT_CONTRAST_COLOR;
} else {
return DARK_CONTRAST_COLOR;
}
}
}
});
}
}
Expand Down
Loading