From a745bdea1bef844835a9f859059fe7ddb3d1b512 Mon Sep 17 00:00:00 2001 From: Jonathan Zempel Date: Mon, 22 Apr 2024 14:16:21 -0400 Subject: [PATCH] feat(theming): add new `getCheckeredBackground` utility (#1793) --- .../styled/ColorPicker/StyledAlphaRange.ts | 16 ++--- .../src/styled/ColorPicker/StyledPreview.ts | 16 +++-- .../ColorPickerDialog/StyledButtonPreview.ts | 23 ++++--- .../src/styled/common/checkeredBackground.ts | 32 --------- .../theming/demo/stories/GetColorStory.tsx | 30 ++------- packages/theming/src/index.ts | 2 + packages/theming/src/types/index.ts | 8 +++ .../src/utils/getCheckeredBackground.spec.ts | 65 +++++++++++++++++++ .../src/utils/getCheckeredBackground.ts | 52 +++++++++++++++ packages/theming/src/utils/getColor.ts | 2 +- 10 files changed, 169 insertions(+), 77 deletions(-) delete mode 100644 packages/colorpickers/src/styled/common/checkeredBackground.ts create mode 100644 packages/theming/src/utils/getCheckeredBackground.spec.ts create mode 100644 packages/theming/src/utils/getCheckeredBackground.ts diff --git a/packages/colorpickers/src/styled/ColorPicker/StyledAlphaRange.ts b/packages/colorpickers/src/styled/ColorPicker/StyledAlphaRange.ts index 17867ff99fc..efcbc302b33 100644 --- a/packages/colorpickers/src/styled/ColorPicker/StyledAlphaRange.ts +++ b/packages/colorpickers/src/styled/ColorPicker/StyledAlphaRange.ts @@ -6,9 +6,8 @@ */ import styled, { DefaultTheme, ThemeProps } from 'styled-components'; -import { DEFAULT_THEME } from '@zendeskgarden/react-theming'; +import { DEFAULT_THEME, getCheckeredBackground } from '@zendeskgarden/react-theming'; import { getTrackHeight, getTrackMargin, StyledRange } from '../common/StyledRange'; -import { checkeredBackground } from '../common/checkeredBackground'; import { IRGBColor } from '../../types'; const COMPONENT_ID = 'colorpickers.colorpicker_alpha'; @@ -19,14 +18,15 @@ const background = (props: IRGBColor & ThemeProps) => { const toColor = `rgb(${props.red}, ${props.green}, ${props.blue})`; const positionY = getTrackMargin(props); const height = getTrackHeight(props); - const gradientBackground = `linear-gradient(${direction}, ${fromColor}, ${toColor}) 0 ${positionY}px / 100% ${height}px no-repeat`; + const overlay = `linear-gradient(${direction}, ${fromColor}, ${toColor}) 0 ${positionY}px / 100% ${height}px no-repeat`; - return `${gradientBackground}, ${checkeredBackground( - props.theme, - height, + return getCheckeredBackground({ + theme: props.theme, + size: height, positionY, - 'repeat-x' - )}`; + repeat: 'repeat-x', + overlay + }); }; export const StyledAlphaRange = styled(StyledRange as 'input').attrs(props => ({ diff --git a/packages/colorpickers/src/styled/ColorPicker/StyledPreview.ts b/packages/colorpickers/src/styled/ColorPicker/StyledPreview.ts index bea6b454593..01b9f950882 100644 --- a/packages/colorpickers/src/styled/ColorPicker/StyledPreview.ts +++ b/packages/colorpickers/src/styled/ColorPicker/StyledPreview.ts @@ -7,8 +7,11 @@ import styled, { DefaultTheme, ThemeProps } from 'styled-components'; import { rgba } from 'polished'; -import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming'; -import { checkeredBackground } from '../common/checkeredBackground'; +import { + retrieveComponentStyles, + DEFAULT_THEME, + getCheckeredBackground +} from '@zendeskgarden/react-theming'; import { IRGBColor } from '../../types'; const COMPONENT_ID = 'colorpickers.colorpicker_preview_box'; @@ -19,11 +22,14 @@ interface IStyledColorPreviewProps extends IRGBColor { const background = (props: IStyledColorPreviewProps & ThemeProps) => { const alpha = props.alpha ? props.alpha / 100 : 0; - const color = `rgba(${props.red}, ${props.green}, ${props.blue}, ${alpha})`; - let retVal = `linear-gradient(${color}, ${color})`; + let retVal = `rgba(${props.red}, ${props.green}, ${props.blue}, ${alpha})`; if (!props.isOpaque) { - retVal = `${retVal}, ${checkeredBackground(props.theme, 13)}`; + retVal = getCheckeredBackground({ + theme: props.theme, + size: 13, + overlay: retVal + }); } return retVal; diff --git a/packages/colorpickers/src/styled/ColorPickerDialog/StyledButtonPreview.ts b/packages/colorpickers/src/styled/ColorPickerDialog/StyledButtonPreview.ts index a27be52e4d2..10d7d01ad20 100644 --- a/packages/colorpickers/src/styled/ColorPickerDialog/StyledButtonPreview.ts +++ b/packages/colorpickers/src/styled/ColorPickerDialog/StyledButtonPreview.ts @@ -7,8 +7,11 @@ import styled, { ThemeProps, DefaultTheme } from 'styled-components'; import { rgba } from 'polished'; -import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming'; -import { checkeredBackground } from '../common/checkeredBackground'; +import { + retrieveComponentStyles, + DEFAULT_THEME, + getCheckeredBackground +} from '@zendeskgarden/react-theming'; import { IColorPickerDialogProps } from '../../types'; const COMPONENT_ID = 'colorpickers.colordialog_preview'; @@ -19,24 +22,28 @@ export interface IStyleButtonPreviewProps extends ThemeProps { const background = (props: IStyleButtonPreviewProps) => { const { backgroundColor } = props; - let color; + let retVal; if (typeof backgroundColor === 'string') { - color = backgroundColor; + retVal = backgroundColor; } else if (backgroundColor === undefined) { - color = props.theme.palette.white; + retVal = props.theme.palette.white as string; } else { const { red, green, blue, alpha = 1 } = backgroundColor; - color = `rgba(${red}, ${green}, ${blue}, ${alpha ? alpha / 100 : 0})`; + retVal = `rgba(${red}, ${green}, ${blue}, ${alpha ? alpha / 100 : 0})`; } - return `linear-gradient(${color}, ${color})`; + return retVal; }; export const StyledButtonPreview = styled.span.attrs(props => ({ style: { - background: `${background(props)}, ${checkeredBackground(props.theme, 8)}` + background: getCheckeredBackground({ + theme: props.theme, + size: 8, + overlay: background(props) + }) }, 'data-garden-id': COMPONENT_ID, 'data-garden-version': PACKAGE_VERSION, diff --git a/packages/colorpickers/src/styled/common/checkeredBackground.ts b/packages/colorpickers/src/styled/common/checkeredBackground.ts deleted file mode 100644 index 5b4811e08b2..00000000000 --- a/packages/colorpickers/src/styled/common/checkeredBackground.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright Zendesk, Inc. - * - * Use of this source code is governed under the Apache License, Version 2.0 - * found at http://www.apache.org/licenses/LICENSE-2.0. - */ - -import { DefaultTheme } from 'styled-components'; -import { getColorV8 } from '@zendeskgarden/react-theming'; - -export const checkeredBackground = ( - theme: DefaultTheme, - size: number, - positionY = 0, - repeat = 'repeat' -) => { - const color = getColorV8('neutralHue', 400, theme); - const dimensions = `${size}px ${size}px`; - const positionX1 = theme.rtl ? '100%' : '0'; - const positionX2 = theme.rtl ? `calc(100% - ${size / 2}px)` : `${size / 2}px`; - const position1 = `${positionX1} ${positionY}px`; - const position2 = `${positionX2} ${size / 2 + positionY}px`; - const position3 = `${positionX2} ${positionY}px`; - const position4 = `${positionX1} ${size / -2 + positionY}px`; - - return ` - linear-gradient(45deg, ${color} 25%, transparent 25%) ${position1} / ${dimensions} ${repeat}, - linear-gradient(45deg, transparent 75%, ${color} 75%) ${position2} / ${dimensions} ${repeat}, - linear-gradient(135deg, ${color} 25%, transparent 25%) ${position3} / ${dimensions} ${repeat}, - linear-gradient(135deg, transparent 75%, ${color} 75%) ${position4} / ${dimensions} ${repeat} - `; -}; diff --git a/packages/theming/demo/stories/GetColorStory.tsx b/packages/theming/demo/stories/GetColorStory.tsx index e3787d73a80..e2f0a548af3 100644 --- a/packages/theming/demo/stories/GetColorStory.tsx +++ b/packages/theming/demo/stories/GetColorStory.tsx @@ -7,30 +7,10 @@ import React from 'react'; import { StoryFn } from '@storybook/react'; -import styled, { DefaultTheme, useTheme } from 'styled-components'; -import { IGardenTheme, getColor } from '@zendeskgarden/react-theming'; +import styled, { useTheme } from 'styled-components'; +import { IGardenTheme, getCheckeredBackground, getColor } from '@zendeskgarden/react-theming'; import { Tag } from '@zendeskgarden/react-tags'; -const toBackground = (theme: DefaultTheme, backgroundColor: string) => { - const color = getColor({ hue: 'neutralHue', shade: 300, theme }); - const size = 26; - const dimensions = `${size}px ${size}px`; - const positionX1 = theme.rtl ? '100%' : '0'; - const positionX2 = theme.rtl ? `calc(100% - ${size / 2}px)` : `${size / 2}px`; - const position1 = `${positionX1} 0`; - const position2 = `${positionX2} ${size / 2}px`; - const position3 = `${positionX2} 0`; - const position4 = `${positionX1} ${size / -2}px`; - - return ` - linear-gradient(${backgroundColor}, ${backgroundColor}), - linear-gradient(45deg, ${color} 25%, transparent 25%) ${position1} / ${dimensions} repeat, - linear-gradient(45deg, transparent 75%, ${color} 75%) ${position2} / ${dimensions} repeat, - linear-gradient(135deg, ${color} 25%, transparent 25%) ${position3} / ${dimensions} repeat, - linear-gradient(135deg, transparent 75%, ${color} 75%) ${position4} / ${dimensions} repeat - `; -}; - const StyledDiv = styled.div<{ background: string }>` display: flex; align-items: center; @@ -66,7 +46,11 @@ const Color = ({ dark, hue, light, offset, shade, theme, transparency, variable variable }); - background = toBackground(theme, backgroundColor); + background = getCheckeredBackground({ + theme, + size: 26, + overlay: backgroundColor + }); tag = ( {backgroundColor} diff --git a/packages/theming/src/index.ts b/packages/theming/src/index.ts index 02933a944e7..43c1bfc4ad9 100644 --- a/packages/theming/src/index.ts +++ b/packages/theming/src/index.ts @@ -11,6 +11,7 @@ export { default as PALETTE } from './elements/palette'; export { default as PALETTE_V8 } from './elements/palette/v8'; export { default as retrieveComponentStyles } from './utils/retrieveComponentStyles'; export { getArrowPosition } from './utils/getArrowPosition'; +export { getCheckeredBackground } from './utils/getCheckeredBackground'; export { getColor } from './utils/getColor'; export { getColorV8 } from './utils/getColorV8'; export { getFloatingPlacements } from './utils/getFloatingPlacements'; @@ -33,6 +34,7 @@ export { type IGardenTheme, type IThemeProviderProps, type ArrowPosition, + type CheckeredBackgroundParameters, type ColorParameters, type FocusBoxShadowParameters, type FocusStylesParameters, diff --git a/packages/theming/src/types/index.ts b/packages/theming/src/types/index.ts index 14198e5476b..3ea95021730 100644 --- a/packages/theming/src/types/index.ts +++ b/packages/theming/src/types/index.ts @@ -45,6 +45,14 @@ export const PLACEMENT = [ export type Placement = (typeof PLACEMENT)[number]; +export type CheckeredBackgroundParameters = { + overlay?: string; + positionY?: number; + repeat?: 'repeat' | 'repeat-x'; + size: number; + theme: IGardenTheme; +}; + export type ColorParameters = { dark?: { hue?: string; diff --git a/packages/theming/src/utils/getCheckeredBackground.spec.ts b/packages/theming/src/utils/getCheckeredBackground.spec.ts new file mode 100644 index 00000000000..f9e7eff59bb --- /dev/null +++ b/packages/theming/src/utils/getCheckeredBackground.spec.ts @@ -0,0 +1,65 @@ +/** + * Copyright Zendesk, Inc. + * + * Use of this source code is governed under the Apache License, Version 2.0 + * found at http://www.apache.org/licenses/LICENSE-2.0. + */ + +import { getCheckeredBackground } from './getCheckeredBackground'; +import DEFAULT_THEME from '../elements/theme'; +import PALETTE from '../elements/palette'; + +describe('getCheckeredBackground', () => { + it('returns a valid background', () => { + const size = 10; + const result = getCheckeredBackground({ theme: DEFAULT_THEME, size }); + const expected1 = `0 0px / ${size}px ${size}px repeat`; + const expected2 = `${size / 2}px ${size / 2}px / ${size}px ${size}px repeat`; + + expect(result).toContain(expected1); + expect(result).toContain(expected2); + }); + + it('handles color overlay as expected', () => { + const expected = `${PALETTE.blue[700]}80`; + const result = getCheckeredBackground({ theme: DEFAULT_THEME, size: 10, overlay: expected }); + + expect(result).toContain(`linear-gradient(${expected}, ${expected})`); + }); + + it('handles linear-gradient overlay as expected', () => { + const expected = `linear-gradient(to right, ${PALETTE.white}, ${PALETTE.black})`; + const result = getCheckeredBackground({ theme: DEFAULT_THEME, size: 10, overlay: expected }); + + expect(result).toContain(`${expected},`); + }); + + it('handles vertical position as expected', () => { + const size = 10; + const positionY = DEFAULT_THEME.space.base; + const result = getCheckeredBackground({ theme: DEFAULT_THEME, size, positionY }); + const expected1 = `0 ${positionY}px`; + const expected2 = `${size / 2}px ${size / 2 + positionY}px`; + + expect(result).toContain(expected1); + expect(result).toContain(expected2); + }); + + it('handles repeat as expected', () => { + const repeat = 'repeat-x'; + const result = getCheckeredBackground({ theme: DEFAULT_THEME, size: 10, repeat }); + + expect(result).toContain(repeat); + }); + + it('handles RTL as expected', () => { + const theme = { ...DEFAULT_THEME, rtl: true }; + const size = 10; + const result = getCheckeredBackground({ theme, size }); + const expected1 = `100% 0px / ${size}px ${size}px repeat`; + const expected2 = `calc(100% - ${size / 2}px) ${size / 2}px / ${size}px ${size}px repeat`; + + expect(result).toContain(expected1); + expect(result).toContain(expected2); + }); +}); diff --git a/packages/theming/src/utils/getCheckeredBackground.ts b/packages/theming/src/utils/getCheckeredBackground.ts new file mode 100644 index 00000000000..08572b40596 --- /dev/null +++ b/packages/theming/src/utils/getCheckeredBackground.ts @@ -0,0 +1,52 @@ +/** + * Copyright Zendesk, Inc. + * + * Use of this source code is governed under the Apache License, Version 2.0 + * found at http://www.apache.org/licenses/LICENSE-2.0. + */ + +import { CheckeredBackgroundParameters } from '../types'; +import { getColor } from './getColor'; + +/** + * Get a checkered background image. + * + * @param {Object} options.theme Provides information for pattern color and LTR/RTL layout + * @param {number} options.size The pixel size of the checkered pattern + * @param {string} [options.overlay] Optional + * [color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) with transparency or + * [`linear-gradient()`](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient) + * overlay to apply on top of the checkered pattern + * @param {number} [options.positionY=0] Optional vertical position for starting the pattern (default `0`) + * @param {string} [options.repeat='repeat'] Optional repeat value for the pattern; either `'repeat'` or `'repeat-x'` (default `'repeat'`) + */ +export const getCheckeredBackground = ({ + theme, + size, + overlay, + positionY = 0, + repeat = 'repeat' +}: CheckeredBackgroundParameters) => { + const color = getColor({ theme, variable: 'border.default' }); + const dimensions = `${size}px ${size}px`; + const positionX1 = theme.rtl ? '100%' : '0'; + const positionX2 = theme.rtl ? `calc(100% - ${size / 2}px)` : `${size / 2}px`; + const position1 = `${positionX1} ${positionY}px`; + const position2 = `${positionX2} ${size / 2 + positionY}px`; + const position3 = `${positionX2} ${positionY}px`; + const position4 = `${positionX1} ${size / -2 + positionY}px`; + let retVal = ` + linear-gradient(45deg, ${color} 25%, transparent 25%) ${position1} / ${dimensions} ${repeat}, + linear-gradient(45deg, transparent 75%, ${color} 75%) ${position2} / ${dimensions} ${repeat}, + linear-gradient(135deg, ${color} 25%, transparent 25%) ${position3} / ${dimensions} ${repeat}, + linear-gradient(135deg, transparent 75%, ${color} 75%) ${position4} / ${dimensions} ${repeat} + `; + + if (overlay) { + retVal = overlay.startsWith('linear-gradient') + ? `${overlay}, ${retVal}` + : `linear-gradient(${overlay}, ${overlay}), ${retVal}`; + } + + return retVal; +}; diff --git a/packages/theming/src/utils/getColor.ts b/packages/theming/src/utils/getColor.ts index a314d54b737..87bdcf1c3a0 100644 --- a/packages/theming/src/utils/getColor.ts +++ b/packages/theming/src/utils/getColor.ts @@ -134,7 +134,7 @@ const toProperty = (object: object, path: string) => { * `shade`, `offset`, and `transparency` are used as fallbacks to determine the * color. * - * @param {Object} [options.theme] Provides values used to resolve the desired color + * @param {Object} options.theme Provides values used to resolve the desired color * @param {string} [options.variable] A variable key (i.e. `'background.default'`) used to resolve a color value for the theme color base * @param {Object} [options.dark] An object with `hue`, `shade`, `offset`, and `transparency` values to be used in dark mode * @param {Object} [options.light] An object with `hue`, `shade`, `offset`, and `transparency` values to be used in light mode