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 PublicArticleSearch from './components/PublicArticleSearch.vue';
|
||||||
import TableOfContents from './components/TableOfContents.vue';
|
import TableOfContents from './components/TableOfContents.vue';
|
||||||
|
|
||||||
export const getHeadingsfromTheArticle = () => {
|
export const getHeadingsFromTheArticle = () => {
|
||||||
const rows = [];
|
const rows = [];
|
||||||
const articleElement = document.getElementById('cw-article-content');
|
const articleElement = document.getElementById('cw-article-content');
|
||||||
articleElement.querySelectorAll('h1, h2, h3').forEach(element => {
|
articleElement.querySelectorAll('h1, h2, h3').forEach(element => {
|
||||||
@@ -21,6 +21,53 @@ export const getHeadingsfromTheArticle = () => {
|
|||||||
return rows;
|
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 = {
|
export const InitializationHelpers = {
|
||||||
navigateToLocalePage: () => {
|
navigateToLocalePage: () => {
|
||||||
const allLocaleSwitcher = document.querySelector('.locale-switcher');
|
const allLocaleSwitcher = document.querySelector('.locale-switcher');
|
||||||
@@ -36,7 +83,7 @@ export const InitializationHelpers = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
initalizeSearch: () => {
|
initializeSearch: () => {
|
||||||
const isSearchContainerAvailable = document.querySelector('#search-wrap');
|
const isSearchContainerAvailable = document.querySelector('#search-wrap');
|
||||||
if (isSearchContainerAvailable) {
|
if (isSearchContainerAvailable) {
|
||||||
new Vue({
|
new Vue({
|
||||||
@@ -51,7 +98,7 @@ export const InitializationHelpers = {
|
|||||||
if (isOnArticlePage) {
|
if (isOnArticlePage) {
|
||||||
new Vue({
|
new Vue({
|
||||||
components: { TableOfContents },
|
components: { TableOfContents },
|
||||||
data: { rows: getHeadingsfromTheArticle() },
|
data: { rows: getHeadingsFromTheArticle() },
|
||||||
template: '<table-of-contents :rows="rows" />',
|
template: '<table-of-contents :rows="rows" />',
|
||||||
}).$mount('#cw-hc-toc');
|
}).$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: () => {
|
initialize: () => {
|
||||||
if (window.portalConfig.isPlainLayoutEnabled === 'true') {
|
if (window.portalConfig.isPlainLayoutEnabled === 'true') {
|
||||||
InitializationHelpers.appendPlainParamToURLs();
|
InitializationHelpers.appendPlainParamToURLs();
|
||||||
} else {
|
} else {
|
||||||
InitializationHelpers.navigateToLocalePage();
|
InitializationHelpers.navigateToLocalePage();
|
||||||
InitializationHelpers.initalizeSearch();
|
InitializationHelpers.initializeSearch();
|
||||||
InitializationHelpers.initializeTableOfContents();
|
InitializationHelpers.initializeTableOfContents();
|
||||||
|
// InitializationHelpers.initializeTheme();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { InitializationHelpers } from '../portalHelpers';
|
import {
|
||||||
|
InitializationHelpers,
|
||||||
|
generatePortalBgColor,
|
||||||
|
generatePortalBg,
|
||||||
|
generateGradientToBottom,
|
||||||
|
setPortalStyles,
|
||||||
|
setPortalClass,
|
||||||
|
updateThemeStyles,
|
||||||
|
} from '../portalHelpers';
|
||||||
|
|
||||||
describe('#navigateToLocalePage', () => {
|
describe('#navigateToLocalePage', () => {
|
||||||
it('returns correct cookie name', () => {
|
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>
|
<script>
|
||||||
window.portalConfig = {
|
window.portalConfig = {
|
||||||
portalSlug: '<%= @portal.slug %>',
|
portalSlug: '<%= @portal.slug %>',
|
||||||
|
portalColor: '<%= @portal.color %>',
|
||||||
|
theme: '<%= @theme %>',
|
||||||
localeCode: '<%= @locale %>',
|
localeCode: '<%= @locale %>',
|
||||||
searchTranslations: {
|
searchTranslations: {
|
||||||
searchPlaceholder: '<%= I18n.t('public_portal.search.search_placeholder') %>',
|
searchPlaceholder: '<%= I18n.t('public_portal.search.search_placeholder') %>',
|
||||||
|
|||||||
Reference in New Issue
Block a user