From 51185df7eb9ae75f6fdb586ea5ad2ae13075d885 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Mon, 23 Sep 2024 14:12:49 -0700 Subject: [PATCH] [docs] Spike out contrast switchers + massive cleanup/refactor of src-docs theme context, prefer toggle for light/dark mode + remove defunct tour on language selector --- packages/eui/.storybook/decorator.tsx | 18 +++ packages/eui/.storybook/preview.tsx | 1 + .../guide_locale_selector.js | 40 ------ .../components/guide_locale_selector/index.js | 1 - .../guide_theme_selector.tsx | 128 +++++++++--------- .../src/components/with_theme/index.ts | 6 +- .../with_theme/language_selector.tsx | 66 ++------- .../components/with_theme/theme_context.tsx | 119 +++++++++++----- packages/eui/src-docs/src/index.js | 7 +- .../eui/src-docs/src/services/theme/theme.js | 7 +- .../eui/src-docs/src/views/app_context.js | 7 +- packages/eui/src/themes/themes.ts | 9 +- 12 files changed, 193 insertions(+), 216 deletions(-) delete mode 100644 packages/eui/src-docs/src/components/guide_locale_selector/guide_locale_selector.js delete mode 100644 packages/eui/src-docs/src/components/guide_locale_selector/index.js diff --git a/packages/eui/.storybook/decorator.tsx b/packages/eui/.storybook/decorator.tsx index 6b78a2e2353e..6c8dc3267edc 100644 --- a/packages/eui/.storybook/decorator.tsx +++ b/packages/eui/.storybook/decorator.tsx @@ -86,6 +86,13 @@ const storybookToolbarColorModes: Array< { value: 'dark', title: 'Dark mode', icon: 'circle' }, ]; +const storybookToolbarHighContrastMode: Array< + ToolbarDisplay & { value: boolean } +> = [ + { value: false, title: 'High contrast off', icon: 'circlehollow' }, + { value: true, title: 'High contrast on', icon: 'circle' }, +]; + const storybookToolbarWritingModes: Array< ToolbarDisplay & { value: WritingModes } > = [ @@ -112,6 +119,17 @@ export const euiProviderDecoratorGlobals: Preview['globalTypes'] = { dynamicTitle: true, }, }, + highContrastMode: { + description: 'High contrast mode for EuiProvider theme', + defaultValue: window?.matchMedia?.('(prefers-contrast: more)').matches + ? true + : false, + toolbar: { + title: 'Contrast mode', + items: storybookToolbarHighContrastMode, + dynamicTitle: true, + }, + }, writingMode: { description: 'Writing mode for testing logical property directions', defaultValue: 'ltr', diff --git a/packages/eui/.storybook/preview.tsx b/packages/eui/.storybook/preview.tsx index c34435f50320..d230e00baeed 100644 --- a/packages/eui/.storybook/preview.tsx +++ b/packages/eui/.storybook/preview.tsx @@ -46,6 +46,7 @@ const preview: Preview = { (Story, context) => ( diff --git a/packages/eui/src-docs/src/components/guide_locale_selector/guide_locale_selector.js b/packages/eui/src-docs/src/components/guide_locale_selector/guide_locale_selector.js deleted file mode 100644 index 4b53fc35199a..000000000000 --- a/packages/eui/src-docs/src/components/guide_locale_selector/guide_locale_selector.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import moment from 'moment'; -import { translateUsingPseudoLocale } from '../../../src/services/string/pseudo_locale_translator'; - -// For testing/demoing EuiDatePicker, process moment's `en` locale config into a babelfished version -const enConfig = moment.localeData('en')._config; -moment.defineLocale('en-xa', { - ...enConfig, - months: enConfig.months.map(translateUsingPseudoLocale), - monthsShort: enConfig.monthsShort.map(translateUsingPseudoLocale), - weekdays: enConfig.weekdays.map(translateUsingPseudoLocale), - weekdaysMin: enConfig.weekdaysMin.map(translateUsingPseudoLocale), - weekdaysShort: enConfig.weekdaysShort.map(translateUsingPseudoLocale), -}); -// Reset default moment locale after using `defineLocale` -moment.locale('en'); - -import { EuiSwitch, EuiToolTip } from '../../../../src/components'; - -export const GuideLocaleSelector = ({ selectedLocale, onToggleLocale }) => { - return ( - - - onToggleLocale(selectedLocale === 'en' ? 'en-xa' : 'en') - } - /> - - ); -}; - -GuideLocaleSelector.propTypes = { - onToggleLocale: PropTypes.func.isRequired, - selectedLocale: PropTypes.string.isRequired, -}; diff --git a/packages/eui/src-docs/src/components/guide_locale_selector/index.js b/packages/eui/src-docs/src/components/guide_locale_selector/index.js deleted file mode 100644 index 7f4a4fbcfb41..000000000000 --- a/packages/eui/src-docs/src/components/guide_locale_selector/index.js +++ /dev/null @@ -1 +0,0 @@ -export { GuideLocaleSelector } from './guide_locale_selector'; diff --git a/packages/eui/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx b/packages/eui/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx index bc9d4449b9df..d474aad5de0a 100644 --- a/packages/eui/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx +++ b/packages/eui/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx @@ -1,78 +1,37 @@ /* eslint-disable no-restricted-globals */ -import React, { useState } from 'react'; +import React, { useState, useContext } from 'react'; -import { - EuiThemeProvider, - useEuiTheme, - useIsWithinBreakpoints, -} from '../../../../src/services'; +import { EuiThemeProvider, useEuiTheme } from '../../../../src/services'; import { EUI_THEME, EUI_THEMES } from '../../../../src/themes'; import { ThemeContext } from '../with_theme'; -// @ts-ignore Not TS -import { GuideLocaleSelector } from '../guide_locale_selector'; import { EuiPopover, EuiHorizontalRule, EuiButton, EuiContextMenuPanel, EuiContextMenuItem, + EuiSwitch, + EuiSwitchEvent, } from '../../../../src/components'; type GuideThemeSelectorProps = { - onToggleLocale: () => {}; + onToggleLocale: Function; selectedLocale: string; - context?: any; }; export const GuideThemeSelector: React.FunctionComponent< GuideThemeSelectorProps -> = ({ ...rest }) => { - return ( - - {(context) => } - - ); -}; - -const GuideThemeSelectorComponent: React.FunctionComponent< - GuideThemeSelectorProps -> = ({ context, onToggleLocale, selectedLocale }) => { - const isMobileSize = useIsWithinBreakpoints(['xs', 's']); - const [isPopoverOpen, setPopover] = useState(false); - - const onButtonClick = () => { - setPopover(!isPopoverOpen); - }; - - const closePopover = () => { - setPopover(false); - }; - - const systemColorMode = useEuiTheme().colorMode.toLowerCase(); +> = ({ onToggleLocale, selectedLocale }) => { + const context = useContext(ThemeContext); + const euiThemeContext = useEuiTheme(); + const colorMode = context.colorMode ?? euiThemeContext.colorMode; const currentTheme: EUI_THEME = - EUI_THEMES.find( - (theme) => theme.value === (context.theme ?? systemColorMode) - ) || EUI_THEMES[0]; + EUI_THEMES.find((theme) => theme.value === context.theme) || EUI_THEMES[0]; - const getIconType = (value: EUI_THEME['value']) => { - return value === currentTheme.value ? 'check' : 'empty'; - }; - - const items = EUI_THEMES.map((theme) => { - return ( - { - closePopover(); - context.changeTheme(theme.value); - }} - > - {theme.text} - - ); - }); + const [isPopoverOpen, setPopover] = useState(false); + const onButtonClick = () => setPopover(!isPopoverOpen); + const closePopover = () => setPopover(false); const button = ( @@ -84,11 +43,34 @@ const GuideThemeSelectorComponent: React.FunctionComponent< minWidth={0} onClick={onButtonClick} > - {isMobileSize ? 'Theme' : currentTheme.text} + Theme ); + const toggles = [ + { + label: 'Dark mode', + checked: colorMode.toLowerCase() === 'dark', + onChange: (e: EuiSwitchEvent) => + context.setContext({ + colorMode: e.target.checked ? 'DARK' : 'LIGHT', + }), + }, + { + label: 'High contrast', + checked: context.highContrastMode ?? euiThemeContext.highContrastMode, + onChange: (e: EuiSwitchEvent) => + context.setContext({ highContrastMode: e.target.checked }), + }, + location.host.includes('803') && { + label: 'i18n testing', + checked: selectedLocale === 'en-xa', + onChange: (e: EuiSwitchEvent) => + onToggleLocale(e.target.checked ? 'en-xa' : 'en'), + }, + ]; + return ( - - {location.host.includes('803') && ( - <> - -
- { + return ( + { + closePopover(); + context.setContext({ theme: theme.value }); + }} + > + {theme.text} + + ); + })} + /> + + {toggles.map((item) => + item ? ( +
({ padding: euiTheme.size.s })}> +
- + ) : null )} ); diff --git a/packages/eui/src-docs/src/components/with_theme/index.ts b/packages/eui/src-docs/src/components/with_theme/index.ts index 46d6e96d7c14..7cc4f1146276 100644 --- a/packages/eui/src-docs/src/components/with_theme/index.ts +++ b/packages/eui/src-docs/src/components/with_theme/index.ts @@ -1,2 +1,6 @@ -export { ThemeProvider, ThemeContext } from './theme_context'; +export { + ThemeProvider, + ThemeContext, + type ThemeContextType, +} from './theme_context'; export { LanguageSelector } from './language_selector'; diff --git a/packages/eui/src-docs/src/components/with_theme/language_selector.tsx b/packages/eui/src-docs/src/components/with_theme/language_selector.tsx index 45931a8d423b..aea26bd226bb 100644 --- a/packages/eui/src-docs/src/components/with_theme/language_selector.tsx +++ b/packages/eui/src-docs/src/components/with_theme/language_selector.tsx @@ -1,12 +1,6 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext } from 'react'; -import { - EuiButtonGroup, - EuiIcon, - EuiLink, - EuiText, - EuiTourStep, -} from '../../../../src/components'; +import { EuiButtonGroup } from '../../../../src/components'; import { ThemeContext, @@ -14,62 +8,28 @@ import { THEME_LANGUAGES, } from './theme_context'; -const NOTIF_STORAGE_KEY = 'js_vs_sass_notification'; -const NOTIF_STORAGE_VALUE = 'dismissed'; - export const LanguageSelector = ({ onChange, - showTour = false, }: { onChange?: (id: string) => void; - showTour?: boolean; }) => { const themeContext = useContext(ThemeContext); const toggleIdSelected = themeContext.themeLanguage; const onLanguageChange = (optionId: string) => { - themeContext.changeThemeLanguage(optionId as THEME_LANGUAGES['id']); + themeContext.setContext({ + themeLanguage: optionId as THEME_LANGUAGES['id'], + }); onChange?.(optionId); - setTourIsOpen(false); - localStorage.setItem(NOTIF_STORAGE_KEY, NOTIF_STORAGE_VALUE); - }; - - const [isTourOpen, setTourIsOpen] = useState( - localStorage.getItem(NOTIF_STORAGE_KEY) === NOTIF_STORAGE_VALUE - ? false - : showTour - ); - - const onTourDismiss = () => { - setTourIsOpen(false); - localStorage.setItem(NOTIF_STORAGE_KEY, NOTIF_STORAGE_VALUE); }; return ( - -

Select your preferred styling language with this toggle button.

- - } - isStepOpen={isTourOpen} - onFinish={onTourDismiss} - step={1} - stepsTotal={1} - title={ - <> -   Theming update - - } - footerAction={Got it!} - > - onLanguageChange(id)} - /> -
+ onLanguageChange(id)} + /> ); }; diff --git a/packages/eui/src-docs/src/components/with_theme/theme_context.tsx b/packages/eui/src-docs/src/components/with_theme/theme_context.tsx index df4e642d0208..0403dd01f640 100644 --- a/packages/eui/src-docs/src/components/with_theme/theme_context.tsx +++ b/packages/eui/src-docs/src/components/with_theme/theme_context.tsx @@ -1,17 +1,37 @@ import React, { PropsWithChildren } from 'react'; -import { EUI_THEMES, EUI_THEME } from '../../../../src/themes'; +import { + EUI_THEMES, + EUI_THEME, + AMSTERDAM_NAME_KEY, +} from '../../../../src/themes'; +import { EuiThemeColorModeStandard } from '../../../../src/services'; // @ts-ignore importing from a JS file -import { applyTheme } from '../../services'; +import { applyTheme, registerTheme } from '../../services'; + +// @ts-ignore Sass +import amsterdamThemeLight from '../../theme_light.scss'; +// @ts-ignore Sass +import amsterdamThemeDark from '../../theme_dark.scss'; +const THEME_CSS_MAP = { + [AMSTERDAM_NAME_KEY]: { + LIGHT: amsterdamThemeLight, + DARK: amsterdamThemeDark, + }, +}; +EUI_THEMES.forEach((theme) => { + registerTheme( + theme.value, + THEME_CSS_MAP[theme.value as keyof typeof THEME_CSS_MAP] + ); +}); +const THEME_NAMES = EUI_THEMES.map(({ value }) => value); -const STYLE_STORAGE_KEY = 'js_vs_sass_preference'; const URL_PARAM_KEY = 'themeLanguage'; - export type THEME_LANGUAGES = { id: 'language--js' | 'language--sass'; label: string; title: string; }; - export const theme_languages: THEME_LANGUAGES[] = [ { id: 'language--js', @@ -24,26 +44,27 @@ export const theme_languages: THEME_LANGUAGES[] = [ title: 'Language selector: Sass', }, ]; - -const THEME_NAMES = EUI_THEMES.map(({ value }) => value); const THEME_LANGS = theme_languages.map(({ id }) => id); -const defaultState = { - themeLanguage: THEME_LANGS[0], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - changeThemeLanguage: (language: THEME_LANGUAGES['id']) => {}, - theme: undefined as string | undefined, - changeTheme: (themeValue: EUI_THEME['value']) => { - applyTheme(themeValue); - }, -}; - -interface State { +export type ThemeContextType = { theme?: EUI_THEME['value']; + colorMode?: EuiThemeColorModeStandard; + highContrastMode?: boolean; themeLanguage: THEME_LANGUAGES['id']; -} + setContext: (context: Partial) => void; +}; +export const ThemeContext = React.createContext({ + theme: undefined, + colorMode: undefined, + highContrastMode: undefined, + themeLanguage: THEME_LANGS[0], + setContext: () => {}, +}); -export const ThemeContext = React.createContext(defaultState); +type State = Pick< + ThemeContextType, + 'theme' | 'colorMode' | 'highContrastMode' | 'themeLanguage' +>; export class ThemeProvider extends React.Component { constructor(props: object) { @@ -52,21 +73,51 @@ export class ThemeProvider extends React.Component { const theme = localStorage.getItem('theme') || undefined; applyTheme(theme && THEME_NAMES.includes(theme) ? theme : THEME_NAMES[0]); + const colorMode = + (localStorage.getItem('colorMode') as EuiThemeColorModeStandard) || + undefined; + + const highContrastMode = localStorage.getItem('highContrastMode') + ? localStorage.getItem('highContrastMode') === 'true' + : undefined; + const themeLanguage = this.getThemeLanguage(); this.state = { theme, + colorMode, + highContrastMode, themeLanguage, }; } - changeTheme = (themeValue: EUI_THEME['value']) => { - this.setState({ theme: themeValue }, () => { - localStorage.setItem('theme', themeValue); - applyTheme(themeValue); - }); + setContext = (state: Partial) => { + this.setState(state as State); }; + componentDidUpdate(_prevProps: never, prevState: State) { + const stateToSetInLocalStorage = [ + 'theme', + 'colorMode', + 'highContrastMode', + 'themeLanguage', + ] as const; + + stateToSetInLocalStorage.forEach((key) => { + if (prevState[key] !== this.state[key]) { + localStorage.setItem(key, String(this.state[key])); + + // Side effects + if (key === 'theme') { + applyTheme(this.state.theme); + } + if (key === 'themeLanguage') { + this.setThemeLanguageParam(this.state.themeLanguage!); + } + } + }); + } + getThemeLanguage = () => { // Allow theme language to be set by URL param, so we can link people // to specific docs, e.g. ?themeLanguage=js, ?themeLanguage=sass @@ -74,7 +125,7 @@ export class ThemeProvider extends React.Component { const urlParams = window?.location?.href?.split('?')[1]; // Note: we can't use location.search because of our hash router const fromUrlParam = new URLSearchParams(urlParams).get(URL_PARAM_KEY); // Otherwise, obtain it from localStorage - const fromLocalStorage = localStorage.getItem(STYLE_STORAGE_KEY); + const fromLocalStorage = localStorage.getItem(URL_PARAM_KEY); let themeLanguage = ( fromUrlParam ? `language--${fromUrlParam}` : fromLocalStorage @@ -82,7 +133,7 @@ export class ThemeProvider extends React.Component { // If not set by either param or storage, or an invalid value, use the default if (!themeLanguage || !THEME_LANGS.includes(themeLanguage)) - themeLanguage = defaultState.themeLanguage; + themeLanguage = THEME_LANGS[0]; return themeLanguage; }; @@ -98,23 +149,17 @@ export class ThemeProvider extends React.Component { window.location.hash = `${hash[0]}?${params.toString()}`; }; - changeThemeLanguage = (language: THEME_LANGUAGES['id']) => { - this.setState({ themeLanguage: language }, () => { - localStorage.setItem(STYLE_STORAGE_KEY, language); - this.setThemeLanguageParam(language); - }); - }; - render() { const { children } = this.props; - const { theme, themeLanguage } = this.state; + const { theme, colorMode, highContrastMode, themeLanguage } = this.state; return ( {children} diff --git a/packages/eui/src-docs/src/index.js b/packages/eui/src-docs/src/index.js index a69a07e36131..7132f648180f 100644 --- a/packages/eui/src-docs/src/index.js +++ b/packages/eui/src-docs/src/index.js @@ -10,16 +10,11 @@ import { AppContext } from './views/app_context'; import { AppView } from './views/app_view'; import { HomeView } from './views/home/home_view'; import { NotFoundView } from './views/not_found/not_found_view'; -import { registerTheme, ExampleContext } from './services'; +import { ExampleContext } from './services'; import Routes from './routes'; -import themeLight from './theme_light.scss'; -import themeDark from './theme_dark.scss'; import { ThemeProvider } from './components/with_theme/theme_context'; -registerTheme('light', [themeLight]); -registerTheme('dark', [themeDark]); - // Set up app // Whether the docs app should be wrapped in diff --git a/packages/eui/src-docs/src/services/theme/theme.js b/packages/eui/src-docs/src/services/theme/theme.js index a3bbf6030c29..8f4e077b0918 100644 --- a/packages/eui/src-docs/src/services/theme/theme.js +++ b/packages/eui/src-docs/src/services/theme/theme.js @@ -4,9 +4,10 @@ export function registerTheme(theme, cssFiles) { themes[theme] = cssFiles; } -export function applyTheme(newTheme) { +export function applyTheme(newTheme, colorMode = 'LIGHT') { Object.keys(themes).forEach((theme) => - themes[theme].forEach((cssFile) => cssFile.unuse()) + Object.values(themes[theme]).forEach((cssFile) => cssFile.unuse()) ); - themes[newTheme]?.forEach((cssFile) => cssFile.use()); + console.log(newTheme, themes[newTheme]?.[colorMode]); + themes[newTheme]?.[colorMode]?.use(); } diff --git a/packages/eui/src-docs/src/views/app_context.js b/packages/eui/src-docs/src/views/app_context.js index 729f4c940797..fdfda5b269d6 100644 --- a/packages/eui/src-docs/src/views/app_context.js +++ b/packages/eui/src-docs/src/views/app_context.js @@ -33,7 +33,7 @@ const utilityCache = createCache({ }); export const AppContext = ({ children }) => { - const { theme } = useContext(ThemeContext); + const { theme, colorMode, highContrastMode } = useContext(ThemeContext); const locale = useSelector((state) => getLocale(state)); const mappingFuncs = { @@ -56,9 +56,8 @@ export const AppContext = ({ children }) => { utility: utilityCache, }} theme={EUI_THEMES.find((t) => t.value === theme)?.provider} - colorMode={ - theme ? (theme.includes('light') ? 'light' : 'dark') : undefined - } + colorMode={colorMode} + highContrastMode={highContrastMode} >