Skip to content

Commit 2484385

Browse files
authored
Fix initial focus with up/down arrow keys in select component (#153)
* fix initial focus with up/down arrow keys in select component * fix min item count
1 parent a15f329 commit 2484385

File tree

6 files changed

+44
-2
lines changed

6 files changed

+44
-2
lines changed

playwright/select.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,29 @@ test("options selected", async ({ page }) => {
126126
// Assert the first option is now selected
127127
await expect(firstOption).toHaveAttribute("aria-selected", "true");
128128
});
129+
130+
test("down arrow selects first element", async ({ page }) => {
131+
await page.goto("http://127.0.0.1:8080/component/?name=select&");
132+
// Find Select a fruit...
133+
let selectTrigger = page.locator(".select-trigger");
134+
const selectMenu = page.locator(".select-list");
135+
await selectTrigger.focus();
136+
137+
// Select the first option
138+
await page.keyboard.press("ArrowDown");
139+
const firstOption = selectMenu.getByRole("option", { name: "apple" });
140+
await expect(firstOption).toBeFocused();
141+
});
142+
143+
test("up arrow selects last element", async ({ page }) => {
144+
await page.goto("http://127.0.0.1:8080/component/?name=select&");
145+
// Find Select a fruit...
146+
let selectTrigger = page.locator(".select-trigger");
147+
const selectMenu = page.locator(".select-list");
148+
await selectTrigger.focus();
149+
150+
// Select the first option
151+
await page.keyboard.press("ArrowUp");
152+
const firstOption = selectMenu.getByRole("option", { name: "other" });
153+
await expect(firstOption).toBeFocused();
154+
});

primitives/src/focus.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ impl FocusState {
173173
self.item_count += 1;
174174
}
175175

176+
pub(crate) fn item_count(&self) -> usize {
177+
self.item_count.cloned()
178+
}
179+
176180
pub(crate) fn remove_item(&mut self, index: usize) {
177181
self.item_count -= 1;
178182
if (self.current_focus)() == Some(index) {

primitives/src/select/components/list.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ pub fn SelectList(props: SelectListProps) -> Element {
161161
render: render.into(),
162162
});
163163

164+
use_effect(move || {
165+
if render() {
166+
ctx.focus_state.set_focus(ctx.initial_focus.cloned());
167+
} else {
168+
ctx.initial_focus.set(None);
169+
}
170+
});
171+
164172
rsx! {
165173
if render() {
166174
div {

primitives/src/select/components/select.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ pub fn Select<T: Clone + PartialEq + 'static>(props: SelectProps<T>) -> Element
143143
typeahead_buffer.take();
144144
}
145145
});
146+
let initial_focus = use_signal(|| None);
146147

147148
use_context_provider(|| SelectContext {
148149
typeahead_buffer,
@@ -157,6 +158,7 @@ pub fn Select<T: Clone + PartialEq + 'static>(props: SelectProps<T>) -> Element
157158
placeholder: props.placeholder,
158159
typeahead_clear_task,
159160
typeahead_timeout: props.typeahead_timeout,
161+
initial_focus,
160162
});
161163

162164
rsx! {

primitives/src/select/components/trigger.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ pub fn SelectTrigger(props: SelectTriggerProps) -> Element {
8282
match event.key() {
8383
Key::ArrowUp => {
8484
open.set(true);
85-
ctx.focus_state.focus_last();
85+
ctx.initial_focus.set(ctx.focus_state.item_count().checked_sub(1));
8686
event.prevent_default();
8787
event.stop_propagation();
8888
}
8989
Key::ArrowDown => {
9090
open.set(true);
91-
ctx.focus_state.focus_first();
91+
ctx.initial_focus.set((ctx.focus_state.item_count() > 0).then_some(0));
9292
event.prevent_default();
9393
event.stop_propagation();
9494
}

primitives/src/select/context.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ pub(super) struct SelectContext {
7373
pub typeahead_clear_task: Signal<Option<Task>>,
7474
/// Timeout before clearing typeahead buffer
7575
pub typeahead_timeout: ReadSignal<Duration>,
76+
/// The initial element to focus once the list is rendered
77+
pub initial_focus: Signal<Option<usize>>,
7678
}
7779

7880
impl SelectContext {

0 commit comments

Comments
 (0)