Expose builder chip selection beyond color, raise unselected contrast, and make the five-value limit visible #76

Merged
an.di merged 2 commits from adilallo/fix/CR-186-builder-chips into main 2026-09-10 17:15:36 +00:00
2 changed files with 99 additions and 67 deletions
Showing only changes of commit 6c1d83e89d - Show all commits
+89 -67
View File
@@ -1,8 +1,81 @@
"use client";
import { memo } from "react";
import { memo, type CSSProperties } from "react";
import type { ChipViewProps } from "./Chip.types";
function chipSurfaceStyle(
state: ChipViewProps["state"],
palette: ChipViewProps["palette"],
size: ChipViewProps["size"],
): CSSProperties {
const borderWidth = size === "s" ? "1.25px" : "2px";
const reset: CSSProperties = {
appearance: "none",
WebkitAppearance: "none",
backgroundImage: "none",
};
const paint = (
backgroundColor: string,
color: string,
borderColor?: string,
): CSSProperties => ({
...reset,
backgroundColor,
color,
WebkitTextFillColor: color,
...(borderColor
? { borderWidth, borderStyle: "solid", borderColor }
: { borderWidth: 0, borderStyle: "none", borderColor: "transparent" }),
});
if (palette === "inverse") {
if (state === "disabled") {
return paint(
"var(--color-surface-inverse-tertiary)",
"var(--color-content-inverse-primary)",
);
}
if (state === "selected") {
return paint(
"var(--color-surface-default-semi-opaque)",
"var(--color-content-inverse-primary)",
"var(--color-border-default-primary)",
);
}
return paint(
"transparent",
"var(--color-content-inverse-primary)",
"var(--color-border-default-primary)",
);
}
if (state === "custom") {
return paint(
"var(--color-surface-default-secondary)",
"var(--color-content-default-tertiary)",
);
}
if (state === "disabled") {
return paint(
"var(--color-surface-default-secondary)",
"var(--color-content-invert-tertiary)",
);
}
if (state === "selected") {
return paint(
"var(--color-surface-invert-brand-primary, #fefcc9)",
"var(--color-content-invert-primary, #000)",
"var(--color-border-default-brand-primary, #fdfaa8)",
);
}
return paint(
"transparent",
"var(--color-content-default-brand-primary, #fefcc9)",
"var(--color-border-default-tertiary, #464646)",
);
}
function ChipView({
label,
state,
@@ -30,10 +103,8 @@ function ChipView({
const isCustom = state === "custom";
const isToggle = !isCustom;
const isInverse = palette === "inverse";
const isDefault = palette === "default";
const isSmall = size === "s";
const surfaceStyle = chipSurfaceStyle(state, palette, size);
// Size-based styles from Figma tokens
// Custom state has different padding
@@ -45,59 +116,8 @@ function ChipView({
? "h-[30px] px-[var(--measures-spacing-200,8px)] gap-[var(--measures-spacing-050,2px)] text-x-small-label"
: "px-[var(--measures-spacing-300,12px)] py-[var(--measures-spacing-300,12px)] gap-[var(--measures-spacing-150,6px)] text-medium-label";
// Palette + state styling based on Figma examples
// Use consistent border width to prevent layout shift
const borderWidth = isSmall ? "border-[1.25px]" : "border-2";
let background =
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
let border = `${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
let textColor =
"text-[color:var(--color-content-default-brand-primary,#fefcc9)]";
if (isDefault) {
if (state === "custom") {
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
border = "border-none";
textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
} else if (state === "disabled") {
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
border = "border-none";
// Per Figma (node 19839:13842) disabled uses invert-tertiary for the
// strongly dimmed look, not default-tertiary.
textColor = "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
} else if (isSelected) {
background = "bg-[var(--color-surface-invert-brand-primary,#fefcc9)]"; // yellow selected
border = `${borderWidth} border-[var(--color-border-default-brand-primary,#fdfaa8)]`;
textColor = "text-[color:var(--color-content-invert-primary,black)]";
} else {
// Unselected default
background =
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
border = `${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
textColor =
"text-[color:var(--color-content-default-brand-primary,#fefcc9)]";
}
} else if (isInverse) {
if (state === "disabled") {
background = "bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
border = "border-none";
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
} else if (isSelected) {
background =
"bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.05))]";
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
} else {
// Unselected / custom inverse
background =
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
}
}
const baseClasses = `
appearance-none
inline-flex
max-w-full
items-center
@@ -107,7 +127,7 @@ function ChipView({
box-border
focus:outline-none
focus-visible:ring-2
focus-visible:ring-[var(--color-border-default-primary,#141414)]
focus-visible:ring-[var(--color-border-default-primary)]
focus-visible:ring-offset-2
focus-visible:ring-offset-transparent
transition-[background,border-color,color,box-shadow]
@@ -121,15 +141,7 @@ function ChipView({
? "cursor-not-allowed opacity-60"
: "cursor-pointer";
const combinedClasses = [
baseClasses,
sizeClasses,
background,
border,
textColor,
stateClasses,
className,
]
const combinedClasses = [baseClasses, sizeClasses, stateClasses, className]
.filter(Boolean)
.join(" ");
@@ -153,6 +165,7 @@ function ChipView({
return (
<div
className={combinedClasses}
style={surfaceStyle}
role="button"
tabIndex={0}
onClick={handleClick}
@@ -262,6 +275,7 @@ function ChipView({
<button
type="button"
className={combinedClasses}
style={surfaceStyle}
disabled={isDisabled}
onClick={handleClick}
{...sharedA11y}
@@ -283,7 +297,15 @@ function ChipView({
/>
</svg>
) : null}
<span className="min-w-0 truncate">{label}</span>
<span
className="min-w-0 truncate"
style={{
color: surfaceStyle.color,
WebkitTextFillColor: surfaceStyle.WebkitTextFillColor,
}}
>
{label}
</span>
{onRemove && !isDisabled && (
<button
type="button"
+10
View File
@@ -41,6 +41,16 @@ describe("Chip", () => {
expect(chip.querySelector("svg")).toBeNull();
});
it("paints unselected chips on the element so native button styles cannot win", () => {
render(<Chip label="Worker cooperative" state="unselected" />);
const chip = screen.getByRole("button", { name: "Worker cooperative" });
expect(chip.style.color).toContain("--color-content-default-brand-primary");
expect(chip.style.backgroundColor).toBe("transparent");
expect(chip.style.borderColor).toContain(
"--color-border-default-tertiary",
);
});
it("exposes aria-pressed and a check mark when selected", () => {
render(<Chip label="Worker cooperative" state="selected" />);
const chip = screen.getByRole("button", { name: "Worker cooperative" });