Skip to content

Commit 8aa012c

Browse files
committed
fix(core): keep disabled SelectableCard focusable with aria-disabled
1 parent 592d080 commit 8aa012c

4 files changed

Lines changed: 46 additions & 6 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@astryxdesign/core': patch
3+
---
4+
5+
[fix] SelectableCard: keep disabled cards in sequential focus navigation with aria-disabled, form detachment, and gated interaction handlers.
6+
7+
@Geervan

apps/storybook/stories/SelectableCard.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export const Disabled: Story = {
234234
docs: {
235235
description: {
236236
story:
237-
'`isDisabled` suppresses toggle, hover, focus. Accent border remains visible on disabled+selected cards.',
237+
'`isDisabled` suppresses toggle and hover; the control remains focusable and exposes aria-disabled. Accent border remains visible on disabled+selected cards.',
238238
},
239239
},
240240
},

packages/core/src/SelectableCard/SelectableCard.test.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe('SelectableCard', () => {
7878
expect(handleChange).toHaveBeenCalledWith(true);
7979
});
8080

81-
it('disabled checkbox is disabled', () => {
81+
it('applies aria-disabled, drops native disabled, and detaches form when isDisabled', () => {
8282
const handleChange = vi.fn();
8383
render(
8484
<SelectableCard
@@ -90,7 +90,28 @@ describe('SelectableCard', () => {
9090
</SelectableCard>,
9191
);
9292
const checkbox = screen.getByRole('checkbox', {name: 'Disabled'});
93-
expect(checkbox).toBeDisabled();
93+
expect(checkbox).toHaveAttribute('aria-disabled', 'true');
94+
expect(checkbox).not.toHaveAttribute('disabled');
95+
expect(checkbox).toHaveAttribute('form', '');
96+
checkbox.focus();
97+
expect(checkbox).toHaveFocus();
98+
});
99+
100+
it('does not toggle on Space or direct checkbox click when disabled', () => {
101+
const handleChange = vi.fn();
102+
render(
103+
<SelectableCard
104+
label="Disabled"
105+
isSelected={false}
106+
onChange={handleChange}
107+
isDisabled>
108+
Content
109+
</SelectableCard>,
110+
);
111+
const checkbox = screen.getByRole('checkbox', {name: 'Disabled'});
112+
fireEvent.click(checkbox);
113+
fireEvent.keyDown(checkbox, {key: ' '});
114+
expect(handleChange).not.toHaveBeenCalled();
94115
});
95116

96117
it('does not call onChange when disabled card is clicked', () => {

packages/core/src/SelectableCard/SelectableCard.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,13 @@ export function SelectableCard({
332332
// handling, so we deliberately do not toggle on Space here (would double).
333333
const handleKeyDown = useCallback(
334334
(event: KeyboardEvent<HTMLInputElement>) => {
335-
if (!isDisabled && event.key === 'Enter') {
335+
if (isDisabled) {
336+
if (event.key === ' ' || event.key === 'Enter') {
337+
event.preventDefault();
338+
}
339+
return;
340+
}
341+
if (event.key === 'Enter') {
336342
event.preventDefault();
337343
onChange(!isSelected);
338344
}
@@ -397,8 +403,14 @@ export function SelectableCard({
397403
type="checkbox"
398404
checked={isSelected}
399405
aria-label={label}
400-
disabled={isDisabled}
401-
onChange={() => onChange(!isSelected)}
406+
aria-disabled={isDisabled ? 'true' : undefined}
407+
form={isDisabled ? '' : undefined}
408+
onChange={() => {
409+
if (isDisabled) {
410+
return;
411+
}
412+
onChange(!isSelected);
413+
}}
402414
onKeyDown={handleKeyDown}
403415
{...stylex.props(styles.srOnly)}
404416
/>

0 commit comments

Comments
 (0)