Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 114 additions & 13 deletions playwright/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ test("test", async ({ page }) => {
timeout: 20 * 60 * 1000,
}); // Increase timeout to 20 minutes
// Find Select a fruit...
let selectTrigger = page.locator(".select-trigger");
let selectTrigger = page.locator("#select-main .select-trigger");
await selectTrigger.click();
// Assert the select menu is open
const selectMenu = page.locator(".select-list");
const selectMenu = page.locator("#select-main .select-list");
await expect(selectMenu).toHaveAttribute("data-state", "open");

// Assert the menu is focused
Expand Down Expand Up @@ -64,10 +64,10 @@ test("test", async ({ page }) => {
test("tabbing out of menu closes the select menu", async ({ page }) => {
await page.goto("http://127.0.0.1:8080/component/?name=select&");
// Find Select a fruit...
let selectTrigger = page.locator(".select-trigger");
let selectTrigger = page.locator("#select-main .select-trigger");
await selectTrigger.click();
// Assert the select menu is open
const selectMenu = page.locator(".select-list");
const selectMenu = page.locator("#select-main .select-list");
await expect(selectMenu).toHaveAttribute("data-state", "open");

// Assert the menu is focused
Expand All @@ -80,10 +80,10 @@ test("tabbing out of menu closes the select menu", async ({ page }) => {
test("tabbing out of item closes the select menu", async ({ page }) => {
await page.goto("http://127.0.0.1:8080/component/?name=select&");
// Find Select a fruit...
let selectTrigger = page.locator(".select-trigger");
let selectTrigger = page.locator("#select-main .select-trigger");
await selectTrigger.click();
// Assert the select menu is open
const selectMenu = page.locator(".select-list");
const selectMenu = page.locator("#select-main .select-list");
await expect(selectMenu).toHaveAttribute("data-state", "open");

// Assert the menu is focused
Expand All @@ -101,10 +101,10 @@ test("tabbing out of item closes the select menu", async ({ page }) => {
test("options selected", async ({ page }) => {
await page.goto("http://127.0.0.1:8080/component/?name=select&");
// Find Select a fruit...
let selectTrigger = page.locator(".select-trigger");
let selectTrigger = page.locator("#select-main .select-trigger");
await selectTrigger.click();
// Assert the select menu is open
const selectMenu = page.locator(".select-list");
const selectMenu = page.locator("#select-main .select-list");
await expect(selectMenu).toHaveAttribute("data-state", "open");

// Assert no items have aria-selected
Expand All @@ -130,25 +130,126 @@ test("options selected", async ({ page }) => {
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");
let selectTrigger = page.locator("#select-main .select-trigger");
const selectMenu = page.locator("#select-main .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();

// Same thing but with the first option disabled
let disabledSelectTrigger = page.locator("#select-disabled .select-trigger");
const disabledSelectMenu = page.locator("#select-disabled .select-list");
await disabledSelectTrigger.focus();
await page.keyboard.press("ArrowDown");
const disabledFirstOption = disabledSelectMenu.getByRole("option", { name: "banana" });
await expect(disabledFirstOption).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");
let selectTrigger = page.locator("#select-main .select-trigger");
const selectMenu = page.locator("#select-main .select-list");
await selectTrigger.focus();

// Select the first option
await page.keyboard.press("ArrowUp");
const firstOption = selectMenu.getByRole("option", { name: "other" });
const lastOption = selectMenu.getByRole("option", { name: "other" });
await expect(lastOption).toBeFocused();

// Same thing but with the last option disabled
let disabledSelectTrigger = page.locator("#select-disabled .select-trigger");
const disabledSelectMenu = page.locator("#select-disabled .select-list");
await disabledSelectTrigger.focus();

await page.keyboard.press("ArrowUp");
const disabledLastOption = disabledSelectMenu.getByRole("option", { name: "watermelon" });
await expect(disabledLastOption).toBeFocused();
});

test("rollover on top and bottom", async ({ page }) => {
await page.goto("http://127.0.0.1:8080/component/?name=select&");

// Find Select a fruit...
let selectTrigger = page.locator("#select-main .select-trigger");
const selectMenu = page.locator("#select-main .select-list");
await selectTrigger.focus();

// open the list and select first option
await page.keyboard.press("ArrowDown");
const firstOption = selectMenu.getByRole("option", { name: "apple" });
await expect(firstOption).toBeFocused();

// up arrow to select last option (rollover)
await page.keyboard.press("ArrowUp");
const lastOption = selectMenu.getByRole("option", { name: "other" });
await expect(lastOption).toBeFocused();

// down arrow to select first option (rollover)
await page.keyboard.press("ArrowDown");
await expect(firstOption).toBeFocused();

// Same thing but with first and last options disabled
let disabledSelectTrigger = page.locator("#select-disabled .select-trigger");
const disabledSelectMenu = page.locator("#select-disabled .select-list");
await disabledSelectTrigger.focus();

// open the list and select first option
await page.keyboard.press("ArrowDown");
const disabledFirstOption = disabledSelectMenu.getByRole("option", { name: "banana" });
await expect(disabledFirstOption).toBeFocused();

// up arrow to select last option (rollover)
await page.keyboard.press("ArrowUp");
const disabledLastOption = disabledSelectMenu.getByRole("option", { name: "watermelon" });
await expect(disabledLastOption).toBeFocused();

// down arrow to select first option (rollover)
await page.keyboard.press("ArrowDown");
await expect(disabledFirstOption).toBeFocused();
});

test("disabled elements are skipped", async ({ page }) => {
await page.goto("http://127.0.0.1:8080/component/?name=select&");

// Find Select a fruit...
let selectTrigger = page.locator("#select-disabled .select-trigger");
const selectMenu = page.locator("#select-disabled .select-list");
await selectTrigger.focus();

// open the list and select first enabled option
await page.keyboard.press("ArrowDown");
const firstOption = selectMenu.getByRole("option", { name: "banana" });
await expect(firstOption).toBeFocused();

// down arrow to select second enabled option
await page.keyboard.press("ArrowDown");
const secondOption = selectMenu.getByRole("option", { name: "strawberry" });
await expect(secondOption).toBeFocused();

// up arrow to select first enabled option
await page.keyboard.press("ArrowUp");
await expect(firstOption).toBeFocused();
});

test("aria active descendant", async ({ page }) => {
await page.goto("http://127.0.0.1:8080/component/?name=select&");

// Find Select a fruit...
let selectTrigger = page.locator("#select-main .select-trigger");
const selectMenu = page.locator("#select-main .select-list");
await selectTrigger.focus();

// select first option
await page.keyboard.press("ArrowDown");
const firstOption = selectMenu.getByRole("option", { name: "apple" });
await expect(selectTrigger).toHaveAttribute("aria-activedescendant", await firstOption.getAttribute("id"));

// select second option
await page.keyboard.press("ArrowDown");
const secondOption = selectMenu.getByRole("option", { name: "banana" });
await expect(selectTrigger).toHaveAttribute("aria-activedescendant", await secondOption.getAttribute("id"));
});
2 changes: 1 addition & 1 deletion preview/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ examples!(
progress,
radio_group,
scroll_area,
select,
select[disabled],
separator,
skeleton,
sheet,
Expand Down
6 changes: 1 addition & 5 deletions preview/src/components/select/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
.select-option[data-disabled="true"] {
color: var(--secondary-color-5);
cursor: not-allowed;
opacity: 0.5;
}

.select-option:hover:not([data-disabled="true"]),
Expand All @@ -148,8 +149,3 @@
color: var(--secondary-color-5);
font-size: 0.75rem;
}

[data-disabled="true"] {
cursor: not-allowed;
opacity: 0.5;
}
71 changes: 71 additions & 0 deletions preview/src/components/select/variants/disabled/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use super::super::component::*;
use dioxus::prelude::*;
use strum::{EnumCount, IntoEnumIterator};

#[derive(Debug, Clone, Copy, PartialEq, strum::EnumCount, strum::EnumIter, strum::Display)]
enum Fruit {
Apple,
Banana,
Orange,
Strawberry,
Watermelon,
}

impl Fruit {
const fn emoji(&self) -> &'static str {
match self {
Fruit::Apple => "🍎",
Fruit::Banana => "🍌",
Fruit::Orange => "🍊",
Fruit::Strawberry => "🍓",
Fruit::Watermelon => "🍉",
}
}

const fn disabled(&self) -> bool {
match self {
Fruit::Apple => true,
Fruit::Orange => true,
_ => false
}
}
}

#[component]
pub fn Demo() -> Element {
let fruits = Fruit::iter().enumerate().map(|(i, f)| {
rsx! {
SelectOption::<Option<Fruit>> {
index: i,
value: f,
text_value: "{f}",
disabled: f.disabled(),
{format!("{} {f}", f.emoji())}
SelectItemIndicator {}
}
}
});

rsx! {
Select::<Option<Fruit>> { id: "select-disabled", placeholder: "Select a fruit...",
SelectTrigger { aria_label: "Select Trigger", width: "12rem", SelectValue {} }
SelectList { aria_label: "Select Demo",
SelectGroup {
SelectGroupLabel { "Fruits" }
{fruits}
}
SelectGroup {
SelectGroupLabel { "Other" }
SelectOption::<Option<Fruit>> {
index: Fruit::COUNT,
value: None,
text_value: "Other",
disabled: true,
"Other"
SelectItemIndicator {}
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion preview/src/components/select/variants/main/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub fn Demo() -> Element {

rsx! {

Select::<Option<Fruit>> { placeholder: "Select a fruit...",
Select::<Option<Fruit>> { id: "select-main", placeholder: "Select a fruit...",
SelectTrigger { aria_label: "Select Trigger", width: "12rem", SelectValue {} }
SelectList { aria_label: "Select Demo",
SelectGroup {
Expand Down
2 changes: 1 addition & 1 deletion primitives/src/select/components/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ pub fn SelectGroupLabel(props: SelectGroupLabelProps) -> Element {
let render = use_context::<SelectListContext>().render;

rsx! {
if render () {
if render() {
div {
// Set the ID for the label
id,
Expand Down
20 changes: 13 additions & 7 deletions primitives/src/select/components/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub fn SelectList(props: SelectListProps) -> Element {

let mut open = ctx.open;
let mut listbox_ref: Signal<Option<std::rc::Rc<MountedData>>> = use_signal(|| None);
let focused = move || open() && !ctx.focus_state.any_focused();
let focused = move || open() && !ctx.any_focused();

use_effect(move || {
let Some(listbox_ref) = listbox_ref() else {
Expand Down Expand Up @@ -125,19 +125,19 @@ pub fn SelectList(props: SelectListProps) -> Element {
}
Key::ArrowUp => {
arrow_key_navigation(event);
ctx.focus_state.focus_prev();
ctx.focus_prev();
}
Key::End => {
arrow_key_navigation(event);
ctx.focus_state.focus_last();
ctx.focus_last();
}
Key::ArrowDown => {
arrow_key_navigation(event);
ctx.focus_state.focus_next();
ctx.focus_next();
}
Key::Home => {
arrow_key_navigation(event);
ctx.focus_state.focus_first();
ctx.focus_first();
}
Key::Enter => {
ctx.select_current_item();
Expand All @@ -163,9 +163,15 @@ pub fn SelectList(props: SelectListProps) -> Element {

use_effect(move || {
if render() {
ctx.focus_state.set_focus(ctx.initial_focus.cloned());
if let Some(last) = (ctx.initial_focus_last)() {
if last {
ctx.focus_last();
} else {
ctx.focus_first();
}
}
} else {
ctx.initial_focus.set(None);
ctx.initial_focus_last.set(None);
}
});

Expand Down
Loading
Loading