From c774f7da8feaea1260a600a0e7d0d7b69fb96544 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Mon, 27 Jan 2025 12:36:50 -0500 Subject: [PATCH] [WB-1847] Dropdown: Update `SelectOpener` to match Design specs. (#2438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: - Updated `SelectOpener` to match design specs. - Also converted `color` tokens to use `semanticColor` tokens. Figma: https://www.figma.com/design/VbVu3h2BpBhH80niq101MHHE/%F0%9F%92%A0-Main-Components?node-id=13693-11133&t=W16iu9a1X5vqz5ez-4 NOTE: The `light` version was removed on a separate PR, so those design specs are not included here anymore. Issue: https://khanacademy.atlassian.net/browse/WB-1847 ## Test plan: Verify that the `SelectOpener` component looks as expected in the All Variants stories. Author: jandrade Reviewers: jandrade, marcysutton, beaesguerra Required Reviewers: Approved By: beaesguerra Checks: ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Test / Test (ubuntu-latest, 20.x, 2/2), ✅ Test / Test (ubuntu-latest, 20.x, 1/2), ✅ Lint / Lint (ubuntu-latest, 20.x), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Chromatic - Build and test on regular PRs / chromatic (ubuntu-latest, 20.x), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ⏭️ dependabot Pull Request URL: https://github.com/Khan/wonder-blocks/pull/2438 --- .changeset/six-jobs-promise.md | 5 + .../src/components/select-opener.tsx | 124 ++++++++++++------ 2 files changed, 87 insertions(+), 42 deletions(-) create mode 100644 .changeset/six-jobs-promise.md diff --git a/.changeset/six-jobs-promise.md b/.changeset/six-jobs-promise.md new file mode 100644 index 000000000..ae10e585f --- /dev/null +++ b/.changeset/six-jobs-promise.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/wonder-blocks-dropdown": patch +--- + +Updates `SelectOpener` (internal component) from `Dropdown` to match Design specs. Also converts `color` tokens to use `semanticColor` tokens. diff --git a/packages/wonder-blocks-dropdown/src/components/select-opener.tsx b/packages/wonder-blocks-dropdown/src/components/select-opener.tsx index ba9cf1a8e..acb5684bc 100644 --- a/packages/wonder-blocks-dropdown/src/components/select-opener.tsx +++ b/packages/wonder-blocks-dropdown/src/components/select-opener.tsx @@ -6,7 +6,11 @@ import type {AriaProps} from "@khanacademy/wonder-blocks-core"; import {addStyle} from "@khanacademy/wonder-blocks-core"; import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon"; import {LabelMedium} from "@khanacademy/wonder-blocks-typography"; -import * as tokens from "@khanacademy/wonder-blocks-tokens"; +import { + border, + semanticColor, + spacing, +} from "@khanacademy/wonder-blocks-tokens"; import caretDownIcon from "@phosphor-icons/core/bold/caret-down-bold.svg"; import {DROPDOWN_ITEM_HEIGHT} from "../util/constants"; import {OptionLabel} from "../util/types"; @@ -138,8 +142,8 @@ export default class SelectOpener extends React.Component< const stateStyles = _generateStyles(isPlaceholder, error); const iconColor = disabled - ? tokens.color.offBlack32 - : tokens.color.offBlack64; + ? semanticColor.action.disabled.default + : semanticColor.icon.primary; const style = [ styles.shared, @@ -189,15 +193,15 @@ const styles = StyleSheet.create({ display: "inline-flex", alignItems: "center", justifyContent: "space-between", - color: tokens.color.offBlack, + color: semanticColor.text.primary, height: DROPDOWN_ITEM_HEIGHT, // This asymmetry arises from the Icon on the right side, which has // extra padding built in. To have the component look more balanced, // we need to take off some paddingRight here. - paddingLeft: tokens.spacing.medium_16, - paddingRight: tokens.spacing.small_12, + paddingLeft: spacing.medium_16, + paddingRight: spacing.small_12, borderWidth: 0, - borderRadius: tokens.border.radius.medium_4, + borderRadius: border.radius.medium_4, borderStyle: "solid", outline: "none", textDecoration: "none", @@ -209,7 +213,7 @@ const styles = StyleSheet.create({ }, text: { - marginRight: tokens.spacing.xSmall_8, + marginRight: spacing.xSmall_8, whiteSpace: "nowrap", userSelect: "none", overflow: "hidden", @@ -221,12 +225,6 @@ const styles = StyleSheet.create({ }, }); -// These values are default padding (16 and 12) minus 1, because -// changing the borderWidth to 2 messes up the button width -// and causes it to move a couple pixels. This fixes that. -const adjustedPaddingLeft = tokens.spacing.medium_16 - 1; -const adjustedPaddingRight = tokens.spacing.small_12 - 1; - const stateStyles: Record = {}; const _generateStyles = (placeholder: boolean, error: boolean) => { @@ -236,54 +234,96 @@ const _generateStyles = (placeholder: boolean, error: boolean) => { return stateStyles[styleKey]; } + // The different states that the component can be in. + const states = { + // Resting state + default: { + border: semanticColor.border.strong, + background: semanticColor.surface.primary, + foreground: semanticColor.text.primary, + }, + disabled: { + border: semanticColor.border.primary, + background: semanticColor.action.disabled.secondary, + foreground: semanticColor.text.secondary, + }, + // Form validation error state + error: { + border: semanticColor.status.critical.foreground, + background: semanticColor.status.critical.background, + foreground: semanticColor.text.primary, + }, + }; + + // The color is based on the action color. + const actionType = error ? "destructive" : "progressive"; + // NOTE: We are using the outlined action type for all the non-resting + // states as the opener is a bit different from a regular button in its + // resting/default state. + const action = semanticColor.action.outlined[actionType]; + + // TODO(WB-1856): Define global semantic outline tokens. + const sharedOutlineStyling = { + // Outline sits inside the border (inset) + outlineOffset: -border.width.thin, + outlineStyle: "solid", + outlineWidth: border.width.thin, + }; + const focusHoverStyling = { - borderColor: error ? tokens.color.red : tokens.color.blue, - borderWidth: tokens.border.width.thin, - paddingLeft: adjustedPaddingLeft, - paddingRight: adjustedPaddingRight, + // TODO(WB-1856): Use `border.focus` when we define a global pattern for + // focus indicators. + outlineColor: action.hover.border, + ...sharedOutlineStyling, }; - const activePressedStyling = { - background: error ? tokens.color.fadedRed : tokens.color.fadedBlue, - borderColor: error ? tokens.color.red : tokens.color.activeBlue, - borderWidth: tokens.border.width.thin, - paddingLeft: adjustedPaddingLeft, - paddingRight: adjustedPaddingRight, + const pressStyling = { + background: action.press.background, + color: placeholder + ? error + ? semanticColor.text.secondary + : semanticColor.action.outlined.progressive.press.foreground + : semanticColor.text.primary, + outlineColor: action.press.border, + ...sharedOutlineStyling, }; - const newStyles: Record = { + const currentState = error ? states.error : states.default; + + const newStyles = { default: { - background: error ? tokens.color.fadedRed8 : tokens.color.white, - borderColor: error ? tokens.color.red : tokens.color.offBlack50, - borderWidth: tokens.border.width.hairline, + background: currentState.background, + borderColor: currentState.border, + borderWidth: border.width.hairline, color: placeholder - ? tokens.color.offBlack64 - : tokens.color.offBlack, + ? semanticColor.text.secondary + : currentState.foreground, ":hover:not([aria-disabled=true])": focusHoverStyling, // Allow hover styles on non-touch devices only. This prevents an // issue with hover being sticky on touch devices (e.g. mobile). ["@media not (hover: hover)"]: { ":hover:not([aria-disabled=true])": { - borderColor: error - ? tokens.color.red - : tokens.color.offBlack50, - borderWidth: tokens.border.width.hairline, - paddingLeft: tokens.spacing.medium_16, - paddingRight: tokens.spacing.small_12, + borderColor: currentState.border, + borderWidth: border.width.hairline, + paddingLeft: spacing.medium_16, + paddingRight: spacing.small_12, }, }, ":focus-visible:not([aria-disabled=true])": focusHoverStyling, - ":active:not([aria-disabled=true])": activePressedStyling, + ":active:not([aria-disabled=true])": pressStyling, }, disabled: { - background: tokens.color.offWhite, - borderColor: tokens.color.offBlack16, - color: tokens.color.offBlack64, + background: states.disabled.background, + borderColor: states.disabled.border, + color: states.disabled.foreground, cursor: "not-allowed", ":focus-visible": { - boxShadow: `0 0 0 1px ${tokens.color.white}, 0 0 0 3px ${tokens.color.offBlack32}`, + // TODO(WB-1856): Use `border.focus` when we define a global + // pattern for focus indicators. + outlineColor: semanticColor.action.disabled.default, + ...sharedOutlineStyling, }, }, - press: activePressedStyling, + press: pressStyling, }; stateStyles[styleKey] = StyleSheet.create(newStyles);