diff --git a/playwright/select.spec.ts b/playwright/select.spec.ts index bf0f48fe..cb612ebc 100644 --- a/playwright/select.spec.ts +++ b/playwright/select.spec.ts @@ -126,3 +126,29 @@ test("options selected", async ({ page }) => { // Assert the first option is now selected await expect(firstOption).toHaveAttribute("aria-selected", "true"); }); + +test("down arrow selects first element", async ({ page }) => { + await page.goto("http://127.0.0.1:8080/component/?name=select&"); + // Find Select a fruit... + let selectTrigger = page.locator(".select-trigger"); + const selectMenu = page.locator(".select-list"); + await selectTrigger.focus(); + + // Select the first option + await page.keyboard.press("ArrowDown"); + const firstOption = selectMenu.getByRole("option", { name: "apple" }); + await expect(firstOption).toBeFocused(); +}); + +test("up arrow selects last element", async ({ page }) => { + await page.goto("http://127.0.0.1:8080/component/?name=select&"); + // Find Select a fruit... + let selectTrigger = page.locator(".select-trigger"); + const selectMenu = page.locator(".select-list"); + await selectTrigger.focus(); + + // Select the first option + await page.keyboard.press("ArrowUp"); + const firstOption = selectMenu.getByRole("option", { name: "other" }); + await expect(firstOption).toBeFocused(); +}); diff --git a/primitives/src/focus.rs b/primitives/src/focus.rs index 11845e72..1d574f9a 100644 --- a/primitives/src/focus.rs +++ b/primitives/src/focus.rs @@ -173,6 +173,10 @@ impl FocusState { self.item_count += 1; } + pub(crate) fn item_count(&self) -> usize { + self.item_count.cloned() + } + pub(crate) fn remove_item(&mut self, index: usize) { self.item_count -= 1; if (self.current_focus)() == Some(index) { diff --git a/primitives/src/select/components/list.rs b/primitives/src/select/components/list.rs index 4fe24553..019d9318 100644 --- a/primitives/src/select/components/list.rs +++ b/primitives/src/select/components/list.rs @@ -161,6 +161,14 @@ pub fn SelectList(props: SelectListProps) -> Element { render: render.into(), }); + use_effect(move || { + if render() { + ctx.focus_state.set_focus(ctx.initial_focus.cloned()); + } else { + ctx.initial_focus.set(None); + } + }); + rsx! { if render() { div { diff --git a/primitives/src/select/components/select.rs b/primitives/src/select/components/select.rs index e9465417..6844b6c2 100644 --- a/primitives/src/select/components/select.rs +++ b/primitives/src/select/components/select.rs @@ -143,6 +143,7 @@ pub fn Select(props: SelectProps) -> Element typeahead_buffer.take(); } }); + let initial_focus = use_signal(|| None); use_context_provider(|| SelectContext { typeahead_buffer, @@ -157,6 +158,7 @@ pub fn Select(props: SelectProps) -> Element placeholder: props.placeholder, typeahead_clear_task, typeahead_timeout: props.typeahead_timeout, + initial_focus, }); rsx! { diff --git a/primitives/src/select/components/trigger.rs b/primitives/src/select/components/trigger.rs index 66e66db9..f6162f6a 100644 --- a/primitives/src/select/components/trigger.rs +++ b/primitives/src/select/components/trigger.rs @@ -82,13 +82,13 @@ pub fn SelectTrigger(props: SelectTriggerProps) -> Element { match event.key() { Key::ArrowUp => { open.set(true); - ctx.focus_state.focus_last(); + ctx.initial_focus.set(ctx.focus_state.item_count().checked_sub(1)); event.prevent_default(); event.stop_propagation(); } Key::ArrowDown => { open.set(true); - ctx.focus_state.focus_first(); + ctx.initial_focus.set((ctx.focus_state.item_count() > 0).then_some(0)); event.prevent_default(); event.stop_propagation(); } diff --git a/primitives/src/select/context.rs b/primitives/src/select/context.rs index 70fb86fa..459292f7 100644 --- a/primitives/src/select/context.rs +++ b/primitives/src/select/context.rs @@ -73,6 +73,8 @@ pub(super) struct SelectContext { pub typeahead_clear_task: Signal>, /// Timeout before clearing typeahead buffer pub typeahead_timeout: ReadSignal, + /// The initial element to focus once the list is rendered + pub initial_focus: Signal>, } impl SelectContext {