feat: Helper to support dynamic system theme in public portal (#8206)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import Vue from 'vue';
|
||||
import PublicArticleSearch from './components/PublicArticleSearch.vue';
|
||||
import TableOfContents from './components/TableOfContents.vue';
|
||||
|
||||
export const getHeadingsfromTheArticle = () => {
|
||||
export const getHeadingsFromTheArticle = () => {
|
||||
const rows = [];
|
||||
const articleElement = document.getElementById('cw-article-content');
|
||||
articleElement.querySelectorAll('h1, h2, h3').forEach(element => {
|
||||
@@ -21,6 +21,53 @@ export const getHeadingsfromTheArticle = () => {
|
||||
return rows;
|
||||
};
|
||||
|
||||
export const generatePortalBgColor = (portalColor, theme) => {
|
||||
const baseColor = theme === 'dark' ? 'black' : 'white';
|
||||
return `color-mix(in srgb, ${portalColor} 20%, ${baseColor})`;
|
||||
};
|
||||
|
||||
export const generatePortalBg = (portalColor, theme) => {
|
||||
const bgImage = theme === 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg';
|
||||
return `background: url(/assets/images/hc/${bgImage}) ${generatePortalBgColor(
|
||||
portalColor,
|
||||
theme
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const generateGradientToBottom = theme => {
|
||||
return `background-image: linear-gradient(to bottom, transparent, ${
|
||||
theme === 'dark' ? '#151718' : 'white'
|
||||
})`;
|
||||
};
|
||||
|
||||
export const setPortalStyles = theme => {
|
||||
const portalColor = window.portalConfig.portalColor;
|
||||
const portalBgDiv = document.querySelector('#portal-bg');
|
||||
const portalBgGradientDiv = document.querySelector('#portal-bg-gradient');
|
||||
|
||||
if (portalBgDiv) {
|
||||
// Set background for #portal-bg
|
||||
portalBgDiv.setAttribute('style', generatePortalBg(portalColor, theme));
|
||||
}
|
||||
|
||||
if (portalBgGradientDiv) {
|
||||
// Set gradient background for #portal-bg-gradient
|
||||
portalBgGradientDiv.setAttribute('style', generateGradientToBottom(theme));
|
||||
}
|
||||
};
|
||||
|
||||
export const setPortalClass = theme => {
|
||||
const portalDiv = document.querySelector('#portal');
|
||||
portalDiv.classList.remove('light', 'dark');
|
||||
if (!portalDiv) return;
|
||||
portalDiv.classList.add(theme);
|
||||
};
|
||||
|
||||
export const updateThemeStyles = theme => {
|
||||
setPortalStyles(theme);
|
||||
setPortalClass(theme);
|
||||
};
|
||||
|
||||
export const InitializationHelpers = {
|
||||
navigateToLocalePage: () => {
|
||||
const allLocaleSwitcher = document.querySelector('.locale-switcher');
|
||||
@@ -36,7 +83,7 @@ export const InitializationHelpers = {
|
||||
return false;
|
||||
},
|
||||
|
||||
initalizeSearch: () => {
|
||||
initializeSearch: () => {
|
||||
const isSearchContainerAvailable = document.querySelector('#search-wrap');
|
||||
if (isSearchContainerAvailable) {
|
||||
new Vue({
|
||||
@@ -51,7 +98,7 @@ export const InitializationHelpers = {
|
||||
if (isOnArticlePage) {
|
||||
new Vue({
|
||||
components: { TableOfContents },
|
||||
data: { rows: getHeadingsfromTheArticle() },
|
||||
data: { rows: getHeadingsFromTheArticle() },
|
||||
template: '<table-of-contents :rows="rows" />',
|
||||
}).$mount('#cw-hc-toc');
|
||||
}
|
||||
@@ -68,13 +115,30 @@ export const InitializationHelpers = {
|
||||
});
|
||||
},
|
||||
|
||||
initializeTheme: () => {
|
||||
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const getThemePreference = () =>
|
||||
mediaQueryList.matches ? 'dark' : 'light';
|
||||
const themeFromServer = window.portalConfig.theme;
|
||||
if (themeFromServer === 'system') {
|
||||
// Handle dynamic theme changes for system theme
|
||||
mediaQueryList.addEventListener('change', event => {
|
||||
const newTheme = event.matches ? 'dark' : 'light';
|
||||
updateThemeStyles(newTheme);
|
||||
});
|
||||
const themePreference = getThemePreference();
|
||||
updateThemeStyles(themePreference);
|
||||
}
|
||||
},
|
||||
|
||||
initialize: () => {
|
||||
if (window.portalConfig.isPlainLayoutEnabled === 'true') {
|
||||
InitializationHelpers.appendPlainParamToURLs();
|
||||
} else {
|
||||
InitializationHelpers.navigateToLocalePage();
|
||||
InitializationHelpers.initalizeSearch();
|
||||
InitializationHelpers.initializeSearch();
|
||||
InitializationHelpers.initializeTableOfContents();
|
||||
// InitializationHelpers.initializeTheme();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { InitializationHelpers } from '../portalHelpers';
|
||||
import {
|
||||
InitializationHelpers,
|
||||
generatePortalBgColor,
|
||||
generatePortalBg,
|
||||
generateGradientToBottom,
|
||||
setPortalStyles,
|
||||
setPortalClass,
|
||||
updateThemeStyles,
|
||||
} from '../portalHelpers';
|
||||
|
||||
describe('#navigateToLocalePage', () => {
|
||||
it('returns correct cookie name', () => {
|
||||
@@ -21,3 +29,198 @@ describe('#navigateToLocalePage', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Theme Functions', () => {
|
||||
describe('#generatePortalBgColor', () => {
|
||||
it('returns mixed color for dark theme', () => {
|
||||
const result = generatePortalBgColor('#FF5733', 'dark');
|
||||
expect(result).toBe('color-mix(in srgb, #FF5733 20%, black)');
|
||||
});
|
||||
|
||||
it('returns mixed color for light theme', () => {
|
||||
const result = generatePortalBgColor('#FF5733', 'light');
|
||||
expect(result).toBe('color-mix(in srgb, #FF5733 20%, white)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generatePortalBg', () => {
|
||||
it('returns background for dark theme', () => {
|
||||
const result = generatePortalBg('#FF5733', 'dark');
|
||||
expect(result).toBe(
|
||||
'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #FF5733 20%, black)'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns background for light theme', () => {
|
||||
const result = generatePortalBg('#FF5733', 'light');
|
||||
expect(result).toBe(
|
||||
'background: url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #FF5733 20%, white)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generateGradientToBottom', () => {
|
||||
it('returns gradient for dark theme', () => {
|
||||
const result = generateGradientToBottom('dark');
|
||||
expect(result).toBe(
|
||||
'background-image: linear-gradient(to bottom, transparent, #151718)'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns gradient for light theme', () => {
|
||||
const result = generateGradientToBottom('light');
|
||||
expect(result).toBe(
|
||||
'background-image: linear-gradient(to bottom, transparent, white)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setPortalStyles', () => {
|
||||
let mockPortalBgDiv;
|
||||
let mockPortalBgGradientDiv;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mocking portal background div
|
||||
mockPortalBgDiv = document.createElement('div');
|
||||
mockPortalBgDiv.id = 'portal-bg';
|
||||
document.body.appendChild(mockPortalBgDiv);
|
||||
|
||||
// Mocking portal background gradient div
|
||||
mockPortalBgGradientDiv = document.createElement('div');
|
||||
mockPortalBgGradientDiv.id = 'portal-bg-gradient';
|
||||
document.body.appendChild(mockPortalBgGradientDiv);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('sets styles for portal background based on theme', () => {
|
||||
window.portalConfig = { portalColor: '#FF5733' };
|
||||
|
||||
setPortalStyles('dark');
|
||||
const expectedPortalBgStyle =
|
||||
'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #FF5733 20%, black)';
|
||||
const expectedGradientStyle =
|
||||
'background-image: linear-gradient(to bottom, transparent, #151718)';
|
||||
|
||||
expect(mockPortalBgDiv.getAttribute('style')).toBe(expectedPortalBgStyle);
|
||||
expect(mockPortalBgGradientDiv.getAttribute('style')).toBe(
|
||||
expectedGradientStyle
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setPortalClass', () => {
|
||||
let mockPortalDiv;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mocking portal div
|
||||
mockPortalDiv = document.createElement('div');
|
||||
mockPortalDiv.id = 'portal';
|
||||
mockPortalDiv.classList.add('light');
|
||||
document.body.appendChild(mockPortalDiv);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('sets portal class based on theme', () => {
|
||||
setPortalClass('dark');
|
||||
|
||||
expect(mockPortalDiv.classList.contains('dark')).toBe(true);
|
||||
expect(mockPortalDiv.classList.contains('light')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateThemeStyles', () => {
|
||||
let mockPortalDiv;
|
||||
let mockPortalBgDiv;
|
||||
let mockPortalBgGradientDiv;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mocking portal div
|
||||
mockPortalDiv = document.createElement('div');
|
||||
mockPortalDiv.id = 'portal';
|
||||
document.body.appendChild(mockPortalDiv);
|
||||
|
||||
// Mocking portal background div
|
||||
mockPortalBgDiv = document.createElement('div');
|
||||
mockPortalBgDiv.id = 'portal-bg';
|
||||
document.body.appendChild(mockPortalBgDiv);
|
||||
|
||||
// Mocking portal background gradient div
|
||||
mockPortalBgGradientDiv = document.createElement('div');
|
||||
mockPortalBgGradientDiv.id = 'portal-bg-gradient';
|
||||
document.body.appendChild(mockPortalBgGradientDiv);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('updates theme styles based on theme', () => {
|
||||
window.portalConfig = { portalColor: '#FF5733' };
|
||||
|
||||
updateThemeStyles('dark');
|
||||
|
||||
const expectedPortalBgStyle =
|
||||
'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #FF5733 20%, black)';
|
||||
const expectedGradientStyle =
|
||||
'background-image: linear-gradient(to bottom, transparent, #151718)';
|
||||
|
||||
expect(mockPortalDiv.classList.contains('dark')).toBe(true);
|
||||
expect(mockPortalBgDiv.getAttribute('style')).toBe(expectedPortalBgStyle);
|
||||
expect(mockPortalBgGradientDiv.getAttribute('style')).toBe(
|
||||
expectedGradientStyle
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#initializeTheme', () => {
|
||||
let mockPortalDiv;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPortalDiv = document.createElement('div');
|
||||
mockPortalDiv.id = 'portal';
|
||||
document.body.appendChild(mockPortalDiv);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('updates theme based on system preferences', () => {
|
||||
const mediaQueryList = {
|
||||
matches: true,
|
||||
addEventListener: jest.fn(),
|
||||
};
|
||||
window.matchMedia = jest.fn().mockReturnValue(mediaQueryList);
|
||||
window.portalConfig = { theme: 'system' };
|
||||
|
||||
InitializationHelpers.initializeTheme();
|
||||
|
||||
expect(mediaQueryList.addEventListener).toBeCalledWith(
|
||||
'change',
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(mockPortalDiv.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not update theme if themeFromServer is not "system"', () => {
|
||||
const mediaQueryList = {
|
||||
matches: true,
|
||||
addEventListener: jest.fn(),
|
||||
};
|
||||
window.matchMedia = jest.fn().mockReturnValue(mediaQueryList);
|
||||
window.portalConfig = { theme: 'dark' };
|
||||
|
||||
InitializationHelpers.initializeTheme();
|
||||
|
||||
expect(mediaQueryList.addEventListener).not.toBeCalled();
|
||||
expect(mockPortalDiv.classList.contains('dark')).toBe(false);
|
||||
expect(mockPortalDiv.classList.contains('light')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,8 @@ By default, it renders:
|
||||
<script>
|
||||
window.portalConfig = {
|
||||
portalSlug: '<%= @portal.slug %>',
|
||||
portalColor: '<%= @portal.color %>',
|
||||
theme: '<%= @theme %>',
|
||||
localeCode: '<%= @locale %>',
|
||||
searchTranslations: {
|
||||
searchPlaceholder: '<%= I18n.t('public_portal.search.search_placeholder') %>',
|
||||
|
||||
Reference in New Issue
Block a user