diff --git a/modules/docs/package.json b/modules/docs/package.json index 9a283be72d..a420b55b88 100644 --- a/modules/docs/package.json +++ b/modules/docs/package.json @@ -49,7 +49,7 @@ "@workday/canvas-kit-react": "^12.2.2", "@workday/canvas-kit-styling": "^12.2.2", "@workday/canvas-system-icons-web": "^3.0.0", - "@workday/canvas-tokens-web": "^2.0.1", + "@workday/canvas-tokens-web": "^2.1.0", "markdown-to-jsx": "^7.2.0", "react-syntax-highlighter": "^15.5.0", "ts-node": "^10.9.1" diff --git a/modules/labs-react/package.json b/modules/labs-react/package.json index e6d370e27a..c33011e16b 100644 --- a/modules/labs-react/package.json +++ b/modules/labs-react/package.json @@ -49,7 +49,7 @@ "@workday/canvas-kit-react": "^12.2.2", "@workday/canvas-kit-styling": "^12.2.2", "@workday/canvas-system-icons-web": "^3.0.0", - "@workday/canvas-tokens-web": "^2.0.1", + "@workday/canvas-tokens-web": "^2.1.0", "@workday/design-assets-types": "^0.2.8", "chroma-js": "^2.2.0", "lodash.flatten": "^4.4.0", diff --git a/modules/preview-react/package.json b/modules/preview-react/package.json index 1439470f0f..ff72c71fda 100644 --- a/modules/preview-react/package.json +++ b/modules/preview-react/package.json @@ -49,7 +49,7 @@ "@workday/canvas-kit-react": "^12.2.2", "@workday/canvas-kit-styling": "^12.2.2", "@workday/canvas-system-icons-web": "^3.0.0", - "@workday/canvas-tokens-web": "^2.0.1", + "@workday/canvas-tokens-web": "^2.1.0", "@workday/design-assets-types": "^0.2.8" }, "devDependencies": { diff --git a/modules/react/button/lib/PrimaryButton.tsx b/modules/react/button/lib/PrimaryButton.tsx index 67164c933c..1a1d25ce79 100644 --- a/modules/react/button/lib/PrimaryButton.tsx +++ b/modules/react/button/lib/PrimaryButton.tsx @@ -23,38 +23,50 @@ const primaryButtonStencil = createStencil({ extends: buttonStencil, base: { // Base Styles - [buttonStencil.vars.background]: brand.primary.base, + [buttonStencil.vars.background]: cssVar(brand.action.base, brand.primary.base), [buttonStencil.vars.borderRadius]: system.shape.round, - [buttonStencil.vars.label]: brand.primary.accent, - [systemIconStencil.vars.color]: cssVar(buttonColorPropVars.default.icon, brand.primary.accent), + [buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent), + [systemIconStencil.vars.color]: cssVar( + buttonColorPropVars.default.icon, + cssVar(brand.action.accent, brand.primary.accent) + ), // Focus Styles '&:focus-visible, &.focus': { - [buttonStencil.vars.background]: brand.primary.base, - [buttonStencil.vars.label]: brand.primary.accent, + [buttonStencil.vars.background]: cssVar(brand.action.base, brand.primary.base), + [buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent), [buttonStencil.vars.boxShadowInner]: system.color.border.inverse, [buttonStencil.vars.boxShadowOuter]: brand.common.focusOutline, - [systemIconStencil.vars.color]: cssVar(buttonColorPropVars.focus.icon, brand.primary.accent), + [systemIconStencil.vars.color]: cssVar( + buttonColorPropVars.focus.icon, + cssVar(brand.action.accent, brand.primary.accent) + ), }, // Hover Styles '&:hover, &.hover': { - [buttonStencil.vars.background]: brand.primary.dark, - [buttonStencil.vars.label]: brand.primary.accent, - [systemIconStencil.vars.color]: cssVar(buttonColorPropVars.hover.icon, brand.primary.accent), + [buttonStencil.vars.background]: cssVar(brand.action.dark, brand.primary.dark), + [buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent), + [systemIconStencil.vars.color]: cssVar( + buttonColorPropVars.hover.icon, + cssVar(brand.action.accent, brand.primary.accent) + ), }, // Active Styles '&:active, &.active': { - [buttonStencil.vars.background]: brand.primary.darkest, - [buttonStencil.vars.label]: brand.primary.accent, - [systemIconStencil.vars.color]: cssVar(buttonColorPropVars.active.icon, brand.primary.accent), + [buttonStencil.vars.background]: cssVar(brand.action.darkest, brand.primary.darkest), + [buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent), + [systemIconStencil.vars.color]: cssVar( + buttonColorPropVars.active.icon, + cssVar(brand.action.accent, brand.primary.accent) + ), }, // Disabled Styles '&:disabled, &.disabled': { - [buttonStencil.vars.background]: brand.primary.base, - [buttonStencil.vars.label]: brand.primary.accent, + [buttonStencil.vars.background]: cssVar(brand.action.base, brand.primary.base), + [buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent), [buttonStencil.vars.opacity]: system.opacity.disabled, [systemIconStencil.vars.color]: cssVar( buttonColorPropVars.disabled.icon, - brand.primary.accent + cssVar(brand.action.accent, brand.primary.accent) ), }, }, diff --git a/modules/react/button/stories/button/Button.mdx b/modules/react/button/stories/button/Button.mdx index ee00a5b545..7cc19dabff 100644 --- a/modules/react/button/stories/button/Button.mdx +++ b/modules/react/button/stories/button/Button.mdx @@ -14,6 +14,7 @@ import { TertiaryInverse } from './examples/TertiaryInverse'; import { Delete } from './examples/Delete'; import { Grow } from './examples/Grow'; import { CustomStyles } from './examples/CustomStyles'; +import { ThemeOverrides } from './examples/ThemeOverrides'; import * as ButtonStories from './Button.stories'; @@ -94,6 +95,12 @@ or view the example below. +### Theme Overrides + +The most common way to theme our buttons is to pass a `theme` object at the root level of the application via the `CanvasProvider`. In the example below, our buttons use our `brand.action.**` tokens with the fallback being `brand.primary.**`. + + + ### Accessible Use of the `as` Prop Like many of our components, Buttons accept an `as` prop, which lets you change the underlying diff --git a/modules/react/button/stories/button/Button.stories.tsx b/modules/react/button/stories/button/Button.stories.tsx index a863207de8..fc1305d7b3 100644 --- a/modules/react/button/stories/button/Button.stories.tsx +++ b/modules/react/button/stories/button/Button.stories.tsx @@ -9,6 +9,7 @@ import {Tertiary as TertiaryExample} from './examples/Tertiary'; import {TertiaryInverse as TertiaryInverseExample} from './examples/TertiaryInverse'; import {Delete as DeleteExample} from './examples/Delete'; import {CustomStyles as CustomStylesExample} from './examples/CustomStyles'; +import {ThemeOverrides as ThemeOverridesExample} from './examples/ThemeOverrides'; export default { title: 'Components/Buttons', @@ -44,3 +45,7 @@ export const Delete = { export const CustomStyles = { render: CustomStylesExample, }; + +export const ThemeOverrides = { + render: ThemeOverridesExample, +}; diff --git a/modules/react/button/stories/button/examples/ThemeOverrides.tsx b/modules/react/button/stories/button/examples/ThemeOverrides.tsx new file mode 100644 index 0000000000..3319ca50e3 --- /dev/null +++ b/modules/react/button/stories/button/examples/ThemeOverrides.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import {PrimaryButton} from '@workday/canvas-kit-react/button'; +import {Flex} from '@workday/canvas-kit-react/layout'; +import { + plusIcon, + relatedActionsVerticalIcon, + caretDownIcon, +} from '@workday/canvas-system-icons-web'; +import {createStyles} from '@workday/canvas-kit-styling'; +import {system} from '@workday/canvas-tokens-web'; +import {CanvasProvider} from '@workday/canvas-kit-react/common'; +import {Heading} from '@workday/canvas-kit-react/text'; + +const parentContainerStyles = createStyles({ + gap: system.space.x4, + padding: system.space.x4, +}); + +export const ThemeOverrides = () => ( +
+ + Override Primary Color + + + + Primary + + Primary + + + Primary + + + + + + Override Action Color + + + + Primary + + Primary + + + Primary + + + + +
+); diff --git a/modules/react/button/stories/visual-testing/PrimaryButton.stories.tsx b/modules/react/button/stories/visual-testing/PrimaryButton.stories.tsx index dad869830e..0acf0452d7 100644 --- a/modules/react/button/stories/visual-testing/PrimaryButton.stories.tsx +++ b/modules/react/button/stories/visual-testing/PrimaryButton.stories.tsx @@ -9,6 +9,7 @@ import {customColorTheme} from '../../../../../utils/storybook'; import {playCircleIcon, relatedActionsVerticalIcon} from '@workday/canvas-system-icons-web'; import {PrimaryButton} from '@workday/canvas-kit-react/button'; import {Container, stateTableColumnProps} from './utils'; +import {customColorThemeWithAction} from '../../../../../utils/storybook/customThemes'; export default { title: 'Testing/Buttons/Button/Primary Button', @@ -99,6 +100,10 @@ export const PrimaryButtonThemedStates = { render: () => , }; +export const PrimaryButtonThemedActionStates = { + render: () => , +}; + export const PrimaryIconButtonThemedStates = { render: () => , }; diff --git a/modules/react/common/lib/CanvasProvider.tsx b/modules/react/common/lib/CanvasProvider.tsx index 95c69ddc34..7ec9bd5622 100644 --- a/modules/react/common/lib/CanvasProvider.tsx +++ b/modules/react/common/lib/CanvasProvider.tsx @@ -44,6 +44,12 @@ const defaultBranding = createStyles({ [brand.primary.base]: base.blueberry400, [brand.primary.light]: base.blueberry200, [brand.primary.lightest]: base.blueberry100, + [brand.action.accent]: base.frenchVanilla100, + [brand.action.darkest]: base.blueberry600, + [brand.action.dark]: base.blueberry500, + [brand.action.base]: base.blueberry400, + [brand.action.light]: base.blueberry200, + [brand.action.lightest]: base.blueberry100, [brand.gradient .primary]: `linear-gradient(90deg, ${brand.primary.base} 0%, ${brand.primary.dark} 100%)`, }); @@ -65,20 +71,22 @@ export const useCanvasThemeToCssVars = ( const className = (elemProps.className || '').split(' ').concat(defaultBranding).join(' '); const style = elemProps.style || {}; const {palette} = filledTheme.canvas; - (['common', 'primary', 'error', 'alert', 'success', 'neutral'] as const).forEach(color => { - if (color === 'common') { - // @ts-ignore - style[brand.common.focusOutline] = palette.common.focusOutline; - } - (['lightest', 'light', 'main', 'dark', 'darkest', 'contrast'] as const).forEach(key => { - // We only want to set custom colors if they do not match the default. The `defaultBranding` class will take care of the rest. - // @ts-ignore - if (palette[color][key] !== defaultCanvasTheme.palette[color][key]) { + (['common', 'primary', 'error', 'alert', 'success', 'neutral', 'action'] as const).forEach( + color => { + if (color === 'common') { // @ts-ignore - style[brand[color][mappedKeys[key]]] = palette[color][key]; + style[brand.common.focusOutline] = palette.common.focusOutline; } - }); - }); + (['lightest', 'light', 'main', 'dark', 'darkest', 'contrast'] as const).forEach(key => { + // We only want to set custom colors if they do not match the default. The `defaultBranding` class will take care of the rest. + // @ts-ignore + if (palette[color][key] !== defaultCanvasTheme.palette[color][key]) { + // @ts-ignore + style[brand[color][mappedKeys[key]]] = palette[color][key]; + } + }); + } + ); return {...elemProps, className, style}; }; diff --git a/modules/react/common/lib/theming/createCanvasTheme.ts b/modules/react/common/lib/theming/createCanvasTheme.ts index f8cda7d6f6..6188586f95 100644 --- a/modules/react/common/lib/theming/createCanvasTheme.ts +++ b/modules/react/common/lib/theming/createCanvasTheme.ts @@ -80,12 +80,13 @@ function fillPalette( function calculateCanvasTheme(partialTheme: PartialCanvasTheme): CanvasTheme { const {palette = {}, breakpoints = {}, direction, ...extraFields} = partialTheme; - const {primary, alert, error, success, neutral, common = {}} = palette!; + const {primary, alert, error, success, neutral, action, common = {}} = palette!; const mergeable: PartialCanvasTheme = { palette: { common, primary: fillPalette(defaultCanvasTheme.palette.primary, primary), + action: fillPalette(defaultCanvasTheme.palette.primary, action || primary), alert: fillPalette(defaultCanvasTheme.palette.alert, alert), error: fillPalette(defaultCanvasTheme.palette.error, error), success: fillPalette(defaultCanvasTheme.palette.success, success), diff --git a/modules/react/common/lib/theming/theme.ts b/modules/react/common/lib/theming/theme.ts index f9b2fa97eb..eef779f1d5 100644 --- a/modules/react/common/lib/theming/theme.ts +++ b/modules/react/common/lib/theming/theme.ts @@ -12,6 +12,14 @@ export const defaultCanvasTheme: CanvasTheme = { darkest: colors.blueberry600, contrast: colors.frenchVanilla100, }, + action: { + lightest: colors.blueberry100, + light: colors.blueberry200, + main: colors.blueberry400, + dark: colors.blueberry500, + darkest: colors.blueberry600, + contrast: colors.frenchVanilla100, + }, alert: { lightest: colors.cantaloupe100, light: colors.cantaloupe300, diff --git a/modules/react/common/lib/theming/types.ts b/modules/react/common/lib/theming/types.ts index 8e7c8be176..8e7bc7c6ba 100644 --- a/modules/react/common/lib/theming/types.ts +++ b/modules/react/common/lib/theming/types.ts @@ -37,6 +37,7 @@ export interface CanvasTheme { alert: CanvasThemePalette; success: CanvasThemePalette; neutral: CanvasThemePalette; + action: CanvasThemePalette; }; /** * ### Theme Breakpoints diff --git a/modules/react/common/spec/createCanvasTheme.spec.tsx b/modules/react/common/spec/createCanvasTheme.spec.tsx index 3a7200a479..cedecfe0b4 100644 --- a/modules/react/common/spec/createCanvasTheme.spec.tsx +++ b/modules/react/common/spec/createCanvasTheme.spec.tsx @@ -23,10 +23,20 @@ describe('createCanvasTheme', () => { const input = { palette: { primary: palette, + action: palette, }, }; const theme = createCanvasTheme(input); + const expected = {...defaultCanvasTheme}; + expected.palette.action = { + lightest: 'orange', + light: 'orange', + main: 'orange', + dark: 'orange', + darkest: 'orange', + contrast: 'orange', + }; expected.palette.primary = palette; expect(theme).toEqual(expected); @@ -51,6 +61,15 @@ describe('createCanvasTheme', () => { contrast: '#494949', }; + expected.palette.action = { + lightest: '#ffff7d', + light: '#ffd64a', + main: 'orange', + dark: '#c67600', + darkest: '#904a00', + contrast: '#494949', + }; + expect(theme).toEqual(expected); }); @@ -65,7 +84,40 @@ describe('createCanvasTheme', () => { const theme = createCanvasTheme(input); const expected = {...defaultCanvasTheme}; expected.palette.primary.dark = 'black'; + expected.palette.action.dark = 'black'; + expect(theme).toEqual(expected); + }); + + test('calling with a custom palette with action colors should keep the default primary color and only set action', () => { + const input = { + palette: { + primary: { + dark: 'black', + }, + action: { + dark: 'navy', + }, + }, + }; + const theme = createCanvasTheme(input); + const expected = {...defaultCanvasTheme}; + expected.palette.primary.dark = 'black'; + expected.palette.action.dark = 'navy'; + expect(theme).toEqual(expected); + }); + test('if not action color is defined, it should default to primary color', () => { + const input = { + palette: { + primary: { + dark: 'black', + }, + }, + }; + const theme = createCanvasTheme(input); + const expected = {...defaultCanvasTheme}; + expected.palette.primary.dark = 'black'; + expected.palette.action.dark = 'black'; expect(theme).toEqual(expected); }); diff --git a/modules/react/package.json b/modules/react/package.json index 5d1f733e0c..ae8ca4a1e9 100644 --- a/modules/react/package.json +++ b/modules/react/package.json @@ -52,7 +52,7 @@ "@workday/canvas-kit-popup-stack": "^12.2.2", "@workday/canvas-kit-styling": "^12.2.2", "@workday/canvas-system-icons-web": "^3.0.0", - "@workday/canvas-tokens-web": "^2.0.1", + "@workday/canvas-tokens-web": "^2.1.0", "@workday/design-assets-types": "^0.2.8", "chroma-js": "^2.2.0", "csstype": "^3.0.2", diff --git a/modules/styling-transform/package.json b/modules/styling-transform/package.json index 0e206a202c..2ccbfb74dd 100644 --- a/modules/styling-transform/package.json +++ b/modules/styling-transform/package.json @@ -35,7 +35,7 @@ "dependencies": { "@emotion/serialize": "^1.0.2", "@workday/canvas-kit-styling": "^12.2.2", - "@workday/canvas-tokens-web": "^2.0.1", + "@workday/canvas-tokens-web": "^2.1.0", "stylis": "4.0.13", "ts-node": "^10.9.1", "typescript": "5.0" diff --git a/modules/styling/package.json b/modules/styling/package.json index 862d45087e..4b2b5591e8 100644 --- a/modules/styling/package.json +++ b/modules/styling/package.json @@ -55,7 +55,7 @@ "@emotion/styled": "^11.6.0", "@workday/canvas-kit-react": "^12.2.2", "@workday/canvas-system-icons-web": "^3.0.0", - "@workday/canvas-tokens-web": "^2.0.1", + "@workday/canvas-tokens-web": "^2.1.0", "typescript": "5.0" } } diff --git a/package.json b/package.json index 2465bb3188..34904cefea 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "@workday/canvas-accent-icons-web": "^3.0.9", "@workday/canvas-applet-icons-web": "^2.0.9", "@workday/canvas-system-icons-web": "^3.0.0", - "@workday/canvas-tokens-web": "^2.0.1", + "@workday/canvas-tokens-web": "^2.1.0", "jest-environment-jsdom": "^29.7.0", "ts-jest": "^29.2.4" }, diff --git a/utils/storybook/customThemes.ts b/utils/storybook/customThemes.ts index 16da8f120a..b93181a830 100644 --- a/utils/storybook/customThemes.ts +++ b/utils/storybook/customThemes.ts @@ -23,3 +23,30 @@ export const customColorTheme: PartialCanvasTheme = { }, }, }; + +export const customColorThemeWithAction: PartialCanvasTheme = { + palette: { + primary: { + main: 'purple', + contrast: 'turquoise', + }, + action: { + main: 'teal', + }, + alert: { + main: 'coral', + }, + error: { + main: 'crimson', + }, + success: { + main: 'aquamarine', + }, + neutral: { + main: 'gray', + }, + common: { + focusOutline: 'turquoise', + }, + }, +}; diff --git a/yarn.lock b/yarn.lock index 34fcc999f0..32ecdfef83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6158,10 +6158,10 @@ dependencies: "@workday/design-assets-types" "0.2.8" -"@workday/canvas-tokens-web@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@workday/canvas-tokens-web/-/canvas-tokens-web-2.0.1.tgz#b7d3df6849a869b0adf57e9b9ab924c6a4f778da" - integrity sha512-PRfhnqFEe08kjn0vQPicfHh5zCCIYgunnCLYfSpVkwBX1OZHEFvCpB0pmz67P3313sty0/BmYWgzPmv3VT9mkA== +"@workday/canvas-tokens-web@^2.1.0": + version "2.1.0" + resolved "https://registry.npmjs.org/@workday/canvas-tokens-web/-/canvas-tokens-web-2.1.0.tgz#3fa68416c003dd84a217909db556ab385360a34e" + integrity sha512-qDkhronvKRmautzHWLAlGYwI0UsrXmRN07EFCHPROFAbVglMdd1D3LIeP2ECtQ76qOcEVJM79vQJdwai4B9K7g== "@workday/design-assets-types@0.2.10": version "0.2.10" @@ -15684,6 +15684,7 @@ string-replace-loader@^3.1.0: schema-utils "^3.0.0" "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16987,6 +16988,7 @@ wordwrap@^1.0.0: integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==