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
13 changes: 7 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
2 changes: 1 addition & 1 deletion preview/src/components/select/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub fn SelectOption<T: Clone + PartialEq + 'static>(props: SelectOptionProps<T>)
text_value: props.text_value,
disabled: props.disabled,
id: props.id,
index: props.index,
tab_index: props.tab_index,
aria_label: props.aria_label,
aria_roledescription: props.aria_roledescription,
attributes: props.attributes,
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>> {
tab_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>> {
tab_index: Fruit::COUNT,
value: None,
text_value: "Other",
disabled: true,
"Other"
SelectItemIndicator {}
}
}
}
}
}
}
6 changes: 3 additions & 3 deletions preview/src/components/select/variants/main/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl Fruit {
pub fn Demo() -> Element {
let fruits = Fruit::iter().enumerate().map(|(i, f)| {
rsx! {
SelectOption::<Option<Fruit>> { index: i, value: f, text_value: "{f}",
SelectOption::<Option<Fruit>> { tab_index: i, value: f, text_value: "{f}",
{format!("{} {f}", f.emoji())}
SelectItemIndicator {}
}
Expand All @@ -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 All @@ -46,7 +46,7 @@ pub fn Demo() -> Element {
SelectGroup {
SelectGroupLabel { "Other" }
SelectOption::<Option<Fruit>> {
index: Fruit::COUNT,
tab_index: Fruit::COUNT,
value: None,
text_value: "Other",
"Other"
Expand Down
1 change: 1 addition & 0 deletions primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dioxus-sdk-time = "0.7.0"
time = { version = "0.3.41", features = ["std", "macros", "parsing"] }
num-integer = "0.1.46"
tracing.workspace = true
indexmap = "2.12.1"

[build-dependencies]
lazy-js-bundle = "0.6.2"
Expand Down
2 changes: 1 addition & 1 deletion primitives/src/context_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ pub fn ContextMenuItem(props: ContextMenuItemProps) -> Element {
let focused = move || ctx.focus.is_focused(props.index.cloned());

// Handle settings focus
let onmounted = use_focus_controlled_item(props.index);
let onmounted = use_focus_controlled_item(props.index.cloned(), props.index);

// Determine if this item is currently focused
let tab_index = use_memo(move || if focused() { "0" } else { "-1" });
Expand Down
2 changes: 1 addition & 1 deletion primitives/src/date_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ fn DateSegment<T: Clone + Copy + Integer + FromStr + Display + 'static>(
}
};

let onmounted = use_focus_controlled_item(props.index);
let onmounted = use_focus_controlled_item(props.index.cloned(), props.index);

let span_id = use_unique_id();
let id = use_memo(move || format!("span-{span_id}"));
Expand Down
2 changes: 1 addition & 1 deletion primitives/src/dropdown_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ pub fn DropdownMenuItem<T: Clone + PartialEq + 'static>(
let disabled = move || (ctx.disabled)() || (props.disabled)();
let focused = move || ctx.focus.is_focused((props.index)());

let onmounted = use_focus_controlled_item(props.index);
let onmounted = use_focus_controlled_item(props.index.cloned(), props.index);

rsx! {
div {
Expand Down
Loading
Loading