From e17028253b255c696faf979ffcccb43324e347d4 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Thu, 19 Dec 2024 10:15:59 -0500 Subject: [PATCH 1/9] Use pseudo-classes in Button --- .../wonder-blocks-button/button.stories.tsx | 18 +++ .../src/components/button-core.tsx | 153 +++++++++--------- .../src/themes/default.ts | 9 +- 3 files changed, 95 insertions(+), 85 deletions(-) diff --git a/__docs__/wonder-blocks-button/button.stories.tsx b/__docs__/wonder-blocks-button/button.stories.tsx index 433dc8e6fa..6060451527 100644 --- a/__docs__/wonder-blocks-button/button.stories.tsx +++ b/__docs__/wonder-blocks-button/button.stories.tsx @@ -135,6 +135,24 @@ export const Tertiary: StoryComponentType = { }, }; +export const Focus: StoryComponentType = { + args: { + onClick: () => {}, + kind: "primary", + children: "Hello, world!", + }, + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + // Get HTML elements + const button = canvas.getByRole("button"); + + // Focus style + await userEvent.tab(button); + await expect(button).toHaveFocus(); + }, +}; + export const styles: StyleDeclaration = StyleSheet.create({ row: { flexDirection: "row", diff --git a/packages/wonder-blocks-button/src/components/button-core.tsx b/packages/wonder-blocks-button/src/components/button-core.tsx index b420d1ce36..3c3668484d 100644 --- a/packages/wonder-blocks-button/src/components/button-core.tsx +++ b/packages/wonder-blocks-button/src/components/button-core.tsx @@ -84,12 +84,12 @@ const ButtonCore: React.ForwardRefExoticComponent< kind !== "tertiary" && !disabled && (pressed - ? buttonStyles.active - : (hovered || focused) && buttonStyles.focus), + ? buttonStyles.pressed + : focused && buttonStyles.focused), kind === "tertiary" && !pressed && focused && [ - buttonStyles.focus, + buttonStyles.focused, disabled && buttonStyles.disabledFocus, ], size === "small" && sharedStyles.small, @@ -276,7 +276,10 @@ const themedSharedStyles: ThemedStylesFn = (theme) => ({ alignItems: "center", fontWeight: theme.font.weight.default, whiteSpace: "nowrap", + // TODO(juan): Figure out how to use text-underline with this for + // tertiary variant overflow: "hidden", + // contain: "paint", textOverflow: "ellipsis", display: "inline-block", // allows the button text to truncate pointerEvents: "none", // fix Safari bug where the browser was eating mouse events @@ -362,9 +365,17 @@ export const _generateStyles = ( let newStyles: Record = {}; if (kind === "primary") { - const boxShadowInnerColor: string = light - ? theme.color.bg.primary.inverse - : theme.color.bg.primary.default; + const focusStyling = { + outlineColor: light ? theme.color.bg.primary.default : color, + outlineOffset: 2, + outlineStyle: "solid", + outlineWidth: 2, + }; + + const activePressedStyling = { + backgroundColor: activeColor, + outlineColor: light ? fadedColor : activeColor, + }; newStyles = { default: { @@ -372,37 +383,26 @@ export const _generateStyles = ( color: light ? color : theme.color.text.inverse, paddingLeft: padding, paddingRight: padding, + // TODO(juan): Change this when we get final designs for hover. + [":hover:not([aria-disabled=true])" as any]: focusStyling, + [":focus-visible:not([aria-disabled=true])" as any]: + focusStyling, + [":active:not([aria-disabled=true])" as any]: + activePressedStyling, }, - focus: { - // This assumes a background of white for the regular button and - // a background of darkBlue for the light version. The inner - // box shadow/ring is also small enough for a slight variation - // in the background color not to matter too much. - boxShadow: `0 0 0 1px ${boxShadowInnerColor}, 0 0 0 3px ${ - light ? theme.color.bg.primary.default : color - }`, - }, - active: { - boxShadow: `0 0 0 1px ${boxShadowInnerColor}, 0 0 0 3px ${ - light ? fadedColor : activeColor - }`, - background: light ? fadedColor : activeColor, - color: light ? activeColor : fadedColor, - }, + // focused: focusStyling, + pressed: activePressedStyling, disabled: { background: light ? fadedColor : theme.color.bg.primary.disabled, color: light ? color : theme.color.text.primary.disabled, cursor: "default", - ":focus": { - boxShadow: `0 0 0 1px ${ - light - ? theme.color.bg.primary.disabled - : theme.color.bg.primary.default - }, 0 0 0 3px ${ - light ? fadedColor : theme.color.bg.primary.disabled - }`, + ":focus-visible": { + ...focusStyling, + outlineColor: light + ? fadedColor + : theme.color.bg.primary.disabled, }, }, }; @@ -416,6 +416,25 @@ export const _generateStyles = ( ? theme.color.bg.secondary.active.critical : theme.color.bg.secondary.active.action; + const focusStyling = { + background: light + ? theme.color.bg.secondary.inverse + : theme.color.bg.secondary.focus, + borderColor: "transparent", + outlineColor: light ? theme.color.border.primary.inverse : color, + outlineStyle: "solid", + outlineWidth: theme.border.width.focused, + }; + + const activePressedStyling = { + background: light ? activeColor : secondaryActiveColor, + color: light ? fadedColor : activeColor, + borderColor: "transparent", + outlineColor: light ? fadedColor : activeColor, + outlineStyle: "solid", + outlineWidth: theme.border.width.focused, + }; + newStyles = { default: { background: light @@ -429,34 +448,21 @@ export const _generateStyles = ( borderWidth: theme.border.width.secondary, paddingLeft: padding, paddingRight: padding, + [":hover:not([aria-disabled=true])" as any]: focusStyling, + [":focus-visible:not([aria-disabled=true])" as any]: + focusStyling, + [":active:not([aria-disabled=true])" as any]: + activePressedStyling, }, - focus: { - background: light - ? theme.color.bg.secondary.inverse - : theme.color.bg.secondary.focus, - borderColor: "transparent", - outlineColor: light - ? theme.color.border.primary.inverse - : color, - outlineStyle: "solid", - outlineWidth: theme.border.width.focused, - }, - - active: { - background: light ? activeColor : secondaryActiveColor, - color: light ? fadedColor : activeColor, - borderColor: "transparent", - outlineColor: light ? fadedColor : activeColor, - outlineStyle: "solid", - outlineWidth: theme.border.width.focused, - }, + focused: focusStyling, + pressed: activePressedStyling, disabled: { color: light ? theme.color.text.secondary.inverse : theme.color.text.disabled, outlineColor: light ? fadedColor : theme.color.border.disabled, cursor: "default", - ":focus": { + ":focus-visible": { outlineColor: light ? theme.color.border.secondary.inverse : theme.color.border.disabled, @@ -466,40 +472,33 @@ export const _generateStyles = ( }, }; } else if (kind === "tertiary") { + const focusStyling = { + outlineStyle: "solid", + outlineColor: light ? theme.color.border.tertiary.inverse : color, + outlineWidth: theme.border.width.focused, + borderRadius: theme.border.radius.default, + }; + const activePressedStyling = { + color: light ? fadedColor : activeColor, + }; + newStyles = { default: { background: "none", color: light ? theme.color.text.inverse : color, paddingLeft: 0, paddingRight: 0, - }, - hover: { - ":after": { - content: "''", - position: "absolute", - height: theme.size.height.tertiaryHover, - width: "100%", - right: 0, - bottom: 0, - background: light ? theme.color.bg.tertiary.hover : color, - borderRadius: theme.border.radius.tertiary, - }, - }, - focus: { - outlineStyle: "solid", - outlineColor: light - ? theme.color.border.tertiary.inverse - : color, - outlineWidth: theme.border.width.focused, - borderRadius: theme.border.radius.default, - }, - active: { - color: light ? fadedColor : activeColor, - ":after": { - height: theme.size.height.tertiaryHover, - background: light ? fadedColor : activeColor, + [":hover:not([aria-disabled=true])" as any]: { + textUnderlineOffset: 3, + textDecoration: "underline", }, + [":focus-visible:not([aria-disabled=true])" as any]: + focusStyling, + [":active:not([aria-disabled=true])" as any]: + activePressedStyling, }, + focused: focusStyling, + pressed: activePressedStyling, disabled: { color: light ? fadedColor : theme.color.text.disabled, cursor: "default", diff --git a/packages/wonder-blocks-button/src/themes/default.ts b/packages/wonder-blocks-button/src/themes/default.ts index fde16661e2..8ec588e386 100644 --- a/packages/wonder-blocks-button/src/themes/default.ts +++ b/packages/wonder-blocks-button/src/themes/default.ts @@ -25,8 +25,6 @@ const theme = { primary: { default: tokens.color.white, disabled: tokens.color.offBlack32, - // used in boxShadow - inverse: tokens.color.darkBlue, }, secondary: { @@ -39,10 +37,6 @@ const theme = { }, }, - tertiary: { - hover: tokens.color.white, - }, - /** * Icons */ @@ -113,7 +107,7 @@ const theme = { // default default: tokens.border.radius.medium_4, // tertiary - tertiary: tokens.border.radius.xSmall_2, + // tertiary: tokens.border.radius.xSmall_2, // small button small: tokens.border.radius.medium_4, // large button @@ -127,7 +121,6 @@ const theme = { }, size: { height: { - tertiaryHover: tokens.spacing.xxxxSmall_2, small: tokens.spacing.xLarge_32, // NOTE: These height tokens are specific to this component. medium: 40, From ff165fa918ca08696f9fea8ab33b05f3bbd5439d Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Thu, 19 Dec 2024 10:17:06 -0500 Subject: [PATCH 2/9] docs(changeset): Use pseudo-classes for styling states (:hover, :focus-visible). Keep some clickable states for programmatic focus and preserve active/pressed overrides. --- .changeset/rich-days-count.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rich-days-count.md diff --git a/.changeset/rich-days-count.md b/.changeset/rich-days-count.md new file mode 100644 index 0000000000..8e99f82538 --- /dev/null +++ b/.changeset/rich-days-count.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/wonder-blocks-button": patch +--- + +Use pseudo-classes for styling states (:hover, :focus-visible). Keep some clickable states for programmatic focus and preserve active/pressed overrides. From bcb6ced751e6b4ec74f34da6a51e1018b115d9e8 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Thu, 19 Dec 2024 12:37:58 -0500 Subject: [PATCH 3/9] Fix underline --- .../wonder-blocks-button/button.stories.tsx | 18 ---------------- .../src/components/button-core.tsx | 21 ++++++++++++------- .../src/themes/default.ts | 18 +++++++++++++--- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/__docs__/wonder-blocks-button/button.stories.tsx b/__docs__/wonder-blocks-button/button.stories.tsx index 6060451527..433dc8e6fa 100644 --- a/__docs__/wonder-blocks-button/button.stories.tsx +++ b/__docs__/wonder-blocks-button/button.stories.tsx @@ -135,24 +135,6 @@ export const Tertiary: StoryComponentType = { }, }; -export const Focus: StoryComponentType = { - args: { - onClick: () => {}, - kind: "primary", - children: "Hello, world!", - }, - play: async ({canvasElement}) => { - const canvas = within(canvasElement); - - // Get HTML elements - const button = canvas.getByRole("button"); - - // Focus style - await userEvent.tab(button); - await expect(button).toHaveFocus(); - }, -}; - export const styles: StyleDeclaration = StyleSheet.create({ row: { flexDirection: "row", diff --git a/packages/wonder-blocks-button/src/components/button-core.tsx b/packages/wonder-blocks-button/src/components/button-core.tsx index 3c3668484d..acc184a267 100644 --- a/packages/wonder-blocks-button/src/components/button-core.tsx +++ b/packages/wonder-blocks-button/src/components/button-core.tsx @@ -110,6 +110,7 @@ const ButtonCore: React.ForwardRefExoticComponent<