From 3c8135fbe1a18def940484141b075d51b52402be Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Tue, 16 Jan 2024 18:25:18 -0500 Subject: [PATCH 1/3] feat: add set custom primary colors method in initialize flow --- src/config.js | 2 + src/constants.js | 14 +++++++ src/index.js | 1 + src/initialize.js | 38 +++++++++++++++++++ src/initialize.test.js | 85 ++++++++++++++++++++++++++++++++++++++++++ src/utils.js | 34 +++++++++++++++++ src/utils.test.js | 19 ++++++++++ 7 files changed, 193 insertions(+) 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 d0499f9e3..428012f3e 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ export { convertKeyNames, getQueryParameters, ensureDefinedConfig, + mix, } from './utils'; export { APP_TOPIC, diff --git a/src/initialize.js b/src/initialize.js index 7073c57a0..7b4e32b1b 100644 --- a/src/initialize.js +++ b/src/initialize.js @@ -87,8 +87,10 @@ import { APP_LOGGING_INITIALIZED, APP_ANALYTICS_INITIALIZED, APP_READY, APP_INIT_ERROR, + PRIMARY_COLOR_DEFINITIONS, } from './constants'; import configureCache from './auth/LocalForageCache'; +import { mix } from './utils'; /** * A browser history or memory history object created by the [history](https://github.com/ReactTraining/history) @@ -193,6 +195,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. * @@ -294,6 +331,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 b15458001..d33e68b56 100644 --- a/src/initialize.test.js +++ b/src/initialize.test.js @@ -350,4 +350,89 @@ 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(); + }); }); diff --git a/src/utils.js b/src/utils.js index be16cff22..e173ca75f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -165,3 +165,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 016129040..1c366c772 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -4,6 +4,7 @@ import { snakeCaseObject, convertKeyNames, getQueryParameters, + mix, } from '.'; describe('modifyObjectKeys', () => { @@ -113,3 +114,21 @@ describe('getQueryParameters', () => { }); }); }); + +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); + }); +}); From fea0345d55a5b894fd3d3d6c628618abcc0a1814 Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Tue, 16 Jan 2024 18:11:01 -0500 Subject: [PATCH 2/3] fix: set custom primary colors settings in test case --- src/react/AppProvider.test.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/react/AppProvider.test.jsx b/src/react/AppProvider.test.jsx index 9aa1a4b74..c0eea5c9e 100644 --- a/src/react/AppProvider.test.jsx +++ b/src/react/AppProvider.test.jsx @@ -33,6 +33,7 @@ jest.mock('../config', () => ({ REFRESH_ACCESS_TOKEN_ENDPOINT: 'localhost:18000/oauth2/access_token', ACCESS_TOKEN_COOKIE_NAME: 'access_token', CSRF_TOKEN_API_PATH: 'localhost:18000/csrf', + CUSTOM_PRIMARY_COLORS: {}, }), })); From 8a3c344887f30212963f94a68acad04bfb291118 Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Tue, 16 Jan 2024 16:58:54 -0500 Subject: [PATCH 3/3] feat: add paragon theme condition If the PARAGON_THEME_URLS has been set the primary colors wont' be set on runtime --- src/initialize.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/initialize.js b/src/initialize.js index 7b4e32b1b..80d4f35df 100644 --- a/src/initialize.js +++ b/src/initialize.js @@ -204,9 +204,10 @@ export function loadExternalScripts(externalScripts, data) { */ export function setCustomPrimaryColors() { const { CUSTOM_PRIMARY_COLORS } = getConfig(); + const { PARAGON_THEME_URLS } = getConfig(); const primary = CUSTOM_PRIMARY_COLORS['pgn-color-primary-base']; - if (!primary) { + if (!primary || PARAGON_THEME_URLS != null) { return; } document.documentElement.style.setProperty('--pgn-color-primary-base', primary);