Expose builder chip selection beyond color, raise unselected contrast, and make the five-value limit visible #76
@@ -1,8 +1,81 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo } from "react";
|
import { memo, type CSSProperties } from "react";
|
||||||
import type { ChipViewProps } from "./Chip.types";
|
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({
|
function ChipView({
|
||||||
label,
|
label,
|
||||||
state,
|
state,
|
||||||
@@ -30,10 +103,8 @@ function ChipView({
|
|||||||
const isCustom = state === "custom";
|
const isCustom = state === "custom";
|
||||||
const isToggle = !isCustom;
|
const isToggle = !isCustom;
|
||||||
|
|
||||||
const isInverse = palette === "inverse";
|
|
||||||
const isDefault = palette === "default";
|
|
||||||
|
|
||||||
const isSmall = size === "s";
|
const isSmall = size === "s";
|
||||||
|
const surfaceStyle = chipSurfaceStyle(state, palette, size);
|
||||||
|
|
||||||
// Size-based styles from Figma tokens
|
// Size-based styles from Figma tokens
|
||||||
// Custom state has different padding
|
// 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"
|
? "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";
|
: "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 = `
|
const baseClasses = `
|
||||||
|
appearance-none
|
||||||
inline-flex
|
inline-flex
|
||||||
max-w-full
|
max-w-full
|
||||||
items-center
|
items-center
|
||||||
@@ -107,7 +127,7 @@ function ChipView({
|
|||||||
box-border
|
box-border
|
||||||
focus:outline-none
|
focus:outline-none
|
||||||
focus-visible:ring-2
|
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-2
|
||||||
focus-visible:ring-offset-transparent
|
focus-visible:ring-offset-transparent
|
||||||
transition-[background,border-color,color,box-shadow]
|
transition-[background,border-color,color,box-shadow]
|
||||||
@@ -121,15 +141,7 @@ function ChipView({
|
|||||||
? "cursor-not-allowed opacity-60"
|
? "cursor-not-allowed opacity-60"
|
||||||
: "cursor-pointer";
|
: "cursor-pointer";
|
||||||
|
|
||||||
const combinedClasses = [
|
const combinedClasses = [baseClasses, sizeClasses, stateClasses, className]
|
||||||
baseClasses,
|
|
||||||
sizeClasses,
|
|
||||||
background,
|
|
||||||
border,
|
|
||||||
textColor,
|
|
||||||
stateClasses,
|
|
||||||
className,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
@@ -153,6 +165,7 @@ function ChipView({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={combinedClasses}
|
className={combinedClasses}
|
||||||
|
style={surfaceStyle}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
@@ -262,6 +275,7 @@ function ChipView({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={combinedClasses}
|
className={combinedClasses}
|
||||||
|
style={surfaceStyle}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...sharedA11y}
|
{...sharedA11y}
|
||||||
@@ -283,7 +297,15 @@ function ChipView({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
) : null}
|
) : 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 && (
|
{onRemove && !isDisabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ describe("Chip", () => {
|
|||||||
expect(chip.querySelector("svg")).toBeNull();
|
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", () => {
|
it("exposes aria-pressed and a check mark when selected", () => {
|
||||||
render(<Chip label="Worker cooperative" state="selected" />);
|
render(<Chip label="Worker cooperative" state="selected" />);
|
||||||
const chip = screen.getByRole("button", { name: "Worker cooperative" });
|
const chip = screen.getByRole("button", { name: "Worker cooperative" });
|
||||||
|
|||||||
Reference in New Issue
Block a user