Skip to content

Commit 1733412

Browse files
authored
next: improve radio item selection (#927)
1 parent 5f15b29 commit 1733412

File tree

3 files changed

+54
-22
lines changed

3 files changed

+54
-22
lines changed

.changeset/wild-dogs-attend.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": patch
3+
---
4+
5+
fix: don't auto select first radio item unless there is already a selected value

packages/bits-ui/src/lib/bits/radio-group/radio-group.svelte.ts

+10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
useRovingFocus,
99
} from "$lib/internal/use-roving-focus.svelte.js";
1010
import { createContext } from "$lib/internal/create-context.js";
11+
import { kbd } from "$lib/internal/kbd.js";
1112

1213
const RADIO_GROUP_ROOT_ATTR = "data-radio-group-root";
1314
const RADIO_GROUP_ITEM_ATTR = "data-radio-group-item";
@@ -32,6 +33,7 @@ class RadioGroupRootState {
3233
name: RadioGroupRootStateProps["name"];
3334
value: RadioGroupRootStateProps["value"];
3435
rovingFocusGroup: UseRovingFocusReturn;
36+
hasValue = $derived.by(() => this.value.current !== "");
3537

3638
constructor(props: RadioGroupRootStateProps) {
3739
this.#id = props.id;
@@ -42,6 +44,7 @@ class RadioGroupRootState {
4244
this.name = props.name;
4345
this.value = props.value;
4446
this.#ref = props.ref;
47+
4548
this.rovingFocusGroup = useRovingFocus({
4649
rootNodeId: this.#id,
4750
candidateAttr: RADIO_GROUP_ITEM_ATTR,
@@ -129,10 +132,17 @@ class RadioGroupItemState {
129132
};
130133

131134
#onfocus = () => {
135+
if (!this.#root.hasValue) return;
132136
this.#root.setValue(this.#value.current);
133137
};
134138

135139
#onkeydown = (e: KeyboardEvent) => {
140+
if (this.#isDisabled) return;
141+
if (e.key === kbd.SPACE) {
142+
e.preventDefault();
143+
this.#root.setValue(this.#value.current);
144+
return;
145+
}
136146
this.#root.rovingFocusGroup.handleKeydown(this.#ref.current, e, true);
137147
};
138148

packages/tests/src/tests/radio-group/radio-group.test.ts

+39-22
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,19 @@ describe("radio group", () => {
9191
const item2 = getByTestId(itemIds[2] as string);
9292
const item3 = getByTestId(itemIds[3] as string);
9393
item0.focus();
94-
await waitFor(() => expect(item0).toHaveFocus());
94+
expect(item0).toHaveFocus();
9595
await user.keyboard(kbd.ARROW_DOWN);
96-
await waitFor(() => expect(item1).toHaveFocus());
96+
expect(item1).toHaveFocus();
9797
await user.keyboard(kbd.ARROW_DOWN);
98-
await waitFor(() => expect(item2).toHaveFocus());
98+
expect(item2).toHaveFocus();
9999
await user.keyboard(kbd.ARROW_DOWN);
100-
await waitFor(() => expect(item3).toHaveFocus());
100+
expect(item3).toHaveFocus();
101101
await user.keyboard(kbd.ARROW_UP);
102-
await waitFor(() => expect(item2).toHaveFocus());
102+
expect(item2).toHaveFocus();
103103
await user.keyboard(kbd.ARROW_UP);
104-
await waitFor(() => expect(item1).toHaveFocus());
104+
expect(item1).toHaveFocus();
105105
await user.keyboard(kbd.ARROW_UP);
106-
await waitFor(() => expect(item0).toHaveFocus());
106+
expect(item0).toHaveFocus();
107107
});
108108

109109
it("should navigate through the items using the keyboard (left and right)", async () => {
@@ -114,19 +114,19 @@ describe("radio group", () => {
114114
const item2 = getByTestId(itemIds[2] as string);
115115
const item3 = getByTestId(itemIds[3] as string);
116116
item0.focus();
117-
await waitFor(() => expect(item0).toHaveFocus());
117+
expect(item0).toHaveFocus();
118118
await user.keyboard(kbd.ARROW_RIGHT);
119-
await waitFor(() => expect(item1).toHaveFocus());
119+
expect(item1).toHaveFocus();
120120
await user.keyboard(kbd.ARROW_RIGHT);
121-
await waitFor(() => expect(item2).toHaveFocus());
121+
expect(item2).toHaveFocus();
122122
await user.keyboard(kbd.ARROW_RIGHT);
123-
await waitFor(() => expect(item3).toHaveFocus());
123+
expect(item3).toHaveFocus();
124124
await user.keyboard(kbd.ARROW_LEFT);
125-
await waitFor(() => expect(item2).toHaveFocus());
125+
expect(item2).toHaveFocus();
126126
await user.keyboard(kbd.ARROW_LEFT);
127-
await waitFor(() => expect(item1).toHaveFocus());
127+
expect(item1).toHaveFocus();
128128
await user.keyboard(kbd.ARROW_LEFT);
129-
await waitFor(() => expect(item0).toHaveFocus());
129+
expect(item0).toHaveFocus();
130130
});
131131

132132
it("should respect the loop prop", async () => {
@@ -137,14 +137,14 @@ describe("radio group", () => {
137137
const item0 = getByTestId(itemIds[0] as string);
138138
const item3 = getByTestId(itemIds[3] as string);
139139
item0.focus();
140-
await waitFor(() => expect(item0).toHaveFocus());
140+
expect(item0).toHaveFocus();
141141
await user.keyboard(kbd.ARROW_UP);
142-
await waitFor(() => expect(item0).toHaveFocus());
142+
expect(item0).toHaveFocus();
143143

144144
item3.focus();
145-
await waitFor(() => expect(item3).toHaveFocus());
145+
expect(item3).toHaveFocus();
146146
await user.keyboard(kbd.ARROW_DOWN);
147-
await waitFor(() => expect(item3).toHaveFocus());
147+
expect(item3).toHaveFocus();
148148
});
149149

150150
it("should respect the value prop & binding", async () => {
@@ -170,14 +170,14 @@ describe("radio group", () => {
170170
const item0 = getByTestId(itemIds[0] as string);
171171
const item3 = getByTestId(itemIds[3] as string);
172172
item0.focus();
173-
await waitFor(() => expect(item0).toHaveFocus());
173+
expect(item0).toHaveFocus();
174174
await user.keyboard(kbd.ARROW_LEFT);
175-
await waitFor(() => expect(item0).toHaveFocus());
175+
expect(item0).toHaveFocus();
176176

177177
item3.focus();
178-
await waitFor(() => expect(item3).toHaveFocus());
178+
expect(item3).toHaveFocus();
179179
await user.keyboard(kbd.ARROW_RIGHT);
180-
await waitFor(() => expect(item3).toHaveFocus());
180+
expect(item3).toHaveFocus();
181181
});
182182

183183
it("should not render an input if the `name` prop isn't passed", async () => {
@@ -220,4 +220,21 @@ describe("radio group", () => {
220220

221221
expect(input).toHaveAttribute("disabled");
222222
});
223+
224+
it("should not automatically select the first item focused when the radio group does not have a value", async () => {
225+
const { getByTestId, user, input } = setup({ name: "radio-group" });
226+
227+
const aItem = getByTestId("a-item");
228+
aItem.focus();
229+
expect(input).toHaveValue("");
230+
await user.keyboard(kbd.ARROW_DOWN);
231+
expect(input).toHaveValue("");
232+
const bItem = getByTestId("b-item");
233+
expect(bItem).toHaveFocus();
234+
await user.keyboard(kbd.SPACE);
235+
expect(input).toHaveValue("b");
236+
await user.keyboard(kbd.ARROW_UP);
237+
expect(aItem).toHaveFocus();
238+
expect(input).toHaveValue("a");
239+
});
223240
});

0 commit comments

Comments
 (0)