Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fix-toast-typescript-constraint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zag-js/toast": patch
---

Fix TypeScript compilation errors when building projects that use `@zag-js/toast`.
5 changes: 5 additions & 0 deletions .changeset/twenty-taxis-feel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zag-js/listbox": patch
---

Fix listbox dom ids.
7 changes: 7 additions & 0 deletions e2e/listbox.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ test.describe("listbox", () => {
await I.pressKey("ArrowUp")
await I.seeItemIsHighlighted("Zimbabwe")
})

test.only("should scroll selected option into view", async () => {
await I.tabToContent()
await I.pressKey("End")
await I.seeItemIsHighlighted("Zimbabwe")
await I.seeItemInViewport("Zimbabwe")
})
})
5 changes: 3 additions & 2 deletions e2e/models/listbox.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ export class ListboxModel extends Model {
return expect(this.content.locator(`[data-selected]`).all()).toHaveLength(0)
}

seeItemInViewport(value: string) {
return isInViewport(this.content, this.getItem(value))
seeItemInViewport = async (text: string) => {
const item = this.getItem(text)
expect(await isInViewport(this.content, item)).toBe(true)
}
}
31 changes: 20 additions & 11 deletions examples/next-ts/pages/menu-overflow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as menu from "@zag-js/menu"
import { mergeProps, normalizeProps, useMachine } from "@zag-js/react"
import { mergeProps, normalizeProps, Portal, useMachine } from "@zag-js/react"
import { useId } from "react"

const items = Array.from({ length: 40 }, (_, i) => ({
Expand All @@ -11,21 +11,30 @@ export default function Page() {
const service = useMachine(menu.machine, { id: useId() })
const api = menu.connect(service, normalizeProps)
return (
<main>
<main style={{ padding: 40 }}>
<p style={{ marginBottom: 16, color: "#666" }}>
Use keyboard: open with Enter/Space, then ArrowDown to navigate. The highlighted item should scroll into view.
</p>
<div>
<button {...api.getTriggerProps()}>
Actions <span {...api.getIndicatorProps()}>▾</span>
</button>
{api.open && (
<div {...api.getPositionerProps()}>
<ul {...mergeProps(api.getContentProps(), { style: { maxHeight: "300px", overflowY: "auto" } })}>
{items.map((item) => (
<li key={item.value} {...api.getItemProps({ value: item.value })}>
{item.label}
</li>
))}
</ul>
</div>
<Portal>
<div {...api.getPositionerProps()}>
<ul
{...mergeProps(api.getContentProps(), {
style: { maxHeight: "200px", overflowY: "auto" as const },
})}
>
{items.map((item) => (
<li key={item.value} {...api.getItemProps({ value: item.value })}>
{item.label}
</li>
))}
</ul>
</div>
</Portal>
)}
</div>
</main>
Expand Down
2 changes: 1 addition & 1 deletion examples/nuxt-ts/app/pages/drawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const api = computed(() => drawer.connect(service, normalizeProps))
<div :class="styles.grabber" v-bind="api.getGrabberProps()">
<div :class="styles.grabberIndicator" v-bind="api.getGrabberIndicatorProps()"></div>
</div>
<div>Drawer</div>
<div v-bind="api.getTitleProps()">Drawer</div>
<div data-no-drag="true" :class="styles.noDrag">No drag area</div>
<div :class="styles.scrollable">
<div v-for="(_element, index) in Array.from({ length: 100 })" :key="index">Item {{ index }}</div>
Expand Down
2 changes: 1 addition & 1 deletion examples/solid-ts/src/routes/drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function Page() {
<div class={styles.grabber} {...api().getGrabberProps()}>
<div class={styles.grabberIndicator} {...api().getGrabberIndicatorProps()} />
</div>
<div>Drawer</div>
<div {...api().getTitleProps()}>Drawer</div>
<div data-no-drag="true" class={styles.noDrag}>
No drag area
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/machines/cascade-select/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@zag-js/core": "workspace:*",
"@zag-js/dismissable": "workspace:*",
"@zag-js/dom-query": "workspace:*",
"@zag-js/focus-visible": "workspace:*",
"@zag-js/popper": "workspace:*",
"@zag-js/rect-utils": "workspace:*",
"@zag-js/types": "workspace:*",
Expand Down
18 changes: 13 additions & 5 deletions packages/machines/cascade-select/src/cascade-select.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
dispatchInputValueEvent,
setElementValue,
} from "@zag-js/dom-query"
import { getInteractionModality, setInteractionModality, trackFocusVisible } from "@zag-js/focus-visible"
import { getPlacement, type Placement } from "@zag-js/popper"
import type { Point } from "@zag-js/rect-utils"
import { last, isEmpty, isEqual } from "@zag-js/utils"
Expand Down Expand Up @@ -295,7 +296,7 @@ export const machine = createMachine<CascadeSelectSchema>({
open: {
tags: ["open"],
exit: ["clearHighlightedValue", "scrollContentToTop"],
effects: ["trackDismissableElement", "computePlacement", "scrollToHighlightedItems"],
effects: ["trackDismissableElement", "trackFocusVisible", "computePlacement", "scrollToHighlightedItems"],
on: {
"CONTROLLED.CLOSE": [
{
Expand Down Expand Up @@ -592,6 +593,9 @@ export const machine = createMachine<CascadeSelectSchema>({
},
})
},
trackFocusVisible({ scope }) {
return trackFocusVisible({ root: scope.getRootNode?.() })
},
trackDismissableElement({ scope, send, prop }) {
const contentEl = () => dom.getContentEl(scope)
let restoreFocus = true
Expand Down Expand Up @@ -620,16 +624,17 @@ export const machine = createMachine<CascadeSelectSchema>({
},
})
},
scrollToHighlightedItems({ context, prop, scope, event }) {
scrollToHighlightedItems({ context, prop, scope }) {
let cleanups: VoidFunction[] = []

const exec = (immediate: boolean) => {
const highlightedValue = context.get("highlightedValue")
const highlightedIndexPath = context.get("highlightedIndexPath")
if (!highlightedIndexPath.length) return

// Don't scroll into view if we're using the pointer
if (event.current().type.includes("POINTER")) return
// don't scroll into view if we're using the pointer (or null when focus-trap autofocuses)
const modality = getInteractionModality()
if (modality === "pointer") return

const listEls = dom.getListEls(scope)
listEls.forEach((listEl, index) => {
Expand All @@ -651,7 +656,10 @@ export const machine = createMachine<CascadeSelectSchema>({
})
}

raf(() => exec(true))
raf(() => {
setInteractionModality("virtual")
exec(true)
})

const rafCleanup = raf(() => exec(true))
cleanups.push(rafCleanup)
Expand Down
5 changes: 3 additions & 2 deletions packages/machines/combobox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
"@zag-js/core": "workspace:*",
"@zag-js/dismissable": "workspace:*",
"@zag-js/dom-query": "workspace:*",
"@zag-js/utils": "workspace:*",
"@zag-js/focus-visible": "workspace:*",
"@zag-js/popper": "workspace:*",
"@zag-js/types": "workspace:*"
"@zag-js/types": "workspace:*",
"@zag-js/utils": "workspace:*"
},
"devDependencies": {
"clean-package": "2.2.0"
Expand Down
22 changes: 16 additions & 6 deletions packages/machines/combobox/src/combobox.machine.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { setup } from "@zag-js/core"
import { trackDismissableElement } from "@zag-js/dismissable"
import { clickIfLink, nextTick, observeAttributes, raf, scrollIntoView, setCaretToEnd } from "@zag-js/dom-query"
import { getInteractionModality, setInteractionModality, trackFocusVisible } from "@zag-js/focus-visible"
import { getPlacement } from "@zag-js/popper"
import { addOrRemove, isBoolean, isEqual, match, remove } from "@zag-js/utils"
import { collection } from "./combobox.collection"
Expand Down Expand Up @@ -349,7 +350,7 @@ export const machine = createMachine({
interacting: {
tags: ["open", "focused"],
entry: ["setInitialFocus"],
effects: ["scrollToHighlightedItem", "trackDismissableLayer", "trackPlacement"],
effects: ["trackFocusVisible", "scrollToHighlightedItem", "trackDismissableLayer", "trackPlacement"],
on: {
"CONTROLLED.CLOSE": [
{
Expand Down Expand Up @@ -525,7 +526,7 @@ export const machine = createMachine({

suggesting: {
tags: ["open", "focused"],
effects: ["trackDismissableLayer", "scrollToHighlightedItem", "trackPlacement"],
effects: ["trackFocusVisible", "trackDismissableLayer", "scrollToHighlightedItem", "trackPlacement"],
entry: ["setInitialFocus"],
on: {
"CONTROLLED.CLOSE": [
Expand Down Expand Up @@ -710,6 +711,9 @@ export const machine = createMachine({
},

effects: {
trackFocusVisible({ scope }) {
return trackFocusVisible({ root: scope.getRootNode?.() })
},
trackDismissableLayer({ send, prop, scope }) {
if (prop("disableLayer")) return
const contentEl = () => dom.getContentEl(scope)
Expand Down Expand Up @@ -743,15 +747,18 @@ export const machine = createMachine({
},
})
},
scrollToHighlightedItem({ context, prop, scope, event }) {
scrollToHighlightedItem({ context, prop, scope }) {
const inputEl = dom.getInputEl(scope)

let cleanups: VoidFunction[] = []

const exec = (immediate: boolean) => {
const pointer = event.current().type.includes("POINTER")
// don't scroll into view if we're using the pointer (or null when focus-trap autofocuses)
const modality = getInteractionModality()
if (modality === "pointer") return

const highlightedValue = context.get("highlightedValue")
if (pointer || !highlightedValue) return
if (!highlightedValue) return

const contentEl = dom.getContentEl(scope)

Expand All @@ -773,7 +780,10 @@ export const machine = createMachine({
cleanups.push(raf_cleanup)
}

const rafCleanup = raf(() => exec(true))
const rafCleanup = raf(() => {
setInteractionModality("virtual")
exec(true)
})
cleanups.push(rafCleanup)

const observerCleanup = observeAttributes(inputEl, {
Expand Down
12 changes: 6 additions & 6 deletions packages/machines/listbox/src/listbox.dom.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Scope } from "@zag-js/core"

export const getRootId = (ctx: Scope) => ctx.ids?.root ?? `select:${ctx.id}`
export const getContentId = (ctx: Scope) => ctx.ids?.content ?? `select:${ctx.id}:content`
export const getLabelId = (ctx: Scope) => ctx.ids?.label ?? `select:${ctx.id}:label`
export const getItemId = (ctx: Scope, id: string | number) => ctx.ids?.item?.(id) ?? `select:${ctx.id}:option:${id}`
export const getRootId = (ctx: Scope) => ctx.ids?.root ?? `listbox:${ctx.id}`
export const getContentId = (ctx: Scope) => ctx.ids?.content ?? `listbox:${ctx.id}:content`
export const getLabelId = (ctx: Scope) => ctx.ids?.label ?? `listbox:${ctx.id}:label`
export const getItemId = (ctx: Scope, id: string | number) => ctx.ids?.item?.(id) ?? `listbox:${ctx.id}:item:${id}`
export const getItemGroupId = (ctx: Scope, id: string | number) =>
ctx.ids?.itemGroup?.(id) ?? `select:${ctx.id}:optgroup:${id}`
ctx.ids?.itemGroup?.(id) ?? `listbox:${ctx.id}:item-group:${id}`
export const getItemGroupLabelId = (ctx: Scope, id: string | number) =>
ctx.ids?.itemGroupLabel?.(id) ?? `select:${ctx.id}:optgroup-label:${id}`
ctx.ids?.itemGroupLabel?.(id) ?? `listbox:${ctx.id}:item-group-label:${id}`

export const getContentEl = (ctx: Scope) => ctx.getById(getContentId(ctx))
export const getItemEl = (ctx: Scope, id: string | number) => ctx.getById(getItemId(ctx, id))
11 changes: 7 additions & 4 deletions packages/machines/listbox/src/listbox.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CollectionItem } from "@zag-js/collection"
import { Selection } from "@zag-js/collection"
import { setup } from "@zag-js/core"
import { getByTypeahead, observeAttributes, raf, scrollIntoView } from "@zag-js/dom-query"
import { getInteractionModality, trackFocusVisible as trackFocusVisibleFn } from "@zag-js/focus-visible"
import { getInteractionModality, setInteractionModality, trackFocusVisible } from "@zag-js/focus-visible"
import { isEqual } from "@zag-js/utils"
import { collection } from "./listbox.collection"
import * as dom from "./listbox.dom"
Expand Down Expand Up @@ -173,7 +173,7 @@ export const machine = createMachine({

effects: {
trackFocusVisible: ({ scope, refs }) => {
return trackFocusVisibleFn({
return trackFocusVisible({
root: scope.getRootNode?.(),
onChange(details) {
refs.set("focusVisible", details.isFocusVisible)
Expand All @@ -189,7 +189,7 @@ export const machine = createMachine({
const modality = getInteractionModality()

// don't scroll into view if we're using the pointer
if (modality !== "keyboard") return
if (modality === "pointer") return

const contentEl = dom.getContentEl(scope)

Expand All @@ -210,7 +210,10 @@ export const machine = createMachine({
scrollIntoView(itemEl, { rootEl: contentEl, block: "nearest" })
}

raf(() => exec(true))
raf(() => {
setInteractionModality("virtual")
exec(true)
})

const contentEl = () => dom.getContentEl(scope)
return observeAttributes(contentEl, {
Expand Down
9 changes: 5 additions & 4 deletions packages/machines/menu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@
"dependencies": {
"@zag-js/anatomy": "workspace:*",
"@zag-js/core": "workspace:*",
"@zag-js/dom-query": "workspace:*",
"@zag-js/rect-utils": "workspace:*",
"@zag-js/utils": "workspace:*",
"@zag-js/dismissable": "workspace:*",
"@zag-js/dom-query": "workspace:*",
"@zag-js/focus-visible": "workspace:*",
"@zag-js/popper": "workspace:*",
"@zag-js/types": "workspace:*"
"@zag-js/rect-utils": "workspace:*",
"@zag-js/types": "workspace:*",
"@zag-js/utils": "workspace:*"
},
"devDependencies": {
"clean-package": "2.2.0"
Expand Down
17 changes: 13 additions & 4 deletions packages/machines/menu/src/menu.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
raf,
scrollIntoView,
} from "@zag-js/dom-query"
import { getInteractionModality, setInteractionModality, trackFocusVisible } from "@zag-js/focus-visible"
import { getPlacement, getPlacementSide, type Placement } from "@zag-js/popper"
import { getElementPolygon, isPointInPolygon, type Point } from "@zag-js/rect-utils"
import { isEqual } from "@zag-js/utils"
Expand Down Expand Up @@ -381,7 +382,7 @@ export const machine = createMachine<MenuSchema>({

open: {
tags: ["open"],
effects: ["trackInteractOutside", "trackPositioning", "scrollToHighlightedItem"],
effects: ["trackInteractOutside", "trackFocusVisible", "trackPositioning", "scrollToHighlightedItem"],
entry: ["focusMenu", "resumePointer"],
on: {
"CONTROLLED.CLOSE": [
Expand Down Expand Up @@ -544,6 +545,9 @@ export const machine = createMachine<MenuSchema>({
}, 700)
return () => clearTimeout(timer)
},
trackFocusVisible({ scope }) {
return trackFocusVisible({ root: scope.getRootNode?.() })
},
trackPositioning({ context, prop, scope, refs }) {
if (!!dom.getContextTriggerEl(scope)) return
const positioning = {
Expand Down Expand Up @@ -622,16 +626,21 @@ export const machine = createMachine<MenuSchema>({
}
})
},
scrollToHighlightedItem({ event, scope, computed }) {
scrollToHighlightedItem({ scope, computed }) {
const exec = () => {
if (event.current().type.startsWith("ITEM_POINTER")) return
// don't scroll into view if we're using the pointer (or null when focus-trap autofocuses)
const modality = getInteractionModality()
if (modality === "pointer") return

const itemEl = scope.getById(computed("highlightedId")!)
const contentEl = dom.getContentEl(scope)

scrollIntoView(itemEl, { rootEl: contentEl, block: "nearest" })
}
raf(() => exec())
raf(() => {
setInteractionModality("virtual")
exec()
})

const contentEl = () => dom.getContentEl(scope)
return observeAttributes(contentEl, {
Expand Down
Loading
Loading