Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
3ce0114
feat: copy solid package to lit
nikparo Sep 2, 2025
ed2a9e1
feat: initial claude created lit package
nikparo Sep 2, 2025
681a5bc
test: initial testing setup
nikparo Sep 3, 2025
9d83918
refactor: split out ZagController
nikparo Sep 3, 2025
0d28d3b
fix: refined normalize props?
nikparo Sep 3, 2025
d5f009c
fix: refine normalizeProps for Lit
nikparo Sep 3, 2025
839395c
fix: refine ZagController
nikparo Sep 3, 2025
49b1431
fix: export ZagController
nikparo Sep 3, 2025
680c83d
fix: refine lit example
nikparo Sep 3, 2025
98aa9e9
test: minor lit machine.test tweaks
nikparo Sep 3, 2025
9c4b57e
fix: refine lit example
nikparo Sep 3, 2025
18ab8bb
fix: example adapatations
nikparo Sep 3, 2025
41e89eb
fix: progress on example
nikparo Sep 3, 2025
2a8b8d9
fix: example page styling
nikparo Sep 3, 2025
0ade428
fix: initial working controls
nikparo Sep 3, 2025
52f6972
feat: initial working visualizer
nikparo Sep 3, 2025
99dcb51
fix: typescript tweaks
nikparo Sep 3, 2025
df23884
fix: use slot for state-visualizer
nikparo Sep 3, 2025
0e4071d
refactor: add :host main rule to layout.css
nikparo Sep 3, 2025
c046e4c
fix: use shadow dom for StateVisualizer
nikparo Sep 3, 2025
f973d8a
test: wip a11y testing
nikparo Sep 4, 2025
251629a
test: working shadow-dom a11y testing
nikparo Sep 4, 2025
f1d1c20
fix: implement dom mode switch for lit-ts
nikparo Sep 4, 2025
760e8e0
fix: handle shadow dom root node
nikparo Sep 4, 2025
ee791cb
fix: remove unused (css) parts from accordion
nikparo Sep 4, 2025
2db5977
fix(@zag-js/lit): include undefined prop values
nikparo Sep 4, 2025
caddf80
feat(lit-ts): wip dialog, menu, toggle-group, toggle
nikparo Sep 4, 2025
cb14f36
fix(@zag-js/lit): aria booleans must be strings
nikparo Sep 4, 2025
896bec9
fix(e2e): pass shadowHost to Model
nikparo Sep 4, 2025
d98a2f4
test(e2e): fix issues
nikparo Sep 4, 2025
cc3a89d
test(e2e): create playwright.lit.config for subset testing
nikparo Sep 6, 2025
a6b517c
refactor(lit): rename ZagController to MachineController
nikparo Sep 6, 2025
00db01b
feat(lit-ts): add checkbox, popover. switch, tabs
nikparo Sep 7, 2025
c25af38
fix(e2e): handle shadow-dom in checkbox, popover. switch, tabs
nikparo Sep 7, 2025
0a877ef
refactor(e2e): create CheckboxModel
nikparo Sep 7, 2025
4607e94
feat(e2e): add popover blur close test
nikparo Sep 7, 2025
d031894
refactor(e2e): switch to this.host pattern
nikparo Sep 7, 2025
03357da
feat(e2e): add reset form test
nikparo Sep 7, 2025
43c5302
fix(lit): add machine.started and start after initial render
nikparo Sep 7, 2025
7609226
feat(lit-ts): add radio-group
nikparo Sep 7, 2025
6f57ba8
feat(lit-ts): add collapsible
nikparo Sep 7, 2025
7e72aae
fix(e2e): re-enable collapsible tab key test
nikparo Sep 7, 2025
253928f
feat(lit-ts): add slider and range-slider
nikparo Sep 7, 2025
2b81fbe
feat(lit-ts): add menu-options
nikparo Sep 7, 2025
d55b5f5
feat(lit-ts): create nested-menu with issues
nikparo Sep 7, 2025
bf78848
fix(lit): simplify MachineController getProps type
nikparo Sep 7, 2025
55f2729
fix(lit-ts): stabilize manu ids
nikparo Sep 8, 2025
95117bb
fix(lit-ts): getRootNode type issue
nikparo Sep 8, 2025
8803203
fix(lit-ts): stabilize ids
nikparo Sep 8, 2025
3d7368c
fix(lit-ts): sort menu alphabetically
nikparo Sep 8, 2025
a953f27
fix(lit): mark internal machine properties private + readonly
nikparo Sep 8, 2025
6b79d48
refactor(lit): remove unused machine notify method
nikparo Sep 8, 2025
6b8c21b
fix(lit): account for some machine edge cases
nikparo Sep 8, 2025
eaf04c8
refactor(lit): use 'self' in machine constructor
nikparo Sep 8, 2025
c1923b2
fix(lit): clean up subscriptions
nikparo Sep 9, 2025
5b51fc9
fix(lit): remove unneeded started fn
nikparo Sep 9, 2025
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
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"preact-ts",
"svelte-ts",
"vanilla-ts",
"lit-ts",
"website"
]
}
62 changes: 56 additions & 6 deletions e2e/_utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,65 @@
import AxeBuilder from "@axe-core/playwright"
import { expect, type Locator, type Page } from "@playwright/test"

export async function a11y(page: Page, selector = "[data-part=root]") {
// Types and Framework Context Detection
export type DomMode = "shadow-dom" | "light-dom"

// The context is determined by environment variables at test runtime
// VITE_DOM_MODE is the primary control (matches client-side behavior)
// Examples:
// - `VITE_DOM_MODE=shadow-dom FRAMEWORK=react npx playwright test` (force Shadow DOM in React)
// - `VITE_DOM_MODE=light-dom FRAMEWORK=lit npx playwright test` (force Light DOM in Lit)
// - `FRAMEWORK=lit npx playwright test` (default Lit behavior: Shadow DOM)
export const DOM_MODE: DomMode =
process.env.VITE_DOM_MODE === "light-dom" ? "light-dom" : process.env.FRAMEWORK === "lit" ? "shadow-dom" : "light-dom"

console.log(`Running E2E tests in '${DOM_MODE}' context for '${process.env.FRAMEWORK}'.`)

/**
* Context-aware utility to locate a component part defined by a 'part' or 'data-part' attribute.
* This function abstracts the structural differences between Shadow DOM and Light DOM implementations.
*
* @param parent The root Playwright Page or a parent Locator to search within
* @param hostSelector A unique selector for the component's host/root element (e.g., 'accordion-page', '[data-testid="accordion-root"]')
* @param partName The name of the part to locate (e.g., 'trigger', 'content')
* @returns A Playwright Locator for the requested part
*/
export function getPart(parent: Page | Locator, hostSelector: string, partName: string): Locator {
// Use [part="..."] for CSS Shadow Parts standard, fallback to [data-part="..."]
const partSelector = `[part="${partName}"], [data-part="${partName}"]`

if (DOM_MODE === "shadow-dom") {
// For Shadow DOM, use a descendant combinator. Playwright's engine will
// pierce the shadow root of the element matching hostSelector.
return parent.locator(`${hostSelector} ${partSelector}`)
} else {
// For Light DOM, the structure might be a direct child or a deeper descendant.
// In many simple cases, the selector is identical to the shadow DOM version.
return parent.locator(`${hostSelector} ${partSelector}`)
}
}

/**
* Performs an accessibility scan, optionally scoped to a specific selector.
* @param page The Playwright Page object
* @param selector Optional selector to scope the scan (defaults to "[data-part=root]")
* @param hostSelector A CSS selector for a shadow host element
*/
export async function a11y(page: Page, selector = "[data-part=root]", hostSelector?: string) {
await page.waitForSelector(selector)

const results = await new AxeBuilder({ page: page as any })
.disableRules(["color-contrast"])
.include(selector)
.analyze()
let selection = new AxeBuilder({ page: page as any }).disableRules(["color-contrast"])

if (hostSelector && DOM_MODE === "shadow-dom") {
selection = selection.include(hostSelector)
}
if (selector) {
selection = selection.include(selector)
}

const accessibilityScanResults = await selection.analyze()

expect(results.violations).toEqual([])
expect(accessibilityScanResults.violations).toEqual([])
}

export const testid = (part: string) => `[data-testid=${esc(part)}]`
Expand Down
62 changes: 32 additions & 30 deletions e2e/checkbox.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,66 @@
import { expect, type Page, test } from "@playwright/test"
import { a11y, controls, part, testid } from "./_utils"
import { expect, test } from "@playwright/test"
import { CheckboxModel } from "./models/checkbox.model"

const root = part("root")
const label = part("label")
const control = part("control")
const input = testid("hidden-input")

const expectToBeChecked = async (page: Page) => {
await expect(page.locator(root)).toHaveAttribute("data-state", "checked")
await expect(page.locator(label)).toHaveAttribute("data-state", "checked")
await expect(page.locator(control)).toHaveAttribute("data-state", "checked")
}
let I: CheckboxModel

test.beforeEach(async ({ page }) => {
await page.goto("/checkbox")
I = new CheckboxModel(page)
await I.goto()
})

test("should have no accessibility violation", async ({ page }) => {
await a11y(page)
test("should have no accessibility violation", async () => {
await I.checkAccessibility()
})

test("should be checked when clicked", async ({ page }) => {
await page.click(root)
await expectToBeChecked(page)
test("should be checked when clicked", async () => {
await I.root.click()
await I.expectToBeChecked()
})

test("should be focused when page is tabbed", async ({ page }) => {
await page.click("main")
await page.keyboard.press("Tab")
await expect(page.locator(input)).toBeFocused()
await expect(page.locator(control)).toHaveAttribute("data-focus", "")
await expect(I.input).toBeFocused()
await expect(I.control).toHaveAttribute("data-focus", "")
})

test("should be checked when spacebar is pressed while focused", async ({ page }) => {
await page.click("main")
await page.keyboard.press("Tab")
await page.keyboard.press(" ")
await expectToBeChecked(page)
await I.expectToBeChecked()
})

test("should have disabled attributes when disabled", async ({ page }) => {
await controls(page).bool("disabled")
await expect(page.locator(input)).toBeDisabled()
test("should have disabled attributes when disabled", async () => {
await I.controls.bool("disabled")
await I.expectToBeDisabled()
})

test("should not be focusable when disabled", async ({ page }) => {
await controls(page).bool("disabled")
await I.controls.bool("disabled")
await page.click("main")
await page.keyboard.press("Tab")
await expect(page.locator(input)).not.toBeFocused()
await expect(I.input).not.toBeFocused()
})

test("input is not blurred on label click", async ({ page }) => {
let blurCount = 0
await page.exposeFunction("trackBlur", () => blurCount++)
await page.locator(input).evaluate((input) => {
await I.input.evaluate((input) => {
input.addEventListener("blur", (window as any).trackBlur)
})
await page.click(label)
await page.click(label)
await page.click(label)
await I.label.click()
await I.label.click()
await I.label.click()
expect(blurCount).toBe(0)
})

test("reset form should restore initial state", async () => {
await expect(I.input).not.toBeChecked()

await I.label.click()
await expect(I.input).toBeChecked()

await I.resetButton.click()
await expect(I.input).not.toBeChecked()
})
36 changes: 20 additions & 16 deletions e2e/collapsible.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import { expect, test } from "@playwright/test"
import { a11y, part } from "./_utils"
import { CollapsibleModel } from "./models/collapsible.model"

const trigger = part("trigger")
const content = part("content")
let I: CollapsibleModel

test.describe("collapsible", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/collapsible")
I = new CollapsibleModel(page)
await I.goto()
})

test("should have no accessibility violation", async ({ page }) => {
await a11y(page)
test("should have no accessibility violation", async () => {
await I.checkAccessibility()
})

test("[toggle] should be open when clicked", async ({ page }) => {
await page.click(trigger)
await expect(page.locator(content)).toBeVisible()
test("[toggle] should be open when clicked", async () => {
await I.clickTrigger()
await I.seeContent()

await page.click(trigger)
await expect(page.locator(content)).not.toBeVisible()
await I.clickTrigger()
await I.dontSeeContent()
})

test.skip("[closed] content should not be reachable via tab key", async ({ page }) => {
await page.click(trigger)
await page.click(trigger)
await page.keyboard.press("Tab")
await expect(page.getByRole("button", { name: "Open" })).toBeFocused()
test("[closed] content should not be reachable via tab key", async () => {
await I.clickTrigger()
await I.seeContent()

await I.clickTrigger()
await I.dontSeeContent()

await I.pressKey("Tab")
await expect(I.host.getByTestId("open-button")).toBeFocused()
})
})
Loading