diff --git a/.changeset/many-apes-play.md b/.changeset/many-apes-play.md new file mode 100644 index 0000000000..78274513b8 --- /dev/null +++ b/.changeset/many-apes-play.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/wonder-blocks-button": patch +--- + +Use `semanticColor` in Button. This replaces the use of `color` primitive tokens in favor of semantic color tokens diff --git a/__docs__/wonder-blocks-button/button-variants.stories.tsx b/__docs__/wonder-blocks-button/button-variants.stories.tsx index cea281d5c8..068c8a5979 100644 --- a/__docs__/wonder-blocks-button/button-variants.stories.tsx +++ b/__docs__/wonder-blocks-button/button-variants.stories.tsx @@ -17,9 +17,6 @@ import Button from "@khanacademy/wonder-blocks-button"; export default { title: "Packages / Button / All Variants", parameters: { - docs: { - autodocs: false, - }, chromatic: { // NOTE: This is required to prevent Chromatic from cutting off the // dark background in screenshots (accounts for all the space taken @@ -27,6 +24,7 @@ export default { viewports: [1700], }, }, + tags: ["!autodocs"], } as Meta; type StoryComponentType = StoryObj; @@ -115,13 +113,29 @@ const KindVariants = ({light}: {light: boolean}) => { {sizes.map((size) => ( <> {colors.map((color) => ( + <> + + {light && ( + + )} + + ))} + {!light && ( - ))} - + )} ))} diff --git a/__docs__/wonder-blocks-button/button.stories.tsx b/__docs__/wonder-blocks-button/button.stories.tsx index ffe8e691c4..e827f8b221 100644 --- a/__docs__/wonder-blocks-button/button.stories.tsx +++ b/__docs__/wonder-blocks-button/button.stories.tsx @@ -187,26 +187,28 @@ WithColor.parameters = { }; export const Dark: StoryComponentType = () => ( - - + + - - - - + - - - - + - - + + + + + ); diff --git a/packages/wonder-blocks-button/src/components/__tests__/button-with-icon.test.tsx b/packages/wonder-blocks-button/src/components/__tests__/button-with-icon.test.tsx index 88b25fea9d..6ba350ab5b 100644 --- a/packages/wonder-blocks-button/src/components/__tests__/button-with-icon.test.tsx +++ b/packages/wonder-blocks-button/src/components/__tests__/button-with-icon.test.tsx @@ -228,50 +228,4 @@ describe("button with icon", () => { expect(icon).toBeInTheDocument(); expect(icon).toHaveAttribute("aria-hidden", "true"); }); - - test("default theme tertiary button icon has no hover style", async () => { - // Arrange - render( - , - ); - - // Act - const button = await screen.findByTestId("button-icon-test"); - const iconWrapper = await screen.findByTestId( - "button-icon-test-end-icon-wrapper", - ); - await userEvent.hover(button); - - // Assert - expect(iconWrapper).toHaveStyle(`backgroundColor: transparent`); - }); - - test("Khanmigo tertiary button icon has hover style", async () => { - // Arrange - render( - - - , - ); - - // Act - const button = await screen.findByTestId("button-icon-test"); - const iconWrapper = await screen.findByTestId( - "button-icon-test-end-icon-wrapper", - ); - await userEvent.hover(button); - - // Assert - expect(iconWrapper).toHaveStyle( - `backgroundColor: ${color.fadedBlue16}`, - ); - }); }); diff --git a/packages/wonder-blocks-button/src/components/button-core.tsx b/packages/wonder-blocks-button/src/components/button-core.tsx index af7900bbe9..36be2a2c0f 100644 --- a/packages/wonder-blocks-button/src/components/button-core.tsx +++ b/packages/wonder-blocks-button/src/components/button-core.tsx @@ -182,7 +182,7 @@ const ButtonCore: React.ForwardRefExoticComponent< sharedStyles.endIconWrapperTertiary, (focused || hovered) && kind !== "primary" && - sharedStyles.iconWrapperSecondaryHovered, + buttonStyles.iconWrapperHovered, ]} > = (theme) => ({ // this by setting the minWidth to auto. minWidth: "auto", }, - iconWrapperSecondaryHovered: { - backgroundColor: theme.color.bg.icon.secondaryHover, - color: theme.color.text.icon.secondaryHover, - }, endIconWrapper: { marginLeft: theme.padding.small, marginRight: theme.margin.icon.offset, @@ -344,48 +340,48 @@ export const _generateStyles = ( theme: ButtonThemeContract, themeName: string, ) => { - const color: string = - buttonColor === "destructive" - ? theme.color.bg.critical.default - : theme.color.bg.action.default; - - const buttonType = `${color}-${kind}-${light}-${size}-${themeName}`; + const buttonType = `${buttonColor}-${kind}-${light}-${size}-${themeName}`; if (styles[buttonType]) { return styles[buttonType]; } - const fadedColor = - buttonColor === "destructive" - ? theme.color.bg.critical.inverse - : theme.color.bg.action.inverse; - const activeColor = - buttonColor === "destructive" - ? theme.color.bg.critical.active - : theme.color.bg.action.active; const padding = size === "large" ? theme.padding.xLarge : theme.padding.large; + const colorToAction = light + ? buttonColor === "destructive" + ? "destructiveLight" + : "progressiveLight" + : buttonColor === "destructive" + ? "destructive" + : "progressive"; + let newStyles: Record = {}; if (kind === "primary") { + const themeColorAction = theme.color.filled[colorToAction]; + const focusStyling = { - outlineColor: light ? theme.color.bg.primary.default : color, + // TODO(WB-1856): Change with global focus token + outlineColor: themeColorAction.hover.border, outlineOffset: theme.border.offset.primary, outlineStyle: "solid", outlineWidth: theme.border.width.focused, }; const activePressedStyling = { - background: light ? fadedColor : activeColor, - outlineColor: light ? fadedColor : activeColor, + background: themeColorAction.press.background, + outlineColor: themeColorAction.press.border, + outlineOffset: theme.border.offset.primary, + outlineStyle: "solid", + outlineWidth: theme.border.width.focused, }; newStyles = { default: { - background: light ? theme.color.bg.primary.default : color, - color: light ? color : theme.color.text.inverse, - paddingLeft: padding, - paddingRight: padding, + background: themeColorAction.default.background, + color: themeColorAction.default.foreground, + paddingInline: padding, // TODO(WB-1844): Change this when we get final designs for // hover. [":hover:not([aria-disabled=true])" as any]: focusStyling, @@ -397,61 +393,42 @@ export const _generateStyles = ( focused: focusStyling, pressed: activePressedStyling, disabled: { - background: light - ? fadedColor - : theme.color.bg.primary.disabled, - color: light ? color : theme.color.text.primary.disabled, + background: themeColorAction.disabled.background, + color: themeColorAction.disabled.foreground, cursor: "default", ":focus-visible": { ...focusStyling, - outlineColor: light - ? fadedColor - : theme.color.bg.primary.disabled, + outlineColor: themeColorAction.disabled.border, }, }, }; } else if (kind === "secondary") { - const secondaryBorderColor = - buttonColor === "destructive" - ? theme.color.border.secondary.critical - : theme.color.border.secondary.action; - const secondaryActiveColor = - buttonColor === "destructive" - ? theme.color.bg.secondary.active.critical - : theme.color.bg.secondary.active.action; + const themeColorAction = theme.color.outlined[colorToAction]; const focusStyling = { - background: light - ? theme.color.bg.secondary.inverse - : theme.color.bg.secondary.focus, - borderColor: "transparent", - outlineColor: light ? theme.color.border.primary.inverse : color, + background: themeColorAction.hover.background, + outlineColor: themeColorAction.hover.border, outlineStyle: "solid", + outlineOffset: theme.border.offset.secondary, outlineWidth: theme.border.width.focused, }; const activePressedStyling = { - background: light ? activeColor : secondaryActiveColor, - color: light ? fadedColor : activeColor, - borderColor: "transparent", - outlineColor: light ? fadedColor : activeColor, + background: themeColorAction.press.background, + color: themeColorAction.press.foreground, + outlineColor: themeColorAction.press.border, outlineStyle: "solid", outlineWidth: theme.border.width.focused, }; newStyles = { default: { - background: light - ? theme.color.bg.secondary.inverse - : theme.color.bg.secondary.default, - color: light ? theme.color.text.inverse : color, - borderColor: light - ? theme.color.border.secondary.inverse - : secondaryBorderColor, + background: themeColorAction.default.background, + color: themeColorAction.default.foreground, + borderColor: themeColorAction.default.border, borderStyle: "solid", borderWidth: theme.border.width.secondary, - paddingLeft: padding, - paddingRight: padding, + paddingInline: padding, // TODO(WB-1844): Change this when we get final designs for // hover. [":hover:not([aria-disabled=true])" as any]: focusStyling, @@ -463,29 +440,34 @@ export const _generateStyles = ( focused: focusStyling, pressed: activePressedStyling, disabled: { - color: light - ? theme.color.text.secondary.inverse - : theme.color.text.disabled, - outlineColor: light ? fadedColor : theme.color.border.disabled, + color: themeColorAction.disabled.foreground, + borderColor: themeColorAction.disabled.border, cursor: "default", ":focus-visible": { - outlineColor: light - ? theme.color.border.secondary.inverse - : theme.color.border.disabled, + borderColor: themeColorAction.disabled.border, + outlineColor: themeColorAction.disabled.border, + outlineOffset: theme.border.offset.secondary, outlineStyle: "solid", outlineWidth: theme.border.width.disabled, }, }, + iconWrapperHovered: { + backgroundColor: themeColorAction.hover.icon, + color: themeColorAction.hover.foreground, + }, }; } else if (kind === "tertiary") { + const themeColorAction = theme.color.text[colorToAction]; + const focusStyling = { outlineStyle: "solid", - outlineColor: light ? theme.color.border.tertiary.inverse : color, + borderColor: "transparent", + outlineColor: themeColorAction.hover.border, outlineWidth: theme.border.width.focused, borderRadius: theme.border.radius.default, }; const activePressedStyling = { - color: light ? fadedColor : activeColor, + color: themeColorAction.press.foreground, textDecoration: "underline", textDecorationThickness: theme.size.underline.active, textUnderlineOffset: theme.font.offset.default, @@ -493,10 +475,9 @@ export const _generateStyles = ( newStyles = { default: { - background: "none", - color: light ? theme.color.text.inverse : color, - paddingLeft: 0, - paddingRight: 0, + background: themeColorAction.default.background, + color: themeColorAction.default.foreground, + paddingInline: 0, [":hover:not([aria-disabled=true])" as any]: { textUnderlineOffset: theme.font.offset.default, textDecoration: "underline", @@ -510,13 +491,16 @@ export const _generateStyles = ( focused: focusStyling, pressed: activePressedStyling, disabled: { - color: light ? fadedColor : theme.color.text.disabled, + color: themeColorAction.disabled.foreground, cursor: "default", + ":focus-visible": { + outlineColor: themeColorAction.disabled.border, + outlineStyle: "solid", + outlineWidth: theme.border.width.disabled, + }, }, disabledFocus: { - outlineColor: light - ? theme.color.border.tertiary.inverse - : theme.color.border.disabled, + outlineColor: themeColorAction.disabled.border, }, }; } else { diff --git a/packages/wonder-blocks-button/src/themes/default.ts b/packages/wonder-blocks-button/src/themes/default.ts index 95a79a41cc..a6c4ea5ef3 100644 --- a/packages/wonder-blocks-button/src/themes/default.ts +++ b/packages/wonder-blocks-button/src/themes/default.ts @@ -1,5 +1,7 @@ import * as tokens from "@khanacademy/wonder-blocks-tokens"; +const {semanticColor} = tokens; + // The underline-offset is the distance between the text baseline and the // bottom of the underline. This is necessary to prevent the underline from // breaking with descenders. @@ -7,95 +9,290 @@ const textUnderlineOffset = tokens.spacing.xxxSmall_4; const theme = { color: { - bg: { - /** - * Color - */ - // color="default" - action: { - default: tokens.color.blue, - active: tokens.color.activeBlue, - inverse: tokens.color.fadedBlue, - }, - // color="destructive" - critical: { - default: tokens.color.red, - active: tokens.color.activeRed, - inverse: tokens.color.fadedRed, + /** + * Primary + */ + filled: { + // kind=primary / color=default / light=false + progressive: { + ...semanticColor.action.filled.progressive, + disabled: { + border: semanticColor.action.disabled.default, + background: semanticColor.action.disabled.default, + foreground: semanticColor.action.disabled.secondary, + }, }, - /** - * Kind - */ - primary: { - default: tokens.color.white, - disabled: tokens.color.offBlack32, + // kind=primary / color=default / light=true + // NOTE: These colors will be removed from WB as soon as we remove the + // light variant. + progressiveLight: { + ...semanticColor.action.outlined.progressive, + hover: { + ...semanticColor.action.outlined.progressive.hover, + border: semanticColor.border.inverse, + }, + press: { + ...semanticColor.action.outlined.progressive.press, + border: semanticColor.action.outlined.progressive.press + .background, + }, + disabled: { + border: semanticColor.action.outlined.progressive.press + .background, + background: + semanticColor.action.outlined.progressive.press + .background, + foreground: + semanticColor.action.outlined.progressive.default + .foreground, + }, }, - - secondary: { - default: "none", - inverse: "none", - focus: tokens.color.white, - active: { - action: tokens.color.fadedBlue, - critical: tokens.color.fadedRed, + // kind=primary / color=destructive / light=false + destructive: { + ...semanticColor.action.filled.destructive, + disabled: { + border: semanticColor.action.disabled.default, + background: semanticColor.action.disabled.default, + foreground: semanticColor.action.disabled.secondary, }, }, - - /** - * Icons - */ - icon: { - secondaryHover: "transparent", + // kind=primary / color=destructive / light=true + // NOTE: These colors will be removed from WB as soon as we remove the + // light variant. + destructiveLight: { + ...semanticColor.action.outlined.destructive, + hover: { + ...semanticColor.action.outlined.progressive.hover, + border: semanticColor.border.inverse, + }, + press: { + ...semanticColor.action.outlined.destructive.press, + border: semanticColor.action.outlined.destructive.press + .background, + }, + disabled: { + border: semanticColor.action.outlined.destructive.press + .background, + background: + semanticColor.action.outlined.destructive.press + .background, + foreground: + semanticColor.action.outlined.destructive.default + .foreground, + }, }, }, - text: { - /** - * Default - */ - // kind="secondary, tertiary", disabled=true, light=false - disabled: tokens.color.offBlack32, - // kind="primary", light=false | kind="secondary, tertiary", light=true - inverse: tokens.color.white, - /** - * Kind - */ - primary: { - disabled: tokens.color.white64, + /** + * Secondary + * + * Outlined buttons + */ + outlined: { + // kind=secondary / color=default / light=false + progressive: { + ...semanticColor.action.outlined.progressive, + default: { + ...semanticColor.action.outlined.progressive.default, + // NOTE: This is a special case for the secondary button + background: "transparent", + }, + hover: { + ...semanticColor.action.outlined.progressive.hover, + // NOTE: This is a special case for the secondary button + background: "transparent", + icon: "transparent", + }, + disabled: { + border: semanticColor.action.disabled.default, + background: + semanticColor.action.outlined.progressive.press + .background, + foreground: semanticColor.text.disabled, + }, }, - secondary: { - inverse: tokens.color.white50, + // kind=secondary / color=default / light=true + // NOTE: These colors will be removed from WB as soon as we remove the + // light variant. + progressiveLight: { + default: { + border: tokens.color.white64, + background: "transparent", + foreground: semanticColor.text.inverse, + }, + hover: { + border: semanticColor.border.inverse, + background: "transparent", + foreground: semanticColor.text.inverse, + // NOTE: Not used, but included for type safety. + icon: "transparent", + }, + press: { + border: tokens.color.fadedBlue, + background: + semanticColor.action.filled.progressive.press + .background, + foreground: semanticColor.text.inverse, + }, + disabled: { + border: semanticColor.action.outlined.progressive.press + .background, + background: + semanticColor.action.outlined.progressive.press + .background, + // NOTE: Using primitive token, but this will go away once + // we remove the light variant. + foreground: tokens.color.white50, + }, }, - - /** - * Icons - */ - icon: { - // Allows the icon to be visible on hover in both light and dark - // backgrounds. - secondaryHover: "inherit", + // kind=secondary / color=destructive / light=false + destructive: { + ...semanticColor.action.outlined.destructive, + hover: { + ...semanticColor.action.outlined.destructive.hover, + // NOTE: This is a special case for the secondary button + background: "transparent", + icon: "transparent", + }, + disabled: { + border: semanticColor.action.disabled.default, + background: + semanticColor.action.outlined.destructive.press + .background, + foreground: semanticColor.text.disabled, + }, + }, + // kind=secondary / color=destructive / light=true + // NOTE: These colors will be removed from WB as soon as we remove the + // light variant. + destructiveLight: { + default: { + border: tokens.color.white64, + background: "transparent", + foreground: semanticColor.text.inverse, + }, + hover: { + border: semanticColor.border.inverse, + background: "transparent", + foreground: semanticColor.text.inverse, + // NOTE: Not used, but included for type safety. + icon: "transparent", + }, + press: { + border: tokens.color.fadedRed, + background: + semanticColor.action.filled.destructive.press + .background, + foreground: semanticColor.text.inverse, + }, + disabled: { + border: semanticColor.action.outlined.destructive.press + .background, + background: + semanticColor.action.outlined.destructive.press + .background, + foreground: tokens.color.white50, + }, }, }, - border: { - /** - * Default - */ - // kind="secondary", light=false | kind="tertiary", light=false - disabled: tokens.color.offBlack32, - /** - * Kind - */ - primary: { - inverse: tokens.color.white, + /** + * Tertiary + * + * Text buttons + */ + text: { + // kind=tertiary / color=default / light=false + progressive: { + default: { + background: "transparent", + foreground: + semanticColor.action.outlined.progressive.default + .foreground, + }, + hover: { + border: semanticColor.action.outlined.progressive.hover + .border, + }, + press: { + foreground: + semanticColor.action.outlined.progressive.press + .foreground, + }, + disabled: { + border: semanticColor.action.disabled.default, + foreground: semanticColor.text.disabled, + }, }, - secondary: { - action: tokens.color.offBlack50, - critical: tokens.color.offBlack50, - inverse: tokens.color.white50, + // kind=tertiary / color=default / light=true + // NOTE: These colors will be removed from WB as soon as we remove the + // light variant. + progressiveLight: { + default: { + border: tokens.color.white64, + background: "transparent", + foreground: semanticColor.text.inverse, + }, + hover: { + border: semanticColor.border.inverse, + background: "transparent", + foreground: semanticColor.text.inverse, + }, + press: { + border: semanticColor.border.inverse, + foreground: tokens.color.fadedBlue, + }, + disabled: { + border: semanticColor.action.outlined.progressive.press + .background, + foreground: tokens.color.white50, + }, }, - tertiary: { - inverse: tokens.color.white, + // kind=tertiary / color=destructive / light=false + destructive: { + default: { + background: "transparent", + foreground: + semanticColor.action.outlined.destructive.default + .foreground, + }, + hover: { + border: semanticColor.action.outlined.destructive.hover + .border, + }, + press: { + foreground: + semanticColor.action.outlined.destructive.press + .foreground, + }, + disabled: { + border: semanticColor.action.disabled.default, + foreground: semanticColor.text.disabled, + }, + }, + // kind=tertiary / color=destructive / light=true + // NOTE: These colors will be removed from WB as soon as we remove the + // light variant. + destructiveLight: { + default: { + border: tokens.color.white64, + background: "transparent", + foreground: semanticColor.text.inverse, + }, + hover: { + border: semanticColor.border.inverse, + background: "transparent", + foreground: semanticColor.text.inverse, + }, + press: { + border: semanticColor.border.inverse, + foreground: tokens.color.fadedRed, + }, + disabled: { + border: semanticColor.action.outlined.destructive.press + .background, + foreground: tokens.color.white50, + }, }, }, }, @@ -110,6 +307,7 @@ const theme = { }, offset: { primary: tokens.spacing.xxxxSmall_2, + secondary: -tokens.spacing.xxxxSmall_2, }, radius: { // default diff --git a/packages/wonder-blocks-button/src/themes/khanmigo.ts b/packages/wonder-blocks-button/src/themes/khanmigo.ts index 12b031ad5e..d940d55e38 100644 --- a/packages/wonder-blocks-button/src/themes/khanmigo.ts +++ b/packages/wonder-blocks-button/src/themes/khanmigo.ts @@ -2,33 +2,45 @@ import {mergeTheme} from "@khanacademy/wonder-blocks-theming"; import * as tokens from "@khanacademy/wonder-blocks-tokens"; import defaultTheme from "./default"; +const secondaryBgColor = tokens.color.offWhite; + /** * The overrides for the Khanmigo theme. */ const theme = mergeTheme(defaultTheme, { color: { - bg: { - secondary: { - default: tokens.color.offWhite, - active: { - action: tokens.color.fadedBlue8, - critical: tokens.color.fadedRed8, + outlined: { + progressive: { + default: { + border: tokens.color.fadedBlue, + background: secondaryBgColor, + }, + hover: { + background: secondaryBgColor, + icon: tokens.color.fadedBlue16, + foreground: + tokens.semanticColor.action.outlined.progressive.default + .foreground, + }, + press: { + background: tokens.color.fadedBlue8, }, - focus: tokens.color.offWhite, - }, - icon: { - secondaryHover: tokens.color.fadedBlue16, - }, - }, - border: { - secondary: { - action: tokens.color.fadedBlue, - critical: tokens.color.fadedRed, }, - }, - text: { - icon: { - secondaryHover: tokens.color.blue, + destructive: { + default: { + border: tokens.color.fadedRed, + background: secondaryBgColor, + }, + hover: { + background: secondaryBgColor, + icon: tokens.color.fadedRed16, + foreground: + tokens.semanticColor.action.outlined.destructive.default + .foreground, + }, + press: { + background: tokens.color.fadedRed8, + }, }, }, },