MOBILE-4653 color: Validate colors and handle alpha

main
Dani Palou 2024-12-16 11:33:19 +01:00
parent d23160df19
commit a73ea2b6a9
3 changed files with 119 additions and 63 deletions

View File

@ -42,11 +42,25 @@ export enum CoreIonicColorNames {
*/ */
export class CoreColors { export class CoreColors {
protected static readonly BLACK_TRANSPARENT_COLORS =
['rgba(0, 0, 0, 0)', 'transparent', '#00000000', '#0000', 'hsl(0, 0%, 0%, 0)'];
// Avoid creating singleton instances. // Avoid creating singleton instances.
private constructor() { private constructor() {
// Nothing to do. // Nothing to do.
} }
/**
* Check if a color is valid.
* Accepted formats are rgb, rgba, hsl, hsla, hex and named colors.
*
* @param color Color in any format.
* @returns Whether color is valid.
*/
static isValid(color: string): boolean {
return CoreColors.getColorRGBA(color).length >= 3;
}
/** /**
* Returns better contrast color. * Returns better contrast color.
* *
@ -58,23 +72,30 @@ export class CoreColors {
} }
/** /**
* Returns the same color % darker. * Returns the same color % darker. Returned color is always hex, unless the color isn't valid.
* *
* @param color Color to get darker. * @param color Color to get darker.
* @returns Darker Hex RGB color. * @returns Darker Hex RGB color.
*/ */
static darker(color: string, percent: number = 48): string { static darker(color: string, percent: number = 48): string {
const inversePercent = 1 - (percent / 100); const inversePercent = 1 - (percent / 100);
const components = CoreColors.hexToRGB(color);
components.red = Math.floor(components.red * inversePercent);
components.green = Math.floor(components.green * inversePercent);
components.blue = Math.floor(components.blue * inversePercent);
return CoreColors.RGBToHex(components); const rgba = CoreColors.getColorRGBA(color);
if (rgba.length < 3) {
return color; // Color not valid, return original value.
}
const red = Math.floor(rgba[0] * inversePercent);
const green = Math.floor(rgba[1] * inversePercent);
const blue = Math.floor(rgba[2] * inversePercent);
return rgba[3] !== undefined ?
CoreColors.getColorHex(`rgba(${red}, ${green}, ${blue}, ${rgba[3]})`) :
CoreColors.getColorHex(`rgb(${red}, ${green}, ${blue})`);
} }
/** /**
* Returns the same color % lighter. * Returns the same color % lighter. Returned color is always hex, unless the color isn't valid.
* *
* @param color Color to get lighter. * @param color Color to get lighter.
* @returns Lighter Hex RGB color. * @returns Lighter Hex RGB color.
@ -83,12 +104,18 @@ export class CoreColors {
percent = percent / 100; percent = percent / 100;
const inversePercent = 1 - percent; const inversePercent = 1 - percent;
const components = CoreColors.hexToRGB(color); const rgba = CoreColors.getColorRGBA(color);
components.red = Math.floor(255 * percent + components.red * inversePercent); if (rgba.length < 3) {
components.green = Math.floor(255 * percent + components.green * inversePercent); return color; // Color not valid, return original value.
components.blue = Math.floor(255 * percent + components.blue * inversePercent); }
return CoreColors.RGBToHex(components); const red = Math.floor(255 * percent + rgba[0] * inversePercent);
const green = Math.floor(255 * percent + rgba[1] * inversePercent);
const blue = Math.floor(255 * percent + rgba[2] * inversePercent);
return rgba[3] !== undefined ?
CoreColors.getColorHex(`rgba(${red}, ${green}, ${blue}, ${rgba[3]})`) :
CoreColors.getColorHex(`rgb(${red}, ${green}, ${blue})`);
} }
/** /**
@ -99,19 +126,24 @@ export class CoreColors {
*/ */
static getColorHex(color: string): string { static getColorHex(color: string): string {
const rgba = CoreColors.getColorRGBA(color); const rgba = CoreColors.getColorRGBA(color);
if (rgba.length === 0) { if (rgba.length < 3) {
return ''; return '';
} }
const hex = [0,1,2].map( let hex = [0,1,2].map(
(idx) => CoreColors.componentToHex(rgba[idx]), (idx) => CoreColors.componentToHex(rgba[idx]),
).join(''); ).join('');
if (rgba.length > 3) {
hex += CoreColors.componentToHex(Math.round(rgba[3] * 255));
}
return '#' + hex; return '#' + hex;
} }
/** /**
* Returns RGBA color from any color format. * Returns RGBA color from any color format.
* Only works with RGB, RGBA, HSL, HSLA, hex and named colors.
* *
* @param color Color in any format. * @param color Color in any format.
* @returns Red, green, blue and alpha. * @returns Red, green, blue and alpha.
@ -119,15 +151,28 @@ export class CoreColors {
static getColorRGBA(color: string): number[] { static getColorRGBA(color: string): number[] {
if (!color.match(/rgba?\(.*\)/)) { if (!color.match(/rgba?\(.*\)/)) {
// Convert the color to RGB format. // Convert the color to RGB format.
// Use backgroundColor instead of color because it detects invalid colors like rgb(0, 80) or #0F.
const originalColor = color;
const d = document.createElement('span'); const d = document.createElement('span');
d.style.color = color; d.style.backgroundColor = color;
document.body.appendChild(d); document.body.appendChild(d);
color = getComputedStyle(d).color; color = getComputedStyle(d).backgroundColor;
document.body.removeChild(d); document.body.removeChild(d);
// Check that the color is valid. Some invalid colors return rgba(0, 0, 0, 0).
if (
!color.match(/rgba?\(.*\)/) ||
(color === 'rgba(0, 0, 0, 0)' && !CoreColors.BLACK_TRANSPARENT_COLORS.includes(originalColor))
) {
return [];
}
} }
const matches = color.match(/\d+[^.]|\d*\.\d*/g) || []; const matches = color.match(/\d+(\.\d+)?|\.\d+/g) || [];
if (matches.length < 3) {
return [];
}
return matches.map((a, index) => index < 3 ? parseInt(a, 10) : parseFloat(a)); return matches.map((a, index) => index < 3 ? parseInt(a, 10) : parseFloat(a));
} }
@ -139,9 +184,12 @@ export class CoreColors {
* @returns Luma number based on SMPTE C, Rec. 709 weightings. * @returns Luma number based on SMPTE C, Rec. 709 weightings.
*/ */
protected static luma(color: string): number { protected static luma(color: string): number {
const rgb = CoreColors.hexToRGB(color); const rgba = CoreColors.getColorRGBA(color);
if (rgba.length < 3) {
return 0; // Color not valid.
}
return (rgb.red * 0.2126) + (rgb.green * 0.7152) + (rgb.blue * 0.0722); return (rgba[0] * 0.2126) + (rgba[1] * 0.7152) + (rgba[2] * 0.0722);
} }
/** /**
@ -149,39 +197,19 @@ export class CoreColors {
* *
* @param color Hexadec RGB Color. * @param color Hexadec RGB Color.
* @returns RGB color components. * @returns RGB color components.
* @deprecated since 5.0. Use getColorRGBA instead.
*/ */
static hexToRGB(color: string): ColorComponents { static hexToRGB(color: string): ColorComponents {
if (color.charAt(0) == '#') { const rgba = CoreColors.getColorRGBA(color);
color = color.substring(1);
}
if (color.length === 3) {
color = color.charAt(0) + color.charAt(0) + color.charAt(1) + color.charAt(1) + color.charAt(2) + color.charAt(2);
} else if (color.length !== 6) {
throw('Invalid hex color: ' + color);
}
return { return {
red: parseInt(color.substring(0, 2), 16), red: rgba[0] ?? 0,
green: parseInt(color.substring(2, 4), 16), green: rgba[1] ?? 0,
blue: parseInt(color.substring(4, 6), 16), blue: rgba[2] ?? 0,
}; };
} }
/**
* Converts RGB components to Hex string.
*
* @param color Color components.
* @returns RGB color in string.
*/
protected static RGBToHex(color: ColorComponents): string {
return '#' + CoreColors.componentToHex(color.red) +
CoreColors.componentToHex(color.green) +
CoreColors.componentToHex(color.blue);
}
/** /**
* Converts a color component from decimal to hexadec. * Converts a color component from decimal to hexadec.
* *

View File

@ -16,6 +16,25 @@ import { CoreColors } from '@singletons/colors';
describe('CoreColors singleton', () => { describe('CoreColors singleton', () => {
it('checks if a color is valid', () => {
expect(CoreColors.isValid('')).toBe(false);
expect(CoreColors.isValid('#000000')).toBe(true);
expect(CoreColors.isValid('#00000000')).toBe(true);
expect(CoreColors.isValid('#000')).toBe(true);
expect(CoreColors.isValid('#0000')).toBe(true);
expect(CoreColors.isValid('#00')).toBe(false);
expect(CoreColors.isValid('#00000')).toBe(false);
expect(CoreColors.isValid('rgb(0,0,0)')).toBe(true);
expect(CoreColors.isValid('rgba(0,0,0,0)')).toBe(true);
expect(CoreColors.isValid('rgb(0,0)')).toBe(false);
expect(CoreColors.isValid('hsl(0, 100%, 0%)')).toBe(true);
expect(CoreColors.isValid('hsla(0, 100%, 0%, 0)')).toBe(true);
expect(CoreColors.isValid('hsl(0, 100%)')).toBe(false);
// @todo There are problems when testing color names (e.g. violet).
// They work fine in real browsers but not in unit tests.
});
it('determines if white contrast is better', () => { it('determines if white contrast is better', () => {
expect(CoreColors.isWhiteContrastingBetter('#000000')).toBe(true); expect(CoreColors.isWhiteContrastingBetter('#000000')).toBe(true);
expect(CoreColors.isWhiteContrastingBetter('#999999')).toBe(true); expect(CoreColors.isWhiteContrastingBetter('#999999')).toBe(true);
@ -26,6 +45,12 @@ describe('CoreColors singleton', () => {
expect(CoreColors.isWhiteContrastingBetter('#0000ff')).toBe(true); expect(CoreColors.isWhiteContrastingBetter('#0000ff')).toBe(true);
expect(CoreColors.isWhiteContrastingBetter('#ff00ff')).toBe(true); expect(CoreColors.isWhiteContrastingBetter('#ff00ff')).toBe(true);
expect(CoreColors.isWhiteContrastingBetter('#ffff00')).toBe(false); expect(CoreColors.isWhiteContrastingBetter('#ffff00')).toBe(false);
expect(CoreColors.isWhiteContrastingBetter('rgb(0, 0, 0)')).toBe(true);
expect(CoreColors.isWhiteContrastingBetter('rgb(153, 153, 153)')).toBe(true);
expect(CoreColors.isWhiteContrastingBetter('rgb(255, 255, 255)')).toBe(false);
expect(CoreColors.isWhiteContrastingBetter('hsl(0, 100%, 0%)')).toBe(true);
expect(CoreColors.isWhiteContrastingBetter('hsl(0, 0%, 60%)')).toBe(true);
expect(CoreColors.isWhiteContrastingBetter('hsl(0, 0%, 100%)')).toBe(false);
}); });
it('makes color darker', () => { it('makes color darker', () => {
@ -33,6 +58,13 @@ describe('CoreColors singleton', () => {
expect(CoreColors.darker('#ffffff', 20)).toEqual('#cccccc'); expect(CoreColors.darker('#ffffff', 20)).toEqual('#cccccc');
expect(CoreColors.darker('#ffffff', 80)).toEqual('#323232'); expect(CoreColors.darker('#ffffff', 80)).toEqual('#323232');
expect(CoreColors.darker('#aabbcc', 40)).toEqual('#66707a'); expect(CoreColors.darker('#aabbcc', 40)).toEqual('#66707a');
expect(CoreColors.darker('rgb(255, 255, 255)', 50)).toEqual('#7f7f7f');
expect(CoreColors.darker('rgba(255, 255, 255, 0.5)', 50)).toEqual('#7f7f7f80');
expect(CoreColors.darker('hsl(0, 0%, 100%)', 50)).toEqual('#7f7f7f');
expect(CoreColors.darker('hsla(0, 0%, 100%, 0.8)', 50)).toEqual('#7f7f7fcc');
// Invalid colors return original value.
expect(CoreColors.darker('#fffff', 50)).toEqual('#fffff');
}); });
it('makes color lighter', () => { it('makes color lighter', () => {
@ -40,12 +72,22 @@ describe('CoreColors singleton', () => {
expect(CoreColors.lighter('#000000', 20)).toEqual('#333333'); expect(CoreColors.lighter('#000000', 20)).toEqual('#333333');
expect(CoreColors.lighter('#000000', 80)).toEqual('#cccccc'); expect(CoreColors.lighter('#000000', 80)).toEqual('#cccccc');
expect(CoreColors.lighter('#223344', 40)).toEqual('#7a848e'); expect(CoreColors.lighter('#223344', 40)).toEqual('#7a848e');
expect(CoreColors.lighter('rgb(0, 0, 0)', 50)).toEqual('#7f7f7f');
expect(CoreColors.lighter('rgba(0, 0, 0, 0.5)', 50)).toEqual('#7f7f7f80');
expect(CoreColors.lighter('hsl(0, 100%, 0%)', 50)).toEqual('#7f7f7f');
expect(CoreColors.lighter('hsla(0, 100%, 0%, 0.4)', 50)).toEqual('#7f7f7f66');
// Invalid colors return original value.
expect(CoreColors.darker('#00000', 50)).toEqual('#00000');
}); });
it('gets color hex value', () => { it('gets color hex value', () => {
expect(CoreColors.getColorHex('#123456')).toEqual('#123456'); expect(CoreColors.getColorHex('#123456')).toEqual('#123456');
expect(CoreColors.getColorHex('#12345680')).toEqual('#12345680');
expect(CoreColors.getColorHex('rgb(255, 100, 70)')).toEqual('#ff6446'); expect(CoreColors.getColorHex('rgb(255, 100, 70)')).toEqual('#ff6446');
expect(CoreColors.getColorHex('rgba(255, 100, 70, 0.5)')).toEqual('#ff6446'); expect(CoreColors.getColorHex('rgba(255, 100, 70, 0.5)')).toEqual('#ff644680');
expect(CoreColors.getColorHex('hsl(0, 0%, 60%)')).toEqual('#999999');
expect(CoreColors.getColorHex('hsla(0, 0%, 60%, 0.8)')).toEqual('#999999cc');
// @todo There are problems when testing color names (e.g. violet) or hsf colors. // @todo There are problems when testing color names (e.g. violet) or hsf colors.
// They work fine in real browsers but not in unit tests. // They work fine in real browsers but not in unit tests.
@ -53,31 +95,16 @@ describe('CoreColors singleton', () => {
it('gets color RGBA value', () => { it('gets color RGBA value', () => {
expect(CoreColors.getColorRGBA('#123456')).toEqual([18, 52, 86]); expect(CoreColors.getColorRGBA('#123456')).toEqual([18, 52, 86]);
expect(CoreColors.getColorRGBA('#123456cc')).toEqual([18, 52, 86, 0.8]);
expect(CoreColors.getColorRGBA('rgb(255, 100, 70)')).toEqual([255, 100, 70]); expect(CoreColors.getColorRGBA('rgb(255, 100, 70)')).toEqual([255, 100, 70]);
expect(CoreColors.getColorRGBA('rgba(255, 100, 70, 0.5)')).toEqual([255, 100, 70, 0.5]); expect(CoreColors.getColorRGBA('rgba(255, 100, 70, 0.5)')).toEqual([255, 100, 70, 0.5]);
expect(CoreColors.getColorRGBA('hsl(0, 0%, 60%)')).toEqual([153, 153, 153]);
expect(CoreColors.getColorRGBA('hsl(0, 0%, 60%, 0.3)')).toEqual([153, 153, 153, 0.3]);
// @todo There are problems when testing color names (e.g. violet) or hsf colors. // @todo There are problems when testing color names (e.g. violet) or hsf colors.
// They work fine in real browsers but not in unit tests. // They work fine in real browsers but not in unit tests.
}); });
it('converts hex to rgb', () => {
expect(CoreColors.hexToRGB('#000000')).toEqual({
red: 0,
green: 0,
blue: 0,
});
expect(CoreColors.hexToRGB('#ffffff')).toEqual({
red: 255,
green: 255,
blue: 255,
});
expect(CoreColors.hexToRGB('#aabbcc')).toEqual({
red: 170,
green: 187,
blue: 204,
});
});
it('gets toolbar background color', () => { it('gets toolbar background color', () => {
document.body.style.setProperty('--core-header-toolbar-background', '#aabbcc'); document.body.style.setProperty('--core-header-toolbar-background', '#aabbcc');
expect(CoreColors.getToolbarBackgroundColor()).toEqual('#aabbcc'); expect(CoreColors.getToolbarBackgroundColor()).toEqual('#aabbcc');

View File

@ -6,6 +6,7 @@ For more information about upgrading, read the official documentation: https://m
- The logout process has been refactored, now it uses a logout page to trigger Angular guards. CoreSites.logout now uses this process, and CoreSites.logoutForRedirect is deprecated and shouldn't be used anymore. - The logout process has been refactored, now it uses a logout page to trigger Angular guards. CoreSites.logout now uses this process, and CoreSites.logoutForRedirect is deprecated and shouldn't be used anymore.
- The parameters of treatDownloadedFile of plugin file handlers have changed. Now the third parameter is an object with all the optional parameters. - The parameters of treatDownloadedFile of plugin file handlers have changed. Now the third parameter is an object with all the optional parameters.
- Some CoreColors functions have been refactored to handle alpha and to validate colors.
=== 4.5.0 === === 4.5.0 ===