Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: redesign splitter machine #2355

Merged
merged 11 commits into from
Mar 20, 2025
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
35 changes: 35 additions & 0 deletions .changeset/wild-crabs-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
"@zag-js/splitter": minor
---

[Breaking] Redesign splitter machine to support more use cases and improve DX.

The key breaking change is:

Before:

```ts
const service = useMachine(splitter.machine, {
id: useId(),
defaultSize: [
{ id: "a", size: 50 },
{ id: "b", size: 50 },
],
})
```

After:

```ts
const service = useMachine(splitter.machine, {
id: useId(),
panels: [{ id: "a" }, { id: "b" }],
defaultSize: [50, 50],
})
```

The also comes with new features such as:

- Support for collapsible panels
- Support for collapse and expand events
- New `api` methods for resizing the panels programmatically
48 changes: 48 additions & 0 deletions examples/next-ts/pages/splitter-collapsible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { normalizeProps, useMachine } from "@zag-js/react"
import { splitterControls } from "@zag-js/shared"
import * as splitter from "@zag-js/splitter"
import { useId } from "react"
import { StateVisualizer } from "../components/state-visualizer"
import { Toolbar } from "../components/toolbar"
import { useControls } from "../hooks/use-controls"

export default function Page() {
const controls = useControls(splitterControls)

const service = useMachine(splitter.machine, {
id: useId(),
panels: [
{ id: "a", collapsible: true, collapsedSize: 5, minSize: 10, maxSize: 20 },
{ id: "b", minSize: 50 },
],
defaultSize: [15, 85],
onCollapse: (details) => {
console.log("onCollapse", details)
},
onExpand: (details) => {
console.log("onExpand", details)
},
})

const api = splitter.connect(service, normalizeProps)

return (
<>
<main className="splitter">
<div {...api.getRootProps()}>
<div {...api.getPanelProps({ id: "a" })}>
<p>A</p>
</div>
<div {...api.getResizeTriggerProps({ id: "a:b" })} />
<div {...api.getPanelProps({ id: "b" })}>
<p>B</p>
</div>
</div>
</main>

<Toolbar controls={controls.ui}>
<StateVisualizer state={service} />
</Toolbar>
</>
)
}
83 changes: 83 additions & 0 deletions examples/next-ts/pages/splitter-conditional.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { normalizeProps, useMachine } from "@zag-js/react"
import { splitterControls } from "@zag-js/shared"
import * as splitter from "@zag-js/splitter"
import { useId, useState, useRef } from "react"
import { StateVisualizer } from "../components/state-visualizer"
import { Toolbar } from "../components/toolbar"
import { useControls } from "../hooks/use-controls"

export default function Page() {
const controls = useControls(splitterControls)

const initialPanels = [
{ id: "a", order: 0 },
{ id: "b", order: 1 },
{ id: "c", order: 2 },
]
const initialLayout = splitter.layout(initialPanels)

const [panels, setPanels] = useState(initialPanels)

const [sizes, setSizes] = useState<number[]>([])
const layoutCache = useRef<Record<string, number[]>>({
[initialLayout]: [],
})

const service = useMachine(splitter.machine, {
id: useId(),
panels,
size: sizes,
onResize({ layout, size }) {
setSizes(size)
layoutCache.current[layout] = size
},
})

const api = splitter.connect(service, normalizeProps)

const hidePanel = (id: string) => {
const index = panels.findIndex((panel) => panel.id === id)
const newPanels = [...panels]
newPanels.splice(index, 1)
setPanels(newPanels)
layoutCache.current[api.getLayout()] = sizes
const newSizes = [...sizes]
newSizes.splice(index, 1)
setSizes(newSizes)
}

const showPanel = (id: string) => {
const panel = initialPanels.find((panel) => panel.id === id)
if (!panel) return
const nextPanels = [...panels, panel]
const nextLayout = splitter.layout(nextPanels)
setPanels(nextPanels)
setSizes(layoutCache.current[nextLayout])
}

return (
<>
<main className="splitter">
<pre>{JSON.stringify(sizes, null, 2)}</pre>
<button onClick={() => hidePanel("a")}>Hide Panel A</button>
<button onClick={() => showPanel("a")}>Show Panel A</button>
<div {...api.getRootProps()}>
{api.getItems().map((item) => {
if (item.type === "panel") {
return (
<div key={item.id} {...api.getPanelProps({ id: item.id })}>
<p>{item.id}</p>
</div>
)
}
return <div key={item.id} {...api.getResizeTriggerProps({ id: item.id })} />
})}
</div>
</main>

<Toolbar controls={controls.ui}>
<StateVisualizer state={service} />
</Toolbar>
</>
)
}
72 changes: 72 additions & 0 deletions examples/next-ts/pages/splitter-multiple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { normalizeProps, useMachine } from "@zag-js/react"
import * as splitter from "@zag-js/splitter"
import { createContext, useContext, useId } from "react"

const PanelContext = createContext<splitter.Api>({} as any)

const PanelGroup = (props: React.PropsWithChildren<Omit<splitter.Props, "id">>) => {
const id = useId()
const service = useMachine(splitter.machine, { ...props, id })
const api = splitter.connect(service, normalizeProps)
return (
<PanelContext.Provider value={api}>
<div {...api.getRootProps()}>{props.children}</div>
</PanelContext.Provider>
)
}

const Panel = (props: React.PropsWithChildren<splitter.PanelProps>) => {
const api = useContext(PanelContext)
return <div {...api.getPanelProps(props)}>{props.children}</div>
}

const ResizeTrigger = (props: splitter.ResizeTriggerProps) => {
const api = useContext(PanelContext)
return <div {...api.getResizeTriggerProps(props)} />
}

export default function Page() {
return (
<main>
<PanelGroup
orientation="horizontal"
defaultSize={[20, 60, 20]}
panels={[
{ id: "a", order: 0, minSize: 20 },
{ id: "b", order: 1, collapsible: true },
{ id: "c", order: 2, minSize: 20 },
]}
>
<Panel id="a">Left</Panel>
<ResizeTrigger id="a:b" />
<Panel id="b">
<PanelGroup
orientation="vertical"
panels={[
{ id: "b1", order: 0 },
{ id: "b2", order: 1, collapsible: true },
]}
>
<Panel id="b1">top</Panel>
<ResizeTrigger id="b1:b2" />
<Panel id="b2">
<PanelGroup
orientation="horizontal"
panels={[
{ id: "b21", order: 0 },
{ id: "b22", order: 1, collapsible: true },
]}
>
<Panel id="b21">left</Panel>
<ResizeTrigger id="b21:b22" />
<Panel id="b22">right</Panel>
</PanelGroup>
</Panel>
</PanelGroup>
</Panel>
<ResizeTrigger id="b:c" />
<Panel id="c">right</Panel>
</PanelGroup>
</main>
)
}
17 changes: 9 additions & 8 deletions examples/next-ts/pages/splitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,32 @@ export default function Page() {

const service = useMachine(splitter.machine, {
id: useId(),
defaultSize: [
{ id: "a", size: 50 },
{ id: "b", size: 50 },
],
...controls.context,
panels: [{ id: "a" }, { id: "b" }, { id: "c" }],
})

const api = splitter.connect(service, normalizeProps)

return (
<>
<main className="splitter">
<pre>{JSON.stringify(api.getSizes(), null, 2)}</pre>
<div {...api.getRootProps()}>
<div {...api.getPanelProps({ id: "a" })}>
<p>A</p>
<p>Left</p>
</div>
<div {...api.getResizeTriggerProps({ id: "a:b" })} />
<div {...api.getPanelProps({ id: "b" })}>
<p>B</p>
<p>Middle</p>
</div>
<div {...api.getResizeTriggerProps({ id: "b:c" })} />
<div {...api.getPanelProps({ id: "c" })}>
<p>Right</p>
</div>
</div>
</main>

<Toolbar controls={controls.ui}>
<StateVisualizer state={service} omit={["previousPanels", "initialSize"]} />
<StateVisualizer state={service} />
</Toolbar>
</>
)
Expand Down
5 changes: 1 addition & 4 deletions examples/nuxt-ts/pages/splitter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ const controls = useControls(splitterControls)
const service = useMachine(splitter.machine, {
id: useId(),
defaultSize: [
{ id: "a", size: 50 },
{ id: "b", size: 50 },
],
panels: [{ id: "a" }, { id: "b" }],
})
const api = computed(() => splitter.connect(service, normalizeProps))
Expand Down
5 changes: 1 addition & 4 deletions examples/solid-ts/src/routes/splitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ export default function Page() {
splitter.machine,
controls.mergeProps<splitter.Props>({
id: createUniqueId(),
defaultSize: [
{ id: "a", size: 50 },
{ id: "b", size: 50 },
],
panels: [{ id: "a" }, { id: "b" }],
}),
)

Expand Down
5 changes: 1 addition & 4 deletions examples/svelte-ts/src/routes/splitter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@
const service = useMachine(splitter.machine, {
id: "1",
defaultSize: [
{ id: "a", size: 50 },
{ id: "b", size: 50 },
],
panels: [{ id: "a" }, { id: "b" }],
})
const api = $derived(splitter.connect(service, normalizeProps))
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ContextParams<T extends Dict> {
scope: Scope
getContext: () => BindableContext<T>
getComputed: () => ComputedFn<T>
getRefs: () => BindableRefs<T>
flush: (fn: VoidFunction) => void
}

Expand Down
Loading
Loading