Skip to content

Commit

Permalink
[WB-1847] Dropdown: Update SelectOpener to match Design specs. (#2438)
Browse files Browse the repository at this point in the history
## 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: #2438
  • Loading branch information
jandrade authored Jan 27, 2025
1 parent a00747f commit c774f7d
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/six-jobs-promise.md
Original file line number Diff line number Diff line change
@@ -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.
124 changes: 82 additions & 42 deletions packages/wonder-blocks-dropdown/src/components/select-opener.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -209,7 +213,7 @@ const styles = StyleSheet.create({
},

text: {
marginRight: tokens.spacing.xSmall_8,
marginRight: spacing.xSmall_8,
whiteSpace: "nowrap",
userSelect: "none",
overflow: "hidden",
Expand All @@ -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<string, any> = {};

const _generateStyles = (placeholder: boolean, error: boolean) => {
Expand All @@ -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<string, any> = {
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);
Expand Down

0 comments on commit c774f7d

Please sign in to comment.