diff --git a/src/config.js b/src/config.js index 2e345c8eb..1134d88ee 100644 --- a/src/config.js +++ b/src/config.js @@ -171,6 +171,7 @@ let config = { MFE_CONFIG_API_URL: process.env.MFE_CONFIG_API_URL, APP_ID: process.env.APP_ID, SUPPORT_URL: process.env.SUPPORT_URL, + CUSTOM_PRIMARY_COLORS: process.env.CUSTOM_PRIMARY_COLORS || {}, }; /** @@ -325,4 +326,5 @@ export function ensureConfig(keys, requester = 'unspecified application code') { * @property {string} APP_ID * @property {string} SUPPORT_URL * @property {string} PARAGON_THEME_URLS + * @property {Object} CUSTOM_PRIMARY_COLORS */ diff --git a/src/constants.js b/src/constants.js index 99bddd833..f2e70eb51 100644 --- a/src/constants.js +++ b/src/constants.js @@ -64,3 +64,17 @@ export const APP_INIT_ERROR = `${APP_TOPIC}.INIT_ERROR`; export const CONFIG_TOPIC = 'CONFIG'; export const CONFIG_CHANGED = `${CONFIG_TOPIC}.CHANGED`; + +export const PRIMARY_COLOR_DEFINITIONS = { + 'pgn-color-primary-100': { '#FFFFFF': 94 }, + 'pgn-color-primary-200': { '#FFFFFF': 75 }, + 'pgn-color-primary-300': { '#FFFFFF': 50 }, + 'pgn-color-primary-400': { '#FFFFFF': 25 }, + 'pgn-color-primary-500': { '#FFFFFF': 0 }, + 'pgn-color-primary-600': { '#000000': 10 }, + 'pgn-color-primary-700': { '#000000': 20 }, + 'pgn-color-primary-800': { '#000000': 25 }, + 'pgn-color-primary-900': { '#000000': 30 }, + 'pgn-color-link-base': { '#FFFFFF': 35 }, + 'pgn-color-link-hover': { '#FFFFFF': 0 }, +}; diff --git a/src/index.js b/src/index.js index 9a4137740..c224dbc06 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ export { ensureDefinedConfig, parseURL, getPath, + mix, } from './utils'; export { APP_TOPIC, diff --git a/src/initialize.js b/src/initialize.js index 507d53bba..ae551ec61 100644 --- a/src/initialize.js +++ b/src/initialize.js @@ -54,7 +54,7 @@ Note that the env.config.js file in frontend-platform's root directory is NOT us initialization code, it's just there for the test suite and example application. */ import envConfig from 'env.config'; // eslint-disable-line import/no-unresolved -import { getPath } from './utils'; +import { getPath, mix } from './utils'; import { publish, } from './pubSub'; @@ -87,6 +87,7 @@ import { APP_LOGGING_INITIALIZED, APP_ANALYTICS_INITIALIZED, APP_READY, APP_INIT_ERROR, + PRIMARY_COLOR_DEFINITIONS, } from './constants'; import configureCache from './auth/LocalForageCache'; @@ -205,6 +206,41 @@ export function loadExternalScripts(externalScripts, data) { }); } +/* + * Set custom colors based on the config content. + * This method allows to change primary colors and its levels on runtime, + * if a specific level is already in the configuration that level will have + * priority otherwise the level will be calculated based on primary color by + * using the mix function. + */ +export function setCustomPrimaryColors() { + const { CUSTOM_PRIMARY_COLORS } = getConfig(); + const primary = CUSTOM_PRIMARY_COLORS['pgn-color-primary-base']; + + if (!primary) { + return; + } + document.documentElement.style.setProperty('--pgn-color-primary-base', primary); + + Object.keys(PRIMARY_COLOR_DEFINITIONS).forEach((key) => { + let color; + + if (key in CUSTOM_PRIMARY_COLORS) { + color = CUSTOM_PRIMARY_COLORS[key]; + } else { + try { + const [base, weight] = Object.entries(PRIMARY_COLOR_DEFINITIONS[key])[0]; + + color = mix(base, primary, weight); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error setting custom colors', error.message); + } + } + document.documentElement.style.setProperty('--'.concat(key), color); + }); +} + /** * The default handler for the initialization lifecycle's `analytics` phase. * @@ -306,6 +342,7 @@ export async function initialize({ await handlers.config(); await jsFileConfig(); await runtimeConfig(); + setCustomPrimaryColors(); publish(APP_CONFIG_INITIALIZED); loadExternalScripts(externalScripts, { diff --git a/src/initialize.test.js b/src/initialize.test.js index 5276f705e..137cf43dc 100644 --- a/src/initialize.test.js +++ b/src/initialize.test.js @@ -352,6 +352,91 @@ describe('initialize', () => { expect(hydrateAuthenticatedUser).not.toHaveBeenCalled(); expect(logError).not.toHaveBeenCalled(); }); + + it('should not set any color', async () => { + const messages = { i_am: 'a message' }; + await initialize({ + messages, + }); + + // eslint-disable-next-line no-underscore-dangle + expect(document.documentElement.style._values).toEqual({}); + expect(logError).not.toHaveBeenCalled(); + }); + + it('should set primary color and calculate its levels', async () => { + config.CUSTOM_PRIMARY_COLORS = { 'pgn-color-primary-base': '#A000000' }; + const messages = { i_am: 'a message' }; + const expectedKeys = [ + '--pgn-color-primary-base', + '--pgn-color-primary-100', + '--pgn-color-primary-200', + '--pgn-color-primary-300', + '--pgn-color-primary-400', + '--pgn-color-primary-500', + '--pgn-color-primary-600', + '--pgn-color-primary-700', + '--pgn-color-primary-800', + '--pgn-color-primary-900', + '--pgn-color-link-base', + '--pgn-color-link-hover', + ]; + + await initialize({ + messages, + }); + + // eslint-disable-next-line no-underscore-dangle + expect(Object.keys(document.documentElement.style._values)).toEqual(expectedKeys); + expect(logError).not.toHaveBeenCalled(); + }); + + it('should set primary color and its levels from config', async () => { + config.CUSTOM_PRIMARY_COLORS = { + 'pgn-color-primary-base': '#A000000', + 'pgn-color-primary-100': '#A001000', + 'pgn-color-primary-200': '#A000000', + 'pgn-color-primary-300': '#A045000', + 'pgn-color-primary-400': '#A07AB00', + 'pgn-color-primary-500': '#A000B12', + 'pgn-color-primary-600': '#A087400', + 'pgn-color-primary-700': '#A0abc00', + 'pgn-color-primary-800': '#AABCFA0', + 'pgn-color-primary-900': '#A014200', + 'pgn-color-link-base': '#FF0056', + 'pgn-color-link-hover': '#AFFCDA', + }; + const messages = { i_am: 'a message' }; + + await initialize({ + messages, + }); + + // eslint-disable-next-line no-underscore-dangle + expect(Object.values(document.documentElement.style._values)).toEqual(Object.values(config.CUSTOM_PRIMARY_COLORS)); + expect(logError).not.toHaveBeenCalled(); + }); + it('should log error when color is invalid', async () => { + // eslint-disable-next-line no-console + console.error = jest.fn(); + configureCache.mockReturnValueOnce(Promise.resolve({ + get: (url) => { + const params = new URL(url).search; + const mfe = new URLSearchParams(params).get('mfe'); + return ({ data: { ...newConfig.common, ...newConfig[mfe] } }); + }, + })); + config.CUSTOM_PRIMARY_COLORS = { 'pgn-color-primary-base': '#AB' }; + const messages = { i_am: 'a message' }; + + await initialize({ + messages, + }); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenNthCalledWith(9, 'Error setting custom colors', 'Parameter color does not have format #RRGGBB'); + expect(logError).not.toHaveBeenCalled(); + }); }); describe('history', () => { diff --git a/src/utils.js b/src/utils.js index 7d7412839..e7764c13e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -201,3 +201,37 @@ export function ensureDefinedConfig(object, requester) { } }); } + +/** + * This function is the javascript version of SASS mix() function, + * https://sass-lang.com/documentation/modules/color#mix + * + * @param {string} First color in hexadecimal. + * @param {string} Second color in hexadecimal. + * @param {number} Relative opacity of each color. + * @returns {string} Returns a color that’s a mixture of color1 and color2. + */ +export function mix(color1, color2, weight = 50) { + let color = '#'; + + function d2h(d) { return d.toString(16); } // convert a decimal value to hex + function h2d(h) { return parseInt(h, 16); } // convert a hex value to decimal + + if (color1.length < 6 || color2.length < 6) { + throw new Error('Parameter color does not have format #RRGGBB'); + } + + for (let i = 0; i <= 5; i += 2) { // loop through each of the 3 hex pairs—red, green, and blue + const v1 = h2d(color1.replace('#', '').substr(i, 2)); + const v2 = h2d(color2.replace('#', '').substr(i, 2)); + let val = d2h(Math.round(v2 + (v1 - v2) * (weight / 100.0))); + + while (val.length < 2) { + val = '0'.concat(val); + } + + color += val; + } + + return color; +} diff --git a/src/utils.test.js b/src/utils.test.js index 6b0723af0..2df98d381 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -6,6 +6,7 @@ import { parseURL, getPath, getQueryParameters, + mix, } from '.'; describe('modifyObjectKeys', () => { @@ -207,3 +208,21 @@ describe('getPath', () => { expect(getPath(testURL)).toEqual('/learning/'); }); }); + +describe('mix', () => { + it('should return rigth value', () => { + const expected = '#546e88'; // This value was calculated in https://sass.js.org/ by using sass mix function + + expect(mix('#FFFFFF', '#0A3055', 30)).toBe(expected); + }); + + it('should thow error', () => { + expect(() => mix('#FFFFFF', '#0A3')).toThrow('Parameter color does not have format #RRGGBB'); + }); + + it('should return rigth value without hash symbol on parameters', () => { + const expected = '#8598aa'; // This value was calculated in https://sass.js.org/ by using sass mix function + + expect(mix('FFFFFF', '0A3055')).toBe(expected); + }); +});