feat(option-tile): align OptionTile and OptionTileGroup - #8127
feat(option-tile): align OptionTile and OptionTileGroup#8127tamas-sage wants to merge 12 commits into
Conversation
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
This PR introduces a new OptionTile / OptionTileGroup component pair to the Carbon React component library and exports them from the package entrypoint, with updated Storybook, Jest, and Playwright coverage to align single- vs multi-select usage patterns and keyboard interactions.
Changes:
- Added
OptionTilevariants (single,multiple,custom) andOptionTileGroupwith single/multiple selection context. - Implemented keyboard behaviors (Enter/Space toggles, number-key hotkeys for single groups) and enforced
customusage constraints. - Added Storybook stories plus Jest + Playwright tests for accessibility and interactions.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/index.ts | Exposes OptionTile / OptionTileGroup and related types from the package entrypoint. |
| src/components/option-tile/index.ts | Barrel export for the component and public prop/type surface. |
| src/components/option-tile/option-tile.component.tsx | Implements the component variants, group context, and keyboard behaviors. |
| src/components/option-tile/option-tile.style.ts | Adds styled-components styling for tiles, custom input, and group layout. |
| src/components/option-tile/option-tile.stories.tsx | Adds Storybook examples for single/custom/multiple and grouped usage. |
| src/components/option-tile/option-tile.test.tsx | Adds Jest tests covering rendering, interaction, constraints, and a11y attributes. |
| src/components/option-tile/option-tile.pw.tsx | Adds Playwright CT coverage for accessibility and key interactions. |
| src/components/option-tile/components.test-pw.tsx | Adds Playwright component fixtures used by CT tests. |
Files excluded by content exclusion policy (3)
- skills/carbon-react/components/option-tile-group.md
- skills/carbon-react/components/option-tile.md
- skills/carbon-react/index.md
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (8)
src/components/option-tile/option-tile.component.tsx:283
- Number-key hotkeys are handled at the group level and currently fire for any bubbled keydown. This will also trigger when focus is inside the custom tile text input, so typing digits can unexpectedly change the selected tile. Consider ignoring hotkey handling when the event originated from an input/textarea or a contenteditable element.
const onGroupKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>) => {
if (selectionType !== "single") return;
if (!/^[1-9]$/.test(ev.key)) return;
src/components/option-tile/option-tile.stories.tsx:100
- This Storybook example currently renders 3 tiles, but the PR description says multi-select groups should be standardized to 4 options. Adding a fourth option here keeps the documented examples consistent.
<OptionTileGroup selectionType="multiple" aria-label="Delivery options">
<OptionTile
variant="multiple"
label="Email receipt"
checked={selected.includes("email")}
src/components/option-tile/option-tile.test.tsx:345
- The console.error spy is not restored at the end of this test. Using mockReset keeps the spy installed and can leak into subsequent tests, potentially masking real console errors. Restoring the spy avoids cross-test side effects.
const consoleSpy = jest
.spyOn(global.console, "error")
.mockImplementation(() => undefined);
expect(() =>
render(
<OptionTileGroup selectionType="multiple" aria-label="Options">
<OptionTile
variant="custom"
title="Something else"
inputAriaLabel="Custom option value"
/>
</OptionTileGroup>,
),
).toThrow(
"OptionTile with variant='custom' can only be used within a single selection OptionTileGroup.",
);
consoleSpy.mockReset();
});
skills/carbon-react/components/option-tile.md:90
- The SingleSelectGroup example shows 3 options, but the PR description states single-select groups should be standardized to 4 options. Updating the docs example (3 standard + custom) will keep the documented guidance consistent.
<OptionTileGroup selectionType="single" aria-label="Payment options">
<OptionTile
variant="single"
number={1}
title="Pay now"
onClick={() => setSelected("pay-now")}
data-element={selected === "pay-now" ? "selected" : undefined}
skills/carbon-react/components/option-tile.md:133
- The MultiSelectGroup example shows 3 options, but the PR description states multi-select groups should be standardized to 4 options. Updating the docs example keeps it aligned with the intended usage.
<OptionTileGroup selectionType="multiple" aria-label="Delivery options">
<OptionTile
variant="multiple"
label="Email receipt"
checked={selected.includes("email")}
onChange={toggle("email")}
/>
src/components/option-tile/option-tile.style.ts:101
- The tile has a fixed width of 424px, which can cause horizontal overflow on narrow viewports/containers (e.g. 320px). Clamping with max-width keeps the intended size without breaking responsive layouts.
box-sizing: border-box;
width: 424px;
min-height: var(--global-size-m);
display: flex;
src/components/option-tile/option-tile.stories.tsx:79
- The PR description states both single-select and multi-select groups should be standardized to 4 options, but these Storybook examples currently render 3 tiles in each group. Updating the stories to match the documented behavior will avoid confusion for consumers.
This issue also appears on line 96 of the same file.
<OptionTileGroup selectionType="single" aria-label="Payment options">
<OptionTile
variant="single"
number={1}
title="Pay now"
skills/carbon-react/components/option-tile.md:18
- The props table lists
variantas only "single", but the component supports "single" | "multiple" | "custom". Updating this row prevents consumers from missing the other supported variants.
This issue also appears in the following locations of the same file:
- line 84
- line 127
| Name | Type | Required | Literals | Description | Default |
| --- | --- | --- | --- | --- | --- |
| variant | "single" | Yes | | The single-select visual variant. | |
| disabled | boolean \| undefined | No | | Applies a disabled state to the tile. | |
| data-element | string \| undefined | No | | Identifier used for testing purposes, applied to the root element of the component. | |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/components/option-tile/option-tile.style.ts:100
tileBaseStyleshard-codes awidth: 424px;, which contradicts the PR goal of keeping tiles content-sized (it forces a fixed width regardless of content). Consider switching to an intrinsic width with a max-width cap so tiles size to content but don’t grow unbounded.
box-sizing: border-box;
width: 424px;
min-height: var(--global-size-m);
display: flex;
src/components/option-tile/option-tile.component.tsx:166
- The invariant message is misleading: the code only disallows
variant="custom"inside a multiple selectionOptionTileGroup, but the message says it can only be used within a single-selection group (even though standalone usage is allowed). Update the error text to reflect the actual constraint.
invariant(
selectionType !== "multiple",
"OptionTile with variant='custom' can only be used within a single selection OptionTileGroup.",
);
src/components/option-tile/option-tile.stories.tsx:79
- PR description says both single- and multi-select group examples should be standardized to 4 options, but the Storybook group examples currently render 3 tiles each. Update these examples so consumers see the intended 4-option layout.
<OptionTileGroup selectionType="single" aria-label="Payment options">
<OptionTile
variant="single"
number={1}
title="Pay now"
onClick={() => setSelected("pay-now")}
data-element={selected === "pay-now" ? "selected" : undefined}
/>
<OptionTile
variant="single"
number={2}
title="Pay later"
onClick={() => setSelected("pay-later")}
data-element={selected === "pay-later" ? "selected" : undefined}
/>
<OptionTile
variant="custom"
title="Something else"
inputAriaLabel="Custom amount"
inputPlaceholder="Type amount"
/>
</OptionTileGroup>
src/components/option-tile/components.test-pw.tsx:73
- Similarly to the Storybook examples, the Playwright helper components meant to showcase grouped tiles don’t match the described standard of 4 options per group (single group has 2, multiple group has 2). Aligning these fixtures to 4 options will keep visual/a11y coverage representative of the intended usage.
export const SingleSelectGroupOptionTileComponent = () => {
const [selected, setSelected] = useState("none");
return (
<>
<OptionTileGroup selectionType="single" aria-label="Payment options">
<OptionTile
variant="single"
number={1}
title="Pay now"
onClick={() => setSelected("Pay now")}
/>
<OptionTile
variant="single"
number={2}
title="Pay later"
onClick={() => setSelected("Pay later")}
/>
</OptionTileGroup>
<p>{`Selected: ${selected}`}</p>
</>
);
};
export const GroupedOptionTileComponent = () => {
const [emailChecked, setEmailChecked] = useState(false);
const [smsChecked, setSmsChecked] = useState(false);
return (
<OptionTileGroup selectionType="multiple" aria-label="Delivery options">
<OptionTile
variant="multiple"
label="Email receipt"
checked={emailChecked}
onChange={setEmailChecked}
/>
<OptionTile
variant="multiple"
label="SMS updates"
checked={smsChecked}
onChange={setSmsChecked}
/>
</OptionTileGroup>
);
skills/carbon-react/components/option-tile.md:21
- The generated props table for
OptionTileis incomplete: it only documentsvariant: "single"plus a few common props, but omits key props for all variants (e.g.number,title,label,checked,onChange,inputAriaLabel, etc.). This can mislead consumers of the skills docs; the props section should reflect the full union type (OptionTileProps) and its variant-specific fields.
## Props
| Name | Type | Required | Literals | Description | Default |
| --- | --- | --- | --- | --- | --- |
| variant | "single" | Yes | | The single-select visual variant. | |
| disabled | boolean \| undefined | No | | Applies a disabled state to the tile. | |
| data-element | string \| undefined | No | | Identifier used for testing purposes, applied to the root element of the component. | |
| data-role | string \| undefined | No | | Identifier used for testing purposes, applied to the root element of the component. | |
2c27677 to
7f12fc8
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Suppressed comments (8)
src/components/option-tile/option-tile.test.tsx:182
getByTestId("option-tile-icon")will fail because the icon usesdata-role="option-tile-icon"(nodata-testid). Query the icon via itsdata-rolefrom within the tile button.
const icon = screen.getByTestId("option-tile-icon");
expect(icon).toHaveAttribute("type", "edit");
expect(icon).toHaveAttribute("aria-hidden", "true");
src/components/option-tile/option-tile.component.tsx:235
- When the custom tile is active, the input should receive
disabled={disabled}so it can't be edited if the tile is disabled (including whendisabledchanges after activation).
<StyledCustomInput
ref={inputRef}
aria-label={inputAriaLabel}
placeholder={inputPlaceholder}
value={customValue}
onChange={(ev) => onCustomValueChange(ev.target.value)}
onKeyDown={onActiveKeyDown}
/>
src/components/option-tile/option-tile.stories.tsx:124
- The PR description says multi-select groups should be standardized to 4 options; this Story currently renders only 3. Add a 4th option so the example matches the stated behavior.
<OptionTileGroup selectionType="multiple" aria-label="Delivery options">
<OptionTile
variant="multiple"
label="Email receipt"
checked={selected.includes("email")}
onChange={toggle("email")}
/>
<OptionTile
variant="multiple"
label="SMS updates"
checked={selected.includes("sms")}
onChange={toggle("sms")}
/>
<OptionTile
variant="multiple"
label="Printed statement"
checked={selected.includes("print")}
onChange={toggle("print")}
/>
</OptionTileGroup>
src/components/option-tile/components.test-pw.tsx:79
- The PR description states grouped examples should be standardized to 4 options; this Playwright multi-select group example currently renders only 2 tiles. Expand it to 4 options to reflect the intended guidance.
export const GroupedOptionTileComponent = () => {
const [emailChecked, setEmailChecked] = useState(false);
const [smsChecked, setSmsChecked] = useState(false);
return (
<OptionTileGroup selectionType="multiple" aria-label="Delivery options">
<OptionTile
variant="multiple"
label="Email receipt"
checked={emailChecked}
onChange={setEmailChecked}
/>
<OptionTile
variant="multiple"
label="SMS updates"
checked={smsChecked}
onChange={setSmsChecked}
/>
</OptionTileGroup>
);
src/components/option-tile/option-tile.component.tsx:175
- If
disabledchanges totruewhile the custom tile is active, the input remains editable and the tile stays active. The component should deactivate and notifyonCustomActiveChange(false)when it becomes disabled.
This issue also appears on line 228 of the same file.
const setActive = (next: boolean) => {
setIsActive(next);
onCustomActiveChange?.(next);
};
src/components/option-tile/option-tile.stories.tsx:88
- The PR description says single-select groups should be standardized to 4 options; this Story currently renders only 3 (2 single + 1 custom). Add another single option so the example matches the stated behavior.
This issue also appears on line 105 of the same file.
<OptionTileGroup selectionType="single" aria-label="Payment options">
<OptionTile
variant="single"
number={1}
title="Pay now"
onClick={() => setSelected("pay-now")}
data-element={selected === "pay-now" ? "selected" : undefined}
/>
<OptionTile
variant="single"
number={2}
title="Pay later"
onClick={() => setSelected("pay-later")}
data-element={selected === "pay-later" ? "selected" : undefined}
/>
<OptionTile
variant="custom"
title="Something else"
inputAriaLabel="Custom amount"
inputPlaceholder="Type amount"
customValue={customValue}
onCustomValueChange={setCustomValue}
/>
</OptionTileGroup>
src/components/option-tile/components.test-pw.tsx:54
- The PR description states grouped examples should be standardized to 4 options; this Playwright single-select group example currently renders only 2 tiles. Expand it to 4 options to reflect the intended guidance.
This issue also appears on line 60 of the same file.
<OptionTileGroup selectionType="single" aria-label="Payment options">
<OptionTile
variant="single"
number={1}
title="Pay now"
onClick={() => setSelected("Pay now")}
/>
<OptionTile
variant="single"
number={2}
title="Pay later"
onClick={() => setSelected("Pay later")}
/>
</OptionTileGroup>
skills/carbon-react/components/option-tile.md:22
- The OptionTile props table is currently incorrect/incomplete (e.g. it lists
variantas only"single"and omits the variant-specific props likechecked,onChange,customValue, etc.). This will mislead consumers of this generated component reference.
## Props
| Name | Type | Required | Literals | Description | Default |
| --- | --- | --- | --- | --- | --- |
| variant | "single" | Yes | | The single-select visual variant. | |
| disabled | boolean \| undefined | No | | Applies a disabled state to the tile. | |
| data-element | string \| undefined | No | | Identifier used for testing purposes, applied to the root element of the component. | |
| data-role | string \| undefined | No | | Identifier used for testing purposes, applied to the root element of the component. | |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/components/option-tile/option-tile.component.tsx:232
- When the custom tile is active, the input is not marked as disabled, so if
disabledbecomes true while active the custom value can still be edited. The active input should reflect the disabled state.
<StyledCustomInput
ref={inputRef}
aria-label={inputAriaLabel}
placeholder={inputPlaceholder}
value={customValue}
src/components/option-tile/option-tile.style.ts:99
tileBaseStylessets a fixedwidth: 424px;, which makes tiles a fixed size rather than content-sized. This conflicts with the PR description goal of keeping tiles content-sized (not forced to a fixed width).
width: 424px;
skills/carbon-react/components/option-tile.md:20
- The OptionTile docs props table is inaccurate/incomplete: it lists
variantas only "single", but the component supports "single" | "multiple" | "custom" and has many variant-specific required props (e.g.title,label,checked). This should be regenerated/updated so consumers get correct guidance.
| Name | Type | Required | Literals | Description | Default |
| --- | --- | --- | --- | --- | --- |
| variant | "single" | Yes | | The single-select visual variant. | |
| disabled | boolean \| undefined | No | | Applies a disabled state to the tile. | |
| data-element | string \| undefined | No | | Identifier used for testing purposes, applied to the root element of the component. | |
src/components/option-tile/option-tile.component.tsx:161
- The invariant message is misleading: the custom variant is allowed outside a group (it’s used standalone in stories), but the error says it can only be used within a single selection OptionTileGroup. Consider rewording to specifically prohibit use in a multiple-selection group.
invariant(
selectionType !== "multiple",
"OptionTile with variant='custom' can only be used within a single selection OptionTileGroup.",
);
665579a
Proposed behaviour
Standardize both single-select and multi-select groups to 4 options for consistency.
Keep custom option only for single-select groups and prevent it in multi-select groups.
Support numbered-key selection for single-select options.
Keep tiles content-sized (not forced full width) to better match the intended layout.
Current behaviour
Option tiles are displayed in single and multi-select groups with basic click/keyboard selection.





Group examples were inconsistent with the design guidance (option count/usage patterns varied).
Custom input tile behavior exists, but group-level rules were not clearly enforced in all examples.
Checklist
d.tsfile added or updated if requiredQA
Additional context
Testing instructions