diff --git a/scss/_utilities_baseline-grid.scss b/scss/_utilities_baseline-grid.scss index 6b59470b28..32bc5e6ed1 100644 --- a/scss/_utilities_baseline-grid.scss +++ b/scss/_utilities_baseline-grid.scss @@ -34,19 +34,4 @@ } } // stylelint-enable selector-max-type - - .u-baseline-grid__toggle { - bottom: $spv--x-large; - color: $colors--light-theme--text-default; // Force light theme colour because of baseline grid background - position: fixed; - right: $sp-unit * 3; - z-index: 201; - } - - // hide the theme toggle in Percy - @media only percy { - .u-baseline-grid__toggle { - visibility: hidden !important; - } - } } diff --git a/scss/_utilities_theme-toggle.scss b/scss/_utilities_theme-toggle.scss deleted file mode 100644 index f1dfad7bbd..0000000000 --- a/scss/_utilities_theme-toggle.scss +++ /dev/null @@ -1,39 +0,0 @@ -@import 'settings'; - -@mixin vf-u-theme-toggle { - .u-theme-toggle { - bottom: $spv--x-large; - position: fixed; - right: $sp-unit * 30; - z-index: 100; - - .u-theme-toggle__button { - margin: 0; - - &:not(:last-child) { - border-right: 0; - } - } - - .u-theme-toggle__dark { - background-color: $colors--dark-theme--background-default; - color: $colors--dark-theme--text-default; - } - - .u-theme-toggle__light { - background-color: $colors--light-theme--background-default; - color: $colors--light-theme--text-default; - } - .u-theme-toggle__paper { - background-color: $color-paper; - color: $color-x-dark; - } - } - - // hide the theme toggle in Percy - @media only percy { - .u-theme-toggle { - visibility: hidden !important; - } - } -} diff --git a/scss/_vanilla.scss b/scss/_vanilla.scss index e290cf0e6b..90ef9438a4 100644 --- a/scss/_vanilla.scss +++ b/scss/_vanilla.scss @@ -89,7 +89,6 @@ @import 'utilities_no-print'; @import 'utilities_text-max-width'; @import 'utilities_text-figures'; -@import 'utilities_theme-toggle'; // Include all the CSS @mixin vanilla { @@ -183,5 +182,4 @@ @include vf-u-no-print; @include vf-u-text-max-width; @include vf-u-text-figures; - @include vf-u-theme-toggle; } diff --git a/scss/docs/example.scss b/scss/docs/example.scss index 512590db1a..dfaedf7309 100644 --- a/scss/docs/example.scss +++ b/scss/docs/example.scss @@ -2,10 +2,35 @@ @import '../vanilla'; @include vf-u-baseline-grid; -@include vf-u-theme-toggle; +@include vf-b-button; +@include vf-p-segmented-control; +@include vf-p-forms-inline; // stylelint-disable selector-max-type -- examples can use type selectors body { margin: 1rem; } // stylelint-enable selector-max-type + +.p-example-controls { + @include vf-transition($property: #{transform, box-shadow, visibility}, $duration: snap); + align-items: center; + background-color: var(--vf-color-background-alt); + bottom: 0; + box-shadow: $box-shadow--deep; + display: flex; + flex-flow: row wrap; + gap: $sp-unit; + justify-content: space-between; + left: 0; + padding: $spv--x-small $sph--x-small; + position: fixed; + right: 0; + + // Above *all* other elements. + z-index: 1000000; + + @media only percy { + visibility: hidden !important; + } +} diff --git a/templates/static/js/example-tools.js b/templates/static/js/example-tools.js index 993944c920..98f763dc5d 100644 --- a/templates/static/js/example-tools.js +++ b/templates/static/js/example-tools.js @@ -1,3 +1,8 @@ +const SUPPORTED_THEMES = ['light', 'dark', 'paper']; +const [DEFAULT_COLOR_THEME] = SUPPORTED_THEMES; +const COLOR_THEME_QUERY_PARAM_NAME = 'theme'; +var activeTheme = DEFAULT_COLOR_THEME; + (function () { function inIframe() { try { @@ -13,51 +18,154 @@ return temp.content; } + /** + * Gets the current query parameters + * @returns {URLSearchParams} + */ + function getQueryParameters() { + return new URLSearchParams(window.location.search); + } + + /** + * Sets the query parameter value of `key` to `value` + * @param {String} key + * @param {String} value + * @param {Boolean} reload Whether to cause a page load after updating the query parameter + * @returns {URLSearchParams} Query parameters after update + */ + function setQueryParameter(key, value, reload = false) { + var currentQueryParams = getQueryParameters(); + + if (reload && currentQueryParams.get(key) !== value) { + currentQueryParams.set(key, value); + window.location.search = currentQueryParams.toString(); + } else { + var url = new URL(window.location.href); + if (value) { + url.searchParams.set(key, value); + } else { + url.searchParams.delete(key); + } + if (window.location.href !== url.toString()) { + window.history.replaceState(null, null, url.toString()); + } + } + return getQueryParameters(); + } + + /** + * Converts a theme name to its document class name, used for applying that color theme to the body + * @param {String} themeName + * @returns {string} + */ + function convertThemeNameToClassName(themeName) { + return `is-${themeName}`; + } + + /** + * Converts a theme name to its JS toggler identifier name, used for targeting it with JS events + * @param {String} themeName + * @returns {string} + */ + function convertThemeNameToButtonIdentifier(themeName) { + return `js-${themeName}-theme-toggle`; + } + + /** + * Converts a string to titlecase (first letter capitalized & subsequent letters lowercase) + * @param {String} str + * @returns {string} + */ + function titleCase(str) { + return `${str.charAt(0).toUpperCase()}${str.slice(1).toLowerCase()}`; + } + + /** + * Sets a color theme as active; removes all other color themes from active status + * @param {String} themeToSelect + */ + function selectColorTheme(themeToSelect) { + // Apply the color theme to the document body + document.body.classList.add(convertThemeNameToClassName(themeToSelect)); + // Remove the old color theme from the document body + if (themeToSelect !== activeTheme) document.body.classList.remove(convertThemeNameToClassName(activeTheme)); + + // Update address bar to reflect the newly selected color theme + setQueryParameter(COLOR_THEME_QUERY_PARAM_NAME, themeToSelect.toLowerCase()); + + // Update theme selector button states to reflect which one is currently active + var themeButtonToSelect = document.getElementById(convertThemeNameToButtonIdentifier(themeToSelect)); + themeButtonToSelect?.setAttribute('aria-selected', 'true'); + if (activeTheme) { + var themeButtonToDeselect = document.getElementById(convertThemeNameToButtonIdentifier(activeTheme)); + themeButtonToDeselect?.setAttribute('aria-selected', 'false'); + } + + activeTheme = themeToSelect; + } + if (!inIframe()) { document.documentElement.classList.add('u-baseline-grid'); document.addEventListener('DOMContentLoaded', function () { var body = document.body; - var controls = fragmentFromString( - '
', - ); - var themes = fragmentFromString( - '
', - ); + var controls = document.createElement('div'); + controls.classList.add('p-example-controls', 'p-form'); + var queryParameters = getQueryParameters(); + var requestedTheme = queryParameters.get(COLOR_THEME_QUERY_PARAM_NAME); + if (SHOW_THEME_SWITCH) { + // Some examples (i.e., button / dark) are pre-themed by their jinja template. + // if this is the case we don't modify the body class (it's already set); we just mark that theme as active. + var preExistingClassFromTemplate = SUPPORTED_THEMES.find((themeName) => body.classList.contains(convertThemeNameToClassName(themeName))); - body.appendChild(themes); - body.appendChild(controls); + if (preExistingClassFromTemplate) { + activeTheme = preExistingClassFromTemplate; + } else if (!requestedTheme) { + // No template-defined theme & no query-param-requested theme; fallback to default + selectColorTheme(DEFAULT_COLOR_THEME); + } - var toggle = document.querySelector('.js-baseline-toggle'); - toggle.addEventListener('click', function (event) { - body.classList.toggle('u-baseline-grid'); - }); + if (requestedTheme) { + if (SUPPORTED_THEMES.includes(requestedTheme)) { + selectColorTheme(requestedTheme); + } else { + // Query param used to request a theme that is not supported + selectColorTheme(DEFAULT_COLOR_THEME); + } + } - var themeToggles = document.querySelector('.u-theme-toggle'); + if (SUPPORTED_THEMES?.length > 1) { + var themeSwitcherControls = SUPPORTED_THEMES.map( + (themeName) => + ``, + ); + var themeSwitcherSegmentedControl = fragmentFromString( + `
${themeSwitcherControls.join('')}
`, + ); - if (!SHOW_THEME_SWITCH) { - themeToggles.classList.add('u-hide'); + controls.appendChild(themeSwitcherSegmentedControl); + } + } else if (requestedTheme) { + setQueryParameter(COLOR_THEME_QUERY_PARAM_NAME, null); } + var baselineGridControl = fragmentFromString( + '
', + ); + controls.appendChild(baselineGridControl); - var darkTheme = document.querySelector('.js-dark-theme-toggle'); - darkTheme.addEventListener('click', function (event) { - body.classList.add('is-dark'); - body.classList.remove('is-paper'); - body.classList.remove('is-light'); - }); + body.appendChild(controls); - var lightTheme = document.querySelector('.js-light-theme-toggle'); - lightTheme.addEventListener('click', function (event) { - body.classList.add('is-light'); - body.classList.remove('is-dark'); - body.classList.remove('is-paper'); + // Below code relies on the controls already existing in the DOM, so must come after `body.appendChild`. + var themeToggleButtons = document.querySelectorAll('.p-theme-toggle__button'); + themeToggleButtons.forEach((themeToggleButton) => { + themeToggleButton.addEventListener('click', () => { + selectColorTheme(themeToggleButton.getAttribute('data-color-theme-name')); + }); }); - var paperTheme = document.querySelector('.js-paper-theme-toggle'); - paperTheme.addEventListener('click', function (event) { - body.classList.add('is-paper'); - body.classList.remove('is-dark'); - body.classList.remove('is-light'); + var toggle = document.querySelector('.js-baseline-toggle'); + toggle.addEventListener('click', function (event) { + body.classList.toggle('u-baseline-grid'); }); }); } diff --git a/webapp/app.py b/webapp/app.py index 1fb9de765b..1908a167c2 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -319,4 +319,4 @@ def contribute_index(): document_template="/_layouts/docs_discourse.html", url_prefix="/design", ) -discourse_docs.init_app(app) +discourse_docs.init_app(app) \ No newline at end of file