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

Commit 93badb8

Browse files
committed
feat(theming): add contrast opacity values for all color types and hues
Add opacity keyword support (`secondary`, `icon`, `disabled`, `hint`, `divider`) Deprecate documentation of `foreground-*` in favor of `background-default-contrast-*` Allow `foregroundPallete` to override colors on default background and hues of equal contrast type
1 parent 9d525e5 commit 93badb8

File tree

3 files changed

+245
-54
lines changed

3 files changed

+245
-54
lines changed

docs/guides/THEMES_IMPL_NOTES.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ the `$mdTheming` service and tacked into the document head.
1212
* Instead of using hard-coded color or a SCSS variable, the colors are defined with a mini-DSL
1313
(described deblow).
1414
* The build process takes all of those `-theme.scss` files and globs them up into one enourmous
15-
string.
15+
string.
1616
* The build process wraps that string with code to set it an angular module constant:
1717
``` angular.module('material.core').constant('$MD_THEME_CSS', 'HUGE_THEME_STRING'); ```
1818
* That code gets dumped at the end of `angular-material.js`
@@ -24,15 +24,21 @@ mini-DSL, applies the colors for the theme, and appends the resulting CSS into t
2424

2525

2626
### The mini-DSL
27-
* Each color is written in the form `'{{palette-hue-opacity}}'`, where opacity is optional.
27+
* Each color is written in the form `'{{palette-hue-contrast-opacity}}'`, where `hue`, `contrast`,
28+
and opacity are optional.
2829
* For example, `'{{primary-500}}'`
29-
* Palettes are `primary`, `accent`, `warn`, `background`, `foreground`
30-
* The hues for each type except `foreground` use the Material Design hues.
31-
* The `forground` palette is a number from one to four:
32-
* `foreground-1`: text
33-
* `foreground-2`: secondary text, icons
34-
* `foreground-3`: disabled text, hint text
35-
* `foreground-4`: dividers
36-
* There is also a special hue called `contrast` that will give a contrast color (for text).
37-
For example, `accent-contrast` will be a contrast color for the accent color, for use as a text
38-
color on an accent-colored background.
30+
* Palettes are `primary`, `accent`, `warn`, `background`
31+
* The hues for each type use the Material Design hues. When not specified, each palette defaults
32+
`hue` to `500` with the exception of `background`
33+
* The `opacity` value can be a decimal between 0 and 1 or one of the following values based on the
34+
hue's contrast type (dark, light, or strongLight):
35+
* `icon`: icon (0.54 / 0.87 / 1.0)
36+
* `secondary`: secondary text (0.54 / 0.87)
37+
* `disabled`: disabled text or icon (0.38 / 0.54)
38+
* `hint`: hint text (0.38 / 0.50)
39+
* `divider`: divider (0.12)
40+
* `contrast` will give a contrast color (for text) and can be mixed with `opacity`.
41+
For example, `accent-contrast` will be a contrast color for the accent color, for use as a text
42+
color on an accent-colored background. Adding an `opacity` value as in `accent-contrast-icon` will
43+
apply the Material Design icon opacity. Using a decimal opacity value as in `accent-contrast-0.25`
44+
will apply the contrast color for the accent color at 25% opacity.

src/core/services/theming/theming.js

Lines changed: 137 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,9 @@ function detectDisabledThemes($mdThemingProvider) {
134134
* {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules
135135
* {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue
136136
* {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules
137-
*
138-
* Foreground expansion: Applies rgba to black/white foreground text
139-
*
140-
* {{foreground-1}} - used for primary text
141-
* {{foreground-2}} - used for secondary text/divider
142-
* {{foreground-3}} - used for disabled text
143-
* {{foreground-4}} - used for dividers
137+
* {{primary-contrast-divider}} - Apply divider opacity to contrast color
138+
* {{background-default-contrast}} - Apply primary text color for contrasting with default background
139+
* {{background-50-contrast-icon}} - Apply contrast color for icon on background's shade 50 hue
144140
*
145141
*/
146142

@@ -150,21 +146,14 @@ var GENERATED = { };
150146
// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
151147
var PALETTES;
152148

153-
// Text Colors on light and dark backgrounds
149+
// Text colors are automatically generated based on background color when not specified
150+
// Custom palettes can provide override colors
154151
// @see https://www.google.com/design/spec/style/color.html#color-text-background-colors
155152
var DARK_FOREGROUND = {
156153
name: 'dark',
157-
'1': 'rgba(0,0,0,0.87)',
158-
'2': 'rgba(0,0,0,0.54)',
159-
'3': 'rgba(0,0,0,0.38)',
160-
'4': 'rgba(0,0,0,0.12)'
161154
};
162155
var LIGHT_FOREGROUND = {
163156
name: 'light',
164-
'1': 'rgba(255,255,255,1.0)',
165-
'2': 'rgba(255,255,255,0.7)',
166-
'3': 'rgba(255,255,255,0.5)',
167-
'4': 'rgba(255,255,255,0.12)'
168157
};
169158

170159
var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)';
@@ -201,6 +190,34 @@ var DARK_DEFAULT_HUES = {
201190
'hue-3': 'A200'
202191
}
203192
};
193+
194+
// use inactive icon opacity from https://material.google.com/style/color.html#color-text-background-colors
195+
// not inactive icon opacity from https://material.google.com/style/icons.html#icons-system-icons
196+
197+
var DARK_CONTRAST_OPACITY = {
198+
'icon': 0.54,
199+
'secondary': 0.54,
200+
'disabled': 0.38,
201+
'hint': 0.38,
202+
'divider': 0.12,
203+
};
204+
205+
var LIGHT_CONTRAST_OPACITY = {
206+
'icon': 0.87,
207+
'secondary': 0.7,
208+
'disabled': 0.5,
209+
'hint': 0.5,
210+
'divider': 0.12
211+
};
212+
213+
var STRONG_LIGHT_CONTRAST_OPACITY = {
214+
'icon': 1.0,
215+
'secondary': 0.7,
216+
'disabled': 0.5,
217+
'hint': 0.5,
218+
'divider': 0.12
219+
};
220+
204221
THEME_COLOR_TYPES.forEach(function(colorType) {
205222
// Color types with unspecified default hues will use these default hue values
206223
var defaultDefaultHues = {
@@ -617,20 +634,44 @@ function parseRules(theme, colorType, rules) {
617634

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

624643
// find and replace simple variables where we use a specific hue, not an entire palette
625644
// eg. "{{primary-100}}"
626645
//\(' + THEME_COLOR_TYPES.join('\|') + '\)'
627-
rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) {
646+
rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, contrast, opacity) {
647+
var regexColorType = colorType;
628648
if (colorType === 'foreground') {
629649
if (hue == 'shadow') {
630650
return theme.foregroundShadow;
631-
} else {
632-
return theme.foregroundPalette[hue] || theme.foregroundPalette['1'];
651+
} else if (theme.foregroundPalette[hue]) {
652+
// Use user defined palette number (ie: foreground-2)
653+
return rgba( colorToRgbaArray( theme.foregroundPalette[hue] ) );
654+
} else if (theme.foregroundPalette['1']){
655+
return rgba( colorToRgbaArray( theme.foregroundPalette['1'] ) );
656+
}
657+
// Default to background-default-contrast-{opacity}
658+
colorType = 'background';
659+
contrast = 'contrast';
660+
if (!opacity && hue) {
661+
// Convert references to legacy hues to opacities (ie: foreground-4 to *-divider)
662+
switch(hue) {
663+
// hue-1 uses default opacity
664+
case '2':
665+
opacity = 'secondary';
666+
break;
667+
case '3':
668+
opacity = 'disabled';
669+
break;
670+
case '4':
671+
opacity = 'divider';
672+
}
633673
}
674+
hue = 'default';
634675
}
635676

636677
// `default` is also accepted as a hue-value, because the background palettes are
@@ -639,13 +680,51 @@ function parseRules(theme, colorType, rules) {
639680
hue = theme.colors[colorType].hues[hue];
640681
}
641682

642-
return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity );
683+
var colorDetails = (PALETTES[ theme.colors[colorType].name ][hue] || '');
684+
685+
// If user has specified a foreground color, use those
686+
if (colorType === 'background' && contrast && regexColorType !== 'foreground' && colorDetails.contrastType == defaultBgContrastType) {
687+
// Don't process if colorType was changed
688+
switch (opacity) {
689+
case 'secondary':
690+
case 'icon':
691+
if (theme.foregroundPalette['2']) {
692+
return rgba(colorToRgbaArray(theme.foregroundPalette['2']));
693+
}
694+
break;
695+
case 'disabled':
696+
case 'hint':
697+
if (theme.foregroundPalette['3']) {
698+
return rgba(colorToRgbaArray(theme.foregroundPalette['3']));
699+
}
700+
break;
701+
case 'divider':
702+
if (theme.foregroundPalette['4']) {
703+
return rgba(colorToRgbaArray(theme.foregroundPalette['4']));
704+
}
705+
break;
706+
default:
707+
if (theme.foregroundPalette['1']) {
708+
return rgba(colorToRgbaArray(theme.foregroundPalette['1']));
709+
}
710+
break;
711+
}
712+
}
713+
714+
if (contrast && opacity) {
715+
opacity = colorDetails.opacity[opacity] || opacity;
716+
}
717+
718+
return rgba( colorDetails[contrast ? 'contrast' : 'value'], opacity );
643719
});
644720

645721
// For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
646722
angular.forEach(color.hues, function(hueValue, hueName) {
647723
var newRule = rules
648-
.replace(hueRegex, function(match, _, colorType, hueType, opacity) {
724+
.replace(hueRegex, function(match, colorType, hueType, contrast, opacity) {
725+
if (contrast && opacity) {
726+
opacity = palette[hueValue].opacity[opacity] || opacity;
727+
}
649728
return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
650729
});
651730
if (hueName !== 'default') {
@@ -756,6 +835,37 @@ function generateAllThemes($injector, $mdTheming) {
756835
delete palette.contrastStrongLightColors;
757836
delete palette.contrastDarkColors;
758837

838+
function getContrastType(hueName) {
839+
if (defaultContrast === 'light' ? darkColors.indexOf(hueName) !== -1 : lightColors.indexOf(hueName) === -1) {
840+
return 'dark';
841+
}
842+
if (strongLightColors.indexOf(hueName) !== -1) {
843+
return 'strongLight';
844+
}
845+
return 'light';
846+
}
847+
function getContrastColor(contrastType) {
848+
switch(contrastType) {
849+
default:
850+
case 'strongLight':
851+
return STRONG_LIGHT_CONTRAST_COLOR;
852+
case 'light':
853+
return LIGHT_CONTRAST_COLOR;
854+
case 'dark':
855+
return DARK_CONTRAST_COLOR;
856+
}
857+
}
858+
function getOpacityValues(contrastType) {
859+
switch(contrastType) {
860+
default:
861+
case 'strongLight':
862+
return STRONG_LIGHT_CONTRAST_OPACITY;
863+
case 'light':
864+
return LIGHT_CONTRAST_OPACITY;
865+
case 'dark':
866+
return DARK_CONTRAST_OPACITY;
867+
}
868+
}
759869
// Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR }
760870
angular.forEach(palette, function(hueValue, hueName) {
761871
if (angular.isObject(hueValue)) return; // Already converted
@@ -768,27 +878,13 @@ function generateAllThemes($injector, $mdTheming) {
768878
.replace('%3', hueName));
769879
}
770880

881+
var contrastType = getContrastType(hueName);
771882
palette[hueName] = {
772883
value: rgbValue,
773-
contrast: getContrastColor()
884+
contrastType: contrastType,
885+
contrast: getContrastColor(contrastType),
886+
opacity: getOpacityValues(contrastType)
774887
};
775-
function getContrastColor() {
776-
if (defaultContrast === 'light') {
777-
if (darkColors.indexOf(hueName) > -1) {
778-
return DARK_CONTRAST_COLOR;
779-
} else {
780-
return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
781-
: LIGHT_CONTRAST_COLOR;
782-
}
783-
} else {
784-
if (lightColors.indexOf(hueName) > -1) {
785-
return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
786-
: LIGHT_CONTRAST_COLOR;
787-
} else {
788-
return DARK_CONTRAST_COLOR;
789-
}
790-
}
791-
}
792888
});
793889
}
794890
}

0 commit comments

Comments
 (0)