diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9b87ba03..63a5e3a9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -71,7 +71,6 @@ jobs:
- uses: browser-actions/setup-firefox@latest
- run: cargo test --workspace
-
fmt:
if: github.event.pull_request.draft == false
name: Rustfmt
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
new file mode 100644
index 00000000..b982c0b1
--- /dev/null
+++ b/.github/workflows/playwright.yml
@@ -0,0 +1,67 @@
+name: Playwright Tests
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - /**
+ - preview/**/*.rs
+ - preview/**/Cargo.toml
+ - primitives/**/*.rs
+ - primitives/**/Cargo.toml
+ - .github/**
+ - Cargo.toml
+
+ pull_request:
+ types: [opened, synchronize, reopened, ready_for_review]
+ branches:
+ - main
+ paths:
+ - /**
+ - preview/**/*.rs
+ - preview/**/Cargo.toml
+ - primitives/**/*.rs
+ - primitives/**/Cargo.toml
+ - .github/**
+ - Cargo.toml
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ if: github.event.pull_request.draft == false
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+ steps:
+ # Do our best to cache the toolchain and node install steps
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: lts/*
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@1.86.0
+ with:
+ targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
+ - uses: Swatinem/rust-cache@v2
+ with:
+ cache-all-crates: "true"
+ cache-on-failure: "true"
+ - name: Clear base path
+ run: rm -f preview/Dioxus.toml
+ - name: Install dx
+ run: cargo install dioxus-cli@0.7.0-alpha.1
+ - name: Install dependencies
+ run: cd ./playwright && npm ci
+ - name: Install Playwright Browsers
+ run: cd ./playwright && npx playwright install --with-deps
+ - name: Run Playwright tests
+ run: cd ./playwright && npx playwright test
+ - uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
diff --git a/.gitignore b/.gitignore
index 48b1e1b1..bc8a3e9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,11 @@
target
**/.claude/settings.local.json
+node_modules
+
+# Playwright
+/test-results/
+/playwright/playwright-report/
+/playwright/test-results/
+/blob-report/
+/playwright/.cache/
diff --git a/playwright/package-lock.json b/playwright/package-lock.json
new file mode 100644
index 00000000..1447213e
--- /dev/null
+++ b/playwright/package-lock.json
@@ -0,0 +1,216 @@
+{
+ "name": "playwright",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "@axe-core/playwright": "^4.10.2",
+ "@playwright/test": "^1.53.0",
+ "axe-playwright": "^2.1.0",
+ "playwright": "^1.53.0"
+ }
+ },
+ "node_modules/@axe-core/playwright": {
+ "version": "4.10.2",
+ "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.2.tgz",
+ "integrity": "sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "axe-core": "~4.10.3"
+ },
+ "peerDependencies": {
+ "playwright-core": ">= 1.0.0"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.53.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz",
+ "integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.53.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@types/junit-report-builder": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz",
+ "integrity": "sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/axe-core": {
+ "version": "4.10.3",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
+ "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axe-html-reporter": {
+ "version": "2.2.11",
+ "resolved": "https://registry.npmjs.org/axe-html-reporter/-/axe-html-reporter-2.2.11.tgz",
+ "integrity": "sha512-WlF+xlNVgNVWiM6IdVrsh+N0Cw7qupe5HT9N6Uyi+aN7f6SSi92RDomiP1noW8OWIV85V6x404m5oKMeqRV3tQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mustache": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ },
+ "peerDependencies": {
+ "axe-core": ">=3"
+ }
+ },
+ "node_modules/axe-playwright": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/axe-playwright/-/axe-playwright-2.1.0.tgz",
+ "integrity": "sha512-tY48SX56XaAp16oHPyD4DXpybz8Jxdz9P7exTjF/4AV70EGUavk+1fUPWirM0OYBR+YyDx6hUeDvuHVA6fB9YA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/junit-report-builder": "^3.0.2",
+ "axe-core": "^4.10.1",
+ "axe-html-reporter": "2.2.11",
+ "junit-report-builder": "^5.1.1",
+ "picocolors": "^1.1.1"
+ },
+ "peerDependencies": {
+ "playwright": ">1.0.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/junit-report-builder": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-5.1.1.tgz",
+ "integrity": "sha512-ZNOIIGMzqCGcHQEA2Q4rIQQ3Df6gSIfne+X9Rly9Bc2y55KxAZu8iGv+n2pP0bLf0XAOctJZgeloC54hWzCahQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.21",
+ "make-dir": "^3.1.0",
+ "xmlbuilder": "^15.1.1"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mustache": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
+ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mustache": "bin/mustache"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/playwright": {
+ "version": "1.53.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz",
+ "integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.53.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.53.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz",
+ "integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
+ "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0"
+ }
+ }
+ }
+}
diff --git a/playwright/package.json b/playwright/package.json
new file mode 100644
index 00000000..3c3b838f
--- /dev/null
+++ b/playwright/package.json
@@ -0,0 +1,8 @@
+{
+ "devDependencies": {
+ "@axe-core/playwright": "^4.10.2",
+ "@playwright/test": "^1.53.0",
+ "axe-playwright": "^2.1.0",
+ "playwright": "^1.53.0"
+ }
+}
diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts
new file mode 100644
index 00000000..f4491f9d
--- /dev/null
+++ b/playwright/playwright.config.ts
@@ -0,0 +1,76 @@
+import { defineConfig, devices } from "@playwright/test";
+const path = require("path");
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// import dotenv from 'dotenv';
+// import path from 'path';
+// dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: ".",
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: "html",
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ // baseURL: 'http://localhost:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: "on-first-retry",
+ },
+
+ // Each test is given 20 minutes.
+ timeout: 20 * 60 * 1000,
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+
+ {
+ name: "firefox",
+ use: { ...devices["Desktop Firefox"] },
+ },
+
+ {
+ name: "webkit",
+ use: { ...devices["Desktop Safari"] },
+ },
+
+ /* Test against mobile viewports. */
+ {
+ name: "Mobile Chrome",
+ use: { ...devices["Pixel 5"] },
+ },
+ {
+ name: "Mobile Safari",
+ use: { ...devices["iPhone 12"] },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ cwd: path.join(process.cwd(), "../preview"),
+ command: "dx serve --platform web",
+ port: 8080,
+ timeout: 50 * 60 * 1000,
+ reuseExistingServer: !process.env.CI,
+ stdout: "pipe",
+ },
+});
diff --git a/playwright/preview.spec.ts b/playwright/preview.spec.ts
new file mode 100644
index 00000000..c0766ad6
--- /dev/null
+++ b/playwright/preview.spec.ts
@@ -0,0 +1,20 @@
+import { test, expect } from "@playwright/test";
+import AxeBuilder from "@axe-core/playwright";
+
+test.describe("homepage", () => {
+ test("should not have any automatically detectable accessibility issues", async ({
+ page,
+ }) => {
+ await page.goto("http://127.0.0.1:8080/", { timeout: 20 * 60 * 1000 }); // Increase timeout to 20 minutes
+
+ // Wait for the page to fully load
+ let heroSection = page.locator("#hero");
+ await heroSection.waitFor({ state: "visible" });
+
+ const accessibilityScanResults = await new AxeBuilder({ page })
+ .disableRules("color-contrast")
+ .analyze();
+
+ expect(accessibilityScanResults.violations).toEqual([]);
+ });
+});
diff --git a/preview/index.html b/preview/index.html
new file mode 100644
index 00000000..f616cc56
--- /dev/null
+++ b/preview/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Dioxus Components
+
+
+
+
+
diff --git a/preview/src/components/avatar/mod.rs b/preview/src/components/avatar/mod.rs
index adf3cd02..dd6de40b 100644
--- a/preview/src/components/avatar/mod.rs
+++ b/preview/src/components/avatar/mod.rs
@@ -16,6 +16,7 @@ pub(super) fn Demo() -> Element {
on_state_change: move |state| {
avatar_state.set(format!("Avatar 1: {state:?}"));
},
+ aria_label: "Basic avatar",
AvatarImage {
class: "avatar-image",
src: "https://avatars.githubusercontent.com/u/66571940?s=96&v=4",
@@ -31,6 +32,7 @@ pub(super) fn Demo() -> Element {
on_state_change: move |state| {
avatar_state.set(format!("Avatar 2: {state:?}"));
},
+ aria_label: "Error avatar",
AvatarImage {
class: "avatar-image",
src: "https://invalid-url.example/image.jpg",
@@ -46,6 +48,7 @@ pub(super) fn Demo() -> Element {
on_state_change: move |state| {
avatar_state.set(format!("Avatar 4: {state:?}"));
},
+ aria_label: "Large avatar",
AvatarImage {
class: "avatar-image",
src: asset!("/assets/dioxus-logo.png", ImageAssetOptions::new().with_avif()),
diff --git a/preview/src/components/checkbox/mod.rs b/preview/src/components/checkbox/mod.rs
index a6a2f760..14754bd4 100644
--- a/preview/src/components/checkbox/mod.rs
+++ b/preview/src/components/checkbox/mod.rs
@@ -7,7 +7,10 @@ pub(super) fn Demo() -> Element {
rel: "stylesheet",
href: asset!("/src/components/checkbox/style.css"),
}
- Checkbox { class: "checkbox", name: "tos-check",
+ Checkbox {
+ class: "checkbox",
+ name: "tos-check",
+ aria_label: "Demo Checkbox",
CheckboxIndicator {
class: "checkbox-indicator",
svg {
diff --git a/preview/src/components/hover_card/mod.rs b/preview/src/components/hover_card/mod.rs
index f38ccd5e..adf4514c 100644
--- a/preview/src/components/hover_card/mod.rs
+++ b/preview/src/components/hover_card/mod.rs
@@ -13,7 +13,7 @@ pub(super) fn Demo() -> Element {
style: "padding: 50px; display: flex; flex-direction: row; flex-wrap: wrap; gap: 40px; justify-content: center; align-items: center;",
HoverCard { class: "hover-card",
HoverCardTrigger { class: "hover-card-trigger",
- "@johndoe"
+ i { "Dioxus" }
}
HoverCardContent { class: "hover-card-content", side: HoverCardSide::Bottom,
div {
diff --git a/preview/src/components/progress/mod.rs b/preview/src/components/progress/mod.rs
index a0197574..50e65e8c 100644
--- a/preview/src/components/progress/mod.rs
+++ b/preview/src/components/progress/mod.rs
@@ -23,7 +23,10 @@ pub(super) fn Demo() -> Element {
rel: "stylesheet",
href: asset!("/src/components/progress/style.css"),
}
- Progress { class: "progress", value: progress() as f64,
+ Progress {
+ aria_label: "Progressbar Demo",
+ class: "progress",
+ value: progress() as f64,
ProgressIndicator { class: "progress-indicator" }
}
}
diff --git a/preview/src/components/scroll_area/mod.rs b/preview/src/components/scroll_area/mod.rs
index 07eeb876..5c23ac84 100644
--- a/preview/src/components/scroll_area/mod.rs
+++ b/preview/src/components/scroll_area/mod.rs
@@ -14,6 +14,7 @@ pub(super) fn Demo() -> Element {
border_radius: "0.5em",
padding: "0 1em 1em 1em",
direction: ScrollDirection::Vertical,
+ tabindex: "0",
div { class: "scroll-content",
for i in 1..=20 {
p {
diff --git a/preview/src/components/select/mod.rs b/preview/src/components/select/mod.rs
index c1fb889c..d5e96bca 100644
--- a/preview/src/components/select/mod.rs
+++ b/preview/src/components/select/mod.rs
@@ -10,6 +10,7 @@ pub(super) fn Demo() -> Element {
Select {
class: "select",
placeholder: "Select a fruit...",
+ aria_label: "Select Demo",
SelectGroup { label: "Fruits".to_string(),
SelectOption { value: "apple".to_string(), "Apple" }
SelectOption { value: "banana".to_string(), "Banana" }
diff --git a/preview/src/components/slider/mod.rs b/preview/src/components/slider/mod.rs
index 31967476..f1bb61a3 100644
--- a/preview/src/components/slider/mod.rs
+++ b/preview/src/components/slider/mod.rs
@@ -10,10 +10,13 @@ pub(super) fn Demo() -> Element {
}
Slider {
class: "slider",
+ label: "Demo Slider",
horizontal: true,
SliderTrack { class: "slider-track",
SliderRange { class: "slider-range" }
- SliderThumb { class: "slider-thumb" }
+ SliderThumb {
+ class: "slider-thumb"
+ }
}
}
}
diff --git a/preview/src/components/switch/mod.rs b/preview/src/components/switch/mod.rs
index 0bc3126a..a79bedac 100644
--- a/preview/src/components/switch/mod.rs
+++ b/preview/src/components/switch/mod.rs
@@ -12,6 +12,7 @@ pub(super) fn Demo() -> Element {
Switch {
class: "switch",
checked: checked(),
+ aria_label: "Switch Demo",
on_checked_change: move |new_checked| {
checked.set(new_checked);
tracing::info!("Switch toggled: {new_checked}");
diff --git a/preview/src/components/tabs/docs.md b/preview/src/components/tabs/docs.md
index ce350f9e..ef44be81 100644
--- a/preview/src/components/tabs/docs.md
+++ b/preview/src/components/tabs/docs.md
@@ -5,17 +5,22 @@ The Tabs component is used to create a tabbed interface, allowing users to switc
```rust
// The Tabs component wraps all tab triggers and contents and orders them based on their index.
Tabs {
- // The TabTrigger component is used to create a clickable tab button that switches the active tab.
- TabTrigger {
- // The index of the tab trigger, used to determine the focus order of the tabs.
- index: 0,
- // The value of the tab trigger, which must be unique and is used to identify the active tab.
- value: "tab1",
- // The contents of the tab trigger button
- {children}
+ // The TabList component contains all the tab triggers
+ TabList {
+ // The TabTrigger component is used to create a clickable tab button that switches the active tab.
+ TabTrigger {
+ // The index of the tab trigger, used to determine the focus order of the tabs.
+ index: 0,
+ // The value of the tab trigger, which must be unique and is used to identify the active tab.
+ value: "tab1",
+ // The contents of the tab trigger button
+ {children}
+ }
}
// The TabContent component contains the content that is displayed when the corresponding tab is active.
TabContent {
+ // The index of the tab content, used to determine the focus order of the tabs.
+ index: 0,
// The value of the tab content, which must match the value of the corresponding TabTrigger to be displayed.
value: "tab1",
// The content of the tab, which is displayed when the tab is active.
diff --git a/preview/src/components/tabs/mod.rs b/preview/src/components/tabs/mod.rs
index aef304c7..935ff8a6 100644
--- a/preview/src/components/tabs/mod.rs
+++ b/preview/src/components/tabs/mod.rs
@@ -1,5 +1,5 @@
use dioxus::prelude::*;
-use dioxus_primitives::tabs::{TabContent, TabTrigger, Tabs};
+use dioxus_primitives::tabs::{TabContent, TabTrigger, Tabs, TabList};
#[component]
pub(super) fn Demo() -> Element {
rsx! {
@@ -9,7 +9,7 @@ pub(super) fn Demo() -> Element {
default_value: "tab1".to_string(),
horizontal: true,
max_width: "16rem",
- div { class: "tabs-list",
+ TabList { class: "tabs-list",
TabTrigger {
class: "tabs-trigger",
value: "tab1".to_string(),
@@ -29,7 +29,10 @@ pub(super) fn Demo() -> Element {
"Tab 3"
}
}
- TabContent { class: "tabs-content", value: "tab1".to_string(),
+ TabContent {
+ index: 0usize,
+ class: "tabs-content",
+ value: "tab1".to_string(),
div {
width: "100%",
height: "5rem",
@@ -39,7 +42,10 @@ pub(super) fn Demo() -> Element {
"Tab 1 Content"
}
}
- TabContent { class: "tabs-content", value: "tab2".to_string(),
+ TabContent {
+ index: 1usize,
+ class: "tabs-content",
+ value: "tab2".to_string(),
div {
width: "100%",
height: "5rem",
@@ -49,7 +55,10 @@ pub(super) fn Demo() -> Element {
"Tab 2 Content"
}
}
- TabContent { class: "tabs-content", value: "tab3".to_string(),
+ TabContent {
+ index: 2usize,
+ class: "tabs-content",
+ value: "tab3".to_string(),
div {
width: "100%",
height: "5rem",
diff --git a/preview/src/main.rs b/preview/src/main.rs
index c67dd655..d34b47c1 100644
--- a/preview/src/main.rs
+++ b/preview/src/main.rs
@@ -202,6 +202,7 @@ fn ComponentCode(rs_highlighted: HighlightedCode, css_highlighted: HighlightedCo
justify_content: "center",
align_items: "center",
TabContent {
+ index: 0usize,
class: "tabs-content",
value: "main.rs",
width: "100%",
@@ -210,6 +211,7 @@ fn ComponentCode(rs_highlighted: HighlightedCode, css_highlighted: HighlightedCo
{expand.clone()}
}
TabContent {
+ index: 1usize,
class: "tabs-content",
value: "style.css",
width: "100%",
@@ -276,27 +278,30 @@ fn Home() -> Element {
let mut search = use_signal(String::new);
rsx! {
- div { id: "hero",
- h1 { "Dioxus Primitives" }
- h2 {
- b { "Accessible" }
- ", "
- i { "unstyled" }
- " foundational components for Dioxus."
- }
- div { id: "hero-search-container",
- input {
- id: "hero-search-input",
- r#type: "search",
- placeholder: "Search components...",
- value: search,
- oninput: move |e| {
- search.set(e.value());
- },
+ main {
+ role: "main",
+ div { id: "hero",
+ h1 { "Dioxus Primitives" }
+ h2 {
+ b { "Accessible" }
+ ", "
+ i { "unstyled" }
+ " foundational components for Dioxus."
+ }
+ div { id: "hero-search-container",
+ input {
+ id: "hero-search-input",
+ r#type: "search",
+ placeholder: "Search components...",
+ value: search,
+ oninput: move |e| {
+ search.set(e.value());
+ },
+ }
}
}
+ ComponentGallery { search }
}
- ComponentGallery { search }
}
}
@@ -314,6 +319,7 @@ fn ComponentGallery(search: String) -> Element {
margin: "0.5rem",
top: "0",
right: "0",
+ aria_label: "{name} details",
to: Route::ComponentDemo {
component_name: name.to_string(),
},
diff --git a/primitives/src/calendar.rs b/primitives/src/calendar.rs
index 4314da4f..42955072 100644
--- a/primitives/src/calendar.rs
+++ b/primitives/src/calendar.rs
@@ -159,6 +159,41 @@ impl CalendarDate {
pub fn day_of_the_week(&self) -> u32 {
day_of_the_week(self.year, self.month, self.day)
}
+
+ /// Get a human-readable ARIA label for this date
+ pub fn aria_label(&self) -> String {
+ let month_names = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+ ];
+ let day_names = [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ ];
+ let day_of_week = self.day_of_the_week();
+ format!(
+ "{}, {} {}, {}",
+ day_names[day_of_week as usize],
+ month_names[(self.month - 1) as usize],
+ self.day,
+ self.year
+ )
+ }
}
#[test]
@@ -676,7 +711,7 @@ pub fn CalendarGrid(props: CalendarGridProps) -> Element {
..props.attributes,
// Day headers
- thead { role: "row",
+ thead { aria_hidden: "true",
tr {
class: "calendar-grid-header",
// Day name headers
@@ -694,6 +729,7 @@ pub fn CalendarGrid(props: CalendarGridProps) -> Element {
// Display all days in a grid
for row in &*days_grid.read() {
tr {
+ role: "row",
class: "calendar-grid-week",
for date in row.iter().copied() {
td {
@@ -773,6 +809,7 @@ fn CalendarDay(props: CalendarDayProps) -> Element {
class: "calendar-grid-cell",
r#type: "button",
tabindex: (!in_current_month).then_some("-1"),
+ aria_label: props.date.aria_label(),
"data-today": is_today,
"data-selected": is_selected(),
"data-month": "{month}",
@@ -788,63 +825,3 @@ fn CalendarDay(props: CalendarDayProps) -> Element {
}
}
}
-
-// Calendar Cell component props
-#[derive(Props, Clone, PartialEq)]
-pub struct CalendarCellProps {
- /// The date for this cell
- date: CalendarDate,
-
- /// Whether this date is selected
- #[props(default)]
- is_selected: bool,
-
- /// Whether this date is today
- #[props(default)]
- is_today: bool,
-
- /// Whether this date is disabled
- #[props(default)]
- is_disabled: bool,
-
- /// Click handler
- #[props(default)]
- onclick: EventHandler,
-
- #[props(extends = GlobalAttributes)]
- attributes: Vec,
-}
-
-// Calendar Cell component
-#[component]
-pub fn CalendarCell(props: CalendarCellProps) -> Element {
- let _ctx: CalendarContext = use_context();
-
- // Determine cell state classes
- let state_class = if props.is_selected {
- "calendar-grid-cell-selected"
- } else if props.is_today {
- "calendar-grid-cell-today"
- } else {
- ""
- };
-
- rsx! {
- button {
- role: "gridcell",
- class: "calendar-grid-cell {state_class}",
- "aria-selected": props.is_selected,
- "aria-disabled": props.is_disabled,
- r#type: "button",
- disabled: props.is_disabled,
- "data-selected": props.is_selected,
- "data-today": props.is_today,
- "data-disabled": props.is_disabled,
- tabindex: if props.is_selected { "0" } else { "-1" },
- onclick: props.onclick,
- ..props.attributes,
-
- {props.date.day.to_string()}
- }
- }
-}
diff --git a/primitives/src/context_menu.rs b/primitives/src/context_menu.rs
index c9e6e5b6..f13f7dcf 100644
--- a/primitives/src/context_menu.rs
+++ b/primitives/src/context_menu.rs
@@ -145,6 +145,7 @@ pub fn ContextMenuTrigger(props: ContextMenuTriggerProps) -> Element {
rsx! {
div {
oncontextmenu: handle_context_menu,
+ role: "button",
aria_haspopup: "menu",
aria_expanded: (ctx.open)(),
..props.attributes,
diff --git a/primitives/src/dropdown_menu.rs b/primitives/src/dropdown_menu.rs
index 442d2ba8..60ed5ae7 100644
--- a/primitives/src/dropdown_menu.rs
+++ b/primitives/src/dropdown_menu.rs
@@ -1,4 +1,4 @@
-use crate::{use_controlled, use_effect_cleanup};
+use crate::{use_controlled, use_effect_cleanup, use_unique_id};
use dioxus_lib::prelude::*;
#[derive(Clone, Copy)]
@@ -12,6 +12,9 @@ struct DropdownMenuContext {
item_count: Signal,
recent_focus: Signal,
current_focus: Signal