diff --git a/changelogs/upcoming/7529.md b/changelogs/upcoming/7529.md new file mode 100644 index 00000000000..87aac9f1f6d --- /dev/null +++ b/changelogs/upcoming/7529.md @@ -0,0 +1,3 @@ +**CSS-in-JS conversions** + +- Added a new memoization/performance optimization utility for CSS-in-JS styles diff --git a/src/components/beacon/beacon.tsx b/src/components/beacon/beacon.tsx index acea33586f2..4838fb0ce8f 100644 --- a/src/components/beacon/beacon.tsx +++ b/src/components/beacon/beacon.tsx @@ -11,7 +11,7 @@ import { CommonProps } from '../common'; import classNames from 'classnames'; import { logicalStyles } from '../../global_styling'; -import { useEuiTheme } from '../../services'; +import { useEuiMemoizedStyles } from '../../services'; import { euiBeaconStyles } from './beacon.styles'; @@ -48,9 +48,8 @@ export const EuiBeacon: FunctionComponent = ({ style, ...rest }) => { - const euiTheme = useEuiTheme(); + const styles = useEuiMemoizedStyles(euiBeaconStyles); const classes = classNames('euiBeacon', className); - const styles = euiBeaconStyles(euiTheme); const cssStyles = [styles.euiBeacon, styles[color]]; const beaconStyle = useMemo( diff --git a/src/components/code/code.styles.ts b/src/components/code/code.styles.ts index 363a894f741..7ac66c1c218 100644 --- a/src/components/code/code.styles.ts +++ b/src/components/code/code.styles.ts @@ -9,12 +9,10 @@ import { css } from '@emotion/react'; import { logicalShorthandCSS } from '../../global_styling'; import { UseEuiTheme } from '../../services'; -import { UseEuiCodeSyntaxVariables } from './code_syntax.styles'; +import { euiCodeSyntaxVariables } from './code_syntax.styles'; -export const euiCodeStyles = ( - euiThemeContext: UseEuiTheme, - euiCodeSyntaxVariables: UseEuiCodeSyntaxVariables -) => { +export const euiCodeStyles = (euiThemeContext: UseEuiTheme) => { + const codeSyntaxVariables = euiCodeSyntaxVariables(euiThemeContext); const { euiTheme } = euiThemeContext; return { @@ -25,12 +23,12 @@ export const euiCodeStyles = ( font-family: ${euiTheme.font.familyCode}; font-size: 0.9em; /* 1 */ ${logicalShorthandCSS('padding', '0.2em 0.5em')} /* 1 */ - background: ${euiCodeSyntaxVariables.backgroundColor}; + background: ${codeSyntaxVariables.backgroundColor}; border-radius: ${euiTheme.border.radius.small}; font-weight: ${euiTheme.font.weight.bold}; - color: ${euiCodeSyntaxVariables.inlineCodeColor}; + color: ${codeSyntaxVariables.inlineCodeColor}; - ${euiCodeSyntaxVariables.tokensCss} + ${codeSyntaxVariables.tokensCss} `, transparentBackground: css` background: transparent; diff --git a/src/components/code/code.tsx b/src/components/code/code.tsx index e982e28e405..55676def4b3 100644 --- a/src/components/code/code.tsx +++ b/src/components/code/code.tsx @@ -15,8 +15,7 @@ import { checkSupportedLanguage, getHtmlContent, } from './utils'; -import { useEuiTheme } from '../../services'; -import { useEuiCodeSyntaxVariables } from './code_syntax.styles'; +import { useEuiMemoizedStyles } from '../../services'; import { euiCodeStyles } from './code.styles'; export type EuiCodeProps = EuiCodeSharedProps; @@ -47,7 +46,7 @@ export const EuiCode: FunctionComponent = ({ const classes = classNames('euiCode', className); - const styles = euiCodeStyles(useEuiTheme(), useEuiCodeSyntaxVariables()); + const styles = useEuiMemoizedStyles(euiCodeStyles); const cssStyles = [ styles.euiCode, transparentBackground && styles.transparentBackground, diff --git a/src/components/code/code_block.styles.ts b/src/components/code/code_block.styles.ts index f10cf2f5a7e..8db1e924b96 100644 --- a/src/components/code/code_block.styles.ts +++ b/src/components/code/code_block.styles.ts @@ -22,12 +22,10 @@ import { } from '../../global_styling'; import { UseEuiTheme } from '../../services'; -import { UseEuiCodeSyntaxVariables } from './code_syntax.styles'; +import { euiCodeSyntaxVariables } from './code_syntax.styles'; -export const euiCodeBlockStyles = ( - euiThemeContext: UseEuiTheme, - euiCodeSyntaxVariables: UseEuiCodeSyntaxVariables -) => { +export const euiCodeBlockStyles = (euiThemeContext: UseEuiTheme) => { + const codeSyntaxVariables = euiCodeSyntaxVariables(euiThemeContext); const { euiTheme } = euiThemeContext; return { @@ -35,9 +33,9 @@ export const euiCodeBlockStyles = ( max-inline-size: 100%; display: block; position: relative; - background: ${euiCodeSyntaxVariables.backgroundColor}; + background: ${codeSyntaxVariables.backgroundColor}; - ${euiCodeSyntaxVariables.tokensCss} + ${codeSyntaxVariables.tokensCss} `, // Font size s: css` @@ -134,17 +132,14 @@ export const euiCodeBlockPreStyles = (euiThemeContext: UseEuiTheme) => { }; }; -export const euiCodeBlockCodeStyles = ( - euiThemeContext: UseEuiTheme, - euiCodeSyntaxVariables: UseEuiCodeSyntaxVariables -) => { +export const euiCodeBlockCodeStyles = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; return { euiCodeBlock__code: css` font-family: ${euiTheme.font.familyCode}; font-size: inherit; - color: ${euiCodeSyntaxVariables.color}; + color: ${euiTheme.colors.text}; display: block; `, isVirtualized: css` diff --git a/src/components/code/code_block.tsx b/src/components/code/code_block.tsx index 71eb83f986a..145a3031a4b 100644 --- a/src/components/code/code_block.tsx +++ b/src/components/code/code_block.tsx @@ -9,7 +9,12 @@ import React, { FunctionComponent, useMemo } from 'react'; import { RefractorNode } from 'refractor'; import classNames from 'classnames'; -import { useCombinedRefs, useEuiTheme } from '../../services'; + +import { + useCombinedRefs, + useEuiTheme, + useEuiMemoizedStyles, +} from '../../services'; import { ExclusiveUnion } from '../common'; import { EuiCodeSharedProps, @@ -32,7 +37,6 @@ import { euiCodeBlockPreStyles, euiCodeBlockCodeStyles, } from './code_block.styles'; -import { useEuiCodeSyntaxVariables } from './code_syntax.styles'; // Based on observed line height for non-virtualized code blocks const fontSizeToRowHeightMap = { @@ -123,7 +127,6 @@ export const EuiCodeBlock: FunctionComponent = ({ ...rest }) => { const euiTheme = useEuiTheme(); - const euiCodeSyntaxVariables = useEuiCodeSyntaxVariables(); const language = useMemo( () => checkSupportedLanguage(_language), [_language] @@ -177,7 +180,7 @@ export const EuiCodeBlock: FunctionComponent = ({ const hasControls = !!(copyButton || fullScreenButton); const hasBothControls = !!(copyButton && fullScreenButton); - const styles = euiCodeBlockStyles(euiTheme, euiCodeSyntaxVariables); + const styles = useEuiMemoizedStyles(euiCodeBlockStyles); const cssStyles = [ styles.euiCodeBlock, styles[fontSize], @@ -188,26 +191,26 @@ export const EuiCodeBlock: FunctionComponent = ({ : styles.hasControls[paddingSize]), ]; + const preStyles = useEuiMemoizedStyles(euiCodeBlockPreStyles); const [preProps, preFullscreenProps] = useMemo(() => { const isWhiteSpacePre = whiteSpace === 'pre' || isVirtualized; - const styles = euiCodeBlockPreStyles(euiTheme); const cssStyles = [ - styles.euiCodeBlock__pre, + preStyles.euiCodeBlock__pre, isWhiteSpacePre - ? styles.whiteSpace.pre.pre - : styles.whiteSpace.preWrap.preWrap, + ? preStyles.whiteSpace.pre.pre + : preStyles.whiteSpace.preWrap.preWrap, ]; const preProps = { className: 'euiCodeBlock__pre', css: [ ...cssStyles, - styles.padding[paddingSize], + preStyles.padding[paddingSize], hasControls && (isWhiteSpacePre - ? styles.whiteSpace.pre.controlsOffset[paddingSize] - : styles.whiteSpace.preWrap.controlsOffset[paddingSize]), + ? preStyles.whiteSpace.pre.controlsOffset[paddingSize] + : preStyles.whiteSpace.preWrap.controlsOffset[paddingSize]), ], tabIndex: isVirtualized ? 0 : tabIndex, }; @@ -216,11 +219,11 @@ export const EuiCodeBlock: FunctionComponent = ({ css: [ ...cssStyles, // Force fullscreen to use xl padding - styles.padding.xl, + preStyles.padding.xl, hasControls && (isWhiteSpacePre - ? styles.whiteSpace.pre.controlsOffset.xl - : styles.whiteSpace.preWrap.controlsOffset.xl), + ? preStyles.whiteSpace.pre.controlsOffset.xl + : preStyles.whiteSpace.preWrap.controlsOffset.xl), ], tabIndex: 0, onKeyDown, @@ -228,7 +231,7 @@ export const EuiCodeBlock: FunctionComponent = ({ return [preProps, preFullscreenProps]; }, [ - euiTheme, + preStyles, whiteSpace, isVirtualized, hasControls, @@ -237,11 +240,11 @@ export const EuiCodeBlock: FunctionComponent = ({ tabIndex, ]); + const codeStyles = useEuiMemoizedStyles(euiCodeBlockCodeStyles); const codeProps = useMemo(() => { - const styles = euiCodeBlockCodeStyles(euiTheme, euiCodeSyntaxVariables); const cssStyles = [ - styles.euiCodeBlock__code, - isVirtualized && styles.isVirtualized, + codeStyles.euiCodeBlock__code, + isVirtualized && codeStyles.isVirtualized, ]; return { @@ -250,7 +253,7 @@ export const EuiCodeBlock: FunctionComponent = ({ 'data-code-language': language, ...rest, }; - }, [language, euiTheme, euiCodeSyntaxVariables, isVirtualized, rest]); + }, [codeStyles, language, isVirtualized, rest]); return (
{ +export const euiCodeBlockControlsStyles = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; + const codeSyntaxVariables = euiCodeSyntaxVariables(euiThemeContext); return { euiCodeBlock__controls: css` @@ -30,7 +28,7 @@ export const euiCodeBlockControlsStyles = ( display: flex; flex-direction: column; gap: ${euiTheme.size.xs}; - background: ${euiCodeSyntaxVariables.backgroundColor}; + background: ${codeSyntaxVariables.backgroundColor}; `, offset: { none: css` diff --git a/src/components/code/code_block_controls.tsx b/src/components/code/code_block_controls.tsx index 607f21d6170..d6b133b21b4 100644 --- a/src/components/code/code_block_controls.tsx +++ b/src/components/code/code_block_controls.tsx @@ -7,19 +7,15 @@ */ import React, { FC, Fragment, ReactNode } from 'react'; -import { useEuiTheme } from '../../services'; +import { useEuiMemoizedStyles } from '../../services'; import type { EuiCodeBlockPaddingSize } from './code_block'; -import { useEuiCodeSyntaxVariables } from './code_syntax.styles'; import { euiCodeBlockControlsStyles } from './code_block_controls.styles'; export const EuiCodeBlockControls: FC<{ controls: ReactNode[]; paddingSize: EuiCodeBlockPaddingSize; }> = ({ paddingSize, controls }) => { - const styles = euiCodeBlockControlsStyles( - useEuiTheme(), - useEuiCodeSyntaxVariables() - ); + const styles = useEuiMemoizedStyles(euiCodeBlockControlsStyles); const cssStyles = [styles.euiCodeBlock__controls, styles.offset[paddingSize]]; const hasControls = controls.some((control) => !!control); diff --git a/src/components/code/code_block_full_screen.tsx b/src/components/code/code_block_full_screen.tsx index 21b12d52497..8e20ee5b561 100644 --- a/src/components/code/code_block_full_screen.tsx +++ b/src/components/code/code_block_full_screen.tsx @@ -14,12 +14,11 @@ import React, { useMemo, PropsWithChildren, } from 'react'; -import { keys, useEuiTheme } from '../../services'; +import { keys, useEuiMemoizedStyles } from '../../services'; import { useEuiI18n } from '../i18n'; import { EuiButtonIcon } from '../button'; import { EuiFocusTrap } from '../focus_trap'; import { EuiOverlayMask } from '../overlay_mask'; -import { useEuiCodeSyntaxVariables } from './code_syntax.styles'; import { euiCodeBlockStyles } from './code_block.styles'; /** @@ -92,7 +91,7 @@ export const useFullScreen = ({ export const EuiCodeBlockFullScreenWrapper: FunctionComponent< PropsWithChildren > = ({ children }) => { - const styles = euiCodeBlockStyles(useEuiTheme(), useEuiCodeSyntaxVariables()); + const styles = useEuiMemoizedStyles(euiCodeBlockStyles); const cssStyles = [ styles.euiCodeBlock, styles.l, // Force fullscreen to use large font diff --git a/src/components/code/code_syntax.styles.ts b/src/components/code/code_syntax.styles.ts index 961b2fd5b57..4a7b9e374bb 100644 --- a/src/components/code/code_syntax.styles.ts +++ b/src/components/code/code_syntax.styles.ts @@ -6,56 +6,51 @@ * Side Public License, v 1. */ -import { useMemo } from 'react'; import { - useEuiTheme, + UseEuiTheme, makeHighContrastColor, euiPaletteColorBlind, } from '../../services'; const visColors = euiPaletteColorBlind(); -// These variables are computationally expensive, so it needs -// to be a hook in order to memoize it per theme -export const useEuiCodeSyntaxVariables = () => { - const { euiTheme } = useEuiTheme(); - - return useMemo(() => { - const backgroundColor = euiTheme.colors.lightestShade; - - return { - backgroundColor: backgroundColor, - color: makeHighContrastColor(euiTheme.colors.text)(backgroundColor), - inlineCodeColor: makeHighContrastColor(visColors[3])(backgroundColor), - selectedBackgroundColor: 'inherit', - commentColor: makeHighContrastColor(euiTheme.colors.subduedText)( - backgroundColor - ), - selectorTagColor: 'inherit', - stringColor: makeHighContrastColor(visColors[2])(backgroundColor), - tagColor: makeHighContrastColor(visColors[1])(backgroundColor), - nameColor: makeHighContrastColor(visColors[1])(backgroundColor), - numberColor: makeHighContrastColor(visColors[0])(backgroundColor), - keywordColor: makeHighContrastColor(visColors[3])(backgroundColor), - functionTitleColor: 'inherit', - typeColor: makeHighContrastColor(visColors[1])(backgroundColor), - attributeColor: 'inherit', - symbolColor: makeHighContrastColor(visColors[9])(backgroundColor), - paramsColor: 'inherit', - metaColor: makeHighContrastColor(euiTheme.colors.subduedText)( - backgroundColor - ), - titleColor: makeHighContrastColor(visColors[7])(backgroundColor), - sectionColor: makeHighContrastColor(visColors[9])(backgroundColor), - additionColor: makeHighContrastColor(visColors[0])(backgroundColor), - deletionColor: makeHighContrastColor(euiTheme.colors.danger)( - backgroundColor - ), - selectorClassColor: 'inherit', - selectorIdColor: 'inherit', - - get tokensCss() { - return ` +// These variables are computationally expensive - do not call them outside `useEuiMemoizedStyles` +export const euiCodeSyntaxVariables = ({ euiTheme }: UseEuiTheme) => { + const backgroundColor = euiTheme.colors.lightestShade; + + return { + backgroundColor: backgroundColor, + color: makeHighContrastColor(euiTheme.colors.text)(backgroundColor), + inlineCodeColor: makeHighContrastColor(visColors[3])(backgroundColor), + selectedBackgroundColor: 'inherit', + commentColor: makeHighContrastColor(euiTheme.colors.subduedText)( + backgroundColor + ), + selectorTagColor: 'inherit', + stringColor: makeHighContrastColor(visColors[2])(backgroundColor), + tagColor: makeHighContrastColor(visColors[1])(backgroundColor), + nameColor: makeHighContrastColor(visColors[1])(backgroundColor), + numberColor: makeHighContrastColor(visColors[0])(backgroundColor), + keywordColor: makeHighContrastColor(visColors[3])(backgroundColor), + functionTitleColor: 'inherit', + typeColor: makeHighContrastColor(visColors[1])(backgroundColor), + attributeColor: 'inherit', + symbolColor: makeHighContrastColor(visColors[9])(backgroundColor), + paramsColor: 'inherit', + metaColor: makeHighContrastColor(euiTheme.colors.subduedText)( + backgroundColor + ), + titleColor: makeHighContrastColor(visColors[7])(backgroundColor), + sectionColor: makeHighContrastColor(visColors[9])(backgroundColor), + additionColor: makeHighContrastColor(visColors[0])(backgroundColor), + deletionColor: makeHighContrastColor(euiTheme.colors.danger)( + backgroundColor + ), + selectorClassColor: 'inherit', + selectorIdColor: 'inherit', + + get tokensCss() { + return ` .token.punctuation:not(.interpolation-punctuation):not([class*='attr-']) { opacity: .7; } @@ -181,11 +176,6 @@ export const useEuiCodeSyntaxVariables = () => { .token.entity { cursor: help; }`; - }, - }; - }, [euiTheme]); + }, + }; }; - -export type UseEuiCodeSyntaxVariables = ReturnType< - typeof useEuiCodeSyntaxVariables ->; diff --git a/src/services/theme/index.ts b/src/services/theme/index.ts index d290b7e0a46..2f331d06ba5 100644 --- a/src/services/theme/index.ts +++ b/src/services/theme/index.ts @@ -22,6 +22,7 @@ export { } from './hooks'; export type { EuiThemeProviderProps } from './provider'; export { EuiThemeProvider } from './provider'; +export { useEuiMemoizedStyles } from './style_memoization'; export { getEuiDevProviderWarning, setEuiDevProviderWarning } from './warning'; export { buildTheme, diff --git a/src/services/theme/provider.tsx b/src/services/theme/provider.tsx index 76fab79de5b..aa6a8bf2c26 100644 --- a/src/services/theme/provider.tsx +++ b/src/services/theme/provider.tsx @@ -31,6 +31,7 @@ import { EuiColorModeContext, } from './context'; import { EuiEmotionThemeProvider } from './emotion'; +import { EuiThemeMemoizedStylesProvider } from './style_memoization'; import { buildTheme, getColorMode, getComputed, mergeDeep } from './utils'; import { EuiThemeColorMode, @@ -229,9 +230,11 @@ export const EuiThemeProvider = ({ - - {renderedChildren} - + + + {renderedChildren} + + diff --git a/src/services/theme/style_memoization.test.tsx b/src/services/theme/style_memoization.test.tsx new file mode 100644 index 00000000000..1d6b9751627 --- /dev/null +++ b/src/services/theme/style_memoization.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { css } from '@emotion/react'; +import { fireEvent } from '@testing-library/react'; +import { render, renderHook } from '../../test/rtl'; +import { testOnReactVersion } from '../../test/internal'; + +import type { UseEuiTheme } from './hooks'; +import { EuiThemeProvider } from './provider'; + +import { useEuiMemoizedStyles } from './style_memoization'; + +describe('useEuiMemoizedStyles', () => { + beforeEach(jest.clearAllMocks); + + const componentStyles = jest.fn(({ euiTheme }: UseEuiTheme) => ({ + someComponent: css` + color: ${euiTheme.colors.primaryText}; + `, + })); + const Component = () => { + const [rerender, setRerender] = useState(false); + const styles = useEuiMemoizedStyles(componentStyles); + return ( +