Expose builder chip selection beyond color, raise unselected contrast, and make the five-value limit visible.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import type { MultiSelectProps } from "./MultiSelect.types";
|
||||
const MultiSelectContainer = memo<MultiSelectProps>(
|
||||
({
|
||||
label,
|
||||
showHelpIcon = true,
|
||||
showHelpIcon = false,
|
||||
size: sizeProp = "m",
|
||||
palette: paletteProp = "default",
|
||||
options,
|
||||
@@ -23,6 +23,9 @@ const MultiSelectContainer = memo<MultiSelectProps>(
|
||||
formHeader = true,
|
||||
onCustomChipConfirm,
|
||||
onCustomChipClose,
|
||||
maxSelections,
|
||||
selectionCountText,
|
||||
limitReachedAnnouncement,
|
||||
className = "",
|
||||
}) => {
|
||||
const t = useTranslation("controlsChrome");
|
||||
@@ -46,6 +49,9 @@ const MultiSelectContainer = memo<MultiSelectProps>(
|
||||
formHeader={formHeader}
|
||||
onCustomChipConfirm={onCustomChipConfirm}
|
||||
onCustomChipClose={onCustomChipClose}
|
||||
maxSelections={maxSelections}
|
||||
selectionCountText={selectionCountText}
|
||||
limitReachedAnnouncement={limitReachedAnnouncement}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -62,6 +62,15 @@ export interface MultiSelectProps {
|
||||
* Callback when a custom chip is closed/removed
|
||||
*/
|
||||
onCustomChipClose?: (chipId: string) => void;
|
||||
/**
|
||||
* When set, unselected chips and the add control become non-interactive
|
||||
* once this many options are `selected`.
|
||||
*/
|
||||
maxSelections?: number;
|
||||
/** Visible selection counter, e.g. "3 of 5". */
|
||||
selectionCountText?: string;
|
||||
/** Announced when the selection limit is reached. */
|
||||
limitReachedAnnouncement?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -79,5 +88,8 @@ export interface MultiSelectViewProps {
|
||||
formHeader: boolean;
|
||||
onCustomChipConfirm?: (chipId: string, value: string) => void;
|
||||
onCustomChipClose?: (chipId: string) => void;
|
||||
maxSelections?: number;
|
||||
selectionCountText?: string;
|
||||
limitReachedAnnouncement?: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ function MultiSelectView({
|
||||
formHeader = true,
|
||||
onCustomChipConfirm,
|
||||
onCustomChipClose,
|
||||
maxSelections,
|
||||
selectionCountText,
|
||||
limitReachedAnnouncement,
|
||||
className,
|
||||
}: MultiSelectViewProps) {
|
||||
const isSmall = size === "s";
|
||||
@@ -30,6 +33,11 @@ function MultiSelectView({
|
||||
: "gap-[var(--measures-spacing-300,12px)]";
|
||||
|
||||
const chipSize = size;
|
||||
const selectedCount = options.filter((o) => o.state === "selected").length;
|
||||
const atLimit =
|
||||
maxSelections != null && selectedCount >= maxSelections;
|
||||
const countCaption = selectionCountText?.trim() ?? "";
|
||||
const helperText = countCaption.length > 0 ? countCaption : false;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -41,58 +49,83 @@ function MultiSelectView({
|
||||
label={label}
|
||||
helpIcon={showHelpIcon}
|
||||
asterisk={false}
|
||||
helperText={false}
|
||||
helperText={helperText}
|
||||
size={size}
|
||||
palette={palette}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!formHeader || !label ? (
|
||||
countCaption.length > 0 ? (
|
||||
<p className="w-full text-x-small-paragraph text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
||||
{countCaption}
|
||||
</p>
|
||||
) : null
|
||||
) : null}
|
||||
|
||||
{limitReachedAnnouncement ? (
|
||||
<p className="sr-only" aria-live="polite">
|
||||
{atLimit ? limitReachedAnnouncement : ""}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{/* Chips container */}
|
||||
<div
|
||||
className={`flex flex-wrap ${gapClass} items-center relative shrink-0 w-full`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Chip
|
||||
key={option.id}
|
||||
label={option.state === "custom" ? "" : option.label}
|
||||
state={option.state || "unselected"}
|
||||
palette={palette}
|
||||
size={chipSize}
|
||||
onClick={() => {
|
||||
if (option.state !== "custom" && onChipClick) {
|
||||
onChipClick(option.id);
|
||||
}
|
||||
}}
|
||||
onCheck={(value, e) => {
|
||||
e.stopPropagation();
|
||||
if (onCustomChipConfirm) {
|
||||
onCustomChipConfirm(option.id, value);
|
||||
}
|
||||
}}
|
||||
onClose={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onCustomChipClose) {
|
||||
onCustomChipClose(option.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{options.map((option) => {
|
||||
const isCustom = option.state === "custom";
|
||||
const isSelected = option.state === "selected";
|
||||
const chipDisabled = !isCustom && !isSelected && atLimit;
|
||||
const chipState = chipDisabled
|
||||
? "disabled"
|
||||
: option.state || "unselected";
|
||||
return (
|
||||
<Chip
|
||||
key={option.id}
|
||||
label={isCustom ? "" : option.label}
|
||||
state={chipState}
|
||||
palette={palette}
|
||||
size={chipSize}
|
||||
disabled={chipDisabled}
|
||||
onClick={() => {
|
||||
if (!isCustom && !chipDisabled && onChipClick) {
|
||||
onChipClick(option.id);
|
||||
}
|
||||
}}
|
||||
onCheck={(value, e) => {
|
||||
e.stopPropagation();
|
||||
if (onCustomChipConfirm) {
|
||||
onCustomChipConfirm(option.id, value);
|
||||
}
|
||||
}}
|
||||
onClose={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onCustomChipClose) {
|
||||
onCustomChipClose(option.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Add button — icon-only: bordered circle + brand icon (chips stay yellow). With label: Figma 19688:38288 — brand + icon, primary label text, no fill/border. */}
|
||||
{addButton && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={addButtonAriaLabel}
|
||||
disabled={atLimit}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (atLimit) return;
|
||||
onAddClick?.();
|
||||
}}
|
||||
className={
|
||||
!addButtonText
|
||||
? // Circular button with border (Rule style)
|
||||
`cursor-pointer bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
|
||||
`${atLimit ? "cursor-not-allowed opacity-60" : "cursor-pointer hover:opacity-80"} bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 transition-opacity`
|
||||
: // Text add control (default palette: white label + brand “+”; inverse: inverse primary for both)
|
||||
`cursor-pointer flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity ${
|
||||
`${atLimit ? "cursor-not-allowed opacity-60" : "cursor-pointer hover:opacity-80"} flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 transition-opacity ${
|
||||
isSmall
|
||||
? "gap-[var(--measures-spacing-100,4px)] px-[var(--measures-spacing-300,12px)] py-[var(--measures-spacing-200,8px)]"
|
||||
: "gap-[var(--measures-spacing-150,6px)] px-[var(--space-400,16px)] py-[var(--measures-spacing-300,12px)]"
|
||||
|
||||
Reference in New Issue
Block a user