diff --git a/src/components/button/button_display/_button_display.tsx b/src/components/button/button_display/_button_display.tsx index c73acb9f5a8..b2facd31767 100644 --- a/src/components/button/button_display/_button_display.tsx +++ b/src/components/button/button_display/_button_display.tsx @@ -16,7 +16,7 @@ import React, { // @ts-ignore module doesn't export `createElement` import { createElement } from '@emotion/react'; -import { getSecureRelForTarget, useEuiTheme } from '../../../services'; +import { getSecureRelForTarget, useEuiMemoizedStyles } from '../../../services'; import { CommonProps, @@ -139,9 +139,7 @@ export const EuiButtonDisplay = forwardRef( isLoading, }); - const theme = useEuiTheme(); - - const styles = euiButtonDisplayStyles(theme); + const styles = useEuiMemoizedStyles(euiButtonDisplayStyles); const cssStyles = [ styles.euiButtonDisplay, styles[size], diff --git a/src/components/button/button_empty/button_empty.tsx b/src/components/button/button_empty/button_empty.tsx index acaf74a4ce7..9ce62596f24 100644 --- a/src/components/button/button_empty/button_empty.tsx +++ b/src/components/button/button_empty/button_empty.tsx @@ -15,7 +15,7 @@ import { PropsForAnchor, PropsForButton, } from '../../common'; -import { useEuiTheme, getSecureRelForTarget } from '../../../services'; +import { useEuiMemoizedStyles, getSecureRelForTarget } from '../../../services'; import { EuiButtonDisplayContent, @@ -119,8 +119,7 @@ export const EuiButtonEmpty: FunctionComponent = ({ display: 'empty', }); - const euiTheme = useEuiTheme(); - const styles = euiButtonEmptyStyles(euiTheme); + const styles = useEuiMemoizedStyles(euiButtonEmptyStyles); const cssStyles = [ styles.euiButtonEmpty, styles[size], diff --git a/src/components/button/button_group/button_group.styles.ts b/src/components/button/button_group/button_group.styles.ts index f044b49e193..e2527fb35cd 100644 --- a/src/components/button/button_group/button_group.styles.ts +++ b/src/components/button/button_group/button_group.styles.ts @@ -11,17 +11,15 @@ import { UseEuiTheme } from '../../../services'; import { logicalCSS } from '../../../global_styling'; import { euiFormVariables } from '../../form/form.styles'; -export const euiButtonGroupStyles = () => { - return { - euiButtonGroup: css` - display: inline-block; - ${logicalCSS('max-width', '100%')} - position: relative; /* Ensures the EuiScreenReaderOnly component is positioned relative to this component */ - `, - fullWidth: css` - display: block; - `, - }; +export const euiButtonGroupStyles = { + euiButtonGroup: css` + display: inline-block; + ${logicalCSS('max-width', '100%')} + position: relative; /* Ensures the EuiScreenReaderOnly component is positioned relative to this component */ + `, + fullWidth: css` + display: block; + `, }; export const euiButtonGroupButtonsStyles = (euiThemeContext: UseEuiTheme) => { diff --git a/src/components/button/button_group/button_group.tsx b/src/components/button/button_group/button_group.tsx index 066dffefc08..2cf0064b6c3 100644 --- a/src/components/button/button_group/button_group.tsx +++ b/src/components/button/button_group/button_group.tsx @@ -14,7 +14,7 @@ import React, { ReactNode, } from 'react'; -import { useEuiTheme } from '../../../services'; +import { useEuiMemoizedStyles } from '../../../services'; import { EuiScreenReaderOnly } from '../../accessibility'; import { CommonProps } from '../../common'; @@ -142,15 +142,12 @@ export const EuiButtonGroup: FunctionComponent = ({ type = 'single', ...rest }) => { - const euiTheme = useEuiTheme(); - - const wrapperStyles = euiButtonGroupStyles(); const wrapperCssStyles = [ - wrapperStyles.euiButtonGroup, - isFullWidth && wrapperStyles.fullWidth, + euiButtonGroupStyles.euiButtonGroup, + isFullWidth && euiButtonGroupStyles.fullWidth, ]; - const styles = euiButtonGroupButtonsStyles(euiTheme); + const styles = useEuiMemoizedStyles(euiButtonGroupButtonsStyles); const cssStyles = [ styles.euiButtonGroup__buttons, isFullWidth && styles.fullWidth, diff --git a/src/components/button/button_icon/button_icon.styles.ts b/src/components/button/button_icon/button_icon.styles.ts index e8769c1bc50..a8f38fc73cb 100644 --- a/src/components/button/button_icon/button_icon.styles.ts +++ b/src/components/button/button_icon/button_icon.styles.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ -import { css } from '@emotion/react'; +import { css, type SerializedStyles } from '@emotion/react'; import { UseEuiTheme } from '../../../services'; import { logicalSizeCSS } from '../../../global_styling'; import { + BUTTON_COLORS, _EuiButtonColor, euiButtonEmptyColor, euiButtonSizeMap, @@ -55,14 +56,16 @@ export const euiButtonIconStyles = (euiThemeContext: UseEuiTheme) => { }; }; -export const _emptyHoverStyles = ( - euiThemeContext: UseEuiTheme, - color: _EuiButtonColor -) => { - return css` - &:hover { - background-color: ${euiButtonEmptyColor(euiThemeContext, color) - .backgroundColor}; - } - `; -}; +export const _emptyHoverStyles = (euiThemeContext: UseEuiTheme) => + BUTTON_COLORS.reduce( + (styles, color) => ({ + ...styles, + [color]: css` + &:hover { + background-color: ${euiButtonEmptyColor(euiThemeContext, color) + .backgroundColor}; + } + `, + }), + {} as Record<_EuiButtonColor, SerializedStyles> + ); diff --git a/src/components/button/button_icon/button_icon.tsx b/src/components/button/button_icon/button_icon.tsx index b522ea80394..0cb90b4d606 100644 --- a/src/components/button/button_icon/button_icon.tsx +++ b/src/components/button/button_icon/button_icon.tsx @@ -14,7 +14,7 @@ import React, { } from 'react'; import classNames from 'classnames'; -import { getSecureRelForTarget, useEuiTheme } from '../../../services'; +import { getSecureRelForTarget, useEuiMemoizedStyles } from '../../../services'; import { CommonProps, ExclusiveUnion, @@ -118,7 +118,6 @@ export const EuiButtonIcon: FunctionComponent = ({ isLoading, ...rest }) => { - const euiThemeContext = useEuiTheme(); const isDisabled = isButtonDisabled({ isDisabled: _isDisabled || disabled, href, @@ -137,18 +136,15 @@ export const EuiButtonIcon: FunctionComponent = ({ const buttonColorStyles = useEuiButtonColorCSS({ display }); const buttonFocusStyle = useEuiButtonFocusCSS(); - const emptyHoverStyles = - display === 'empty' && - !isDisabled && - _emptyHoverStyles(euiThemeContext, color); + const emptyHoverStyles = useEuiMemoizedStyles(_emptyHoverStyles); - const styles = euiButtonIconStyles(euiThemeContext); + const styles = useEuiMemoizedStyles(euiButtonIconStyles); const cssStyles = [ styles.euiButtonIcon, styles[size], buttonColorStyles[isDisabled ? 'disabled' : color], buttonFocusStyle, - emptyHoverStyles, + display === 'empty' && !isDisabled && emptyHoverStyles[color], isDisabled && styles.isDisabled, ]; diff --git a/src/themes/amsterdam/global_styling/mixins/__snapshots__/button.test.ts.snap b/src/themes/amsterdam/global_styling/mixins/__snapshots__/button.test.ts.snap new file mode 100644 index 00000000000..e1308b7893e --- /dev/null +++ b/src/themes/amsterdam/global_styling/mixins/__snapshots__/button.test.ts.snap @@ -0,0 +1,181 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useEuiButtonColorCSS base 1`] = ` +Object { + "accent": Object { + "map": undefined, + "name": "1uoldaz-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#a03465;background-color:#fcdcea;;;label:base-accent;", + "toString": [Function], + }, + "danger": Object { + "map": undefined, + "name": "1cquuvx-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#ab231c;background-color:#f2d4d2;;;label:base-danger;", + "toString": [Function], + }, + "disabled": Object { + "map": undefined, + "name": "13xo60b-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#a2abba;background-color:rgba(211,218,230,0.15);;;label:base-disabled;", + "toString": [Function], + }, + "primary": Object { + "map": undefined, + "name": "1thox14-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#0061a6;background-color:#cce4f5;;;label:base-primary;", + "toString": [Function], + }, + "success": Object { + "map": undefined, + "name": "1j0clgi-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#006c66;background-color:#ccf2f0;;;label:base-success;", + "toString": [Function], + }, + "text": Object { + "map": undefined, + "name": "40srs0-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#343741;background-color:#e9edf3;;;label:base-text;", + "toString": [Function], + }, + "warning": Object { + "map": undefined, + "name": "19dh407-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#83650a;background-color:#fff3d0;;;label:base-warning;", + "toString": [Function], + }, +} +`; + +exports[`useEuiButtonColorCSS empty 1`] = ` +Object { + "accent": Object { + "map": undefined, + "name": "cy4kjs-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#a03465;&:focus,&:active{background-color:rgba(240,78,152,0.1);};label:empty-accent;", + "toString": [Function], + }, + "danger": Object { + "map": undefined, + "name": "j8xrk3-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#ab231c;&:focus,&:active{background-color:rgba(189,39,30,0.1);};label:empty-danger;", + "toString": [Function], + }, + "disabled": Object { + "map": undefined, + "name": "2vymtv-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#a2abba;&:focus,&:active{background-color:transparent;};label:empty-disabled;", + "toString": [Function], + }, + "primary": Object { + "map": undefined, + "name": "15isz8i-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#0061a6;&:focus,&:active{background-color:rgba(0,119,204,0.1);};label:empty-primary;", + "toString": [Function], + }, + "success": Object { + "map": undefined, + "name": "h2w3e9-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#006c66;&:focus,&:active{background-color:rgba(0,191,179,0.1);};label:empty-success;", + "toString": [Function], + }, + "text": Object { + "map": undefined, + "name": "1dqg6bz-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#343741;&:focus,&:active{background-color:rgba(211,218,230,0.2);};label:empty-text;", + "toString": [Function], + }, + "warning": Object { + "map": undefined, + "name": "w61e3r-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#83650a;&:focus,&:active{background-color:rgba(254,197,20,0.1);};label:empty-warning;", + "toString": [Function], + }, +} +`; + +exports[`useEuiButtonColorCSS fill 1`] = ` +Object { + "accent": Object { + "map": undefined, + "name": "vi45v3-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#000;background-color:#f583b7;outline-color:#000;;label:fill-accent;", + "toString": [Function], + }, + "danger": Object { + "map": undefined, + "name": "1ge48z4-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#FFF;background-color:#BD271E;outline-color:#000;;label:fill-danger;", + "toString": [Function], + }, + "disabled": Object { + "map": undefined, + "name": "xc42v8-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#a2abba;background-color:rgba(211,218,230,0.15);outline-color:#000;;label:fill-disabled;", + "toString": [Function], + }, + "primary": Object { + "map": undefined, + "name": "btstjy-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#FFF;background-color:#07C;outline-color:#000;;label:fill-primary;", + "toString": [Function], + }, + "success": Object { + "map": undefined, + "name": "p9aexd-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#000;background-color:#4dd2ca;outline-color:#000;;label:fill-success;", + "toString": [Function], + }, + "text": Object { + "map": undefined, + "name": "5c80s5-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#FFF;background-color:#69707D;outline-color:#000;;label:fill-text;", + "toString": [Function], + }, + "warning": Object { + "map": undefined, + "name": "17xxsr5-displaysColorsMap-display-color", + "next": undefined, + "styles": "color:#000;background-color:#FEC514;outline-color:#000;;label:fill-warning;", + "toString": [Function], + }, +} +`; + +exports[`useEuiButtonFocusCSS 1`] = ` +Object { + "map": undefined, + "name": "1s8jae7-focusCSS", + "next": Object { + "name": "animation-70pju1", + "next": undefined, + "styles": "@keyframes animation-70pju1{ + 50% { + transform: translateY(1px); + } +}", + }, + "styles": "@media screen and (prefers-reduced-motion: no-preference){transition:transform 250ms ease-in-out,background-color 250ms ease-in-out;&:hover:not(:disabled){transform:translateY(-1px);}&:focus{animation:animation-70pju1 250ms cubic-bezier(.34, 1.61, .7, 1);}&:active:not(:disabled){transform:translateY(1px);}};", + "toString": [Function], +} +`; diff --git a/src/themes/amsterdam/global_styling/mixins/button.test.ts b/src/themes/amsterdam/global_styling/mixins/button.test.ts new file mode 100644 index 00000000000..573024762d6 --- /dev/null +++ b/src/themes/amsterdam/global_styling/mixins/button.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { renderHook } from '@testing-library/react'; + +import { + useEuiButtonColorCSS, + BUTTON_DISPLAYS, + useEuiButtonFocusCSS, +} from './button'; + +describe('useEuiButtonColorCSS', () => { + BUTTON_DISPLAYS.forEach((display) => { + test(display, () => { + const { result } = renderHook(() => useEuiButtonColorCSS({ display })); + expect(result.current).toMatchSnapshot(); + }); + }); +}); + +test('useEuiButtonFocusCSS', () => { + const { result } = renderHook(() => useEuiButtonFocusCSS()); + expect(result.current).toMatchSnapshot(); +}); diff --git a/src/themes/amsterdam/global_styling/mixins/button.ts b/src/themes/amsterdam/global_styling/mixins/button.ts index b3b63ca94e6..373184fde10 100644 --- a/src/themes/amsterdam/global_styling/mixins/button.ts +++ b/src/themes/amsterdam/global_styling/mixins/button.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { css } from '@emotion/react'; +import { css, keyframes, type SerializedStyles } from '@emotion/react'; import { euiBackgroundColor, euiCanAnimate } from '../../../../global_styling'; import { hexToRgb, @@ -15,8 +15,8 @@ import { shade, tint, transparentize, - useEuiTheme, UseEuiTheme, + useEuiMemoizedStyles, } from '../../../../services'; export const BUTTON_COLORS = [ @@ -27,10 +27,10 @@ export const BUTTON_COLORS = [ 'warning', 'danger', ] as const; - export type _EuiButtonColor = (typeof BUTTON_COLORS)[number]; -export type _EuiButtonDisplay = 'base' | 'fill' | 'empty'; +export const BUTTON_DISPLAYS = ['base', 'fill', 'empty'] as const; +export type _EuiButtonDisplay = (typeof BUTTON_DISPLAYS)[number]; export interface _EuiButtonOptions { display?: _EuiButtonDisplay; } @@ -176,53 +176,81 @@ export const euiButtonEmptyColor = ( * @returns An object of `_EuiButtonColor` keys including `disabled` */ export const useEuiButtonColorCSS = (options: _EuiButtonOptions = {}) => { - const euiThemeContext = useEuiTheme(); + const { display = 'base' } = options; - function stylesByDisplay(color: _EuiButtonColor | 'disabled') { - return { - base: css` - ${euiButtonColor(euiThemeContext, color)} - `, - fill: css` - ${euiButtonFillColor(euiThemeContext, color)} + const colorsDisplaysMap = useEuiMemoizedStyles(euiButtonDisplaysColors); + return colorsDisplaysMap[display]; +}; - /* Use full shade for outline-color except for dark mode text buttons which need to stay currentColor */ - outline-color: ${euiThemeContext.colorMode === 'DARK' && - color === 'text' - ? 'currentColor' - : euiThemeContext.euiTheme.colors.fullShade}; - `, - empty: css` - color: ${euiButtonEmptyColor(euiThemeContext, color).color}; +const euiButtonDisplaysColors = (euiThemeContext: UseEuiTheme) => { + const COLORS = [...BUTTON_COLORS, 'disabled'] as const; + type Colors = (typeof COLORS)[number]; - &:focus, - &:active { - background-color: ${euiButtonEmptyColor(euiThemeContext, color) - .backgroundColor}; - } - `, - }; - } + const displaysColorsMap = {} as Record< + _EuiButtonDisplay, + Record + >; - return { - text: css(stylesByDisplay('text')[options.display || 'base']), - accent: css(stylesByDisplay('accent')[options.display || 'base']), - primary: css(stylesByDisplay('primary')[options.display || 'base']), - success: css(stylesByDisplay('success')[options.display || 'base']), - warning: css(stylesByDisplay('warning')[options.display || 'base']), - danger: css(stylesByDisplay('danger')[options.display || 'base']), - disabled: css(stylesByDisplay('disabled')[options.display || 'base']), - }; + BUTTON_DISPLAYS.forEach((display) => { + displaysColorsMap[display] = {} as Record; + + COLORS.forEach((color) => { + switch (display) { + case 'base': + displaysColorsMap[display][color] = css` + ${euiButtonColor(euiThemeContext, color)} + `; + break; + case 'fill': + displaysColorsMap[display][color] = css` + ${euiButtonFillColor(euiThemeContext, color)} + + /* Use full shade for outline-color except for dark mode text buttons which need to stay currentColor */ + outline-color: ${euiThemeContext.colorMode === 'DARK' && + color === 'text' + ? 'currentColor' + : euiThemeContext.euiTheme.colors.fullShade}; + `; + break; + case 'empty': + displaysColorsMap[display][color] = css` + color: ${euiButtonEmptyColor(euiThemeContext, color).color}; + + &:focus, + &:active { + background-color: ${euiButtonEmptyColor(euiThemeContext, color) + .backgroundColor}; + } + `; + break; + } + + // Tweak auto-generated Emotion label/className output + const emotionOutput = displaysColorsMap[display][color]; + emotionOutput.styles = emotionOutput.styles.replace( + 'label:displaysColorsMap-display-color;', + `label:${display}-${color};` + ); + }); + }); + + return displaysColorsMap; }; /** * Creates the translate animation when button is in focus. * @returns string */ -export const useEuiButtonFocusCSS = () => { - const { euiTheme } = useEuiTheme(); +export const useEuiButtonFocusCSS = () => + useEuiMemoizedStyles(euiButtonFocusCSS); - return ` +const euiButtonFocusAnimation = keyframes` + 50% { + transform: translateY(1px); + } +`; +const euiButtonFocusCSS = ({ euiTheme }: UseEuiTheme) => { + const focusCSS = css` ${euiCanAnimate} { transition: transform ${euiTheme.animation.normal} ease-in-out, background-color ${euiTheme.animation.normal} ease-in-out; @@ -232,7 +260,7 @@ export const useEuiButtonFocusCSS = () => { } &:focus { - animation: euiButtonActive ${euiTheme.animation.normal} + animation: ${euiButtonFocusAnimation} ${euiTheme.animation.normal} ${euiTheme.animation.bounce}; } @@ -241,6 +269,11 @@ export const useEuiButtonFocusCSS = () => { } } `; + // Remove the auto-generated label. + // We could typically avoid a label by using a plain string `` instead of css``, + // but we need css`` for keyframes`` to work for the focus animation + focusCSS.styles = focusCSS.styles.replace('label:focusCSS;', ''); + return focusCSS; }; /**