Skip to content

Commit

Permalink
refactor(tabs): prefer click for pointer interaction
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed May 10, 2024
1 parent 668ec62 commit bd7a2e4
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 169 deletions.
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"nuxt-ts",
"preact-ts",
"svelte-ts",
"vanilla-ts",
"website"
]
}
6 changes: 6 additions & 0 deletions .changeset/wicked-papayas-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@zag-js/tabs": minor
---

When using the pointer, prefer click based selection when using `activationMode=automatic` over focus triggering
selection. For keyboard, selection follows focus as usual.
69 changes: 30 additions & 39 deletions .xstate/tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ const fetchMachine = createMachine({
initial: "idle",
context: {
"selectOnFocus": false,
"isHorizontal": false,
"isHorizontal": false,
"isVertical": false,
"isVertical": false,
"!selectOnFocus": false,
"selectOnFocus": false
"selectOnFocus": false,
"selectOnFocus": false,
"selectOnFocus": false,
"!selectOnFocus": false
},
entry: ["checkRenderedElements", "syncIndicatorRect", "setContentTabIndex"],
exit: ["cleanupObserver"],
Expand All @@ -41,14 +39,10 @@ const fetchMachine = createMachine({
states: {
idle: {
on: {
TAB_FOCUS: [{
cond: "selectOnFocus",
target: "focused",
actions: ["setFocusedValue", "setValue"]
}, {
TAB_FOCUS: {
target: "focused",
actions: "setFocusedValue"
}],
},
TAB_CLICK: {
target: "focused",
actions: ["setFocusedValue", "setValue"]
Expand All @@ -61,38 +55,37 @@ const fetchMachine = createMachine({
target: "focused",
actions: ["setFocusedValue", "setValue"]
},
ARROW_LEFT: {
cond: "isHorizontal",
actions: "focusPrevTab"
},
ARROW_RIGHT: {
cond: "isHorizontal",
actions: "focusNextTab"
},
ARROW_UP: {
cond: "isVertical",
ARROW_PREV: [{
cond: "selectOnFocus",
actions: ["focusPrevTab", "selectFocusedTab"]
}, {
actions: "focusPrevTab"
},
ARROW_DOWN: {
cond: "isVertical",
}],
ARROW_NEXT: [{
cond: "selectOnFocus",
actions: ["focusNextTab", "selectFocusedTab"]
}, {
actions: "focusNextTab"
},
HOME: {
}],
HOME: [{
cond: "selectOnFocus",
actions: ["focusFirstTab", "selectFocusedTab"]
}, {
actions: "focusFirstTab"
},
END: {
}],
END: [{
cond: "selectOnFocus",
actions: ["focusLastTab", "selectFocusedTab"]
}, {
actions: "focusLastTab"
},
}],
ENTER: {
cond: "!selectOnFocus",
actions: "setValue"
actions: "selectFocusedTab"
},
TAB_FOCUS: {
actions: ["setFocusedValue"]
},
TAB_FOCUS: [{
cond: "selectOnFocus",
actions: ["setFocusedValue", "setValue"]
}, {
actions: "setFocusedValue"
}],
TAB_BLUR: {
target: "idle",
actions: "clearFocusedValue"
Expand All @@ -110,8 +103,6 @@ const fetchMachine = createMachine({
},
guards: {
"selectOnFocus": ctx => ctx["selectOnFocus"],
"isHorizontal": ctx => ctx["isHorizontal"],
"isVertical": ctx => ctx["isVertical"],
"!selectOnFocus": ctx => ctx["!selectOnFocus"]
}
});
41 changes: 41 additions & 0 deletions e2e/models/tabs.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { expect, type Page } from "@playwright/test"
import { a11y, testid } from "../_utils"
import { Model } from "./model"

export class TabsModel extends Model {
constructor(public page: Page) {
super(page)
}

checkAccessibility() {
return a11y(this.page)
}

goto() {
return this.page.goto("/tabs")
}

private getTabTrigger = (id: string) => {
return this.page.locator(testid(`${id}-tab`))
}

private getTabContent = (id: string) => {
return this.page.locator(testid(`${id}-tab-panel`))
}

clickTab = async (id: string) => {
await this.getTabTrigger(id).click()
}

seeTabContent = async (id: string) => {
await expect(this.getTabContent(id)).toBeVisible()
}

dontSeeTabContent = async (id: string) => {
await expect(this.getTabContent(id)).not.toBeVisible()
}

setTabIsFocused = async (id: string) => {
await expect(this.getTabTrigger(id)).toBeFocused()
}
}
145 changes: 87 additions & 58 deletions e2e/tabs.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,111 @@
import { expect, test } from "@playwright/test"
import { a11y, testid } from "./_utils"
import { test } from "@playwright/test"
import { TabsModel } from "./models/tabs.model"

const item = (id: string) => ({
tab: testid(`${id}-tab`),
panel: testid(`${id}-tab-panel`),
})

const nils = item("nils")
const agnes = item("agnes")
const joke = item("joke")
let I: TabsModel

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

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

test("should have no accessibility violation", async ({ page }) => {
await a11y(page)
test("on home key, select first tab", async () => {
await I.clickTab("agnes")
await I.pressKey("Home")

await I.setTabIsFocused("nils")
await I.seeTabContent("nils")
})

test.describe("in automatic mode", () => {
test("should select the correct tab on click", async ({ page }) => {
await page.click(nils.tab)
await expect(page.locator(nils.panel)).toBeVisible()
test("on end key, select last tab", async () => {
await I.clickTab("agnes")
await I.pressKey("End")

await I.setTabIsFocused("joke")
await I.seeTabContent("joke")
})

await page.click(agnes.tab)
await expect(page.locator(agnes.panel)).toBeVisible()
test("click tab, select tab", async () => {
await I.clickTab("agnes")
await I.seeTabContent("agnes")
})

await page.click(joke.tab)
await expect(page.locator(joke.panel)).toBeVisible()
})
test("automatic: should select the correct tab on click", async () => {
await I.clickTab("nils")
await I.seeTabContent("nils")

test("on `ArrowRight`: should select & focus the next tab", async ({ page }) => {
await page.focus(nils.tab)
await page.keyboard.press("ArrowRight")
await I.clickTab("agnes")
await I.seeTabContent("agnes")

await expect(page.locator(agnes.tab)).toBeFocused()
await expect(page.locator(agnes.panel)).toBeVisible()
await I.clickTab("joke")
await I.seeTabContent("joke")
})

await page.keyboard.press("ArrowRight")
await expect(page.locator(joke.tab)).toBeFocused()
await expect(page.locator(joke.panel)).toBeVisible()
test("automatic: on arrow right, select + focus next tab", async () => {
await I.clickTab("nils")
await I.pressKey("ArrowRight")

await page.keyboard.press("ArrowRight")
await expect(page.locator(nils.tab)).toBeFocused()
await expect(page.locator(nils.panel)).toBeVisible()
})
await I.setTabIsFocused("agnes")
await I.seeTabContent("agnes")
})

test("on `ArrowLeft`: should select & focus the previous tab", async ({ page }) => {
await page.focus(nils.tab)
await page.keyboard.press("ArrowLeft")
test("automatic: on arrow right, loop focus + selection", async () => {
await I.clickTab("nils")
await I.pressKey("ArrowRight", 3)

await expect(page.locator(joke.tab)).toBeFocused()
await expect(page.locator(joke.panel)).toBeVisible()
await I.setTabIsFocused("nils")
await I.seeTabContent("nils")
})

await page.keyboard.press("ArrowLeft")
await expect(page.locator(agnes.tab)).toBeFocused()
await expect(page.locator(agnes.panel)).toBeVisible()
test("automatic: on arrow left, select + focus the previous tab", async () => {
await I.clickTab("joke")
await I.pressKey("ArrowLeft")

await page.keyboard.press("ArrowLeft")
await expect(page.locator(nils.tab)).toBeFocused()
await expect(page.locator(nils.panel)).toBeVisible()
})
await I.setTabIsFocused("agnes")
await I.seeTabContent("agnes")
})

test("on `Home` should select first tab", async ({ page }) => {
await page.click(joke.tab)
await page.keyboard.press("Home")
test("manual: on arrow right, focus but not select tab", async () => {
await I.controls.select("activationMode", "manual")

await I.clickTab("nils")
await I.pressKey("ArrowRight")

await I.setTabIsFocused("agnes")
await I.dontSeeTabContent("agnes")
})

test("manual: on home key, focus but not select tab", async () => {
await I.controls.select("activationMode", "manual")

await I.clickTab("agnes")
await I.pressKey("Home")

await I.setTabIsFocused("nils")
await I.dontSeeTabContent("nils")
})

test("manual: on navigate, select on enter", async () => {
await I.controls.select("activationMode", "manual")

await I.clickTab("nils")
await I.pressKey("ArrowRight")
await I.pressKey("Enter")

await I.setTabIsFocused("agnes")
await I.seeTabContent("agnes")
})

await expect(page.locator(nils.tab)).toBeFocused()
await expect(page.locator(nils.panel)).toBeVisible()
})
test("loopFocus=false", async () => {
await I.controls.bool("loopFocus", false)

test("on `End` should select last tab", async ({ page }) => {
await page.focus(nils.tab)
await page.keyboard.press("End")
await I.clickTab("joke")
await I.pressKey("ArrowRight")

await expect(page.locator(joke.tab)).toBeFocused()
await expect(page.locator(joke.panel)).toBeVisible()
})
await I.setTabIsFocused("joke")
})
})
2 changes: 1 addition & 1 deletion examples/vanilla-ts/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "vanilla",
"name": "vanilla-ts",
"private": true,
"version": "0.0.0",
"type": "module",
Expand Down

0 comments on commit bd7a2e4

Please sign in to comment.