- 
                Notifications
    
You must be signed in to change notification settings  - Fork 51
 
[WIP-04] [Project Solar / Phase 1 / Showcase] Add support for theming and theme-switching to the showcase #3240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: project-solar/phase-1/HDS-5505_components/modes-css-compilation
Are you sure you want to change the base?
Changes from all commits
2014947
              347d7bf
              834ac51
              be3f646
              ce2a6b6
              cb7d14f
              34775fa
              189dc4d
              d9f7cb9
              4f351ed
              7d0e323
              612d055
              4f7f2e4
              aa7cd45
              b5a1228
              ecee808
              efbe4d2
              20e97f8
              1286d3a
              d089466
              2ca0816
              505160d
              bff955b
              b09a8e3
              7c4b794
              e1e593d
              cec3389
              068fff3
              8a92d04
              a6c8fe2
              a9e24d7
              2c6358f
              036be94
              2b2d7df
              07ab36f
              4c9f2e3
              ff0c674
              196c05d
              28c8bac
              fb75198
              656eb31
              a3e75b4
              40574f4
              98b147d
              0095d99
              1d7eb41
              a03ab42
              33d1b14
              888e15a
              b2c6616
              e3bfc6c
              d48811f
              c7ca011
              964ac32
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| {{! | ||
| Copyright (c) HashiCorp, Inc. | ||
| SPDX-License-Identifier: MPL-2.0 | ||
| }} | ||
| 
     | 
||
| {{! | ||
| ------------------------------------------------------------------------------------------ | ||
| IMPORTANT: this is a temporary implementation, while we wait for the design specifications | ||
| ------------------------------------------------------------------------------------------ | ||
| }} | ||
| 
     | 
||
| <Hds::Dropdown | ||
| @enableCollisionDetection={{true}} | ||
| @matchToggleWidth={{@toggleIsFullWidth}} | ||
| class="hds-theme-switcher-control" | ||
| ...attributes | ||
| as |D| | ||
| > | ||
| <D.ToggleButton | ||
| @color="secondary" | ||
| @size={{this.toggleSize}} | ||
| @isFullWidth={{this.toggleIsFullWidth}} | ||
| @text={{this.toggleContent.label}} | ||
| @icon={{this.toggleContent.icon}} | ||
| /> | ||
| {{#each-in this._options as |key data|}} | ||
| <D.Interactive @icon={{data.icon}} {{on "click" (fn this.onSelectTheme data.theme)}}>{{data.label}}</D.Interactive> | ||
| {{/each-in}} | ||
| </Hds::Dropdown> | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /** | ||
| * Copyright (c) HashiCorp, Inc. | ||
| * SPDX-License-Identifier: MPL-2.0 | ||
| */ | ||
| 
     | 
||
| // ------------------------------------------------------------------------------------------ | ||
| // IMPORTANT: this is a temporary implementation, while we wait for the design specifications | ||
| // ------------------------------------------------------------------------------------------ | ||
| 
     | 
||
| import Component from '@glimmer/component'; | ||
| import { inject as service } from '@ember/service'; | ||
| import { action } from '@ember/object'; | ||
| 
     | 
||
| import type { HdsDropdownSignature } from '../dropdown/index.ts'; | ||
| import type { HdsDropdownToggleButtonSignature } from '../dropdown/toggle/button.ts'; | ||
| import type { HdsIconSignature } from '../icon/index.ts'; | ||
| import type HdsThemingService from '../../../services/hds-theming.ts'; | ||
| import type { | ||
| HdsThemes, | ||
| OnSetThemeCallback, | ||
| } from '../../../services/hds-theming.ts'; | ||
| 
     | 
||
| interface ThemeOption { | ||
| theme: HdsThemes | undefined; | ||
| icon: HdsIconSignature['Args']['name']; | ||
| label: string; | ||
| } | ||
| 
     | 
||
| const OPTIONS: Record<HdsThemes, ThemeOption> = { | ||
| system: { theme: 'system', icon: 'monitor', label: 'System' }, | ||
| light: { theme: 'light', icon: 'sun', label: 'Light' }, | ||
| dark: { theme: 'dark', icon: 'moon', label: 'Dark' }, | ||
| }; | ||
| 
     | 
||
| interface HdsThemeSwitcherSignature { | ||
| Args: { | ||
| toggleSize?: HdsDropdownToggleButtonSignature['Args']['size']; | ||
| toggleIsFullWidth?: HdsDropdownToggleButtonSignature['Args']['isFullWidth']; | ||
| hasSystemOption?: boolean; | ||
| onSetTheme?: OnSetThemeCallback; | ||
| }; | ||
| Element: HdsDropdownSignature['Element']; | ||
| } | ||
| 
     | 
||
| export default class HdsThemeSwitcher extends Component<HdsThemeSwitcherSignature> { | ||
| @service declare readonly hdsTheming: HdsThemingService; | ||
| 
     | 
||
| get toggleSize() { | ||
| return this.args.toggleSize ?? 'small'; | ||
| } | ||
| 
     | 
||
| get toggleIsFullWidth() { | ||
| return this.args.toggleIsFullWidth ?? false; | ||
| } | ||
| 
     | 
||
| get toggleContent() { | ||
| if ( | ||
| (this.currentTheme === 'system' && this.hasSystemOption) || | ||
| this.currentTheme === 'light' || | ||
| this.currentTheme === 'dark' | ||
| ) { | ||
| return { | ||
| label: OPTIONS[this.currentTheme].label, | ||
| icon: OPTIONS[this.currentTheme].icon, | ||
| }; | ||
| } else { | ||
| return { label: 'Theme', icon: undefined }; | ||
| } | ||
| } | ||
| 
     | 
||
| get hasSystemOption() { | ||
| return this.args.hasSystemOption ?? true; | ||
| } | ||
| 
     | 
||
| get _options() { | ||
| const options: Partial<typeof OPTIONS> = { ...OPTIONS }; | ||
| 
     | 
||
| if (!this.hasSystemOption) { | ||
| delete options.system; | ||
| } | ||
| 
     | 
||
| return options; | ||
| } | ||
| 
     | 
||
| get currentTheme() { | ||
| // we get the theme from the global service | ||
| return this.hdsTheming.currentTheme; | ||
| } | ||
| 
     | 
||
| @action | ||
| onSelectTheme(theme: HdsThemes | undefined): void { | ||
| // we set the theme in the global service (and provide an optional user-defined callback) | ||
| this.hdsTheming.setTheme({ theme, onSetTheme: this.args.onSetTheme }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -4,3 +4,5 @@ | |
| */ | ||
| 
     | 
||
| // This file is used to expose public services | ||
| 
     | 
||
| export * from './services/hds-theming.ts'; | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| import Service from '@ember/service'; | ||
| import { tracked } from '@glimmer/tracking'; | ||
| 
     | 
||
| export enum HdsThemeValues { | ||
| // system settings (prefers-color-scheme) | ||
| System = 'system', | ||
| // user settings for dark/light | ||
| Light = 'light', | ||
| Dark = 'dark', | ||
| } | ||
| 
     | 
||
| enum HdsModesBaseValues { | ||
| Hds = 'hds', // TODO understand if it should be `default` | ||
                
      
                  didoo marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
        There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If consumers don't do anything, the "Hds" theme is the one they should get so that would be a "default". Is that what you mean? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess you just meant the naming. This could be called "default", "legacy", "standard" or something else along those lines to make it clearer vs. calling it "hds".  | 
||
| } | ||
| 
     | 
||
| enum HdsModesLightValues { | ||
| CdsG0 = 'cds-g0', | ||
| CdsG10 = 'cds-g10', | ||
| } | ||
| 
     | 
||
| enum HdsModesDarkValues { | ||
| CdsG90 = 'cds-g90', | ||
| CdsG100 = 'cds-g100', | ||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still don't really understand the purpose of having multiple Light & Dark value options. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll explain in today's sync There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @KristinLBradley I just noticed that I didn't explain this in the sync. Essentially, since Carbon supports 4 themes/modes, we should do the same (in case some consumer, for reasons we didn't anticipate, may need to use a different combination that the standard  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I understand we need to or should follow how it's done in Carbon, but I find it confusing.  | 
||
| } | ||
| 
     | 
||
| export enum HdsCssSelectorsValues { | ||
| Data = 'data', | ||
| Class = 'class', | ||
| } | ||
| 
     | 
||
| export type HdsThemes = `${HdsThemeValues}`; | ||
| export type HdsModes = | ||
| | `${HdsModesBaseValues}` | ||
| | `${HdsModesLightValues}` | ||
| | `${HdsModesDarkValues}`; | ||
| export type HdsModesLight = `${HdsModesLightValues}`; | ||
| export type HdsModesDark = `${HdsModesDarkValues}`; | ||
| export type HdsCssSelectors = `${HdsCssSelectorsValues}`; | ||
| 
     | 
||
| type HdsThemingOptions = { | ||
| lightTheme: HdsModesLight; | ||
| darkTheme: HdsModesDark; | ||
| cssSelector: HdsCssSelectors; | ||
| }; | ||
| 
     | 
||
| type SetThemeArgs = { | ||
| theme: HdsThemes | undefined; | ||
| options?: HdsThemingOptions; | ||
| onSetTheme?: OnSetThemeCallback; | ||
| }; | ||
| 
     | 
||
| export type OnSetThemeCallbackArgs = { | ||
| currentTheme: HdsThemes | undefined; | ||
| currentMode: HdsModes | undefined; | ||
| }; | ||
| 
     | 
||
| export type OnSetThemeCallback = (args: OnSetThemeCallbackArgs) => void; | ||
| 
     | 
||
| export const THEMES: HdsThemes[] = Object.values(HdsThemeValues); | ||
| export const MODES_LIGHT: HdsModesLight[] = Object.values(HdsModesLightValues); | ||
| export const MODES_DARK: HdsModesDark[] = Object.values(HdsModesDarkValues); | ||
| export const MODES: HdsModes[] = [ | ||
| ...Object.values(HdsModesBaseValues), | ||
| ...MODES_LIGHT, | ||
| ...MODES_DARK, | ||
| ]; | ||
| 
     | 
||
| export const HDS_THEMING_DATA_SELECTOR = 'data-hds-theme'; | ||
| export const HDS_THEMING_CLASS_SELECTOR_PREFIX = 'hds-theme'; | ||
| export const HDS_THEMING_CLASS_SELECTORS_LIST = [ | ||
| ...MODES_LIGHT, | ||
| ...MODES_DARK, | ||
| ].map((mode) => `${HDS_THEMING_CLASS_SELECTOR_PREFIX}-${mode}`); | ||
| 
     | 
||
| export const HDS_THEMING_LOCALSTORAGE_DATA = 'hds-theming-data'; | ||
| 
     | 
||
| export const DEFAULT_THEMING_OPTION_LIGHT_THEME = HdsModesLightValues.CdsG0; | ||
| export const DEFAULT_THEMING_OPTION_DARK_THEME = HdsModesDarkValues.CdsG100; | ||
| export const DEFAULT_THEMING_OPTION_CSS_SELECTOR = 'data'; | ||
| 
     | 
||
| export default class HdsThemingService extends Service { | ||
| @tracked _currentTheme: HdsThemes | undefined = undefined; | ||
| @tracked _currentMode: HdsModes | undefined = undefined; | ||
| @tracked _currentLightTheme: HdsModesLight = | ||
| DEFAULT_THEMING_OPTION_LIGHT_THEME; | ||
| @tracked _currentDarkTheme: HdsModesDark = DEFAULT_THEMING_OPTION_DARK_THEME; | ||
| @tracked _currentCssSelector: HdsCssSelectors = | ||
| DEFAULT_THEMING_OPTION_CSS_SELECTOR; | ||
| @tracked globalOnSetTheme: OnSetThemeCallback | undefined; | ||
| 
     | 
||
| initializeTheme() { | ||
| const rawStoredThemingData = localStorage.getItem( | ||
| HDS_THEMING_LOCALSTORAGE_DATA | ||
| ); | ||
| if (rawStoredThemingData !== null) { | ||
| const storedThemingData: unknown = JSON.parse(rawStoredThemingData); | ||
| if (storedThemingData) { | ||
| const { theme, options } = storedThemingData as { | ||
| theme: HdsThemes | undefined; | ||
| options: HdsThemingOptions; | ||
| }; | ||
| this.setTheme({ | ||
| theme, | ||
| options, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| 
     | 
||
| setTheme({ theme, options, onSetTheme }: SetThemeArgs) { | ||
| if (options !== undefined) { | ||
| // if we have new options, we override the current ones (`lightTheme` / `darkTheme` / `cssSelector`) | ||
| // these options can be used by consumers that want to customize how they apply theming | ||
| // (and used by the showcase for the custom theming / theme switching logic) | ||
| if ( | ||
| Object.hasOwn(options, 'lightTheme') && | ||
| Object.hasOwn(options, 'darkTheme') && | ||
| Object.hasOwn(options, 'cssSelector') | ||
| ) { | ||
| const { lightTheme, darkTheme, cssSelector } = options; | ||
| 
     | 
||
| this._currentLightTheme = lightTheme; | ||
| this._currentDarkTheme = darkTheme; | ||
| this._currentCssSelector = cssSelector; | ||
| } else { | ||
| // fallback if something goes wrong | ||
| this._currentLightTheme = DEFAULT_THEMING_OPTION_LIGHT_THEME; | ||
| this._currentDarkTheme = DEFAULT_THEMING_OPTION_DARK_THEME; | ||
| this._currentCssSelector = DEFAULT_THEMING_OPTION_CSS_SELECTOR; | ||
| } | ||
| } | ||
| 
     | 
||
| // set the current theme/mode (`currentTheme` / `currentMode`) | ||
| if ( | ||
| theme === undefined || // standard (no theming) | ||
| !THEMES.includes(theme) // handle possible errors | ||
| ) { | ||
| this._currentTheme = undefined; | ||
| this._currentMode = undefined; | ||
| } else if ( | ||
| theme === HdsThemeValues.System // system (prefers-color-scheme) | ||
| ) { | ||
| this._currentTheme = HdsThemeValues.System; | ||
| this._currentMode = undefined; | ||
| } else { | ||
| this._currentTheme = theme; | ||
| if (this._currentTheme === HdsThemeValues.Light) { | ||
| this._currentMode = this._currentLightTheme; | ||
| } | ||
| if (this._currentTheme === HdsThemeValues.Dark) { | ||
| this._currentMode = this._currentDarkTheme; | ||
| } | ||
| } | ||
| 
     | 
||
| // IMPORTANT: for this to work, it needs to be the HTML tag (it's the `:root` in CSS) | ||
| const rootElement = document.querySelector('html'); | ||
| 
     | 
||
| if (!rootElement) { | ||
| return; | ||
| } | ||
| // remove or update the CSS selectors applied to the root element (depending on the `theme` argument) | ||
| rootElement.removeAttribute(HDS_THEMING_DATA_SELECTOR); | ||
| rootElement.classList.remove(...HDS_THEMING_CLASS_SELECTORS_LIST); | ||
| if (this._currentMode !== undefined) { | ||
| if (this._currentCssSelector === 'data') { | ||
| rootElement.setAttribute(HDS_THEMING_DATA_SELECTOR, this._currentMode); | ||
| } else if (this._currentCssSelector === 'class') { | ||
| rootElement.classList.add( | ||
| `${HDS_THEMING_CLASS_SELECTOR_PREFIX}-${this._currentMode}` | ||
| ); | ||
| } | ||
| } | ||
| 
     | 
||
| // store the current theme and theming options in local storage (unless undefined) | ||
| localStorage.setItem( | ||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] There could be use cases where a consumer wants to handle on their own how they keep track of a user's theme. They could want to handle it through their own cookies or storage or some other means. It would be useful to have an argument like  A consumer could then use the  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
 Because of my point above I think it would be useful to keep the callback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the current implementation, what they could do would be to use the  The  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would using  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no,  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
 This was the main use case I was thinking to keep it for. It's good to have some way for consumers to listen for theme changes and   | 
||
| HDS_THEMING_LOCALSTORAGE_DATA, | ||
| JSON.stringify({ | ||
| theme: this._currentTheme, | ||
| options: { | ||
| lightTheme: this._currentLightTheme, | ||
| darkTheme: this._currentDarkTheme, | ||
| cssSelector: this._currentCssSelector, | ||
| }, | ||
| }) | ||
| ); | ||
| 
     | 
||
| // this is a general callback that can be defined globally (by extending the service) | ||
| if (this.globalOnSetTheme) { | ||
| this.globalOnSetTheme({ | ||
| currentTheme: this._currentTheme, | ||
| currentMode: this._currentMode, | ||
| }); | ||
| } | ||
| 
     | 
||
| // this is a "local" callback that can be defined "locally" (eg. in a theme switcher) | ||
| if (onSetTheme) { | ||
| onSetTheme({ | ||
| currentTheme: this._currentTheme, | ||
| currentMode: this._currentMode, | ||
| }); | ||
| } | ||
| } | ||
| 
     | 
||
| // getters used for reactivity in the components/services using this service | ||
| 
     | 
||
| get currentTheme(): HdsThemes | undefined { | ||
| return this._currentTheme; | ||
| } | ||
| 
     | 
||
| get currentMode(): HdsModes | undefined { | ||
| return this._currentMode; | ||
| } | ||
| 
     | 
||
| get currentLightTheme(): HdsModesLight { | ||
| return this._currentLightTheme ?? DEFAULT_THEMING_OPTION_LIGHT_THEME; | ||
| } | ||
| 
     | 
||
| get currentDarkTheme(): HdsModesDark { | ||
| return this._currentDarkTheme ?? DEFAULT_THEMING_OPTION_DARK_THEME; | ||
| } | ||
| 
     | 
||
| get currentCssSelector(): HdsCssSelectors { | ||
| return this._currentCssSelector ?? DEFAULT_THEMING_OPTION_CSS_SELECTOR; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.