From 64a2ef5a004f104c26b8133d6c34a708873ee8ad Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:04:06 -0600 Subject: [PATCH 1/2] Expose builder chip selection beyond color, raise unselected contrast, and make the five-value limit visible. Co-authored-by: Cursor --- .cursor/rules/create-flow.mdc | 2 +- .../components/ApplicableScopeField.tsx | 7 +- .../select/CommunityStructureSelectScreen.tsx | 3 - .../screens/select/CoreValuesSelectScreen.tsx | 22 +++-- app/components/controls/Chip/Chip.types.ts | 7 +- app/components/controls/Chip/Chip.view.tsx | 24 ++++- .../MultiSelect/MultiSelect.container.tsx | 8 +- .../controls/MultiSelect/MultiSelect.types.ts | 12 +++ .../controls/MultiSelect/MultiSelect.view.tsx | 91 +++++++++++++------ messages/en/create/customRule/coreValues.json | 4 +- stories/controls/MultiSelect.stories.js | 2 +- .../components/ApplicableScopeField.test.tsx | 28 ++++++ tests/components/Chip.test.tsx | 19 +++- .../CommunityStructureSelectScreen.test.tsx | 16 ++++ .../CoreValuesSelectScreen.test.tsx | 34 +++++++ tests/components/MultiSelect.test.tsx | 38 ++++++++ 16 files changed, 266 insertions(+), 51 deletions(-) create mode 100644 tests/components/CommunityStructureSelectScreen.test.tsx diff --git a/.cursor/rules/create-flow.mdc b/.cursor/rules/create-flow.mdc index cf8d0a2..0d872bb 100644 --- a/.cursor/rules/create-flow.mdc +++ b/.cursor/rules/create-flow.mdc @@ -33,7 +33,7 @@ Reach for these before writing new markup: | `[– value +]` numeric stepper (± label) | `app/components/controls/Incrementer` / `IncrementerBlock` | | Mid-paragraph "expand / see all" link button | `app/components/buttons/InlineTextButton` | | Help-icon + label above a control | `app/components/type/InputLabel` (`helpIcon` prop) | -| Toggle chip (dim-but-clickable) | `Chip` with `state="Disabled" disabled={false}` | +| Toggle chip | `Chip` with `state="selected"` / `"unselected"` (Chip sets `aria-pressed`; selected includes a check mark) | | Card-click → structured creation modal | `Create` with `backdropVariant="blurredYellow"` | If a screen grows a 2nd inline copy of any pattern above, **extract a shared diff --git a/app/(app)/create/components/ApplicableScopeField.tsx b/app/(app)/create/components/ApplicableScopeField.tsx index bdb0635..61d9467 100644 --- a/app/(app)/create/components/ApplicableScopeField.tsx +++ b/app/(app)/create/components/ApplicableScopeField.tsx @@ -74,11 +74,16 @@ function ApplicableScopeFieldComponent({
{scopes.map((scope) => { const isSelected = selectedScopes.includes(scope); + const chipState = isSelected + ? "selected" + : readOnly + ? "disabled" + : "unselected"; return ( coreValueOptions.filter((o) => o.state === "selected").length, + [coreValueOptions], + ); + const atSelectionLimit = selectedCount >= MAX_CORE_VALUES; + const selectionCountText = cv.multiSelect.selectionCount + .replace("{count}", String(selectedCount)) + .replace("{max}", String(MAX_CORE_VALUES)); + const kebabMenuItems = useMemo(() => { if (!modalSession || !activeModalChipId) return []; - const selectedCount = coreValueOptions.filter( - (o) => o.state === "selected", - ).length; return buildCustomRuleModalKebabMenu(modalKebabMenu, { showCustomize: true, onCustomize: handleCustomize, onDuplicate: (state.editingPublishedRuleId?.trim() ?? "") !== "" || - selectedCount >= MAX_CORE_VALUES + atSelectionLimit ? undefined : handleDuplicateCoreChip, showRemove: modalSession === "editing", @@ -605,7 +611,7 @@ export function CoreValuesSelectScreen() { }); }, [ activeModalChipId, - coreValueOptions, + atSelectionLimit, handleCustomize, handleDuplicateCoreChip, handleRemoveFromKebab, @@ -695,7 +701,8 @@ export function CoreValuesSelectScreen() { @@ -730,6 +737,9 @@ export function CoreValuesSelectScreen() { onCustomChipClose={addHandlers.onCustomChipClose} addButton addButtonText={cv.multiSelect.addButtonText} + maxSelections={MAX_CORE_VALUES} + selectionCountText={selectionCountText} + limitReachedAnnouncement={cv.multiSelect.limitReached} /> {detailModal && ( diff --git a/app/components/controls/Chip/Chip.types.ts b/app/components/controls/Chip/Chip.types.ts index 2a082a6..f0d2974 100644 --- a/app/components/controls/Chip/Chip.types.ts +++ b/app/components/controls/Chip/Chip.types.ts @@ -29,10 +29,9 @@ export interface ChipProps { className?: string; /** * Whether the chip should be non-interactive. Defaults to `true` when - * `state === "disabled"` to preserve historical behavior. Pass - * `disabled={false}` alongside `state="disabled"` to render the dimmed - * "disabled" visual while keeping the chip clickable — useful for toggle - * groups where the unselected state is the disabled visual. + * `state === "disabled"`. Toggle groups use `selected` / `unselected` + * (Chip sets `aria-pressed`); pass `disabled` only when the chip cannot + * be activated. */ disabled?: boolean; onClick?: (event: React.MouseEvent) => void; diff --git a/app/components/controls/Chip/Chip.view.tsx b/app/components/controls/Chip/Chip.view.tsx index 5658f59..378ad54 100644 --- a/app/components/controls/Chip/Chip.view.tsx +++ b/app/components/controls/Chip/Chip.view.tsx @@ -23,12 +23,12 @@ function ChipView({ typeToAddPlaceholder, closeAriaLabel, }: ChipViewProps) { - // The container is the source of truth for `disabled`. This allows - // `state="disabled"` to be used purely as a visual (for toggle-group chips - // that look dimmed while remaining clickable) by passing `disabled={false}`. + // The container is the source of truth for `disabled`. `state="disabled"` + // is the non-interactive visual only — toggle groups use selected/unselected. const isDisabled = disabled ?? false; const isSelected = state === "selected"; const isCustom = state === "custom"; + const isToggle = !isCustom; const isInverse = palette === "inverse"; const isDefault = palette === "default"; @@ -145,6 +145,7 @@ function ChipView({ const sharedA11y = { "aria-label": ariaLabel, + ...(isToggle ? { "aria-pressed": isSelected } : {}), }; // Custom state rendering with check/close buttons @@ -265,6 +266,23 @@ function ChipView({ onClick={handleClick} {...sharedA11y} > + {isSelected ? ( + + + + ) : null} {label} {onRemove && !isDisabled && (