Skip to content

Commit d950415

Browse files
mannycarrera4manuel.carrerajosh-bagwell
authoredJan 9, 2025··
feat: Add action tokens to PrimaryButton with brand base as fallback (#3081)
Fixes: #3072 [category:Components] Release Note: In `@workday/canvas-tokens-web@2.1.0` we've added new `action` tokens. In most cases you'll use `brand.primary.**` tokens to override theme. However, in the unique cases where you need additional control over styling `PrimaryButton` you can override `brand.action.**` tokens for more control. This PR ensures that `PrimaryButton` uses the new `action` tokens with `brand.base.**` being a fallback token. There should be no breaking changes as this is an addition to provide more control of theming our `PrimaryButton` component. **Note: If `band.action.**` token is set at the theme level in the `CanvasProvider`, `brand.pimrary**` will be overwritten.** Co-authored-by: manuel.carrera <manuel.carrera@workday.com> Co-authored-by: @josh-bagwell <44883293+josh-bagwell@users.noreply.github.com>
1 parent 340f917 commit d950415

File tree

19 files changed

+240
-39
lines changed

19 files changed

+240
-39
lines changed
 

‎modules/docs/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"@workday/canvas-kit-react": "^12.2.2",
5050
"@workday/canvas-kit-styling": "^12.2.2",
5151
"@workday/canvas-system-icons-web": "^3.0.0",
52-
"@workday/canvas-tokens-web": "^2.0.1",
52+
"@workday/canvas-tokens-web": "^2.1.0",
5353
"markdown-to-jsx": "^7.2.0",
5454
"react-syntax-highlighter": "^15.5.0",
5555
"ts-node": "^10.9.1"

‎modules/labs-react/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"@workday/canvas-kit-react": "^12.2.2",
5050
"@workday/canvas-kit-styling": "^12.2.2",
5151
"@workday/canvas-system-icons-web": "^3.0.0",
52-
"@workday/canvas-tokens-web": "^2.0.1",
52+
"@workday/canvas-tokens-web": "^2.1.0",
5353
"@workday/design-assets-types": "^0.2.8",
5454
"chroma-js": "^2.2.0",
5555
"lodash.flatten": "^4.4.0",

‎modules/preview-react/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"@workday/canvas-kit-react": "^12.2.2",
5050
"@workday/canvas-kit-styling": "^12.2.2",
5151
"@workday/canvas-system-icons-web": "^3.0.0",
52-
"@workday/canvas-tokens-web": "^2.0.1",
52+
"@workday/canvas-tokens-web": "^2.1.0",
5353
"@workday/design-assets-types": "^0.2.8"
5454
},
5555
"devDependencies": {

‎modules/react/button/lib/PrimaryButton.tsx

+27-15
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,50 @@ const primaryButtonStencil = createStencil({
2323
extends: buttonStencil,
2424
base: {
2525
// Base Styles
26-
[buttonStencil.vars.background]: brand.primary.base,
26+
[buttonStencil.vars.background]: cssVar(brand.action.base, brand.primary.base),
2727
[buttonStencil.vars.borderRadius]: system.shape.round,
28-
[buttonStencil.vars.label]: brand.primary.accent,
29-
[systemIconStencil.vars.color]: cssVar(buttonColorPropVars.default.icon, brand.primary.accent),
28+
[buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent),
29+
[systemIconStencil.vars.color]: cssVar(
30+
buttonColorPropVars.default.icon,
31+
cssVar(brand.action.accent, brand.primary.accent)
32+
),
3033
// Focus Styles
3134
'&:focus-visible, &.focus': {
32-
[buttonStencil.vars.background]: brand.primary.base,
33-
[buttonStencil.vars.label]: brand.primary.accent,
35+
[buttonStencil.vars.background]: cssVar(brand.action.base, brand.primary.base),
36+
[buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent),
3437
[buttonStencil.vars.boxShadowInner]: system.color.border.inverse,
3538
[buttonStencil.vars.boxShadowOuter]: brand.common.focusOutline,
36-
[systemIconStencil.vars.color]: cssVar(buttonColorPropVars.focus.icon, brand.primary.accent),
39+
[systemIconStencil.vars.color]: cssVar(
40+
buttonColorPropVars.focus.icon,
41+
cssVar(brand.action.accent, brand.primary.accent)
42+
),
3743
},
3844
// Hover Styles
3945
'&:hover, &.hover': {
40-
[buttonStencil.vars.background]: brand.primary.dark,
41-
[buttonStencil.vars.label]: brand.primary.accent,
42-
[systemIconStencil.vars.color]: cssVar(buttonColorPropVars.hover.icon, brand.primary.accent),
46+
[buttonStencil.vars.background]: cssVar(brand.action.dark, brand.primary.dark),
47+
[buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent),
48+
[systemIconStencil.vars.color]: cssVar(
49+
buttonColorPropVars.hover.icon,
50+
cssVar(brand.action.accent, brand.primary.accent)
51+
),
4352
},
4453
// Active Styles
4554
'&:active, &.active': {
46-
[buttonStencil.vars.background]: brand.primary.darkest,
47-
[buttonStencil.vars.label]: brand.primary.accent,
48-
[systemIconStencil.vars.color]: cssVar(buttonColorPropVars.active.icon, brand.primary.accent),
55+
[buttonStencil.vars.background]: cssVar(brand.action.darkest, brand.primary.darkest),
56+
[buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent),
57+
[systemIconStencil.vars.color]: cssVar(
58+
buttonColorPropVars.active.icon,
59+
cssVar(brand.action.accent, brand.primary.accent)
60+
),
4961
},
5062
// Disabled Styles
5163
'&:disabled, &.disabled': {
52-
[buttonStencil.vars.background]: brand.primary.base,
53-
[buttonStencil.vars.label]: brand.primary.accent,
64+
[buttonStencil.vars.background]: cssVar(brand.action.base, brand.primary.base),
65+
[buttonStencil.vars.label]: cssVar(brand.action.accent, brand.primary.accent),
5466
[buttonStencil.vars.opacity]: system.opacity.disabled,
5567
[systemIconStencil.vars.color]: cssVar(
5668
buttonColorPropVars.disabled.icon,
57-
brand.primary.accent
69+
cssVar(brand.action.accent, brand.primary.accent)
5870
),
5971
},
6072
},

‎modules/react/button/stories/button/Button.mdx

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { TertiaryInverse } from './examples/TertiaryInverse';
1414
import { Delete } from './examples/Delete';
1515
import { Grow } from './examples/Grow';
1616
import { CustomStyles } from './examples/CustomStyles';
17+
import { ThemeOverrides } from './examples/ThemeOverrides';
1718

1819
import * as ButtonStories from './Button.stories';
1920

@@ -94,6 +95,12 @@ or view the example below.
9495

9596
<ExampleCodeBlock code={CustomStyles} />
9697

98+
### Theme Overrides
99+
100+
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.**`.
101+
102+
<ExampleCodeBlock code={ThemeOverrides} />
103+
97104
### Accessible Use of the `as` Prop
98105

99106
Like many of our components, Buttons accept an `as` prop, which lets you change the underlying

‎modules/react/button/stories/button/Button.stories.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {Tertiary as TertiaryExample} from './examples/Tertiary';
99
import {TertiaryInverse as TertiaryInverseExample} from './examples/TertiaryInverse';
1010
import {Delete as DeleteExample} from './examples/Delete';
1111
import {CustomStyles as CustomStylesExample} from './examples/CustomStyles';
12+
import {ThemeOverrides as ThemeOverridesExample} from './examples/ThemeOverrides';
1213

1314
export default {
1415
title: 'Components/Buttons',
@@ -44,3 +45,7 @@ export const Delete = {
4445
export const CustomStyles = {
4546
render: CustomStylesExample,
4647
};
48+
49+
export const ThemeOverrides = {
50+
render: ThemeOverridesExample,
51+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react';
2+
3+
import {PrimaryButton} from '@workday/canvas-kit-react/button';
4+
import {Flex} from '@workday/canvas-kit-react/layout';
5+
import {
6+
plusIcon,
7+
relatedActionsVerticalIcon,
8+
caretDownIcon,
9+
} from '@workday/canvas-system-icons-web';
10+
import {createStyles} from '@workday/canvas-kit-styling';
11+
import {system} from '@workday/canvas-tokens-web';
12+
import {CanvasProvider} from '@workday/canvas-kit-react/common';
13+
import {Heading} from '@workday/canvas-kit-react/text';
14+
15+
const parentContainerStyles = createStyles({
16+
gap: system.space.x4,
17+
padding: system.space.x4,
18+
});
19+
20+
export const ThemeOverrides = () => (
21+
<div>
22+
<Heading size="medium" as="h3">
23+
Override Primary Color
24+
</Heading>
25+
<CanvasProvider
26+
theme={{
27+
canvas: {
28+
palette: {
29+
action: {
30+
main: 'teal',
31+
},
32+
},
33+
},
34+
}}
35+
>
36+
<Flex cs={parentContainerStyles}>
37+
<PrimaryButton>Primary</PrimaryButton>
38+
<PrimaryButton icon={plusIcon} iconPosition="start">
39+
Primary
40+
</PrimaryButton>
41+
<PrimaryButton icon={caretDownIcon} iconPosition="end">
42+
Primary
43+
</PrimaryButton>
44+
<PrimaryButton aria-label="Related Actions" icon={relatedActionsVerticalIcon} />
45+
</Flex>
46+
</CanvasProvider>
47+
<Heading size="medium" as="h3">
48+
Override Action Color
49+
</Heading>
50+
<CanvasProvider
51+
theme={{
52+
canvas: {
53+
palette: {
54+
primary: {
55+
main: 'navy',
56+
},
57+
},
58+
},
59+
}}
60+
>
61+
<Flex cs={parentContainerStyles}>
62+
<PrimaryButton>Primary</PrimaryButton>
63+
<PrimaryButton icon={plusIcon} iconPosition="start">
64+
Primary
65+
</PrimaryButton>
66+
<PrimaryButton icon={caretDownIcon} iconPosition="end">
67+
Primary
68+
</PrimaryButton>
69+
<PrimaryButton aria-label="Related Actions" icon={relatedActionsVerticalIcon} />
70+
</Flex>
71+
</CanvasProvider>
72+
</div>
73+
);

‎modules/react/button/stories/visual-testing/PrimaryButton.stories.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {customColorTheme} from '../../../../../utils/storybook';
99
import {playCircleIcon, relatedActionsVerticalIcon} from '@workday/canvas-system-icons-web';
1010
import {PrimaryButton} from '@workday/canvas-kit-react/button';
1111
import {Container, stateTableColumnProps} from './utils';
12+
import {customColorThemeWithAction} from '../../../../../utils/storybook/customThemes';
1213

1314
export default {
1415
title: 'Testing/Buttons/Button/Primary Button',
@@ -99,6 +100,10 @@ export const PrimaryButtonThemedStates = {
99100
render: () => <PrimaryButtonTest theme={{canvas: customColorTheme}} />,
100101
};
101102

103+
export const PrimaryButtonThemedActionStates = {
104+
render: () => <PrimaryButtonTest theme={{canvas: customColorThemeWithAction}} />,
105+
};
106+
102107
export const PrimaryIconButtonThemedStates = {
103108
render: () => <PrimaryIconButtonTest theme={{canvas: customColorTheme}} />,
104109
};

‎modules/react/common/lib/CanvasProvider.tsx

+20-12
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ const defaultBranding = createStyles({
4444
[brand.primary.base]: base.blueberry400,
4545
[brand.primary.light]: base.blueberry200,
4646
[brand.primary.lightest]: base.blueberry100,
47+
[brand.action.accent]: base.frenchVanilla100,
48+
[brand.action.darkest]: base.blueberry600,
49+
[brand.action.dark]: base.blueberry500,
50+
[brand.action.base]: base.blueberry400,
51+
[brand.action.light]: base.blueberry200,
52+
[brand.action.lightest]: base.blueberry100,
4753
[brand.gradient
4854
.primary]: `linear-gradient(90deg, ${brand.primary.base} 0%, ${brand.primary.dark} 100%)`,
4955
});
@@ -65,20 +71,22 @@ export const useCanvasThemeToCssVars = (
6571
const className = (elemProps.className || '').split(' ').concat(defaultBranding).join(' ');
6672
const style = elemProps.style || {};
6773
const {palette} = filledTheme.canvas;
68-
(['common', 'primary', 'error', 'alert', 'success', 'neutral'] as const).forEach(color => {
69-
if (color === 'common') {
70-
// @ts-ignore
71-
style[brand.common.focusOutline] = palette.common.focusOutline;
72-
}
73-
(['lightest', 'light', 'main', 'dark', 'darkest', 'contrast'] as const).forEach(key => {
74-
// We only want to set custom colors if they do not match the default. The `defaultBranding` class will take care of the rest.
75-
// @ts-ignore
76-
if (palette[color][key] !== defaultCanvasTheme.palette[color][key]) {
74+
(['common', 'primary', 'error', 'alert', 'success', 'neutral', 'action'] as const).forEach(
75+
color => {
76+
if (color === 'common') {
7777
// @ts-ignore
78-
style[brand[color][mappedKeys[key]]] = palette[color][key];
78+
style[brand.common.focusOutline] = palette.common.focusOutline;
7979
}
80-
});
81-
});
80+
(['lightest', 'light', 'main', 'dark', 'darkest', 'contrast'] as const).forEach(key => {
81+
// We only want to set custom colors if they do not match the default. The `defaultBranding` class will take care of the rest.
82+
// @ts-ignore
83+
if (palette[color][key] !== defaultCanvasTheme.palette[color][key]) {
84+
// @ts-ignore
85+
style[brand[color][mappedKeys[key]]] = palette[color][key];
86+
}
87+
});
88+
}
89+
);
8290
return {...elemProps, className, style};
8391
};
8492

‎modules/react/common/lib/theming/createCanvasTheme.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,13 @@ function fillPalette(
8080

8181
function calculateCanvasTheme(partialTheme: PartialCanvasTheme): CanvasTheme {
8282
const {palette = {}, breakpoints = {}, direction, ...extraFields} = partialTheme;
83-
const {primary, alert, error, success, neutral, common = {}} = palette!;
83+
const {primary, alert, error, success, neutral, action, common = {}} = palette!;
8484

8585
const mergeable: PartialCanvasTheme = {
8686
palette: {
8787
common,
8888
primary: fillPalette(defaultCanvasTheme.palette.primary, primary),
89+
action: fillPalette(defaultCanvasTheme.palette.primary, action || primary),
8990
alert: fillPalette(defaultCanvasTheme.palette.alert, alert),
9091
error: fillPalette(defaultCanvasTheme.palette.error, error),
9192
success: fillPalette(defaultCanvasTheme.palette.success, success),

‎modules/react/common/lib/theming/theme.ts

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ export const defaultCanvasTheme: CanvasTheme = {
1212
darkest: colors.blueberry600,
1313
contrast: colors.frenchVanilla100,
1414
},
15+
action: {
16+
lightest: colors.blueberry100,
17+
light: colors.blueberry200,
18+
main: colors.blueberry400,
19+
dark: colors.blueberry500,
20+
darkest: colors.blueberry600,
21+
contrast: colors.frenchVanilla100,
22+
},
1523
alert: {
1624
lightest: colors.cantaloupe100,
1725
light: colors.cantaloupe300,

‎modules/react/common/lib/theming/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface CanvasTheme {
3737
alert: CanvasThemePalette;
3838
success: CanvasThemePalette;
3939
neutral: CanvasThemePalette;
40+
action: CanvasThemePalette;
4041
};
4142
/**
4243
* ### Theme Breakpoints

‎modules/react/common/spec/createCanvasTheme.spec.tsx

+52
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,20 @@ describe('createCanvasTheme', () => {
2323
const input = {
2424
palette: {
2525
primary: palette,
26+
action: palette,
2627
},
2728
};
2829
const theme = createCanvasTheme(input);
30+
2931
const expected = {...defaultCanvasTheme};
32+
expected.palette.action = {
33+
lightest: 'orange',
34+
light: 'orange',
35+
main: 'orange',
36+
dark: 'orange',
37+
darkest: 'orange',
38+
contrast: 'orange',
39+
};
3040
expected.palette.primary = palette;
3141

3242
expect(theme).toEqual(expected);
@@ -51,6 +61,15 @@ describe('createCanvasTheme', () => {
5161
contrast: '#494949',
5262
};
5363

64+
expected.palette.action = {
65+
lightest: '#ffff7d',
66+
light: '#ffd64a',
67+
main: 'orange',
68+
dark: '#c67600',
69+
darkest: '#904a00',
70+
contrast: '#494949',
71+
};
72+
5473
expect(theme).toEqual(expected);
5574
});
5675

@@ -65,7 +84,40 @@ describe('createCanvasTheme', () => {
6584
const theme = createCanvasTheme(input);
6685
const expected = {...defaultCanvasTheme};
6786
expected.palette.primary.dark = 'black';
87+
expected.palette.action.dark = 'black';
88+
expect(theme).toEqual(expected);
89+
});
90+
91+
test('calling with a custom palette with action colors should keep the default primary color and only set action', () => {
92+
const input = {
93+
palette: {
94+
primary: {
95+
dark: 'black',
96+
},
97+
action: {
98+
dark: 'navy',
99+
},
100+
},
101+
};
102+
const theme = createCanvasTheme(input);
103+
const expected = {...defaultCanvasTheme};
104+
expected.palette.primary.dark = 'black';
105+
expected.palette.action.dark = 'navy';
106+
expect(theme).toEqual(expected);
107+
});
68108

109+
test('if not action color is defined, it should default to primary color', () => {
110+
const input = {
111+
palette: {
112+
primary: {
113+
dark: 'black',
114+
},
115+
},
116+
};
117+
const theme = createCanvasTheme(input);
118+
const expected = {...defaultCanvasTheme};
119+
expected.palette.primary.dark = 'black';
120+
expected.palette.action.dark = 'black';
69121
expect(theme).toEqual(expected);
70122
});
71123

‎modules/react/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@workday/canvas-kit-popup-stack": "^12.2.2",
5353
"@workday/canvas-kit-styling": "^12.2.2",
5454
"@workday/canvas-system-icons-web": "^3.0.0",
55-
"@workday/canvas-tokens-web": "^2.0.1",
55+
"@workday/canvas-tokens-web": "^2.1.0",
5656
"@workday/design-assets-types": "^0.2.8",
5757
"chroma-js": "^2.2.0",
5858
"csstype": "^3.0.2",

0 commit comments

Comments
 (0)
Please sign in to comment.