From 8f60b03c55d97c645103c447da25901c47ae0666 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Sat, 9 Mar 2024 21:36:28 +0000 Subject: [PATCH 01/19] chore: initial scafold --- examples/next-ts/package.json | 3 +- examples/next-ts/pages/floating-panel.tsx | 30 +++++ examples/nuxt-ts/package.json | 1 + examples/nuxt-ts/pages/floating-panel.vue | 20 +++ examples/preact-ts/package.json | 3 +- examples/solid-ts/package.json | 3 +- .../solid-ts/src/pages/floating-panel.tsx | 31 +++++ examples/solid-ts/src/routes.ts | 1 + examples/svelte-ts/package.json | 3 +- examples/vue-ts/package.json | 1 + examples/vue-ts/src/pages/floating-panel.tsx | 38 ++++++ examples/vue-ts/src/routes.ts | 1 + packages/machines/floating-panel/README.md | 19 +++ packages/machines/floating-panel/package.json | 37 ++++++ .../src/floating-panel.anatomy.ts | 16 +++ .../src/floating-panel.connect.ts | 79 ++++++++++++ .../floating-panel/src/floating-panel.dom.ts | 12 ++ .../src/floating-panel.machine.ts | 79 ++++++++++++ .../src/floating-panel.props.ts | 7 ++ .../src/floating-panel.types.ts | 115 ++++++++++++++++++ packages/machines/floating-panel/src/index.ts | 5 + .../src/utils/get-intersection.ts | 32 +++++ .../src/utils/get-resize-axis-style.ts | 81 ++++++++++++ .../machines/floating-panel/tsconfig.json | 7 ++ pnpm-lock.yaml | 40 ++++++ shared/src/routes.ts | 1 + 26 files changed, 661 insertions(+), 4 deletions(-) create mode 100644 examples/next-ts/pages/floating-panel.tsx create mode 100644 examples/nuxt-ts/pages/floating-panel.vue create mode 100644 examples/solid-ts/src/pages/floating-panel.tsx create mode 100644 examples/vue-ts/src/pages/floating-panel.tsx create mode 100644 packages/machines/floating-panel/README.md create mode 100644 packages/machines/floating-panel/package.json create mode 100644 packages/machines/floating-panel/src/floating-panel.anatomy.ts create mode 100644 packages/machines/floating-panel/src/floating-panel.connect.ts create mode 100644 packages/machines/floating-panel/src/floating-panel.dom.ts create mode 100644 packages/machines/floating-panel/src/floating-panel.machine.ts create mode 100644 packages/machines/floating-panel/src/floating-panel.props.ts create mode 100644 packages/machines/floating-panel/src/floating-panel.types.ts create mode 100644 packages/machines/floating-panel/src/index.ts create mode 100644 packages/machines/floating-panel/src/utils/get-intersection.ts create mode 100644 packages/machines/floating-panel/src/utils/get-resize-axis-style.ts create mode 100644 packages/machines/floating-panel/tsconfig.json diff --git a/examples/next-ts/package.json b/examples/next-ts/package.json index 1fbe397ab8..8a34982a9e 100644 --- a/examples/next-ts/package.json +++ b/examples/next-ts/package.json @@ -38,6 +38,7 @@ "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", "@zag-js/file-utils": "workspace:*", + "@zag-js/floating-panel": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", @@ -96,4 +97,4 @@ "typescript": "5.4.2" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/examples/next-ts/pages/floating-panel.tsx b/examples/next-ts/pages/floating-panel.tsx new file mode 100644 index 0000000000..56beb8006e --- /dev/null +++ b/examples/next-ts/pages/floating-panel.tsx @@ -0,0 +1,30 @@ +import * as floating-panel from "@zag-js/floating-panel" +import { useMachine, normalizeProps } from "@zag-js/react" +import { floating-panelControls, floating-panelData } from "@zag-js/shared" +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(floating-panelControls) + + const [state, send] = useMachine(floating-panel.machine({ id: useId() }), { + context: controls.context, + }) + + const api = floating-panel.connect(state, send, normalizeProps) + + return ( + <> +
+
+
+
+ + + + + + ) +} diff --git a/examples/nuxt-ts/package.json b/examples/nuxt-ts/package.json index 9d0bbfb34a..3da1f6af52 100644 --- a/examples/nuxt-ts/package.json +++ b/examples/nuxt-ts/package.json @@ -37,6 +37,7 @@ "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", "@zag-js/file-utils": "workspace:*", + "@zag-js/floating-panel": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", diff --git a/examples/nuxt-ts/pages/floating-panel.vue b/examples/nuxt-ts/pages/floating-panel.vue new file mode 100644 index 0000000000..5c7f30ddc8 --- /dev/null +++ b/examples/nuxt-ts/pages/floating-panel.vue @@ -0,0 +1,20 @@ + + + diff --git a/examples/preact-ts/package.json b/examples/preact-ts/package.json index 9f5943bbe0..6f29ec1f1f 100644 --- a/examples/preact-ts/package.json +++ b/examples/preact-ts/package.json @@ -36,6 +36,7 @@ "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", "@zag-js/file-utils": "workspace:*", + "@zag-js/floating-panel": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", @@ -89,4 +90,4 @@ "typescript": "5.4.2", "vite": "5.1.5" } -} +} \ No newline at end of file diff --git a/examples/solid-ts/package.json b/examples/solid-ts/package.json index 2735185ea2..9abdad281c 100644 --- a/examples/solid-ts/package.json +++ b/examples/solid-ts/package.json @@ -46,6 +46,7 @@ "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", "@zag-js/file-utils": "workspace:*", + "@zag-js/floating-panel": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", @@ -91,4 +92,4 @@ "lucide-solid": "0.354.0", "solid-js": "1.8.15" } -} +} \ No newline at end of file diff --git a/examples/solid-ts/src/pages/floating-panel.tsx b/examples/solid-ts/src/pages/floating-panel.tsx new file mode 100644 index 0000000000..ca99770aa6 --- /dev/null +++ b/examples/solid-ts/src/pages/floating-panel.tsx @@ -0,0 +1,31 @@ +import * as floating-panel from "@zag-js/floating-panel" +import { normalizeProps, useMachine, mergeProps } from "@zag-js/solid" +import { createMemo, createUniqueId } from "solid-js" +import { floating-panelControls, floating-panelData } from "@zag-js/shared" +import { StateVisualizer } from "../components/state-visualizer" +import { Toolbar } from "../components/toolbar" +import { useControls } from "../hooks/use-controls" + +export default function Page() { + const controls = useControls(floating-panelControls) + + const [state, send] = useMachine(floating-panel.machine({ id: createUniqueId() }), { + context: controls.context, + }) + + const api = createMemo(() => floating-panel.connect(state, send, normalizeProps)) + + return ( + <> +
+
+ +
+
+ + + + + + ) +} diff --git a/examples/solid-ts/src/routes.ts b/examples/solid-ts/src/routes.ts index 358f2be5f9..9c8611c646 100644 --- a/examples/solid-ts/src/routes.ts +++ b/examples/solid-ts/src/routes.ts @@ -4,6 +4,7 @@ import { lazy } from "solid-js" import Home from "./pages/home" export const routes: RouteDefinition[] = [ + { path: "/floating-panel", component: lazy(() => import("./pages/floating-panel")) }, { path: "/tour", component: lazy(() => import("./pages/tour")) }, { path: "/collapsible", component: lazy(() => import("./pages/collapsible")) }, { path: "/clipboard", component: lazy(() => import("./pages/clipboard")) }, diff --git a/examples/svelte-ts/package.json b/examples/svelte-ts/package.json index 7eea4718cb..a0ed0c06af 100644 --- a/examples/svelte-ts/package.json +++ b/examples/svelte-ts/package.json @@ -38,6 +38,7 @@ "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", "@zag-js/file-utils": "workspace:*", + "@zag-js/floating-panel": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", @@ -94,4 +95,4 @@ "vite": "5.1.5", "vite-tsconfig-paths": "4.3.1" } -} +} \ No newline at end of file diff --git a/examples/vue-ts/package.json b/examples/vue-ts/package.json index bb75151468..dc2b326467 100644 --- a/examples/vue-ts/package.json +++ b/examples/vue-ts/package.json @@ -38,6 +38,7 @@ "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", "@zag-js/file-utils": "workspace:*", + "@zag-js/floating-panel": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", diff --git a/examples/vue-ts/src/pages/floating-panel.tsx b/examples/vue-ts/src/pages/floating-panel.tsx new file mode 100644 index 0000000000..804ab28003 --- /dev/null +++ b/examples/vue-ts/src/pages/floating-panel.tsx @@ -0,0 +1,38 @@ +import * as floating-panel from "@zag-js/floating-panel" +import { normalizeProps, useMachine, mergeProps } from "@zag-js/vue" +import { computed, defineComponent, h, Fragment } from "vue" +import { floating-panelControls, floating-panelData } from "@zag-js/shared" +import { StateVisualizer } from "../components/state-visualizer" +import { Toolbar } from "../components/toolbar" +import { useControls } from "../hooks/use-controls" + +export default defineComponent({ + name: "floating-panel", + setup() { + const controls = useControls(floating-panelControls) + + const [state, send] = useMachine(floating-panel.machine({ id: "1" }), { + context: controls.context, + }) + + const apiRef = computed(() => floating-panel.connect(state.value, send, normalizeProps)) + + return () => { + const api = apiRef.value + + return ( + <> +
+
+ +
+
+ + + + + + ) + } + }, +}) diff --git a/examples/vue-ts/src/routes.ts b/examples/vue-ts/src/routes.ts index baa4ca1b96..6f770b9625 100644 --- a/examples/vue-ts/src/routes.ts +++ b/examples/vue-ts/src/routes.ts @@ -4,6 +4,7 @@ import Home from "./pages/index" export const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ + { path: "/floating-panel", component: () => import("./pages/floating-panel") }, { path: "/tour", component: () => import("./pages/tour") }, { path: "/collapsible", component: () => import("./pages/collapsible") }, { path: "/clipboard", component: () => import("./pages/clipboard") }, diff --git a/packages/machines/floating-panel/README.md b/packages/machines/floating-panel/README.md new file mode 100644 index 0000000000..16cecc8293 --- /dev/null +++ b/packages/machines/floating-panel/README.md @@ -0,0 +1,19 @@ +# @zag-js/floating-panel + +Core logic for the floating-panel widget implemented as a state machine + +## Installation + +```sh +yarn add @zag-js/floating-panel +# or +npm i @zag-js/floating-panel +``` + +## Contribution + +Yes please! See the [contributing guidelines](https://github.com/chakra-ui/zag/blob/main/CONTRIBUTING.md) for details. + +## Licence + +This project is licensed under the terms of the [MIT license](https://github.com/chakra-ui/zag/blob/main/LICENSE). diff --git a/packages/machines/floating-panel/package.json b/packages/machines/floating-panel/package.json new file mode 100644 index 0000000000..cdd92749df --- /dev/null +++ b/packages/machines/floating-panel/package.json @@ -0,0 +1,37 @@ +{ + "name": "@zag-js/floating-panel", + "version": "0.0.0", + "description": "Core logic for the floating-panel widget implemented as a state machine", + "keywords": ["js", "machine", "xstate", "statechart", "component", "chakra-ui", "floating-panel"], + "author": "Segun Adebayo ", + "homepage": "https://github.com/chakra-ui/zag#readme", + "license": "MIT", + "main": "src/index.ts", + "repository": "https://github.com/chakra-ui/zag/tree/main/packages/floating-panel", + "sideEffects": false, + "files": ["dist", "src"], + "scripts": { + "build": "tsup", + "lint": "eslint src --ext .ts,.tsx", + "typecheck": "tsc --noEmit", + "prepack": "clean-package", + "postpack": "clean-package restore" + }, + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://github.com/chakra-ui/zag/issues" + }, + "dependencies": { + "@zag-js/anatomy": "workspace:*", + "@zag-js/core": "workspace:*", + "@zag-js/dom-query": "workspace:*", + "@zag-js/utils": "workspace:*", + "@zag-js/types": "workspace:*" + }, + "devDependencies": { + "clean-package": "2.2.0" + }, + "clean-package": "../../../clean-package.config.json" +} diff --git a/packages/machines/floating-panel/src/floating-panel.anatomy.ts b/packages/machines/floating-panel/src/floating-panel.anatomy.ts new file mode 100644 index 0000000000..3d7dbae62c --- /dev/null +++ b/packages/machines/floating-panel/src/floating-panel.anatomy.ts @@ -0,0 +1,16 @@ +import { createAnatomy } from "@zag-js/anatomy" + +export const anatomy = createAnatomy("floating-panel").parts( + "trigger", + "positioner", + "content", + "header", + "resizeTrigger", + "dragTrigger", + "minimizeTrigger", + "maximizeTrigger", + "closeTrigger", + "dock", +) + +export const parts = anatomy.build() diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts new file mode 100644 index 0000000000..62868d2b96 --- /dev/null +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -0,0 +1,79 @@ +import { dataAttr } from "@zag-js/dom-query" +import type { NormalizeProps, PropTypes } from "@zag-js/types" +import { parts } from "./floating-panel.anatomy" +import { dom } from "./floating-panel.dom" +import type { DockProps, ResizeTriggerProps, Send, State } from "./floating-panel.types" + +export function connect(state: State, send: Send, normalize: NormalizeProps) { + const isOpen = state.hasTag("open") + const isDragging = state.matches("open.dragging") + const isResizing = state.matches("open.resizing") + + return { + isOpen, + isDragging, + isResizing, + + trigger: normalize.button({ + ...parts.trigger.attrs, + type: "button", + id: dom.getTriggerId(state.context), + }), + + positionerProps: normalize.element({ + ...parts.positioner.attrs, + id: dom.getPositionerId(state.context), + }), + + contentProps: normalize.element({ + ...parts.content.attrs, + role: "application", + id: dom.getContentId(state.context), + style: { + position: "relative", + }, + }), + + closeTriggerProps: normalize.button({ + ...parts.closeTrigger.attrs, + disabled: state.context.disabled, + type: "button", + onClick() { + send("CLOSE") + }, + }), + + getResizeTriggerProps(props: ResizeTriggerProps) { + const disabled = state.context.resizable || state.context.disabled + return normalize.button({ + ...parts.resizeTrigger.attrs, + disabled, + "data-disabled": dataAttr(disabled), + type: "button", + onPointerDown(event) { + if (disabled) return + send({ + type: "RESIZE_START", + axis: props.axis, + position: { x: event.clientX, y: event.clientY }, + }) + }, + }) + }, + + dragTriggerProps: normalize.button({ + ...parts.dragTrigger.attrs, + disabled: state.context.draggable || state.context.disabled, + type: "button", + }), + + getDockProps(props: DockProps) { + const isIntersecting = true + return normalize.element({ + ...parts.dock.attrs, + "data-uid": props.id, + "data-intersecting": dataAttr(isIntersecting), + }) + }, + } +} diff --git a/packages/machines/floating-panel/src/floating-panel.dom.ts b/packages/machines/floating-panel/src/floating-panel.dom.ts new file mode 100644 index 0000000000..2abede6919 --- /dev/null +++ b/packages/machines/floating-panel/src/floating-panel.dom.ts @@ -0,0 +1,12 @@ +import { createScope } from "@zag-js/dom-query" +import type { MachineContext as Ctx } from "./floating-panel.types" + +export const dom = createScope({ + getTriggerId: (ctx: Ctx) => `float:${ctx.id}:trigger`, + getPositionerId: (ctx: Ctx) => `float:${ctx.id}:positioner`, + getContentId: (ctx: Ctx) => `float:${ctx.id}:content`, + + getTriggerEl: (ctx: Ctx) => dom.getById(ctx, dom.getTriggerId(ctx)), + getPositionerEl: (ctx: Ctx) => dom.getById(ctx, dom.getPositionerId(ctx)), + getContentEl: (ctx: Ctx) => dom.getById(ctx, dom.getContentId(ctx)), +}) diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts new file mode 100644 index 0000000000..f03089e4ea --- /dev/null +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -0,0 +1,79 @@ +import { createMachine } from "@zag-js/core" +import { compact } from "@zag-js/utils" +import type { MachineContext, MachineState, UserDefinedContext } from "./floating-panel.types" + +export function machine(userContext: UserDefinedContext) { + const ctx = compact(userContext) + return createMachine( + { + id: "floating-panel", + initial: ctx.open ? "open" : "closed", + context: { + ...ctx, + dragDiff: { x: 0, y: 0 }, + resizeDiff: { x: 0, y: 0 }, + }, + states: { + closed: { + tags: ["closed"], + on: { + OPEN: { target: "open" }, + }, + }, + + open: { + tags: ["open"], + activities: ["trackBoundaryRect"], + on: { + DRAG_START: { + target: "open.dragging", + }, + RESIZE_START: { + target: "open.resizing", + }, + CLOSE: { + target: "closed", + }, + }, + }, + + "open.dragging": { + tags: ["open"], + activities: ["trackBoundaryRect", "trackPointerMove", "trackDockRects"], + exit: ["resetDragDiff"], + on: { + DRAG: {}, + DRAG_END: {}, + CLOSE: { + target: "closed", + }, + }, + }, + + "open.resizing": { + tags: ["open"], + activities: ["trackBoundaryRect", "trackPointerMove"], + exit: ["resetResizeDiff"], + on: { + RESIZE: {}, + RESIZE_END: {}, + CLOSE: { + target: "closed", + }, + }, + }, + }, + }, + { + guards: {}, + actions: { + resetDragDiff(ctx) { + ctx.dragDiff = { x: 0, y: 0 } + }, + resetResizeDiff(ctx) { + ctx.resizeDiff = { x: 0, y: 0 } + }, + }, + }, + ) +} diff --git a/packages/machines/floating-panel/src/floating-panel.props.ts b/packages/machines/floating-panel/src/floating-panel.props.ts new file mode 100644 index 0000000000..efb8bf46aa --- /dev/null +++ b/packages/machines/floating-panel/src/floating-panel.props.ts @@ -0,0 +1,7 @@ +import { createProps } from "@zag-js/types" +import { createSplitProps } from "@zag-js/utils" +import type { UserDefinedContext } from "./floating-panel.types" + +export const props = createProps()([]) + +export const splitProps = createSplitProps>(props) diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts new file mode 100644 index 0000000000..2c8c2765a0 --- /dev/null +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -0,0 +1,115 @@ +import type { StateMachine as S } from "@zag-js/core" +import type { CommonProperties, DirectionProperty, RequiredBy } from "@zag-js/types" + +export interface Position { + x: number + y: number +} + +export interface Size { + width: number + height: number +} + +export interface Rect extends Position, Size {} + +export interface DragDetails { + position: Position +} + +export interface ResizeDetails { + size: Size +} + +export interface OpenChangeDetails { + open: boolean +} + +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * Whether the panel is open + */ + open?: boolean + /** + * Whether the panel is draggable + */ + draggable?: boolean + /** + * Whether the panel is resizable + */ + resizable?: boolean + /** + * The size of the panel + */ + size?: Size + /** + * The position of the panel + */ + position?: Position + /** + * Whether the panel is locked to its aspect ratio + */ + lockAspectRatio?: boolean + /** + * Whether the panel should close when the escape key is pressed + */ + closeOnEscape?: boolean + /** + * The boundary of the panel. Defaults to the window + */ + getBoundaryEl?(): { getBoundingClientRect(): DOMRect } + /** + * Whether the panel is disabled + */ + disabled?: boolean + /** + * Function called when the position of the panel changes via dragging + */ + onPositionChange?(details: DragDetails): void + /** + * Function called when the position of the panel changes via dragging ends + */ + onPositionChangeEnd?(details: DragDetails): void + /** + * Function called when the panel is opened or closed + */ + onOpenChange?(details: OpenChangeDetails): void + /** + * Function called when the size of the panel changes via resizing + */ + onSizeChange?(details: ResizeDetails): void + /** + * Function called when the size of the panel changes via resizing ends + */ + onSizeChangeEnd?(details: ResizeDetails): void +} + +interface PrivateContext { + dragDiff: Position + resizeDiff: Position +} + +type ComputedContext = Readonly<{}> + +export type UserDefinedContext = RequiredBy + +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} + +export interface MachineState { + tags: "open" | "closed" + value: "open" | "open.dragging" | "open.resizing" | "closed" +} + +export type State = S.State + +export type Send = S.Send + +export type ResizeTriggerAxis = "s" | "w" | "e" | "n" | "sw" | "nw" | "se" | "ne" + +export interface ResizeTriggerProps { + axis?: ResizeTriggerAxis +} + +export interface DockProps { + id: string +} diff --git a/packages/machines/floating-panel/src/index.ts b/packages/machines/floating-panel/src/index.ts new file mode 100644 index 0000000000..4ae58ea0b2 --- /dev/null +++ b/packages/machines/floating-panel/src/index.ts @@ -0,0 +1,5 @@ +export { anatomy } from "./floating-panel.anatomy" +export { connect } from "./floating-panel.connect" +export { machine } from "./floating-panel.machine" +export * from "./floating-panel.props" +export type { UserDefinedContext as Context } from "./floating-panel.types" diff --git a/packages/machines/floating-panel/src/utils/get-intersection.ts b/packages/machines/floating-panel/src/utils/get-intersection.ts new file mode 100644 index 0000000000..124391ef6f --- /dev/null +++ b/packages/machines/floating-panel/src/utils/get-intersection.ts @@ -0,0 +1,32 @@ +import type { Rect } from "../floating-panel.types" + +export function getIntersection(r1: Rect, r2: Rect) { + const xOverlap = Math.max(0, Math.min(r1.x + r1.width, r2.x + r2.width) - Math.max(r1.x, r2.x)) + const yOverlap = Math.max(0, Math.min(r1.y + r1.height, r2.y + r2.height) - Math.max(r1.y, r2.y)) + + const intersectionRect = + xOverlap > 0 && yOverlap > 0 + ? { + x: Math.max(r1.x, r2.x), + y: Math.max(r1.y, r2.y), + width: xOverlap, + height: yOverlap, + } + : null + + const intersectionArea = intersectionRect ? intersectionRect.width * intersectionRect.height : 0 + + const r1Area = r1.width * r1.height + const r2Area = r2.width * r2.height + + const unionArea = r1Area + r2Area - intersectionArea + + const intersectionRatio = unionArea > 0 ? intersectionArea / unionArea : 0 + const isIntersecting = intersectionRatio > 0 + + return { + isIntersecting, + intersectionRatio, + intersectionRect, + } +} diff --git a/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts b/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts new file mode 100644 index 0000000000..1f2f534280 --- /dev/null +++ b/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts @@ -0,0 +1,81 @@ +import type { JSX } from "@zag-js/types" +import type { ResizeTriggerAxis } from "../floating-panel.types" + +export function getResizeAxisStyle(axis: ResizeTriggerAxis): JSX.CSSProperties { + switch (axis) { + case "s": + return { + cursor: "ns-resize", + bottom: 0, + left: 0, + right: 0, + height: 8, + } + + case "w": + return { + cursor: "ew-resize", + top: 0, + left: 0, + bottom: 0, + width: 8, + } + + case "e": + return { + cursor: "ew-resize", + top: 0, + right: 0, + bottom: 0, + width: 8, + } + + case "n": + return { + cursor: "ns-resize", + top: 0, + left: 0, + right: 0, + height: 8, + } + + case "sw": + return { + cursor: "sw-resize", + bottom: 0, + left: 0, + width: 8, + height: 8, + } + + case "nw": + return { + cursor: "nw-resize", + top: 0, + left: 0, + width: 8, + height: 8, + } + + case "se": + return { + cursor: "se-resize", + bottom: 0, + right: 0, + width: 8, + height: 8, + } + + case "ne": + return { + cursor: "ne-resize", + top: 0, + right: 0, + width: 8, + height: 8, + } + + default: + throw new Error(`Invalid axis: ${axis}`) + } +} diff --git a/packages/machines/floating-panel/tsconfig.json b/packages/machines/floating-panel/tsconfig.json new file mode 100644 index 0000000000..8e781cd154 --- /dev/null +++ b/packages/machines/floating-panel/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/.tsbuildinfo" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d09e4437ea..50accc2083 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: '@zag-js/file-utils': specifier: workspace:* version: link:../../packages/utilities/file-utils + '@zag-js/floating-panel': + specifier: workspace:* + version: link:../../packages/machines/floating-panel '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -475,6 +478,9 @@ importers: '@zag-js/file-utils': specifier: workspace:* version: link:../../packages/utilities/file-utils + '@zag-js/floating-panel': + specifier: workspace:* + version: link:../../packages/machines/floating-panel '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -713,6 +719,9 @@ importers: '@zag-js/file-utils': specifier: workspace:* version: link:../../packages/utilities/file-utils + '@zag-js/floating-panel': + specifier: workspace:* + version: link:../../packages/machines/floating-panel '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -954,6 +963,9 @@ importers: '@zag-js/file-utils': specifier: workspace:* version: link:../../packages/utilities/file-utils + '@zag-js/floating-panel': + specifier: workspace:* + version: link:../../packages/machines/floating-panel '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -1183,6 +1195,9 @@ importers: '@zag-js/file-utils': specifier: workspace:* version: link:../../packages/utilities/file-utils + '@zag-js/floating-panel': + specifier: workspace:* + version: link:../../packages/machines/floating-panel '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -1430,6 +1445,9 @@ importers: '@zag-js/file-utils': specifier: workspace:* version: link:../../packages/utilities/file-utils + '@zag-js/floating-panel': + specifier: workspace:* + version: link:../../packages/machines/floating-panel '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -2162,6 +2180,28 @@ importers: specifier: 2.2.0 version: 2.2.0 + packages/machines/floating-panel: + dependencies: + '@zag-js/anatomy': + specifier: workspace:* + version: link:../../anatomy + '@zag-js/core': + specifier: workspace:* + version: link:../../core + '@zag-js/dom-query': + specifier: workspace:* + version: link:../../utilities/dom-query + '@zag-js/types': + specifier: workspace:* + version: link:../../types + '@zag-js/utils': + specifier: workspace:* + version: link:../../utilities/core + devDependencies: + clean-package: + specifier: 2.2.0 + version: 2.2.0 + packages/machines/hover-card: dependencies: '@zag-js/anatomy': diff --git a/shared/src/routes.ts b/shared/src/routes.ts index e135679c0a..20bc81f730 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -4,6 +4,7 @@ type RouteData = { } export const routesData: RouteData[] = [ + { label: "Floating-panel", path: "/floating-panel" }, { label: "Tour", path: "/tour" }, { label: "Collapsible", path: "/collapsible" }, { label: "Clipboard", path: "/clipboard" }, From f0529627dc3e047afa14fca88f2bcc73cb1d3332 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Sun, 10 Mar 2024 01:32:05 +0000 Subject: [PATCH 02/19] chore: update --- packages/machines/floating-panel/package.json | 18 +++++- .../src/floating-panel.connect.ts | 33 +++++++++- .../src/floating-panel.props.ts | 20 +++++- .../src/floating-panel.types.ts | 10 +-- .../src/utils/get-resize-axis-style.ts | 64 +++++++------------ pnpm-lock.yaml | 3 + 6 files changed, 98 insertions(+), 50 deletions(-) diff --git a/packages/machines/floating-panel/package.json b/packages/machines/floating-panel/package.json index cdd92749df..780ccf9a44 100644 --- a/packages/machines/floating-panel/package.json +++ b/packages/machines/floating-panel/package.json @@ -2,14 +2,25 @@ "name": "@zag-js/floating-panel", "version": "0.0.0", "description": "Core logic for the floating-panel widget implemented as a state machine", - "keywords": ["js", "machine", "xstate", "statechart", "component", "chakra-ui", "floating-panel"], + "keywords": [ + "js", + "machine", + "xstate", + "statechart", + "component", + "chakra-ui", + "floating-panel" + ], "author": "Segun Adebayo ", "homepage": "https://github.com/chakra-ui/zag#readme", "license": "MIT", - "main": "src/index.ts", + "main": "src/index.ts", "repository": "https://github.com/chakra-ui/zag/tree/main/packages/floating-panel", "sideEffects": false, - "files": ["dist", "src"], + "files": [ + "dist", + "src" + ], "scripts": { "build": "tsup", "lint": "eslint src --ext .ts,.tsx", @@ -27,6 +38,7 @@ "@zag-js/anatomy": "workspace:*", "@zag-js/core": "workspace:*", "@zag-js/dom-query": "workspace:*", + "@zag-js/dom-event": "workspace:*", "@zag-js/utils": "workspace:*", "@zag-js/types": "workspace:*" }, diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts index 62868d2b96..a37cfcfef6 100644 --- a/packages/machines/floating-panel/src/floating-panel.connect.ts +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -1,8 +1,10 @@ import { dataAttr } from "@zag-js/dom-query" +import { getEventKey, type EventKeyMap } from "@zag-js/dom-event" import type { NormalizeProps, PropTypes } from "@zag-js/types" import { parts } from "./floating-panel.anatomy" import { dom } from "./floating-panel.dom" import type { DockProps, ResizeTriggerProps, Send, State } from "./floating-panel.types" +import { getResizeAxisStyle } from "./utils/get-resize-axis-style" export function connect(state: State, send: Send, normalize: NormalizeProps) { const isOpen = state.hasTag("open") @@ -27,11 +29,27 @@ export function connect(state: State, send: Send, normalize contentProps: normalize.element({ ...parts.content.attrs, - role: "application", + role: "dialog", + tabIndex: 0, id: dom.getContentId(state.context), style: { position: "relative", }, + onKeyDown(event) { + const keyMap: EventKeyMap = { + ArrowLeft() {}, + ArrowRight() {}, + ArrowUp() {}, + ArrowDown() {}, + } + + const handler = keyMap[getEventKey(event, state.context)] + + if (handler) { + event.preventDefault() + handler(event) + } + }, }), closeTriggerProps: normalize.button({ @@ -49,6 +67,7 @@ export function connect(state: State, send: Send, normalize ...parts.resizeTrigger.attrs, disabled, "data-disabled": dataAttr(disabled), + "data-axis": props.axis, type: "button", onPointerDown(event) { if (disabled) return @@ -58,6 +77,11 @@ export function connect(state: State, send: Send, normalize position: { x: event.clientX, y: event.clientY }, }) }, + style: { + position: "absolute", + touchAction: "none", + ...getResizeAxisStyle(props.axis), + }, }) }, @@ -65,6 +89,13 @@ export function connect(state: State, send: Send, normalize ...parts.dragTrigger.attrs, disabled: state.context.draggable || state.context.disabled, type: "button", + onPointerDown(event) { + if (state.context.disabled) return + send({ + type: "DRAG_START", + position: { x: event.clientX, y: event.clientY }, + }) + }, }), getDockProps(props: DockProps) { diff --git a/packages/machines/floating-panel/src/floating-panel.props.ts b/packages/machines/floating-panel/src/floating-panel.props.ts index efb8bf46aa..22916371b5 100644 --- a/packages/machines/floating-panel/src/floating-panel.props.ts +++ b/packages/machines/floating-panel/src/floating-panel.props.ts @@ -2,6 +2,24 @@ import { createProps } from "@zag-js/types" import { createSplitProps } from "@zag-js/utils" import type { UserDefinedContext } from "./floating-panel.types" -export const props = createProps()([]) +export const props = createProps()([ + "closeOnEscape", + "dir", + "disabled", + "draggable", + "getBoundaryEl", + "getRootNode", + "id", + "lockAspectRatio", + "onDrag", + "onDragEnd", + "onOpenChange", + "onResize", + "onResizeEnd", + "open", + "position", + "resizable", + "size", +]) export const splitProps = createSplitProps>(props) diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index 2c8c2765a0..accd808520 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -65,11 +65,11 @@ interface PublicContext extends DirectionProperty, CommonProperties { /** * Function called when the position of the panel changes via dragging */ - onPositionChange?(details: DragDetails): void + onDrag?(details: DragDetails): void /** * Function called when the position of the panel changes via dragging ends */ - onPositionChangeEnd?(details: DragDetails): void + onDragEnd?(details: DragDetails): void /** * Function called when the panel is opened or closed */ @@ -77,11 +77,11 @@ interface PublicContext extends DirectionProperty, CommonProperties { /** * Function called when the size of the panel changes via resizing */ - onSizeChange?(details: ResizeDetails): void + onResize?(details: ResizeDetails): void /** * Function called when the size of the panel changes via resizing ends */ - onSizeChangeEnd?(details: ResizeDetails): void + onResizeEnd?(details: ResizeDetails): void } interface PrivateContext { @@ -107,7 +107,7 @@ export type Send = S.Send export type ResizeTriggerAxis = "s" | "w" | "e" | "n" | "sw" | "nw" | "se" | "ne" export interface ResizeTriggerProps { - axis?: ResizeTriggerAxis + axis: ResizeTriggerAxis } export interface DockProps { diff --git a/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts b/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts index 1f2f534280..bab3f0ef1f 100644 --- a/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts +++ b/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts @@ -3,40 +3,39 @@ import type { ResizeTriggerAxis } from "../floating-panel.types" export function getResizeAxisStyle(axis: ResizeTriggerAxis): JSX.CSSProperties { switch (axis) { - case "s": + case "n": return { - cursor: "ns-resize", - bottom: 0, + cursor: "n-resize", left: 0, - right: 0, - height: 8, + width: "100%", } - case "w": + case "e": return { - cursor: "ew-resize", - top: 0, - left: 0, - bottom: 0, - width: 8, + cursor: "e-resize", + right: 0, + height: "100%", } - case "e": + case "s": return { - cursor: "ew-resize", - top: 0, - right: 0, + cursor: "s-resize", bottom: 0, - width: 8, + width: "100%", } - case "n": + case "w": return { - cursor: "ns-resize", - top: 0, + cursor: "w-resize", left: 0, + height: "100%", + } + + case "se": + return { + cursor: "se-resize", + bottom: 0, right: 0, - height: 8, } case "sw": @@ -44,35 +43,20 @@ export function getResizeAxisStyle(axis: ResizeTriggerAxis): JSX.CSSProperties { cursor: "sw-resize", bottom: 0, left: 0, - width: 8, - height: 8, } - case "nw": + case "ne": return { - cursor: "nw-resize", + cursor: "ne-resize", top: 0, - left: 0, - width: 8, - height: 8, - } - - case "se": - return { - cursor: "se-resize", - bottom: 0, right: 0, - width: 8, - height: 8, } - case "ne": + case "nw": return { - cursor: "ne-resize", + cursor: "nw-resize", top: 0, - right: 0, - width: 8, - height: 8, + left: 0, } default: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50accc2083..cdd55f8347 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2188,6 +2188,9 @@ importers: '@zag-js/core': specifier: workspace:* version: link:../../core + '@zag-js/dom-event': + specifier: workspace:* + version: link:../../utilities/dom-event '@zag-js/dom-query': specifier: workspace:* version: link:../../utilities/dom-query From 017cc605268a6c009bd38baec34597b4914c5238 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Sun, 10 Mar 2024 02:00:15 +0000 Subject: [PATCH 03/19] chore: improve set up --- .xstate/floating-panel.js | 79 +++++++++++++++++++ examples/next-ts/pages/floating-panel.tsx | 35 ++++++-- .../src/floating-panel.anatomy.ts | 2 + .../src/floating-panel.connect.ts | 39 +++++++-- .../floating-panel/src/floating-panel.dom.ts | 1 + .../src/floating-panel.machine.ts | 2 + .../src/floating-panel.types.ts | 4 +- shared/src/controls.ts | 8 ++ shared/src/style.css | 51 ++++++++++++ 9 files changed, 207 insertions(+), 14 deletions(-) create mode 100644 .xstate/floating-panel.js diff --git a/.xstate/floating-panel.js b/.xstate/floating-panel.js new file mode 100644 index 0000000000..bee149e2b9 --- /dev/null +++ b/.xstate/floating-panel.js @@ -0,0 +1,79 @@ +"use strict"; + +var _xstate = require("xstate"); +const { + actions, + createMachine, + assign +} = _xstate; +const { + choose +} = actions; +const fetchMachine = createMachine({ + id: "floating-panel", + initial: ctx.open ? "open" : "closed", + context: {}, + on: { + UPDATE_CONTEXT: { + actions: "updateContext" + } + }, + states: { + closed: { + tags: ["closed"], + on: { + OPEN: { + target: "open" + } + } + }, + open: { + tags: ["open"], + activities: ["trackBoundaryRect"], + on: { + DRAG_START: { + target: "open.dragging" + }, + RESIZE_START: { + target: "open.resizing" + }, + CLOSE: { + target: "closed" + } + } + }, + "open.dragging": { + tags: ["open"], + activities: ["trackBoundaryRect", "trackPointerMove", "trackDockRects"], + exit: ["resetDragDiff"], + on: { + DRAG: {}, + DRAG_END: {}, + CLOSE: { + target: "closed" + } + } + }, + "open.resizing": { + tags: ["open"], + activities: ["trackBoundaryRect", "trackPointerMove"], + exit: ["resetResizeDiff"], + on: { + RESIZE: {}, + RESIZE_END: {}, + CLOSE: { + target: "closed" + } + } + } + } +}, { + actions: { + updateContext: assign((context, event) => { + return { + [event.contextKey]: true + }; + }) + }, + guards: {} +}); \ No newline at end of file diff --git a/examples/next-ts/pages/floating-panel.tsx b/examples/next-ts/pages/floating-panel.tsx index 56beb8006e..532ff9f59d 100644 --- a/examples/next-ts/pages/floating-panel.tsx +++ b/examples/next-ts/pages/floating-panel.tsx @@ -1,24 +1,45 @@ -import * as floating-panel from "@zag-js/floating-panel" -import { useMachine, normalizeProps } from "@zag-js/react" -import { floating-panelControls, floating-panelData } from "@zag-js/shared" +import * as floatingPanel from "@zag-js/floating-panel" +import { Portal, normalizeProps, useMachine } from "@zag-js/react" +import { floatingPanelControls } from "@zag-js/shared" +import { XIcon } from "lucide-react" 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(floating-panelControls) + const controls = useControls(floatingPanelControls) - const [state, send] = useMachine(floating-panel.machine({ id: useId() }), { + const [state, send] = useMachine(floatingPanel.machine({ id: useId() }), { context: controls.context, }) - const api = floating-panel.connect(state, send, normalizeProps) + const api = floatingPanel.connect(state, send, normalizeProps) return ( <>
-
+
+ + +
+
+
+
+
+
+
+

Floating Panel

+ +
+
+

Some content

+
+
+
+
diff --git a/packages/machines/floating-panel/src/floating-panel.anatomy.ts b/packages/machines/floating-panel/src/floating-panel.anatomy.ts index 3d7dbae62c..0c3a5effeb 100644 --- a/packages/machines/floating-panel/src/floating-panel.anatomy.ts +++ b/packages/machines/floating-panel/src/floating-panel.anatomy.ts @@ -5,6 +5,8 @@ export const anatomy = createAnatomy("floating-panel").parts( "positioner", "content", "header", + "body", + "title", "resizeTrigger", "dragTrigger", "minimizeTrigger", diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts index a37cfcfef6..71e81a55b1 100644 --- a/packages/machines/floating-panel/src/floating-panel.connect.ts +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -1,5 +1,5 @@ -import { dataAttr } from "@zag-js/dom-query" import { getEventKey, type EventKeyMap } from "@zag-js/dom-event" +import { dataAttr } from "@zag-js/dom-query" import type { NormalizeProps, PropTypes } from "@zag-js/types" import { parts } from "./floating-panel.anatomy" import { dom } from "./floating-panel.dom" @@ -16,7 +16,7 @@ export function connect(state: State, send: Send, normalize isDragging, isResizing, - trigger: normalize.button({ + triggerProps: normalize.button({ ...parts.trigger.attrs, type: "button", id: dom.getTriggerId(state.context), @@ -25,6 +25,14 @@ export function connect(state: State, send: Send, normalize positionerProps: normalize.element({ ...parts.positioner.attrs, id: dom.getPositionerId(state.context), + style: { + position: "absolute", + "--x": `${state.context.position.x}px`, + "--y": `${state.context.position.y}px`, + "--width": `${state.context.size.width}px`, + "--height": `${state.context.size.height}px`, + translate: `var(--x) var(--y)`, + }, }), contentProps: normalize.element({ @@ -32,8 +40,12 @@ export function connect(state: State, send: Send, normalize role: "dialog", tabIndex: 0, id: dom.getContentId(state.context), + "aria-labelledby": dom.getTitleId(state.context), + "data-dragging": dataAttr(isDragging), style: { position: "relative", + width: "var(--width)", + height: "var(--height)", }, onKeyDown(event) { const keyMap: EventKeyMap = { @@ -63,14 +75,14 @@ export function connect(state: State, send: Send, normalize getResizeTriggerProps(props: ResizeTriggerProps) { const disabled = state.context.resizable || state.context.disabled - return normalize.button({ + return normalize.element({ ...parts.resizeTrigger.attrs, disabled, "data-disabled": dataAttr(disabled), "data-axis": props.axis, - type: "button", onPointerDown(event) { if (disabled) return + event.preventDefault() send({ type: "RESIZE_START", axis: props.axis, @@ -91,6 +103,7 @@ export function connect(state: State, send: Send, normalize type: "button", onPointerDown(event) { if (state.context.disabled) return + event.preventDefault() send({ type: "DRAG_START", position: { x: event.clientX, y: event.clientY }, @@ -102,9 +115,25 @@ export function connect(state: State, send: Send, normalize const isIntersecting = true return normalize.element({ ...parts.dock.attrs, - "data-uid": props.id, + "data-ownedby": state.context.id, + "data-dock": props.id, "data-intersecting": dataAttr(isIntersecting), }) }, + + titleProps: normalize.element({ + ...parts.title.attrs, + id: dom.getTitleId(state.context), + }), + + headerProps: normalize.element({ + ...parts.header.attrs, + "data-dragging": dataAttr(isDragging), + }), + + bodyProps: normalize.element({ + ...parts.body.attrs, + "data-dragging": dataAttr(isDragging), + }), } } diff --git a/packages/machines/floating-panel/src/floating-panel.dom.ts b/packages/machines/floating-panel/src/floating-panel.dom.ts index 2abede6919..f0d62ed08b 100644 --- a/packages/machines/floating-panel/src/floating-panel.dom.ts +++ b/packages/machines/floating-panel/src/floating-panel.dom.ts @@ -5,6 +5,7 @@ export const dom = createScope({ getTriggerId: (ctx: Ctx) => `float:${ctx.id}:trigger`, getPositionerId: (ctx: Ctx) => `float:${ctx.id}:positioner`, getContentId: (ctx: Ctx) => `float:${ctx.id}:content`, + getTitleId: (ctx: Ctx) => `float:${ctx.id}:title`, getTriggerEl: (ctx: Ctx) => dom.getById(ctx, dom.getTriggerId(ctx)), getPositionerEl: (ctx: Ctx) => dom.getById(ctx, dom.getPositionerId(ctx)), diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index f03089e4ea..89bbf642ae 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -9,6 +9,8 @@ export function machine(userContext: UserDefinedContext) { id: "floating-panel", initial: ctx.open ? "open" : "closed", context: { + size: { width: 300, height: 300 }, + position: { x: 0, y: 0 }, ...ctx, dragDiff: { x: 0, y: 0 }, resizeDiff: { x: 0, y: 0 }, diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index accd808520..ba9eb2c76d 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -41,11 +41,11 @@ interface PublicContext extends DirectionProperty, CommonProperties { /** * The size of the panel */ - size?: Size + size: Size /** * The position of the panel */ - position?: Position + position: Position /** * Whether the panel is locked to its aspect ratio */ diff --git a/shared/src/controls.ts b/shared/src/controls.ts index 7e20a97300..d5f8bd0144 100644 --- a/shared/src/controls.ts +++ b/shared/src/controls.ts @@ -246,3 +246,11 @@ export const tourControls = defineControls({ closeOnInteractOutside: { type: "boolean", defaultValue: true }, preventInteraction: { type: "boolean", defaultValue: true }, }) + +export const floatingPanelControls = defineControls({ + disabled: { type: "boolean", defaultValue: false }, + resizable: { type: "boolean", defaultValue: false }, + draggable: { type: "boolean", defaultValue: false }, + lockAspectRatio: { type: "boolean", defaultValue: false }, + closeOnEscape: { type: "boolean", defaultValue: true }, +}) diff --git a/shared/src/style.css b/shared/src/style.css index 8232a492de..008436134a 100644 --- a/shared/src/style.css +++ b/shared/src/style.css @@ -1980,6 +1980,57 @@ main [data-testid="scrubber"] { height: 100px; } +/* ----------------------------------------------------------------------------- +* Floating Panel +* -----------------------------------------------------------------------------*/ + +[data-scope="floating-panel"][data-part="content"] { + border: 1px solid #ebebeb; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +[data-scope="floating-panel"][data-part="body"] { + padding: 24px; + background-color: white; +} + +[data-scope="floating-panel"][data-part="header"] { + padding-inline: 16px; + padding-block: 4px; + background-color: #f5f5f5; + border-bottom: 1px solid #ebebeb; + display: flex; + justify-content: space-between; + align-items: center; +} + +[data-scope="floating-panel"][data-part="close-trigger"] { + svg { + width: 1em; + height: 1em; + } +} + +[data-scope="floating-panel"][data-part="resize-trigger"] { + [data-axis="n"], + [data-axis="s"] { + height: 6px; + } + + [data-axis="e"], + [data-axis="w"] { + width: 6px; + } + + [data-axis="ne"], + [data-axis="nw"], + [data-axis="se"], + [data-axis="sw"] { + width: 6px; + height: 6px; + } +} + /* ----------------------------------------------------------------------------- * Page Shell * -----------------------------------------------------------------------------*/ From a07ce370c3200890c0fbd7675a33aa70c9286a37 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Sun, 10 Mar 2024 02:25:41 +0000 Subject: [PATCH 04/19] chore: update styles --- shared/src/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/src/style.css b/shared/src/style.css index 008436134a..ad131811b1 100644 --- a/shared/src/style.css +++ b/shared/src/style.css @@ -1987,6 +1987,7 @@ main [data-testid="scrubber"] { [data-scope="floating-panel"][data-part="content"] { border: 1px solid #ebebeb; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + outline: 0 !important; } [data-scope="floating-panel"][data-part="body"] { From f091ce2aca232be3d0f9a04ea58acabf355fb0e8 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Sun, 10 Mar 2024 02:31:07 +0000 Subject: [PATCH 05/19] chore: rm portal for now --- examples/next-ts/pages/floating-panel.tsx | 34 +++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/examples/next-ts/pages/floating-panel.tsx b/examples/next-ts/pages/floating-panel.tsx index 532ff9f59d..31d51277b6 100644 --- a/examples/next-ts/pages/floating-panel.tsx +++ b/examples/next-ts/pages/floating-panel.tsx @@ -1,5 +1,5 @@ import * as floatingPanel from "@zag-js/floating-panel" -import { Portal, normalizeProps, useMachine } from "@zag-js/react" +import { normalizeProps, useMachine } from "@zag-js/react" import { floatingPanelControls } from "@zag-js/shared" import { XIcon } from "lucide-react" import { useId } from "react" @@ -21,25 +21,23 @@ export default function Page() {
- -
-
-
-
-
-
-
-

Floating Panel

- -
-
-

Some content

-
+
+
+
+
+
+
+
+

Floating Panel

+ +
+
+

Some content

- +
From 8c03d21cc3a7659b4c28d97b8bbcc594f26b23a7 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Sun, 10 Mar 2024 02:38:32 +0000 Subject: [PATCH 06/19] chore: update transitions --- .xstate/floating-panel.js | 16 +++++-- examples/next-ts/pages/floating-panel.tsx | 14 +++--- .../src/floating-panel.connect.ts | 11 ++++- .../src/floating-panel.machine.ts | 43 +++++++++++++++++-- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/.xstate/floating-panel.js b/.xstate/floating-panel.js index bee149e2b9..946dee75fa 100644 --- a/.xstate/floating-panel.js +++ b/.xstate/floating-panel.js @@ -23,7 +23,8 @@ const fetchMachine = createMachine({ tags: ["closed"], on: { OPEN: { - target: "open" + target: "open", + actions: ["invokeOnOpen"] } } }, @@ -48,7 +49,10 @@ const fetchMachine = createMachine({ exit: ["resetDragDiff"], on: { DRAG: {}, - DRAG_END: {}, + DRAG_END: { + target: "open", + actions: ["invokeOnDragEnd"] + }, CLOSE: { target: "closed" } @@ -60,9 +64,13 @@ const fetchMachine = createMachine({ exit: ["resetResizeDiff"], on: { RESIZE: {}, - RESIZE_END: {}, + RESIZE_END: { + target: "open", + actions: ["invokeOnResizeEnd"] + }, CLOSE: { - target: "closed" + target: "closed", + actions: ["invokeOnClose"] } } } diff --git a/examples/next-ts/pages/floating-panel.tsx b/examples/next-ts/pages/floating-panel.tsx index 31d51277b6..7f55ee4848 100644 --- a/examples/next-ts/pages/floating-panel.tsx +++ b/examples/next-ts/pages/floating-panel.tsx @@ -27,11 +27,13 @@ export default function Page() {
-
-

Floating Panel

- +
+
+

Floating Panel

+ +

Some content

@@ -41,7 +43,7 @@ export default function Page() {
- + diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts index 71e81a55b1..4307f3bdcf 100644 --- a/packages/machines/floating-panel/src/floating-panel.connect.ts +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -20,6 +20,9 @@ export function connect(state: State, send: Send, normalize ...parts.trigger.attrs, type: "button", id: dom.getTriggerId(state.context), + onClick() { + send({ type: "OPEN" }) + }, }), positionerProps: normalize.element({ @@ -39,6 +42,8 @@ export function connect(state: State, send: Send, normalize ...parts.content.attrs, role: "dialog", tabIndex: 0, + hidden: !isOpen, + "data-state": isOpen ? "open" : "closed", id: dom.getContentId(state.context), "aria-labelledby": dom.getTitleId(state.context), "data-dragging": dataAttr(isDragging), @@ -97,10 +102,9 @@ export function connect(state: State, send: Send, normalize }) }, - dragTriggerProps: normalize.button({ + dragTriggerProps: normalize.element({ ...parts.dragTrigger.attrs, disabled: state.context.draggable || state.context.disabled, - type: "button", onPointerDown(event) { if (state.context.disabled) return event.preventDefault() @@ -109,6 +113,9 @@ export function connect(state: State, send: Send, normalize position: { x: event.clientX, y: event.clientY }, }) }, + style: { + userSelect: "none", + }, }), getDockProps(props: DockProps) { diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index 89bbf642ae..9909ef2660 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -1,5 +1,7 @@ import { createMachine } from "@zag-js/core" +import { trackPointerMove } from "@zag-js/dom-event" import { compact } from "@zag-js/utils" +import { dom } from "./floating-panel.dom" import type { MachineContext, MachineState, UserDefinedContext } from "./floating-panel.types" export function machine(userContext: UserDefinedContext) { @@ -19,7 +21,10 @@ export function machine(userContext: UserDefinedContext) { closed: { tags: ["closed"], on: { - OPEN: { target: "open" }, + OPEN: { + target: "open", + actions: ["invokeOnOpen"], + }, }, }, @@ -45,7 +50,10 @@ export function machine(userContext: UserDefinedContext) { exit: ["resetDragDiff"], on: { DRAG: {}, - DRAG_END: {}, + DRAG_END: { + target: "open", + actions: ["invokeOnDragEnd"], + }, CLOSE: { target: "closed", }, @@ -58,9 +66,13 @@ export function machine(userContext: UserDefinedContext) { exit: ["resetResizeDiff"], on: { RESIZE: {}, - RESIZE_END: {}, + RESIZE_END: { + target: "open", + actions: ["invokeOnResizeEnd"], + }, CLOSE: { target: "closed", + actions: ["invokeOnClose"], }, }, }, @@ -68,6 +80,19 @@ export function machine(userContext: UserDefinedContext) { }, { guards: {}, + activities: { + trackPointerMove(ctx, _evt, { send }) { + const doc = dom.getDoc(ctx) + return trackPointerMove(doc, { + onPointerMove(details) { + console.log(details) + }, + onPointerUp() { + send("DRAG_END") + }, + }) + }, + }, actions: { resetDragDiff(ctx) { ctx.dragDiff = { x: 0, y: 0 } @@ -75,6 +100,18 @@ export function machine(userContext: UserDefinedContext) { resetResizeDiff(ctx) { ctx.resizeDiff = { x: 0, y: 0 } }, + invokeOnOpen(ctx) { + ctx.onOpenChange?.({ open: true }) + }, + invokeOnClose(ctx) { + ctx.onOpenChange?.({ open: false }) + }, + invokeOnDragEnd(ctx) { + ctx.onDragEnd?.({ position: ctx.position }) + }, + invokeOnResizeEnd(ctx) { + ctx.onResizeEnd?.({ size: ctx.size }) + }, }, }, ) From 05523267ebd71bdfc172bc78742bf82d00814a91 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Tue, 19 Mar 2024 21:09:47 +0000 Subject: [PATCH 07/19] feat: implement dragging --- .xstate/floating-panel.js | 11 ++++-- .../src/floating-panel.connect.ts | 5 ++- .../src/floating-panel.machine.ts | 38 +++++++++++++++++-- .../src/floating-panel.types.ts | 2 + shared/src/style.css | 1 + 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/.xstate/floating-panel.js b/.xstate/floating-panel.js index 946dee75fa..13eeaeae6d 100644 --- a/.xstate/floating-panel.js +++ b/.xstate/floating-panel.js @@ -33,7 +33,8 @@ const fetchMachine = createMachine({ activities: ["trackBoundaryRect"], on: { DRAG_START: { - target: "open.dragging" + target: "open.dragging", + actions: ["setLastPosition"] }, RESIZE_START: { target: "open.resizing" @@ -48,7 +49,9 @@ const fetchMachine = createMachine({ activities: ["trackBoundaryRect", "trackPointerMove", "trackDockRects"], exit: ["resetDragDiff"], on: { - DRAG: {}, + DRAG: { + actions: ["setPosition"] + }, DRAG_END: { target: "open", actions: ["invokeOnDragEnd"] @@ -63,7 +66,9 @@ const fetchMachine = createMachine({ activities: ["trackBoundaryRect", "trackPointerMove"], exit: ["resetResizeDiff"], on: { - RESIZE: {}, + RESIZE: { + actions: ["setSize"] + }, RESIZE_END: { target: "open", actions: ["invokeOnResizeEnd"] diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts index 4307f3bdcf..191bf52bc3 100644 --- a/packages/machines/floating-panel/src/floating-panel.connect.ts +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -34,7 +34,8 @@ export function connect(state: State, send: Send, normalize "--y": `${state.context.position.y}px`, "--width": `${state.context.size.width}px`, "--height": `${state.context.size.height}px`, - translate: `var(--x) var(--y)`, + top: "var(--y)", + left: "var(--x)", }, }), @@ -87,6 +88,7 @@ export function connect(state: State, send: Send, normalize "data-axis": props.axis, onPointerDown(event) { if (disabled) return + event.currentTarget.setPointerCapture(event.pointerId) event.preventDefault() send({ type: "RESIZE_START", @@ -107,6 +109,7 @@ export function connect(state: State, send: Send, normalize disabled: state.context.draggable || state.context.disabled, onPointerDown(event) { if (state.context.disabled) return + event.currentTarget.setPointerCapture(event.pointerId) event.preventDefault() send({ type: "DRAG_START", diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index 9909ef2660..e36d0d2340 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -12,8 +12,10 @@ export function machine(userContext: UserDefinedContext) { initial: ctx.open ? "open" : "closed", context: { size: { width: 300, height: 300 }, - position: { x: 0, y: 0 }, + position: { x: 300, y: 100 }, ...ctx, + lastPosition: null, + lastSize: null, dragDiff: { x: 0, y: 0 }, resizeDiff: { x: 0, y: 0 }, }, @@ -34,6 +36,7 @@ export function machine(userContext: UserDefinedContext) { on: { DRAG_START: { target: "open.dragging", + actions: ["setLastPosition"], }, RESIZE_START: { target: "open.resizing", @@ -49,7 +52,9 @@ export function machine(userContext: UserDefinedContext) { activities: ["trackBoundaryRect", "trackPointerMove", "trackDockRects"], exit: ["resetDragDiff"], on: { - DRAG: {}, + DRAG: { + actions: ["setPosition"], + }, DRAG_END: { target: "open", actions: ["invokeOnDragEnd"], @@ -65,7 +70,9 @@ export function machine(userContext: UserDefinedContext) { activities: ["trackBoundaryRect", "trackPointerMove"], exit: ["resetResizeDiff"], on: { - RESIZE: {}, + RESIZE: { + actions: ["setSize"], + }, RESIZE_END: { target: "open", actions: ["invokeOnResizeEnd"], @@ -85,7 +92,7 @@ export function machine(userContext: UserDefinedContext) { const doc = dom.getDoc(ctx) return trackPointerMove(doc, { onPointerMove(details) { - console.log(details) + send({ type: "DRAG", position: details.point }) }, onPointerUp() { send("DRAG_END") @@ -94,6 +101,29 @@ export function machine(userContext: UserDefinedContext) { }, }, actions: { + setLastPosition(ctx, evt) { + ctx.lastPosition = evt.position + }, + setPosition(ctx, evt) { + const diff = { + x: evt.position.x - ctx.lastPosition!.x, + y: evt.position.y - ctx.lastPosition!.y, + } + ctx.position = { + x: ctx.position.x + diff.x, + y: ctx.position.y + diff.y, + } + }, + setSize(ctx, evt) { + const diff = { + x: evt.position.x - ctx.lastPosition!.x, + y: evt.position.y - ctx.lastPosition!.y, + } + ctx.size = { + width: ctx.size.width + diff.x, + height: ctx.size.height + diff.y, + } + }, resetDragDiff(ctx) { ctx.dragDiff = { x: 0, y: 0 } }, diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index ba9eb2c76d..42ee9877f0 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -87,6 +87,8 @@ interface PublicContext extends DirectionProperty, CommonProperties { interface PrivateContext { dragDiff: Position resizeDiff: Position + lastPosition: Position | null + lastSize: Size | null } type ComputedContext = Readonly<{}> diff --git a/shared/src/style.css b/shared/src/style.css index 7bacf2dcad..80b0d32190 100644 --- a/shared/src/style.css +++ b/shared/src/style.css @@ -1988,6 +1988,7 @@ main [data-testid="scrubber"] { border: 1px solid #ebebeb; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); outline: 0 !important; + background-color: white; } [data-scope="floating-panel"][data-part="body"] { From 2b448737a75b307b903910a83d8be980696f5bbd Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Wed, 20 Mar 2024 18:55:18 +0000 Subject: [PATCH 08/19] chore: get drag and resize to work --- .xstate/floating-panel.js | 20 +- examples/next-ts/pages/floating-panel.tsx | 4 + .../src/floating-panel.connect.ts | 26 ++- .../src/floating-panel.machine.ts | 198 ++++++++++++++---- .../src/floating-panel.types.ts | 7 +- .../src/utils/get-resize-axis-style.ts | 15 +- .../machines/floating-panel/src/utils/math.ts | 7 + shared/src/routes.ts | 2 +- shared/src/style.css | 24 ++- 9 files changed, 229 insertions(+), 74 deletions(-) create mode 100644 packages/machines/floating-panel/src/utils/math.ts diff --git a/.xstate/floating-panel.js b/.xstate/floating-panel.js index 13eeaeae6d..8132f08170 100644 --- a/.xstate/floating-panel.js +++ b/.xstate/floating-panel.js @@ -24,20 +24,20 @@ const fetchMachine = createMachine({ on: { OPEN: { target: "open", - actions: ["invokeOnOpen"] + actions: ["invokeOnOpen", "setPositionStyle", "setSizeStyle"] } } }, open: { tags: ["open"], - activities: ["trackBoundaryRect"], on: { DRAG_START: { target: "open.dragging", - actions: ["setLastPosition"] + actions: ["setPrevPosition"] }, RESIZE_START: { - target: "open.resizing" + target: "open.resizing", + actions: ["setPrevSize"] }, CLOSE: { target: "closed" @@ -46,8 +46,8 @@ const fetchMachine = createMachine({ }, "open.dragging": { tags: ["open"], - activities: ["trackBoundaryRect", "trackPointerMove", "trackDockRects"], - exit: ["resetDragDiff"], + activities: ["trackPointerMove"], + exit: ["clearPrevPosition"], on: { DRAG: { actions: ["setPosition"] @@ -63,13 +63,13 @@ const fetchMachine = createMachine({ }, "open.resizing": { tags: ["open"], - activities: ["trackBoundaryRect", "trackPointerMove"], - exit: ["resetResizeDiff"], + activities: ["trackPointerMove"], + exit: ["clearLastSize"], on: { - RESIZE: { + DRAG: { actions: ["setSize"] }, - RESIZE_END: { + DRAG_END: { target: "open", actions: ["invokeOnResizeEnd"] }, diff --git a/examples/next-ts/pages/floating-panel.tsx b/examples/next-ts/pages/floating-panel.tsx index 7f55ee4848..f9d917217b 100644 --- a/examples/next-ts/pages/floating-panel.tsx +++ b/examples/next-ts/pages/floating-panel.tsx @@ -27,6 +27,10 @@ export default function Page() {
+
+
+
+

Floating Panel

diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts index 191bf52bc3..cbf1ef6b61 100644 --- a/packages/machines/floating-panel/src/floating-panel.connect.ts +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -1,5 +1,5 @@ -import { getEventKey, type EventKeyMap } from "@zag-js/dom-event" -import { dataAttr } from "@zag-js/dom-query" +import { getEventKey, getNativeEvent, type EventKeyMap } from "@zag-js/dom-event" +import { dataAttr, getEventTarget } from "@zag-js/dom-query" import type { NormalizeProps, PropTypes } from "@zag-js/types" import { parts } from "./floating-panel.anatomy" import { dom } from "./floating-panel.dom" @@ -30,10 +30,6 @@ export function connect(state: State, send: Send, normalize id: dom.getPositionerId(state.context), style: { position: "absolute", - "--x": `${state.context.position.x}px`, - "--y": `${state.context.position.y}px`, - "--width": `${state.context.size.width}px`, - "--height": `${state.context.size.height}px`, top: "var(--y)", left: "var(--x)", }, @@ -75,6 +71,7 @@ export function connect(state: State, send: Send, normalize disabled: state.context.disabled, type: "button", onClick() { + debugger send("CLOSE") }, }), @@ -87,9 +84,12 @@ export function connect(state: State, send: Send, normalize "data-disabled": dataAttr(disabled), "data-axis": props.axis, onPointerDown(event) { - if (disabled) return + if (disabled || event.button == 2) return event.currentTarget.setPointerCapture(event.pointerId) + event.preventDefault() + event.stopPropagation() + send({ type: "RESIZE_START", axis: props.axis, @@ -108,11 +108,21 @@ export function connect(state: State, send: Send, normalize ...parts.dragTrigger.attrs, disabled: state.context.draggable || state.context.disabled, onPointerDown(event) { - if (state.context.disabled) return + if (state.context.draggable || state.context.disabled || event.button == 2) return event.currentTarget.setPointerCapture(event.pointerId) + + const target = getEventTarget(getNativeEvent(event)) + + if (target?.closest("[data-part=close-trigger]")) { + return + } + event.preventDefault() + event.stopPropagation() + send({ type: "DRAG_START", + pointerId: event.pointerId, position: { x: event.clientX, y: event.clientY }, }) }, diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index e36d0d2340..81b1be7ce2 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -2,7 +2,15 @@ import { createMachine } from "@zag-js/core" import { trackPointerMove } from "@zag-js/dom-event" import { compact } from "@zag-js/utils" import { dom } from "./floating-panel.dom" -import type { MachineContext, MachineState, UserDefinedContext } from "./floating-panel.types" +import type { + MachineContext, + MachineState, + Position, + ResizeTriggerAxis, + Size, + UserDefinedContext, +} from "./floating-panel.types" +import { addPosition, subtractPosition } from "./utils/math" export function machine(userContext: UserDefinedContext) { const ctx = compact(userContext) @@ -14,10 +22,13 @@ export function machine(userContext: UserDefinedContext) { size: { width: 300, height: 300 }, position: { x: 300, y: 100 }, ...ctx, - lastPosition: null, - lastSize: null, - dragDiff: { x: 0, y: 0 }, - resizeDiff: { x: 0, y: 0 }, + lastEventPosition: null, + prevPosition: null, + prevSize: null, + }, + watch: { + position: ["setPositionStyle"], + size: ["setSizeStyle"], }, states: { closed: { @@ -25,21 +36,21 @@ export function machine(userContext: UserDefinedContext) { on: { OPEN: { target: "open", - actions: ["invokeOnOpen"], + actions: ["invokeOnOpen", "setPositionStyle", "setSizeStyle"], }, }, }, open: { tags: ["open"], - activities: ["trackBoundaryRect"], on: { DRAG_START: { target: "open.dragging", - actions: ["setLastPosition"], + actions: ["setPrevPosition"], }, RESIZE_START: { target: "open.resizing", + actions: ["setPrevSize"], }, CLOSE: { target: "closed", @@ -49,8 +60,8 @@ export function machine(userContext: UserDefinedContext) { "open.dragging": { tags: ["open"], - activities: ["trackBoundaryRect", "trackPointerMove", "trackDockRects"], - exit: ["resetDragDiff"], + activities: ["trackPointerMove"], + exit: ["clearPrevPosition"], on: { DRAG: { actions: ["setPosition"], @@ -67,13 +78,13 @@ export function machine(userContext: UserDefinedContext) { "open.resizing": { tags: ["open"], - activities: ["trackBoundaryRect", "trackPointerMove"], - exit: ["resetResizeDiff"], + activities: ["trackPointerMove"], + exit: ["clearLastSize"], on: { - RESIZE: { + DRAG: { actions: ["setSize"], }, - RESIZE_END: { + DRAG_END: { target: "open", actions: ["invokeOnResizeEnd"], }, @@ -91,8 +102,9 @@ export function machine(userContext: UserDefinedContext) { trackPointerMove(ctx, _evt, { send }) { const doc = dom.getDoc(ctx) return trackPointerMove(doc, { - onPointerMove(details) { - send({ type: "DRAG", position: details.point }) + onPointerMove({ point, event }) { + const altKey = event.altKey + send({ type: "DRAG", position: point, axis: _evt.axis, altKey }) }, onPointerUp() { send("DRAG_END") @@ -101,34 +113,146 @@ export function machine(userContext: UserDefinedContext) { }, }, actions: { - setLastPosition(ctx, evt) { - ctx.lastPosition = evt.position + setPrevPosition(ctx, evt) { + ctx.prevPosition = { ...ctx.position } + ctx.lastEventPosition = evt.position + }, + clearPrevPosition(ctx) { + ctx.prevPosition = null + ctx.lastEventPosition = null }, setPosition(ctx, evt) { - const diff = { - x: evt.position.x - ctx.lastPosition!.x, - y: evt.position.y - ctx.lastPosition!.y, - } - ctx.position = { - x: ctx.position.x + diff.x, - y: ctx.position.y + diff.y, - } + const diff = subtractPosition(evt.position, ctx.lastEventPosition!) + ctx.position = addPosition(ctx.prevPosition!, diff) + }, + setPositionStyle(ctx) { + const el = dom.getPositionerEl(ctx) + el?.style.setProperty("--x", `${ctx.position.x}px`) + el?.style.setProperty("--y", `${ctx.position.y}px`) + }, + setPrevSize(ctx, evt) { + ctx.prevSize = { ...ctx.size } + ctx.prevPosition = { ...ctx.position } + ctx.lastEventPosition = evt.position + }, + clearPrevSize(ctx) { + ctx.prevSize = null + ctx.prevPosition = null + ctx.lastEventPosition = null }, setSize(ctx, evt) { - const diff = { - x: evt.position.x - ctx.lastPosition!.x, - y: evt.position.y - ctx.lastPosition!.y, + if (!ctx.prevSize || !ctx.prevPosition || !ctx.lastEventPosition) return + + const diff = subtractPosition(evt.position, ctx.lastEventPosition) + const factor = evt.altKey ? 2 : 1 + + let nextSize: Size | undefined + let nextPosition: Position | undefined + + switch (evt.axis as ResizeTriggerAxis) { + case "n": { + nextSize = { + width: ctx.prevSize!.width, + height: ctx.prevSize.height - diff.y * factor, + } + nextPosition = { + y: ctx.prevPosition.y + diff.y, + x: ctx.prevPosition.x, + } + break + } + case "e": { + nextSize = { + width: ctx.prevSize.width + diff.x * factor, + height: ctx.prevSize.height, + } + nextPosition = { + y: ctx.prevPosition.y, + x: evt.altKey ? ctx.prevPosition.x - diff.x : ctx.prevPosition.x, + } + break + } + case "w": { + nextSize = { + width: ctx.prevSize.width - diff.x * factor, + height: ctx.prevSize.height, + } + nextPosition = { + y: ctx.prevPosition.y, + x: evt.altKey ? ctx.prevPosition.x + diff.x : ctx.prevPosition.x, + } + break + } + case "s": { + nextSize = { + width: ctx.prevSize.width, + height: ctx.prevSize.height + diff.y * factor, + } + nextPosition = { + y: evt.altKey ? ctx.prevPosition.y - diff.y : ctx.prevPosition.y, + x: ctx.prevPosition.x, + } + break + } + case "ne": { + nextSize = { + width: ctx.prevSize.width + diff.x * factor, + height: ctx.prevSize.height - diff.y * factor, + } + nextPosition = { + y: ctx.prevPosition.y + diff.y, + x: evt.altKey ? ctx.prevPosition.x - diff.x : ctx.prevPosition.x, + } + break + } + case "se": { + nextSize = { + width: ctx.prevSize.width + diff.x * factor, + height: ctx.prevSize.height + diff.y * factor, + } + nextPosition = { + x: evt.altKey ? ctx.prevPosition.x - diff.x : ctx.prevPosition.x, + y: evt.altKey ? ctx.prevPosition.y - diff.y : ctx.prevPosition.y, + } + break + } + case "sw": { + nextSize = { + width: ctx.prevSize.width - diff.x, + height: ctx.prevSize.height + diff.y, + } + nextPosition = { + y: ctx.prevPosition.y, + x: ctx.prevPosition.x + diff.x, + } + break + } + case "nw": { + nextSize = { + width: ctx.prevSize.width - diff.x, + height: ctx.prevSize.height - diff.y, + } + nextPosition = { + y: ctx.prevPosition.y + diff.y, + x: ctx.prevPosition.x + diff.x, + } + break + } + default: { + throw new Error(`Invalid axis: ${evt.axis}`) + } } - ctx.size = { - width: ctx.size.width + diff.x, - height: ctx.size.height + diff.y, + + ctx.size = nextSize + + if (nextPosition) { + ctx.position = nextPosition } }, - resetDragDiff(ctx) { - ctx.dragDiff = { x: 0, y: 0 } - }, - resetResizeDiff(ctx) { - ctx.resizeDiff = { x: 0, y: 0 } + setSizeStyle(ctx) { + const el = dom.getPositionerEl(ctx) + el?.style.setProperty("--width", `${ctx.size.width}px`) + el?.style.setProperty("--height", `${ctx.size.height}px`) }, invokeOnOpen(ctx) { ctx.onOpenChange?.({ open: true }) diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index 42ee9877f0..4ab7c436b5 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -85,10 +85,9 @@ interface PublicContext extends DirectionProperty, CommonProperties { } interface PrivateContext { - dragDiff: Position - resizeDiff: Position - lastPosition: Position | null - lastSize: Size | null + lastEventPosition: Position | null + prevPosition: Position | null + prevSize: Size | null } type ComputedContext = Readonly<{}> diff --git a/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts b/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts index bab3f0ef1f..51227297a4 100644 --- a/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts +++ b/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts @@ -6,29 +6,36 @@ export function getResizeAxisStyle(axis: ResizeTriggerAxis): JSX.CSSProperties { case "n": return { cursor: "n-resize", - left: 0, width: "100%", + left: "50%", + translate: "-50%", } case "e": return { cursor: "e-resize", - right: 0, height: "100%", + right: 0, + top: "50%", + translate: "0 -50%", } case "s": return { cursor: "s-resize", - bottom: 0, width: "100%", + bottom: 0, + left: "50%", + translate: "-50%", } case "w": return { cursor: "w-resize", - left: 0, height: "100%", + left: 0, + top: "50%", + translate: "0 -50%", } case "se": diff --git a/packages/machines/floating-panel/src/utils/math.ts b/packages/machines/floating-panel/src/utils/math.ts new file mode 100644 index 0000000000..cfc30f9861 --- /dev/null +++ b/packages/machines/floating-panel/src/utils/math.ts @@ -0,0 +1,7 @@ +import type { Position, Size } from "../floating-panel.types" + +export const addPosition = (a: Position, b: Position) => ({ x: a.x + b.x, y: a.y + b.y }) +export const subtractPosition = (a: Position, b: Position) => ({ x: a.x - b.x, y: a.y - b.y }) + +export const addSize = (a: Size, b: Size) => ({ width: a.width + b.width, height: a.height + b.height }) +export const subtractSize = (a: Size, b: Size) => ({ width: a.width - b.width, height: a.height - b.height }) diff --git a/shared/src/routes.ts b/shared/src/routes.ts index 20bc81f730..3c8e722afa 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -4,7 +4,7 @@ type RouteData = { } export const routesData: RouteData[] = [ - { label: "Floating-panel", path: "/floating-panel" }, + { label: "Floating Panel", path: "/floating-panel" }, { label: "Tour", path: "/tour" }, { label: "Collapsible", path: "/collapsible" }, { label: "Clipboard", path: "/clipboard" }, diff --git a/shared/src/style.css b/shared/src/style.css index 80b0d32190..cfb1d92665 100644 --- a/shared/src/style.css +++ b/shared/src/style.css @@ -2014,22 +2014,26 @@ main [data-testid="scrubber"] { } [data-scope="floating-panel"][data-part="resize-trigger"] { - [data-axis="n"], - [data-axis="s"] { + background-color: rgba(154, 18, 18, 0.396); + + &[data-axis="n"], + &[data-axis="s"] { height: 6px; + max-width: 90%; } - [data-axis="e"], - [data-axis="w"] { + &[data-axis="e"], + &[data-axis="w"] { width: 6px; + max-height: 90%; } - [data-axis="ne"], - [data-axis="nw"], - [data-axis="se"], - [data-axis="sw"] { - width: 6px; - height: 6px; + &[data-axis="ne"], + &[data-axis="nw"], + &[data-axis="se"], + &[data-axis="sw"] { + width: 10px; + height: 10px; } } From 2fd9aaecbd1d52d0db1dcaf2887d4713495bc033 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Wed, 20 Mar 2024 21:24:36 +0000 Subject: [PATCH 09/19] feat: support min and max size --- .xstate/floating-panel.js | 19 +- packages/machines/floating-panel/package.json | 4 + .../src/floating-panel.connect.ts | 40 ++- .../src/floating-panel.machine.ts | 166 +++------ .../src/floating-panel.props.ts | 2 + .../src/floating-panel.types.ts | 11 +- .../floating-panel/src/utils/get-diff-rect.ts | 122 +++++++ .../machines/floating-panel/src/utils/math.ts | 28 +- .../utilities/dismissable/src/layer-stack.ts | 4 +- pnpm-lock.yaml | 336 +++--------------- shared/src/controls.ts | 4 +- 11 files changed, 316 insertions(+), 420 deletions(-) create mode 100644 packages/machines/floating-panel/src/utils/get-diff-rect.ts diff --git a/.xstate/floating-panel.js b/.xstate/floating-panel.js index 8132f08170..8ad5c71758 100644 --- a/.xstate/floating-panel.js +++ b/.xstate/floating-panel.js @@ -30,6 +30,7 @@ const fetchMachine = createMachine({ }, open: { tags: ["open"], + entry: ["setBoundaryRect"], on: { DRAG_START: { target: "open.dragging", @@ -40,7 +41,12 @@ const fetchMachine = createMachine({ actions: ["setPrevSize"] }, CLOSE: { - target: "closed" + target: "closed", + actions: ["invokeOnClose", "resetPosition", "resetSize"] + }, + ESCAPE: { + target: "closed", + actions: ["invokeOnClose", "resetPosition", "resetSize"] } } }, @@ -57,7 +63,11 @@ const fetchMachine = createMachine({ actions: ["invokeOnDragEnd"] }, CLOSE: { - target: "closed" + target: "closed", + actions: ["invokeOnClose", "resetPosition", "resetSize"] + }, + ESCAPE: { + target: "open" } } }, @@ -75,7 +85,10 @@ const fetchMachine = createMachine({ }, CLOSE: { target: "closed", - actions: ["invokeOnClose"] + actions: ["invokeOnClose", "resetPosition", "resetSize"] + }, + ESCAPE: { + target: "open" } } } diff --git a/packages/machines/floating-panel/package.json b/packages/machines/floating-panel/package.json index 780ccf9a44..daa7f64ba2 100644 --- a/packages/machines/floating-panel/package.json +++ b/packages/machines/floating-panel/package.json @@ -36,10 +36,14 @@ }, "dependencies": { "@zag-js/anatomy": "workspace:*", + "@zag-js/dismissable": "workspace:*", "@zag-js/core": "workspace:*", "@zag-js/dom-query": "workspace:*", "@zag-js/dom-event": "workspace:*", "@zag-js/utils": "workspace:*", + "@zag-js/numeric-range": "workspace:*", + "@zag-js/rect-utils": "workspace:*", + "@zag-js/popper": "workspace:*", "@zag-js/types": "workspace:*" }, "devDependencies": { diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts index cbf1ef6b61..50c4917b32 100644 --- a/packages/machines/floating-panel/src/floating-panel.connect.ts +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -20,6 +20,8 @@ export function connect(state: State, send: Send, normalize ...parts.trigger.attrs, type: "button", id: dom.getTriggerId(state.context), + "data-state": isOpen ? "open" : "closed", + "aria-controls": dom.getContentId(state.context), onClick() { send({ type: "OPEN" }) }, @@ -51,6 +53,9 @@ export function connect(state: State, send: Send, normalize }, onKeyDown(event) { const keyMap: EventKeyMap = { + Escape() { + send("ESCAPE") + }, ArrowLeft() {}, ArrowRight() {}, ArrowUp() {}, @@ -69,15 +74,35 @@ export function connect(state: State, send: Send, normalize closeTriggerProps: normalize.button({ ...parts.closeTrigger.attrs, disabled: state.context.disabled, + "aria-label": "Close Window", type: "button", onClick() { - debugger send("CLOSE") }, }), + minimizeTriggerProps: normalize.button({ + ...parts.minimizeTrigger.attrs, + disabled: state.context.disabled, + "aria-label": "Minimize Window", + type: "button", + onClick() { + send("MINIMIZE") + }, + }), + + maximizeTriggerProps: normalize.button({ + ...parts.maximizeTrigger.attrs, + disabled: state.context.disabled, + "aria-label": "Maximize Window", + type: "button", + onClick() { + send("MAXIMIZE") + }, + }), + getResizeTriggerProps(props: ResizeTriggerProps) { - const disabled = state.context.resizable || state.context.disabled + const disabled = !state.context.resizable || state.context.disabled return normalize.element({ ...parts.resizeTrigger.attrs, disabled, @@ -87,7 +112,6 @@ export function connect(state: State, send: Send, normalize if (disabled || event.button == 2) return event.currentTarget.setPointerCapture(event.pointerId) - event.preventDefault() event.stopPropagation() send({ @@ -108,8 +132,7 @@ export function connect(state: State, send: Send, normalize ...parts.dragTrigger.attrs, disabled: state.context.draggable || state.context.disabled, onPointerDown(event) { - if (state.context.draggable || state.context.disabled || event.button == 2) return - event.currentTarget.setPointerCapture(event.pointerId) + if (!state.context.draggable || state.context.disabled || event.button == 2) return const target = getEventTarget(getNativeEvent(event)) @@ -117,7 +140,7 @@ export function connect(state: State, send: Send, normalize return } - event.preventDefault() + event.currentTarget.setPointerCapture(event.pointerId) event.stopPropagation() send({ @@ -126,8 +149,13 @@ export function connect(state: State, send: Send, normalize position: { x: event.clientX, y: event.clientY }, }) }, + onDoubleClick() { + send("MAXIMIZE") + }, style: { userSelect: "none", + touchAction: "none", + cursor: "move", }, }), diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index 81b1be7ce2..4b4d969330 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -1,16 +1,12 @@ import { createMachine } from "@zag-js/core" import { trackPointerMove } from "@zag-js/dom-event" +import { isHTMLElement } from "@zag-js/dom-query" +import { getElementRect, getWindowRect } from "@zag-js/rect-utils" import { compact } from "@zag-js/utils" import { dom } from "./floating-panel.dom" -import type { - MachineContext, - MachineState, - Position, - ResizeTriggerAxis, - Size, - UserDefinedContext, -} from "./floating-panel.types" -import { addPosition, subtractPosition } from "./utils/math" +import type { MachineContext, MachineState, UserDefinedContext } from "./floating-panel.types" +import { getDiffRect } from "./utils/get-diff-rect" +import { addPosition, clampPosition, clampSize, subtractPosition } from "./utils/math" export function machine(userContext: UserDefinedContext) { const ctx = compact(userContext) @@ -19,17 +15,20 @@ export function machine(userContext: UserDefinedContext) { id: "floating-panel", initial: ctx.open ? "open" : "closed", context: { - size: { width: 300, height: 300 }, + size: { width: 320, height: 400 }, position: { x: 300, y: 100 }, ...ctx, lastEventPosition: null, prevPosition: null, prevSize: null, + boundaryRect: null, }, + watch: { position: ["setPositionStyle"], size: ["setSizeStyle"], }, + states: { closed: { tags: ["closed"], @@ -43,6 +42,7 @@ export function machine(userContext: UserDefinedContext) { open: { tags: ["open"], + entry: ["setBoundaryRect"], on: { DRAG_START: { target: "open.dragging", @@ -54,6 +54,11 @@ export function machine(userContext: UserDefinedContext) { }, CLOSE: { target: "closed", + actions: ["invokeOnClose", "resetPosition", "resetSize"], + }, + ESCAPE: { + target: "closed", + actions: ["invokeOnClose", "resetPosition", "resetSize"], }, }, }, @@ -72,6 +77,10 @@ export function machine(userContext: UserDefinedContext) { }, CLOSE: { target: "closed", + actions: ["invokeOnClose", "resetPosition", "resetSize"], + }, + ESCAPE: { + target: "open", }, }, }, @@ -90,7 +99,10 @@ export function machine(userContext: UserDefinedContext) { }, CLOSE: { target: "closed", - actions: ["invokeOnClose"], + actions: ["invokeOnClose", "resetPosition", "resetSize"], + }, + ESCAPE: { + target: "open", }, }, }, @@ -103,8 +115,8 @@ export function machine(userContext: UserDefinedContext) { const doc = dom.getDoc(ctx) return trackPointerMove(doc, { onPointerMove({ point, event }) { - const altKey = event.altKey - send({ type: "DRAG", position: point, axis: _evt.axis, altKey }) + const { altKey, shiftKey } = event + send({ type: "DRAG", position: point, axis: _evt.axis, altKey, shiftKey }) }, onPointerUp() { send("DRAG_END") @@ -113,6 +125,14 @@ export function machine(userContext: UserDefinedContext) { }, }, actions: { + setBoundaryRect(ctx) { + const el = ctx.getBoundaryEl?.() + const rect = (() => { + if (isHTMLElement(el)) return getElementRect(el) + return getWindowRect(dom.getWin(ctx)) + })() + ctx.boundaryRect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height } + }, setPrevPosition(ctx, evt) { ctx.prevPosition = { ...ctx.position } ctx.lastEventPosition = evt.position @@ -123,13 +143,21 @@ export function machine(userContext: UserDefinedContext) { }, setPosition(ctx, evt) { const diff = subtractPosition(evt.position, ctx.lastEventPosition!) - ctx.position = addPosition(ctx.prevPosition!, diff) + let position = addPosition(ctx.prevPosition!, diff) + position = clampPosition(position, ctx.size, ctx.boundaryRect!) + ctx.position = position }, setPositionStyle(ctx) { const el = dom.getPositionerEl(ctx) el?.style.setProperty("--x", `${ctx.position.x}px`) el?.style.setProperty("--y", `${ctx.position.y}px`) }, + resetPosition(ctx, _evt, { initialContext }) { + ctx.position = initialContext.position + }, + resetSize(ctx, _evt, { initialContext }) { + ctx.size = initialContext.size + }, setPrevSize(ctx, evt) { ctx.prevSize = { ...ctx.size } ctx.prevPosition = { ...ctx.position } @@ -144,109 +172,21 @@ export function machine(userContext: UserDefinedContext) { if (!ctx.prevSize || !ctx.prevPosition || !ctx.lastEventPosition) return const diff = subtractPosition(evt.position, ctx.lastEventPosition) - const factor = evt.altKey ? 2 : 1 - let nextSize: Size | undefined - let nextPosition: Position | undefined - - switch (evt.axis as ResizeTriggerAxis) { - case "n": { - nextSize = { - width: ctx.prevSize!.width, - height: ctx.prevSize.height - diff.y * factor, - } - nextPosition = { - y: ctx.prevPosition.y + diff.y, - x: ctx.prevPosition.x, - } - break - } - case "e": { - nextSize = { - width: ctx.prevSize.width + diff.x * factor, - height: ctx.prevSize.height, - } - nextPosition = { - y: ctx.prevPosition.y, - x: evt.altKey ? ctx.prevPosition.x - diff.x : ctx.prevPosition.x, - } - break - } - case "w": { - nextSize = { - width: ctx.prevSize.width - diff.x * factor, - height: ctx.prevSize.height, - } - nextPosition = { - y: ctx.prevPosition.y, - x: evt.altKey ? ctx.prevPosition.x + diff.x : ctx.prevPosition.x, - } - break - } - case "s": { - nextSize = { - width: ctx.prevSize.width, - height: ctx.prevSize.height + diff.y * factor, - } - nextPosition = { - y: evt.altKey ? ctx.prevPosition.y - diff.y : ctx.prevPosition.y, - x: ctx.prevPosition.x, - } - break - } - case "ne": { - nextSize = { - width: ctx.prevSize.width + diff.x * factor, - height: ctx.prevSize.height - diff.y * factor, - } - nextPosition = { - y: ctx.prevPosition.y + diff.y, - x: evt.altKey ? ctx.prevPosition.x - diff.x : ctx.prevPosition.x, - } - break - } - case "se": { - nextSize = { - width: ctx.prevSize.width + diff.x * factor, - height: ctx.prevSize.height + diff.y * factor, - } - nextPosition = { - x: evt.altKey ? ctx.prevPosition.x - diff.x : ctx.prevPosition.x, - y: evt.altKey ? ctx.prevPosition.y - diff.y : ctx.prevPosition.y, - } - break - } - case "sw": { - nextSize = { - width: ctx.prevSize.width - diff.x, - height: ctx.prevSize.height + diff.y, - } - nextPosition = { - y: ctx.prevPosition.y, - x: ctx.prevPosition.x + diff.x, - } - break - } - case "nw": { - nextSize = { - width: ctx.prevSize.width - diff.x, - height: ctx.prevSize.height - diff.y, - } - nextPosition = { - y: ctx.prevPosition.y + diff.y, - x: ctx.prevPosition.x + diff.x, - } - break - } - default: { - throw new Error(`Invalid axis: ${evt.axis}`) - } - } + const { nextSize, nextPosition } = getDiffRect({ + diff, + axis: evt.axis, + prevPosition: ctx.prevPosition, + prevSize: ctx.prevSize, + altKey: evt.altKey, + }) - ctx.size = nextSize + const size = clampSize(nextSize, ctx.minSize, ctx.maxSize) + ctx.size = size if (nextPosition) { - ctx.position = nextPosition + const position = clampPosition(nextPosition, size, ctx.boundaryRect!) + ctx.position = position } }, setSizeStyle(ctx) { diff --git a/packages/machines/floating-panel/src/floating-panel.props.ts b/packages/machines/floating-panel/src/floating-panel.props.ts index 22916371b5..94bdb3506a 100644 --- a/packages/machines/floating-panel/src/floating-panel.props.ts +++ b/packages/machines/floating-panel/src/floating-panel.props.ts @@ -20,6 +20,8 @@ export const props = createProps()([ "position", "resizable", "size", + "minSize", + "maxSize", ]) export const splitProps = createSplitProps>(props) diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index 4ab7c436b5..d6fab02418 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -42,6 +42,14 @@ interface PublicContext extends DirectionProperty, CommonProperties { * The size of the panel */ size: Size + /** + * The minimum size of the panel + */ + minSize?: Size + /** + * The maximum size of the panel + */ + maxSize?: Size /** * The position of the panel */ @@ -57,7 +65,7 @@ interface PublicContext extends DirectionProperty, CommonProperties { /** * The boundary of the panel. Defaults to the window */ - getBoundaryEl?(): { getBoundingClientRect(): DOMRect } + getBoundaryEl?(): HTMLElement /** * Whether the panel is disabled */ @@ -85,6 +93,7 @@ interface PublicContext extends DirectionProperty, CommonProperties { } interface PrivateContext { + boundaryRect: Rect | null lastEventPosition: Position | null prevPosition: Position | null prevSize: Size | null diff --git a/packages/machines/floating-panel/src/utils/get-diff-rect.ts b/packages/machines/floating-panel/src/utils/get-diff-rect.ts new file mode 100644 index 0000000000..166aac96ae --- /dev/null +++ b/packages/machines/floating-panel/src/utils/get-diff-rect.ts @@ -0,0 +1,122 @@ +import type { Position, ResizeTriggerAxis, Size } from "../floating-panel.types" + +export interface Options { + diff: Position + axis: ResizeTriggerAxis + prevPosition: Position + prevSize: Size + altKey: boolean +} + +export function getDiffRect(opts: Options) { + const { diff, axis, prevPosition, prevSize, altKey } = opts + + const factor = altKey ? 2 : 1 + + let nextSize: Size | undefined + let nextPosition: Position | undefined + + switch (axis as ResizeTriggerAxis) { + case "n": { + nextSize = { + width: prevSize!.width, + height: prevSize.height - diff.y * factor, + } + nextPosition = { + y: prevPosition.y + diff.y, + x: prevPosition.x, + } + break + } + + case "e": { + nextSize = { + width: prevSize.width + diff.x * factor, + height: prevSize.height, + } + nextPosition = { + y: prevPosition.y, + x: altKey ? prevPosition.x - diff.x : prevPosition.x, + } + break + } + + case "w": { + nextSize = { + width: prevSize.width - diff.x * factor, + height: prevSize.height, + } + nextPosition = { + y: prevPosition.y, + x: altKey ? prevPosition.x + diff.x : prevPosition.x, + } + break + } + + case "s": { + nextSize = { + width: prevSize.width, + height: prevSize.height + diff.y * factor, + } + nextPosition = { + y: altKey ? prevPosition.y - diff.y : prevPosition.y, + x: prevPosition.x, + } + break + } + + case "ne": { + nextSize = { + width: prevSize.width + diff.x * factor, + height: prevSize.height - diff.y * factor, + } + nextPosition = { + y: prevPosition.y + diff.y, + x: altKey ? prevPosition.x - diff.x : prevPosition.x, + } + break + } + + case "se": { + nextSize = { + width: prevSize.width + diff.x * factor, + height: prevSize.height + diff.y * factor, + } + nextPosition = { + x: altKey ? prevPosition.x - diff.x : prevPosition.x, + y: altKey ? prevPosition.y - diff.y : prevPosition.y, + } + break + } + + case "sw": { + nextSize = { + width: prevSize.width - diff.x * factor, + height: prevSize.height + diff.y * factor, + } + nextPosition = { + y: altKey ? prevPosition.y - diff.y : prevPosition.y, + x: prevPosition.x + diff.x, + } + break + } + + case "nw": { + nextSize = { + width: prevSize.width - diff.x * factor, + height: prevSize.height - diff.y * factor, + } + nextPosition = { + y: prevPosition.y + diff.y, + x: prevPosition.x + diff.x, + } + break + } + + default: { + throw new Error(`Invalid axis: ${axis}`) + } + } + + return { nextSize, nextPosition } +} diff --git a/packages/machines/floating-panel/src/utils/math.ts b/packages/machines/floating-panel/src/utils/math.ts index cfc30f9861..f0a8bf51b0 100644 --- a/packages/machines/floating-panel/src/utils/math.ts +++ b/packages/machines/floating-panel/src/utils/math.ts @@ -1,7 +1,33 @@ -import type { Position, Size } from "../floating-panel.types" +import { clampValue } from "@zag-js/numeric-range" +import type { Position, Rect, Size } from "../floating-panel.types" export const addPosition = (a: Position, b: Position) => ({ x: a.x + b.x, y: a.y + b.y }) export const subtractPosition = (a: Position, b: Position) => ({ x: a.x - b.x, y: a.y - b.y }) export const addSize = (a: Size, b: Size) => ({ width: a.width + b.width, height: a.height + b.height }) export const subtractSize = (a: Size, b: Size) => ({ width: a.width - b.width, height: a.height - b.height }) + +export const clampSize = (size: Size, minSize?: Size, maxSize?: Size) => { + if (minSize == null && maxSize == null) { + return size + } + + if (minSize == null && maxSize != null) { + return { width: Math.min(size.width, maxSize.width), height: Math.min(size.height, maxSize.height) } + } + + if (minSize != null && maxSize == null) { + return { width: Math.max(size.width, minSize.width), height: Math.max(size.height, minSize.height) } + } + + return { + width: clampValue(size.width, minSize!.width, maxSize!.width), + height: clampValue(size.height, minSize!.height, maxSize!.height), + } +} + +export const clampPosition = (position: Position, size: Size, boundaryRect: Rect) => { + const x = clampValue(position.x, boundaryRect.x, boundaryRect.width - size.width) + const y = clampValue(position.y, boundaryRect.y, boundaryRect.height - size.height) + return { x, y } +} diff --git a/packages/utilities/dismissable/src/layer-stack.ts b/packages/utilities/dismissable/src/layer-stack.ts index 967d6c4f7e..c6a9492277 100644 --- a/packages/utilities/dismissable/src/layer-stack.ts +++ b/packages/utilities/dismissable/src/layer-stack.ts @@ -42,7 +42,8 @@ export const layerStack = { return Array.from(this.branches).some((branch) => contains(branch, target)) }, add(layer: Layer) { - this.layers.push(layer) + const num = this.layers.push(layer) + layer.node.style.setProperty("--stack-index", `${num}`) }, addBranch(node: HTMLElement) { this.branches.push(node) @@ -58,6 +59,7 @@ export const layerStack = { } // remove this layer this.layers.splice(index, 1) + node.style.removeProperty("--stack-index") }, removeBranch(node: HTMLElement) { const index = this.branches.indexOf(node) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ee448a53f..67c4365c34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2191,12 +2191,24 @@ importers: '@zag-js/core': specifier: workspace:* version: link:../../core + '@zag-js/dismissable': + specifier: workspace:* + version: link:../../utilities/dismissable '@zag-js/dom-event': specifier: workspace:* version: link:../../utilities/dom-event '@zag-js/dom-query': specifier: workspace:* version: link:../../utilities/dom-query + '@zag-js/numeric-range': + specifier: workspace:* + version: link:../../utilities/numeric-range + '@zag-js/popper': + specifier: workspace:* + version: link:../../utilities/popper + '@zag-js/rect-utils': + specifier: workspace:* + version: link:../../utilities/rect '@zag-js/types': specifier: workspace:* version: link:../../types @@ -3496,7 +3508,7 @@ importers: version: link:../packages/machines/tooltip contentlayer: specifier: 0.3.4 - version: 0.3.4(esbuild@0.20.1) + version: 0.3.4(esbuild@0.20.2) hastscript: specifier: 9.0.0 version: 9.0.0 @@ -3511,7 +3523,7 @@ importers: version: 14.1.3(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) next-contentlayer: specifier: 0.3.4 - version: 0.3.4(contentlayer@0.3.4)(esbuild@0.20.1)(next@14.1.3)(react-dom@18.2.0)(react@18.2.0) + version: 0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@14.1.3)(react-dom@18.2.0)(react@18.2.0) next-seo: specifier: 6.5.0 version: 6.5.0(next@14.1.3)(react-dom@18.2.0)(react@18.2.0) @@ -4573,10 +4585,10 @@ packages: chalk: 5.3.0 dev: false - /@contentlayer/cli@0.3.4(esbuild@0.20.1): + /@contentlayer/cli@0.3.4(esbuild@0.20.2): resolution: {integrity: sha512-vNDwgLuhYNu+m70NZ3XK9kexKNguuxPXg7Yvzj3B34cEilQjjzSrcTY/i+AIQm9V7uT5GGshx9ukzPf+SmoszQ==} dependencies: - '@contentlayer/core': 0.3.4(esbuild@0.20.1) + '@contentlayer/core': 0.3.4(esbuild@0.20.2) '@contentlayer/utils': 0.3.4 clipanion: 3.2.1(typanion@3.14.0) typanion: 3.14.0 @@ -4587,10 +4599,10 @@ packages: - supports-color dev: false - /@contentlayer/client@0.3.4(esbuild@0.20.1): + /@contentlayer/client@0.3.4(esbuild@0.20.2): resolution: {integrity: sha512-QSlLyc3y4PtdC5lFw0L4wTZUH8BQnv2nk37hNCsPAqGf+dRO7TLAzdc+2/mVIRgK+vSH+pSOzjLsQpFxxXRTZA==} dependencies: - '@contentlayer/core': 0.3.4(esbuild@0.20.1) + '@contentlayer/core': 0.3.4(esbuild@0.20.2) transitivePeerDependencies: - '@effect-ts/otel-node' - esbuild @@ -4598,7 +4610,7 @@ packages: - supports-color dev: false - /@contentlayer/core@0.3.4(esbuild@0.20.1): + /@contentlayer/core@0.3.4(esbuild@0.20.2): resolution: {integrity: sha512-o68oBLwfYZ+2vtgfk1lgHxOl3LoxvRNiUfeQ8IWFWy/L4wnIkKIqLZX01zlRE5IzYM+ZMMN5V0cKQlO7DsyR9g==} peerDependencies: esbuild: 0.17.x || 0.18.x @@ -4612,9 +4624,9 @@ packages: '@contentlayer/utils': 0.3.4 camel-case: 4.1.2 comment-json: 4.2.3 - esbuild: 0.20.1 + esbuild: 0.20.2 gray-matter: 4.0.3 - mdx-bundler: 9.2.1(esbuild@0.20.1) + mdx-bundler: 9.2.1(esbuild@0.20.2) rehype-stringify: 9.0.4 remark-frontmatter: 4.0.1 remark-parse: 10.0.2 @@ -4627,10 +4639,10 @@ packages: - supports-color dev: false - /@contentlayer/source-files@0.3.4(esbuild@0.20.1): + /@contentlayer/source-files@0.3.4(esbuild@0.20.2): resolution: {integrity: sha512-4njyn0OFPu7WY4tAjMxiJgWOKeiHuBOGdQ36EYE03iij/pPPRbiWbL+cmLccYXUFEW58mDwpqROZZm6pnxjRDQ==} dependencies: - '@contentlayer/core': 0.3.4(esbuild@0.20.1) + '@contentlayer/core': 0.3.4(esbuild@0.20.2) '@contentlayer/utils': 0.3.4 chokidar: 3.6.0 fast-glob: 3.3.2 @@ -4648,11 +4660,11 @@ packages: - supports-color dev: false - /@contentlayer/source-remote-files@0.3.4(esbuild@0.20.1): + /@contentlayer/source-remote-files@0.3.4(esbuild@0.20.2): resolution: {integrity: sha512-cyiv4sNUySZvR0uAKlM+kSAELzNd2h2QT1R2e41dRKbwOUVxeLfmGiLugr0aVac6Q3xYcD99dbHyR1xWPV+w9w==} dependencies: - '@contentlayer/core': 0.3.4(esbuild@0.20.1) - '@contentlayer/source-files': 0.3.4(esbuild@0.20.1) + '@contentlayer/core': 0.3.4(esbuild@0.20.2) + '@contentlayer/source-files': 0.3.4(esbuild@0.20.2) '@contentlayer/utils': 0.3.4 transitivePeerDependencies: - '@effect-ts/otel-node' @@ -4864,14 +4876,14 @@ packages: resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} dev: false - /@esbuild-plugins/node-resolve@0.1.4(esbuild@0.20.1): + /@esbuild-plugins/node-resolve@0.1.4(esbuild@0.20.2): resolution: {integrity: sha512-haFQ0qhxEpqtWWY0kx1Y5oE3sMyO1PcoSiWEPrAw6tm/ZOOLXjSs6Q+v1v9eyuVF0nNt50YEvrcrvENmyoMv5g==} peerDependencies: esbuild: '*' dependencies: '@types/resolve': 1.20.6 debug: 4.3.4 - esbuild: 0.20.1 + esbuild: 0.20.2 escape-string-regexp: 4.0.0 resolve: 1.22.8 transitivePeerDependencies: @@ -4886,22 +4898,12 @@ packages: requiresBuild: true optional: true - /@esbuild/aix-ppc64@0.20.1: - resolution: {integrity: sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: false - optional: true - /@esbuild/aix-ppc64@0.20.2: resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] requiresBuild: true - dev: true optional: true /@esbuild/android-arm64@0.17.19: @@ -4921,22 +4923,12 @@ packages: requiresBuild: true optional: true - /@esbuild/android-arm64@0.20.1: - resolution: {integrity: sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false - optional: true - /@esbuild/android-arm64@0.20.2: resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} engines: {node: '>=12'} cpu: [arm64] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-arm@0.17.19: @@ -4956,22 +4948,12 @@ packages: requiresBuild: true optional: true - /@esbuild/android-arm@0.20.1: - resolution: {integrity: sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false - optional: true - /@esbuild/android-arm@0.20.2: resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} engines: {node: '>=12'} cpu: [arm] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-x64@0.17.19: @@ -4991,22 +4973,12 @@ packages: requiresBuild: true optional: true - /@esbuild/android-x64@0.20.1: - resolution: {integrity: sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: false - optional: true - /@esbuild/android-x64@0.20.2: resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} engines: {node: '>=12'} cpu: [x64] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/darwin-arm64@0.17.19: @@ -5026,22 +4998,12 @@ packages: requiresBuild: true optional: true - /@esbuild/darwin-arm64@0.20.1: - resolution: {integrity: sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@esbuild/darwin-arm64@0.20.2: resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] requiresBuild: true - dev: true optional: true /@esbuild/darwin-x64@0.17.19: @@ -5061,22 +5023,12 @@ packages: requiresBuild: true optional: true - /@esbuild/darwin-x64@0.20.1: - resolution: {integrity: sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@esbuild/darwin-x64@0.20.2: resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} engines: {node: '>=12'} cpu: [x64] os: [darwin] requiresBuild: true - dev: true optional: true /@esbuild/freebsd-arm64@0.17.19: @@ -5096,22 +5048,12 @@ packages: requiresBuild: true optional: true - /@esbuild/freebsd-arm64@0.20.1: - resolution: {integrity: sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true - /@esbuild/freebsd-arm64@0.20.2: resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] requiresBuild: true - dev: true optional: true /@esbuild/freebsd-x64@0.17.19: @@ -5131,22 +5073,12 @@ packages: requiresBuild: true optional: true - /@esbuild/freebsd-x64@0.20.1: - resolution: {integrity: sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true - /@esbuild/freebsd-x64@0.20.2: resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] requiresBuild: true - dev: true optional: true /@esbuild/linux-arm64@0.17.19: @@ -5166,22 +5098,12 @@ packages: requiresBuild: true optional: true - /@esbuild/linux-arm64@0.20.1: - resolution: {integrity: sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-arm64@0.20.2: resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} engines: {node: '>=12'} cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-arm@0.17.19: @@ -5201,22 +5123,12 @@ packages: requiresBuild: true optional: true - /@esbuild/linux-arm@0.20.1: - resolution: {integrity: sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-arm@0.20.2: resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} engines: {node: '>=12'} cpu: [arm] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-ia32@0.17.19: @@ -5236,22 +5148,12 @@ packages: requiresBuild: true optional: true - /@esbuild/linux-ia32@0.20.1: - resolution: {integrity: sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-ia32@0.20.2: resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} engines: {node: '>=12'} cpu: [ia32] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-loong64@0.17.19: @@ -5271,22 +5173,12 @@ packages: requiresBuild: true optional: true - /@esbuild/linux-loong64@0.20.1: - resolution: {integrity: sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-loong64@0.20.2: resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-mips64el@0.17.19: @@ -5306,22 +5198,12 @@ packages: requiresBuild: true optional: true - /@esbuild/linux-mips64el@0.20.1: - resolution: {integrity: sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-mips64el@0.20.2: resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-ppc64@0.17.19: @@ -5341,22 +5223,12 @@ packages: requiresBuild: true optional: true - /@esbuild/linux-ppc64@0.20.1: - resolution: {integrity: sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-ppc64@0.20.2: resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-riscv64@0.17.19: @@ -5376,22 +5248,12 @@ packages: requiresBuild: true optional: true - /@esbuild/linux-riscv64@0.20.1: - resolution: {integrity: sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-riscv64@0.20.2: resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-s390x@0.17.19: @@ -5411,22 +5273,12 @@ packages: requiresBuild: true optional: true - /@esbuild/linux-s390x@0.20.1: - resolution: {integrity: sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-s390x@0.20.2: resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-x64@0.17.19: @@ -5446,22 +5298,12 @@ packages: requiresBuild: true optional: true - /@esbuild/linux-x64@0.20.1: - resolution: {integrity: sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-x64@0.20.2: resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} engines: {node: '>=12'} cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/netbsd-x64@0.17.19: @@ -5481,22 +5323,12 @@ packages: requiresBuild: true optional: true - /@esbuild/netbsd-x64@0.20.1: - resolution: {integrity: sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: false - optional: true - /@esbuild/netbsd-x64@0.20.2: resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] requiresBuild: true - dev: true optional: true /@esbuild/openbsd-x64@0.17.19: @@ -5516,22 +5348,12 @@ packages: requiresBuild: true optional: true - /@esbuild/openbsd-x64@0.20.1: - resolution: {integrity: sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: false - optional: true - /@esbuild/openbsd-x64@0.20.2: resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] requiresBuild: true - dev: true optional: true /@esbuild/sunos-x64@0.17.19: @@ -5551,22 +5373,12 @@ packages: requiresBuild: true optional: true - /@esbuild/sunos-x64@0.20.1: - resolution: {integrity: sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: false - optional: true - /@esbuild/sunos-x64@0.20.2: resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} engines: {node: '>=12'} cpu: [x64] os: [sunos] requiresBuild: true - dev: true optional: true /@esbuild/win32-arm64@0.17.19: @@ -5586,22 +5398,12 @@ packages: requiresBuild: true optional: true - /@esbuild/win32-arm64@0.20.1: - resolution: {integrity: sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@esbuild/win32-arm64@0.20.2: resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} engines: {node: '>=12'} cpu: [arm64] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-ia32@0.17.19: @@ -5621,22 +5423,12 @@ packages: requiresBuild: true optional: true - /@esbuild/win32-ia32@0.20.1: - resolution: {integrity: sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@esbuild/win32-ia32@0.20.2: resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-x64@0.17.19: @@ -5656,22 +5448,12 @@ packages: requiresBuild: true optional: true - /@esbuild/win32-x64@0.20.1: - resolution: {integrity: sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@esbuild/win32-x64@0.20.2: resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): @@ -5922,13 +5704,13 @@ packages: resolution: {integrity: sha512-/AHFqy6OeNHS2NNZGFVRgQh+pnW8iAoV3d1fiO9b2PuQ3HzZpC30MrMrHtq1uOGy1/zcK4uPQEyI31jkM0NNAA==} dev: true - /@mdx-js/esbuild@2.3.0(esbuild@0.20.1): + /@mdx-js/esbuild@2.3.0(esbuild@0.20.2): resolution: {integrity: sha512-r/vsqsM0E+U4Wr0DK+0EfmABE/eg+8ITW4DjvYdh3ve/tK2safaqHArNnaqbOk1DjYGrhxtoXoGaM3BY8fGBTA==} peerDependencies: esbuild: '>=0.11.0' dependencies: '@mdx-js/mdx': 2.3.0 - esbuild: 0.20.1 + esbuild: 0.20.2 node-fetch: 3.3.2 vfile: 5.3.7 transitivePeerDependencies: @@ -10089,17 +9871,17 @@ packages: upper-case: 2.0.2 dev: false - /contentlayer@0.3.4(esbuild@0.20.1): + /contentlayer@0.3.4(esbuild@0.20.2): resolution: {integrity: sha512-FYDdTUFaN4yqep0waswrhcXjmMJnPD5iXDTtxcUCGdklfuIrXM2xLx51xl748cHmGA6IsC+27YZFxU6Ym13QIA==} engines: {node: '>=14.18'} hasBin: true requiresBuild: true dependencies: - '@contentlayer/cli': 0.3.4(esbuild@0.20.1) - '@contentlayer/client': 0.3.4(esbuild@0.20.1) - '@contentlayer/core': 0.3.4(esbuild@0.20.1) - '@contentlayer/source-files': 0.3.4(esbuild@0.20.1) - '@contentlayer/source-remote-files': 0.3.4(esbuild@0.20.1) + '@contentlayer/cli': 0.3.4(esbuild@0.20.2) + '@contentlayer/client': 0.3.4(esbuild@0.20.2) + '@contentlayer/core': 0.3.4(esbuild@0.20.2) + '@contentlayer/source-files': 0.3.4(esbuild@0.20.2) + '@contentlayer/source-remote-files': 0.3.4(esbuild@0.20.2) '@contentlayer/utils': 0.3.4 transitivePeerDependencies: - '@effect-ts/otel-node' @@ -10979,37 +10761,6 @@ packages: '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 - /esbuild@0.20.1: - resolution: {integrity: sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.1 - '@esbuild/android-arm': 0.20.1 - '@esbuild/android-arm64': 0.20.1 - '@esbuild/android-x64': 0.20.1 - '@esbuild/darwin-arm64': 0.20.1 - '@esbuild/darwin-x64': 0.20.1 - '@esbuild/freebsd-arm64': 0.20.1 - '@esbuild/freebsd-x64': 0.20.1 - '@esbuild/linux-arm': 0.20.1 - '@esbuild/linux-arm64': 0.20.1 - '@esbuild/linux-ia32': 0.20.1 - '@esbuild/linux-loong64': 0.20.1 - '@esbuild/linux-mips64el': 0.20.1 - '@esbuild/linux-ppc64': 0.20.1 - '@esbuild/linux-riscv64': 0.20.1 - '@esbuild/linux-s390x': 0.20.1 - '@esbuild/linux-x64': 0.20.1 - '@esbuild/netbsd-x64': 0.20.1 - '@esbuild/openbsd-x64': 0.20.1 - '@esbuild/sunos-x64': 0.20.1 - '@esbuild/win32-arm64': 0.20.1 - '@esbuild/win32-ia32': 0.20.1 - '@esbuild/win32-x64': 0.20.1 - dev: false - /esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} @@ -11039,7 +10790,6 @@ packages: '@esbuild/win32-arm64': 0.20.2 '@esbuild/win32-ia32': 0.20.2 '@esbuild/win32-x64': 0.20.2 - dev: true /escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} @@ -14254,17 +14004,17 @@ packages: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} dev: true - /mdx-bundler@9.2.1(esbuild@0.20.1): + /mdx-bundler@9.2.1(esbuild@0.20.2): resolution: {integrity: sha512-hWEEip1KU9MCNqeH2rqwzAZ1pdqPPbfkx9OTJjADqGPQz4t9BO85fhI7AP9gVYrpmfArf9/xJZUN0yBErg/G/Q==} engines: {node: '>=14', npm: '>=6'} peerDependencies: esbuild: 0.* dependencies: '@babel/runtime': 7.24.0 - '@esbuild-plugins/node-resolve': 0.1.4(esbuild@0.20.1) + '@esbuild-plugins/node-resolve': 0.1.4(esbuild@0.20.2) '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@mdx-js/esbuild': 2.3.0(esbuild@0.20.1) - esbuild: 0.20.1 + '@mdx-js/esbuild': 2.3.0(esbuild@0.20.2) + esbuild: 0.20.2 gray-matter: 4.0.3 remark-frontmatter: 4.0.1 remark-mdx-frontmatter: 1.1.1 @@ -15019,7 +14769,7 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: false - /next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.20.1)(next@14.1.3)(react-dom@18.2.0)(react@18.2.0): + /next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@14.1.3)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UtUCwgAl159KwfhNaOwyiI7Lg6sdioyKMeh+E7jxx0CJ29JuXGxBEYmCI6+72NxFGIFZKx8lvttbbQhbnYWYSw==} peerDependencies: contentlayer: 0.3.4 @@ -15027,9 +14777,9 @@ packages: react: '*' react-dom: '*' dependencies: - '@contentlayer/core': 0.3.4(esbuild@0.20.1) + '@contentlayer/core': 0.3.4(esbuild@0.20.2) '@contentlayer/utils': 0.3.4 - contentlayer: 0.3.4(esbuild@0.20.1) + contentlayer: 0.3.4(esbuild@0.20.2) next: 14.1.3(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) diff --git a/shared/src/controls.ts b/shared/src/controls.ts index d5f8bd0144..5ff8d5f9a4 100644 --- a/shared/src/controls.ts +++ b/shared/src/controls.ts @@ -249,8 +249,8 @@ export const tourControls = defineControls({ export const floatingPanelControls = defineControls({ disabled: { type: "boolean", defaultValue: false }, - resizable: { type: "boolean", defaultValue: false }, - draggable: { type: "boolean", defaultValue: false }, + resizable: { type: "boolean", defaultValue: true }, + draggable: { type: "boolean", defaultValue: true }, lockAspectRatio: { type: "boolean", defaultValue: false }, closeOnEscape: { type: "boolean", defaultValue: true }, }) From 321304b736a8b08462d212d8b67b8f25a133fc88 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Wed, 20 Mar 2024 22:44:36 +0000 Subject: [PATCH 10/19] chore: preserve --- .xstate/floating-panel.js | 2 +- .../floating-panel/src/floating-panel.machine.ts | 9 ++++----- .../machines/floating-panel/src/floating-panel.props.ts | 1 + .../machines/floating-panel/src/floating-panel.types.ts | 4 ++++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.xstate/floating-panel.js b/.xstate/floating-panel.js index 8ad5c71758..1ecc87a633 100644 --- a/.xstate/floating-panel.js +++ b/.xstate/floating-panel.js @@ -74,7 +74,7 @@ const fetchMachine = createMachine({ "open.resizing": { tags: ["open"], activities: ["trackPointerMove"], - exit: ["clearLastSize"], + exit: ["clearPrevSize"], on: { DRAG: { actions: ["setSize"] diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index 4b4d969330..d6eef519fe 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -88,7 +88,7 @@ export function machine(userContext: UserDefinedContext) { "open.resizing": { tags: ["open"], activities: ["trackPointerMove"], - exit: ["clearLastSize"], + exit: ["clearPrevSize"], on: { DRAG: { actions: ["setSize"], @@ -109,7 +109,6 @@ export function machine(userContext: UserDefinedContext) { }, }, { - guards: {}, activities: { trackPointerMove(ctx, _evt, { send }) { const doc = dom.getDoc(ctx) @@ -138,7 +137,7 @@ export function machine(userContext: UserDefinedContext) { ctx.lastEventPosition = evt.position }, clearPrevPosition(ctx) { - ctx.prevPosition = null + if (!ctx.preserveOnClose) ctx.prevPosition = null ctx.lastEventPosition = null }, setPosition(ctx, evt) { @@ -164,8 +163,8 @@ export function machine(userContext: UserDefinedContext) { ctx.lastEventPosition = evt.position }, clearPrevSize(ctx) { - ctx.prevSize = null - ctx.prevPosition = null + if (!ctx.preserveOnClose) ctx.prevSize = null + if (!ctx.preserveOnClose) ctx.prevPosition = null ctx.lastEventPosition = null }, setSize(ctx, evt) { diff --git a/packages/machines/floating-panel/src/floating-panel.props.ts b/packages/machines/floating-panel/src/floating-panel.props.ts index 94bdb3506a..ce34d3d78e 100644 --- a/packages/machines/floating-panel/src/floating-panel.props.ts +++ b/packages/machines/floating-panel/src/floating-panel.props.ts @@ -14,6 +14,7 @@ export const props = createProps()([ "onDrag", "onDragEnd", "onOpenChange", + "preserveOnClose", "onResize", "onResizeEnd", "open", diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index d6fab02418..cebd442b85 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -90,6 +90,10 @@ interface PublicContext extends DirectionProperty, CommonProperties { * Function called when the size of the panel changes via resizing ends */ onResizeEnd?(details: ResizeDetails): void + /** + * Whether the panel size and position should be preserved when it is closed + */ + preserveOnClose?: boolean } interface PrivateContext { From 7704238a10ea6d7bfdc2346ce4395f0f560362d6 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Thu, 21 Mar 2024 11:35:06 +0000 Subject: [PATCH 11/19] chore: update preserve --- .../machines/floating-panel/src/floating-panel.machine.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index d6eef519fe..0dabcdd3b5 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -152,9 +152,11 @@ export function machine(userContext: UserDefinedContext) { el?.style.setProperty("--y", `${ctx.position.y}px`) }, resetPosition(ctx, _evt, { initialContext }) { + if (ctx.preserveOnClose) return ctx.position = initialContext.position }, resetSize(ctx, _evt, { initialContext }) { + if (ctx.preserveOnClose) return ctx.size = initialContext.size }, setPrevSize(ctx, evt) { @@ -163,8 +165,8 @@ export function machine(userContext: UserDefinedContext) { ctx.lastEventPosition = evt.position }, clearPrevSize(ctx) { - if (!ctx.preserveOnClose) ctx.prevSize = null - if (!ctx.preserveOnClose) ctx.prevPosition = null + ctx.prevSize = null + ctx.prevPosition = null ctx.lastEventPosition = null }, setSize(ctx, evt) { From 248cb1a461537e8225d57cb53325439745153d2d Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Thu, 21 Mar 2024 22:01:49 +0000 Subject: [PATCH 12/19] feat: complete features --- .xstate/floating-panel.js | 32 +++- examples/next-ts/pages/floating-panel.tsx | 36 ++-- .../src/floating-panel.anatomy.ts | 1 + .../src/floating-panel.connect.ts | 19 +- .../src/floating-panel.machine.ts | 140 ++++++++++---- .../src/floating-panel.props.ts | 9 +- .../src/floating-panel.types.ts | 51 +++-- .../floating-panel/src/utils/get-diff-rect.ts | 122 ------------ .../machines/floating-panel/src/utils/math.ts | 33 ---- .../utilities/rect/src/affine-transform.ts | 177 ++++++++++++++++++ packages/utilities/rect/src/align.ts | 55 ++---- packages/utilities/rect/src/clamp.ts | 26 +++ packages/utilities/rect/src/closest.ts | 19 +- packages/utilities/rect/src/compass.ts | 25 +++ packages/utilities/rect/src/contains.ts | 4 +- packages/utilities/rect/src/distance.ts | 7 +- packages/utilities/rect/src/from-element.ts | 4 +- packages/utilities/rect/src/from-points.ts | 4 +- packages/utilities/rect/src/from-range.ts | 3 +- packages/utilities/rect/src/from-rotation.ts | 4 +- packages/utilities/rect/src/from-window.ts | 3 +- packages/utilities/rect/src/get-polygon.ts | 15 -- packages/utilities/rect/src/index.ts | 4 +- packages/utilities/rect/src/intersection.ts | 4 +- packages/utilities/rect/src/operations.ts | 4 +- packages/utilities/rect/src/polygon.ts | 16 +- packages/utilities/rect/src/rect.ts | 46 +++-- packages/utilities/rect/src/resize.ts | 106 +++++++++++ packages/utilities/rect/src/types.ts | 53 +++++- packages/utilities/rect/src/union.ts | 22 +-- shared/src/style.css | 33 +++- 31 files changed, 714 insertions(+), 363 deletions(-) delete mode 100644 packages/machines/floating-panel/src/utils/get-diff-rect.ts delete mode 100644 packages/machines/floating-panel/src/utils/math.ts create mode 100644 packages/utilities/rect/src/affine-transform.ts create mode 100644 packages/utilities/rect/src/clamp.ts create mode 100644 packages/utilities/rect/src/compass.ts delete mode 100644 packages/utilities/rect/src/get-polygon.ts create mode 100644 packages/utilities/rect/src/resize.ts diff --git a/.xstate/floating-panel.js b/.xstate/floating-panel.js index 1ecc87a633..6024abfa0f 100644 --- a/.xstate/floating-panel.js +++ b/.xstate/floating-panel.js @@ -12,7 +12,11 @@ const { const fetchMachine = createMachine({ id: "floating-panel", initial: ctx.open ? "open" : "closed", - context: {}, + context: { + "!isMaximized": false, + "!isMinimized": false, + "closeOnEsc": false + }, on: { UPDATE_CONTEXT: { actions: "updateContext" @@ -33,20 +37,32 @@ const fetchMachine = createMachine({ entry: ["setBoundaryRect"], on: { DRAG_START: { + cond: "!isMaximized", target: "open.dragging", actions: ["setPrevPosition"] }, RESIZE_START: { + cond: "!isMinimized", target: "open.resizing", actions: ["setPrevSize"] }, CLOSE: { target: "closed", - actions: ["invokeOnClose", "resetPosition", "resetSize"] + actions: ["invokeOnClose", "resetRect"] }, ESCAPE: { + cond: "closeOnEsc", target: "closed", - actions: ["invokeOnClose", "resetPosition", "resetSize"] + actions: ["invokeOnClose", "resetRect"] + }, + MINIMIZE: { + actions: ["setMinimized", "invokeOnMinimize"] + }, + MAXIMIZE: { + actions: ["setMaximized", "invokeOnMaximize"] + }, + RESTORE: { + actions: ["setRestored"] } } }, @@ -64,7 +80,7 @@ const fetchMachine = createMachine({ }, CLOSE: { target: "closed", - actions: ["invokeOnClose", "resetPosition", "resetSize"] + actions: ["invokeOnClose", "resetRect"] }, ESCAPE: { target: "open" @@ -85,7 +101,7 @@ const fetchMachine = createMachine({ }, CLOSE: { target: "closed", - actions: ["invokeOnClose", "resetPosition", "resetSize"] + actions: ["invokeOnClose", "resetRect"] }, ESCAPE: { target: "open" @@ -101,5 +117,9 @@ const fetchMachine = createMachine({ }; }) }, - guards: {} + guards: { + "!isMaximized": ctx => ctx["!isMaximized"], + "!isMinimized": ctx => ctx["!isMinimized"], + "closeOnEsc": ctx => ctx["closeOnEsc"] + } }); \ No newline at end of file diff --git a/examples/next-ts/pages/floating-panel.tsx b/examples/next-ts/pages/floating-panel.tsx index f9d917217b..326f4a16f0 100644 --- a/examples/next-ts/pages/floating-panel.tsx +++ b/examples/next-ts/pages/floating-panel.tsx @@ -1,7 +1,7 @@ import * as floatingPanel from "@zag-js/floating-panel" import { normalizeProps, useMachine } from "@zag-js/react" import { floatingPanelControls } from "@zag-js/shared" -import { XIcon } from "lucide-react" +import { ArrowDownLeft, Maximize2, Minus, XIcon } from "lucide-react" import { useId } from "react" import { StateVisualizer } from "../components/state-visualizer" import { Toolbar } from "../components/toolbar" @@ -23,25 +23,37 @@ export default function Page() {
-
-
-
-
-
-
-
-

Floating Panel

- +
+ + + + +

Some content

+ +
+
+
+
+
+
+
+
diff --git a/packages/machines/floating-panel/src/floating-panel.anatomy.ts b/packages/machines/floating-panel/src/floating-panel.anatomy.ts index 0c3a5effeb..7da5a078d2 100644 --- a/packages/machines/floating-panel/src/floating-panel.anatomy.ts +++ b/packages/machines/floating-panel/src/floating-panel.anatomy.ts @@ -12,6 +12,7 @@ export const anatomy = createAnatomy("floating-panel").parts( "minimizeTrigger", "maximizeTrigger", "closeTrigger", + "restoreTrigger", "dock", ) diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts index 50c4917b32..9108bb0fc7 100644 --- a/packages/machines/floating-panel/src/floating-panel.connect.ts +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -11,6 +11,9 @@ export function connect(state: State, send: Send, normalize const isDragging = state.matches("open.dragging") const isResizing = state.matches("open.resizing") + const isMaximized = state.context.stage === "maximized" + const isMinimized = state.context.stage === "minimized" + return { isOpen, isDragging, @@ -85,6 +88,7 @@ export function connect(state: State, send: Send, normalize ...parts.minimizeTrigger.attrs, disabled: state.context.disabled, "aria-label": "Minimize Window", + hidden: isMinimized || isMaximized, type: "button", onClick() { send("MINIMIZE") @@ -95,12 +99,24 @@ export function connect(state: State, send: Send, normalize ...parts.maximizeTrigger.attrs, disabled: state.context.disabled, "aria-label": "Maximize Window", + hidden: isMaximized || isMinimized, type: "button", onClick() { send("MAXIMIZE") }, }), + restoreTriggerProps: normalize.button({ + ...parts.restoreTrigger.attrs, + disabled: state.context.disabled, + "aria-label": "Restore Window", + hidden: !(isMaximized || isMinimized), + type: "button", + onClick() { + send("RESTORE") + }, + }), + getResizeTriggerProps(props: ResizeTriggerProps) { const disabled = !state.context.resizable || state.context.disabled return normalize.element({ @@ -136,7 +152,7 @@ export function connect(state: State, send: Send, normalize const target = getEventTarget(getNativeEvent(event)) - if (target?.closest("[data-part=close-trigger]")) { + if (target?.closest("button")) { return } @@ -182,6 +198,7 @@ export function connect(state: State, send: Send, normalize bodyProps: normalize.element({ ...parts.body.attrs, "data-dragging": dataAttr(isDragging), + hidden: isMinimized, }), } } diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index 0dabcdd3b5..99609a8f7f 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -1,12 +1,21 @@ -import { createMachine } from "@zag-js/core" +import { createMachine, guards } from "@zag-js/core" import { trackPointerMove } from "@zag-js/dom-event" import { isHTMLElement } from "@zag-js/dom-query" -import { getElementRect, getWindowRect } from "@zag-js/rect-utils" +import { + addPoints, + clampPoint, + clampSize, + createRect, + getElementRect, + getWindowRect, + resizeRect, + subtractPoints, +} from "@zag-js/rect-utils" import { compact } from "@zag-js/utils" import { dom } from "./floating-panel.dom" import type { MachineContext, MachineState, UserDefinedContext } from "./floating-panel.types" -import { getDiffRect } from "./utils/get-diff-rect" -import { addPosition, clampPosition, clampSize, subtractPosition } from "./utils/math" + +const { not } = guards export function machine(userContext: UserDefinedContext) { const ctx = compact(userContext) @@ -17,6 +26,7 @@ export function machine(userContext: UserDefinedContext) { context: { size: { width: 320, height: 400 }, position: { x: 300, y: 100 }, + gridSize: 1, ...ctx, lastEventPosition: null, prevPosition: null, @@ -45,20 +55,32 @@ export function machine(userContext: UserDefinedContext) { entry: ["setBoundaryRect"], on: { DRAG_START: { + guard: not("isMaximized"), target: "open.dragging", actions: ["setPrevPosition"], }, RESIZE_START: { + guard: not("isMinimized"), target: "open.resizing", actions: ["setPrevSize"], }, CLOSE: { target: "closed", - actions: ["invokeOnClose", "resetPosition", "resetSize"], + actions: ["invokeOnClose", "resetRect"], }, ESCAPE: { + guard: "closeOnEsc", target: "closed", - actions: ["invokeOnClose", "resetPosition", "resetSize"], + actions: ["invokeOnClose", "resetRect"], + }, + MINIMIZE: { + actions: ["setMinimized", "invokeOnMinimize"], + }, + MAXIMIZE: { + actions: ["setMaximized", "invokeOnMaximize"], + }, + RESTORE: { + actions: ["setRestored"], }, }, }, @@ -77,7 +99,7 @@ export function machine(userContext: UserDefinedContext) { }, CLOSE: { target: "closed", - actions: ["invokeOnClose", "resetPosition", "resetSize"], + actions: ["invokeOnClose", "resetRect"], }, ESCAPE: { target: "open", @@ -99,7 +121,7 @@ export function machine(userContext: UserDefinedContext) { }, CLOSE: { target: "closed", - actions: ["invokeOnClose", "resetPosition", "resetSize"], + actions: ["invokeOnClose", "resetRect"], }, ESCAPE: { target: "open", @@ -109,6 +131,11 @@ export function machine(userContext: UserDefinedContext) { }, }, { + guards: { + closeOnEsc: (ctx) => !!ctx.closeOnEscape, + isMaximized: (ctx) => ctx.stage === "maximized", + isMinimized: (ctx) => ctx.stage === "minimized", + }, activities: { trackPointerMove(ctx, _evt, { send }) { const doc = dom.getDoc(ctx) @@ -126,10 +153,8 @@ export function machine(userContext: UserDefinedContext) { actions: { setBoundaryRect(ctx) { const el = ctx.getBoundaryEl?.() - const rect = (() => { - if (isHTMLElement(el)) return getElementRect(el) - return getWindowRect(dom.getWin(ctx)) - })() + const win = dom.getWin(ctx) + const rect = isHTMLElement(el) ? getElementRect(el) : getWindowRect(win) ctx.boundaryRect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height } }, setPrevPosition(ctx, evt) { @@ -141,23 +166,26 @@ export function machine(userContext: UserDefinedContext) { ctx.lastEventPosition = null }, setPosition(ctx, evt) { - const diff = subtractPosition(evt.position, ctx.lastEventPosition!) - let position = addPosition(ctx.prevPosition!, diff) - position = clampPosition(position, ctx.size, ctx.boundaryRect!) - ctx.position = position + let diff = subtractPoints(evt.position, ctx.lastEventPosition!) + + diff.x = Math.round(diff.x / ctx.gridSize!) * ctx.gridSize! + diff.y = Math.round(diff.y / ctx.gridSize!) * ctx.gridSize! + + const position = addPoints(ctx.prevPosition!, diff) + const point = clampPoint(position, ctx.size, ctx.boundaryRect!) + ctx.position = point }, setPositionStyle(ctx) { const el = dom.getPositionerEl(ctx) el?.style.setProperty("--x", `${ctx.position.x}px`) el?.style.setProperty("--y", `${ctx.position.y}px`) }, - resetPosition(ctx, _evt, { initialContext }) { - if (ctx.preserveOnClose) return - ctx.position = initialContext.position - }, - resetSize(ctx, _evt, { initialContext }) { - if (ctx.preserveOnClose) return - ctx.size = initialContext.size + resetRect(ctx, _evt, { initialContext }) { + ctx.stage = undefined + if (!ctx.preserveOnClose) { + ctx.position = initialContext.position + ctx.size = initialContext.size + } }, setPrevSize(ctx, evt) { ctx.prevSize = { ...ctx.size } @@ -172,28 +200,68 @@ export function machine(userContext: UserDefinedContext) { setSize(ctx, evt) { if (!ctx.prevSize || !ctx.prevPosition || !ctx.lastEventPosition) return - const diff = subtractPosition(evt.position, ctx.lastEventPosition) + const prevRect = createRect({ ...ctx.prevPosition, ...ctx.prevSize }) + const offset = subtractPoints(evt.position, ctx.lastEventPosition) - const { nextSize, nextPosition } = getDiffRect({ - diff, - axis: evt.axis, - prevPosition: ctx.prevPosition, - prevSize: ctx.prevSize, - altKey: evt.altKey, + const nextRect = resizeRect(prevRect, offset, evt.axis, { + scalingOriginMode: evt.altKey ? "center" : "extent", + lockAspectRatio: !!ctx.lockAspectRatio || evt.shiftKey, }) - const size = clampSize(nextSize, ctx.minSize, ctx.maxSize) - ctx.size = size + let nextSize = { width: nextRect.width, height: nextRect.height } + let nextPosition = { x: nextRect.x, y: nextRect.y } + + nextSize = clampSize(nextSize, ctx.minSize, ctx.maxSize) + ctx.size = nextSize if (nextPosition) { - const position = clampPosition(nextPosition, size, ctx.boundaryRect!) - ctx.position = position + const point = clampPoint(nextPosition, nextSize, ctx.boundaryRect!) + ctx.position = point } }, setSizeStyle(ctx) { const el = dom.getPositionerEl(ctx) - el?.style.setProperty("--width", `${ctx.size.width}px`) - el?.style.setProperty("--height", `${ctx.size.height}px`) + + if (ctx.size.width != null) { + el?.style.setProperty("--width", `${ctx.size.width}px`) + } else { + el?.style.removeProperty("--width") + } + + if (ctx.size.height != null) { + el?.style.setProperty("--height", `${ctx.size.height}px`) + } else { + el?.style.removeProperty("--height") + } + }, + setMaximized(ctx) { + // set max size + ctx.stage = "maximized" + ctx.prevSize = ctx.size + ctx.prevPosition = ctx.position + // update size + ctx.position = { x: 0, y: 0 } + ctx.size = { width: ctx.boundaryRect!.width, height: ctx.boundaryRect!.height } + }, + setMinimized(ctx) { + // set min size + ctx.stage = "minimized" + ctx.prevSize = ctx.size + ctx.prevPosition = ctx.position + // update size + const size: any = { ...ctx.size } + delete size.height + ctx.size = size + }, + setRestored(ctx) { + ctx.stage = undefined + if (!ctx.prevSize || !ctx.prevPosition) return + // restore size + ctx.size = ctx.prevSize + ctx.position = ctx.prevPosition + // clear prev size + ctx.prevSize = null + ctx.prevPosition = null }, invokeOnOpen(ctx) { ctx.onOpenChange?.({ open: true }) diff --git a/packages/machines/floating-panel/src/floating-panel.props.ts b/packages/machines/floating-panel/src/floating-panel.props.ts index ce34d3d78e..caccc17b3d 100644 --- a/packages/machines/floating-panel/src/floating-panel.props.ts +++ b/packages/machines/floating-panel/src/floating-panel.props.ts @@ -9,20 +9,23 @@ export const props = createProps()([ "draggable", "getBoundaryEl", "getRootNode", + "gridSize", "id", "lockAspectRatio", + "maxSize", + "minSize", "onDrag", "onDragEnd", + "onMaximize", + "onMinimize", "onOpenChange", - "preserveOnClose", "onResize", "onResizeEnd", "open", "position", + "preserveOnClose", "resizable", "size", - "minSize", - "maxSize", ]) export const splitProps = createSplitProps>(props) diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index cebd442b85..86437a5109 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -1,20 +1,9 @@ import type { StateMachine as S } from "@zag-js/core" +import type { Point, RectInit, Size } from "@zag-js/rect-utils" import type { CommonProperties, DirectionProperty, RequiredBy } from "@zag-js/types" -export interface Position { - x: number - y: number -} - -export interface Size { - width: number - height: number -} - -export interface Rect extends Position, Size {} - export interface DragDetails { - position: Position + position: Point } export interface ResizeDetails { @@ -53,7 +42,7 @@ interface PublicContext extends DirectionProperty, CommonProperties { /** * The position of the panel */ - position: Position + position: Point /** * Whether the panel is locked to its aspect ratio */ @@ -94,13 +83,41 @@ interface PublicContext extends DirectionProperty, CommonProperties { * Whether the panel size and position should be preserved when it is closed */ preserveOnClose?: boolean + /** + * The snap grid for the panel + */ + gridSize: number + /** + * Function called when the panel is minimized + */ + onMinimize?(): void + /** + * Function called when the panel is maximized + */ + onMaximize?(): void } interface PrivateContext { - boundaryRect: Rect | null - lastEventPosition: Position | null - prevPosition: Position | null + /** + * The rect of the boundary + */ + boundaryRect: RectInit | null + /** + * The last position of the mouse event + */ + lastEventPosition: Point | null + /** + * The previous position of the panel before dragging + */ + prevPosition: Point | null + /** + * The previous size of the panel before resizing + */ prevSize: Size | null + /** + * The stage of the panel + */ + stage?: "minimized" | "maximized" } type ComputedContext = Readonly<{}> diff --git a/packages/machines/floating-panel/src/utils/get-diff-rect.ts b/packages/machines/floating-panel/src/utils/get-diff-rect.ts deleted file mode 100644 index 166aac96ae..0000000000 --- a/packages/machines/floating-panel/src/utils/get-diff-rect.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { Position, ResizeTriggerAxis, Size } from "../floating-panel.types" - -export interface Options { - diff: Position - axis: ResizeTriggerAxis - prevPosition: Position - prevSize: Size - altKey: boolean -} - -export function getDiffRect(opts: Options) { - const { diff, axis, prevPosition, prevSize, altKey } = opts - - const factor = altKey ? 2 : 1 - - let nextSize: Size | undefined - let nextPosition: Position | undefined - - switch (axis as ResizeTriggerAxis) { - case "n": { - nextSize = { - width: prevSize!.width, - height: prevSize.height - diff.y * factor, - } - nextPosition = { - y: prevPosition.y + diff.y, - x: prevPosition.x, - } - break - } - - case "e": { - nextSize = { - width: prevSize.width + diff.x * factor, - height: prevSize.height, - } - nextPosition = { - y: prevPosition.y, - x: altKey ? prevPosition.x - diff.x : prevPosition.x, - } - break - } - - case "w": { - nextSize = { - width: prevSize.width - diff.x * factor, - height: prevSize.height, - } - nextPosition = { - y: prevPosition.y, - x: altKey ? prevPosition.x + diff.x : prevPosition.x, - } - break - } - - case "s": { - nextSize = { - width: prevSize.width, - height: prevSize.height + diff.y * factor, - } - nextPosition = { - y: altKey ? prevPosition.y - diff.y : prevPosition.y, - x: prevPosition.x, - } - break - } - - case "ne": { - nextSize = { - width: prevSize.width + diff.x * factor, - height: prevSize.height - diff.y * factor, - } - nextPosition = { - y: prevPosition.y + diff.y, - x: altKey ? prevPosition.x - diff.x : prevPosition.x, - } - break - } - - case "se": { - nextSize = { - width: prevSize.width + diff.x * factor, - height: prevSize.height + diff.y * factor, - } - nextPosition = { - x: altKey ? prevPosition.x - diff.x : prevPosition.x, - y: altKey ? prevPosition.y - diff.y : prevPosition.y, - } - break - } - - case "sw": { - nextSize = { - width: prevSize.width - diff.x * factor, - height: prevSize.height + diff.y * factor, - } - nextPosition = { - y: altKey ? prevPosition.y - diff.y : prevPosition.y, - x: prevPosition.x + diff.x, - } - break - } - - case "nw": { - nextSize = { - width: prevSize.width - diff.x * factor, - height: prevSize.height - diff.y * factor, - } - nextPosition = { - y: prevPosition.y + diff.y, - x: prevPosition.x + diff.x, - } - break - } - - default: { - throw new Error(`Invalid axis: ${axis}`) - } - } - - return { nextSize, nextPosition } -} diff --git a/packages/machines/floating-panel/src/utils/math.ts b/packages/machines/floating-panel/src/utils/math.ts deleted file mode 100644 index f0a8bf51b0..0000000000 --- a/packages/machines/floating-panel/src/utils/math.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { clampValue } from "@zag-js/numeric-range" -import type { Position, Rect, Size } from "../floating-panel.types" - -export const addPosition = (a: Position, b: Position) => ({ x: a.x + b.x, y: a.y + b.y }) -export const subtractPosition = (a: Position, b: Position) => ({ x: a.x - b.x, y: a.y - b.y }) - -export const addSize = (a: Size, b: Size) => ({ width: a.width + b.width, height: a.height + b.height }) -export const subtractSize = (a: Size, b: Size) => ({ width: a.width - b.width, height: a.height - b.height }) - -export const clampSize = (size: Size, minSize?: Size, maxSize?: Size) => { - if (minSize == null && maxSize == null) { - return size - } - - if (minSize == null && maxSize != null) { - return { width: Math.min(size.width, maxSize.width), height: Math.min(size.height, maxSize.height) } - } - - if (minSize != null && maxSize == null) { - return { width: Math.max(size.width, minSize.width), height: Math.max(size.height, minSize.height) } - } - - return { - width: clampValue(size.width, minSize!.width, maxSize!.width), - height: clampValue(size.height, minSize!.height, maxSize!.height), - } -} - -export const clampPosition = (position: Position, size: Size, boundaryRect: Rect) => { - const x = clampValue(position.x, boundaryRect.x, boundaryRect.width - size.width) - const y = clampValue(position.y, boundaryRect.y, boundaryRect.height - size.height) - return { x, y } -} diff --git a/packages/utilities/rect/src/affine-transform.ts b/packages/utilities/rect/src/affine-transform.ts new file mode 100644 index 0000000000..15c971837a --- /dev/null +++ b/packages/utilities/rect/src/affine-transform.ts @@ -0,0 +1,177 @@ +import type { Point } from "./types" + +export class AffineTransform { + m00: number + m01: number + m02: number + m10: number + m11: number + m12: number + + constructor([m00, m01, m02, m10, m11, m12]: Iterable = [0, 0, 0, 0, 0, 0]) { + this.m00 = m00 + this.m01 = m01 + this.m02 = m02 + this.m10 = m10 + this.m11 = m11 + this.m12 = m12 + } + + applyTo(point: Point): Point { + const { x, y } = point + const { m00, m01, m02, m10, m11, m12 } = this + + return { + x: m00 * x + m01 * y + m02, + y: m10 * x + m11 * y + m12, + } + } + + prepend(other: AffineTransform): AffineTransform { + return new AffineTransform([ + this.m00 * other.m00 + this.m01 * other.m10, // m00 + this.m00 * other.m01 + this.m01 * other.m11, // m01 + this.m00 * other.m02 + this.m01 * other.m12 + this.m02, // m02 + this.m10 * other.m00 + this.m11 * other.m10, // m10 + this.m10 * other.m01 + this.m11 * other.m11, // m11 + this.m10 * other.m02 + this.m11 * other.m12 + this.m12, // m12 + ]) + } + + append(other: AffineTransform): AffineTransform { + return new AffineTransform([ + other.m00 * this.m00 + other.m01 * this.m10, // m00 + other.m00 * this.m01 + other.m01 * this.m11, // m01 + other.m00 * this.m02 + other.m01 * this.m12 + other.m02, // m02 + other.m10 * this.m00 + other.m11 * this.m10, // m10 + other.m10 * this.m01 + other.m11 * this.m11, // m11 + other.m10 * this.m02 + other.m11 * this.m12 + other.m12, // m12 + ]) + } + + get determinant() { + return this.m00 * this.m11 - this.m01 * this.m10 + } + + get isInvertible() { + const det = this.determinant + + return isFinite(det) && isFinite(this.m02) && isFinite(this.m12) && det !== 0 + } + + invert() { + const det = this.determinant + + return new AffineTransform([ + this.m11 / det, // m00 + -this.m01 / det, // m01 + (this.m01 * this.m12 - this.m11 * this.m02) / det, // m02 + -this.m10 / det, // m10 + this.m00 / det, // m11 + (this.m10 * this.m02 - this.m00 * this.m12) / det, // m12 + ]) + } + + get array(): number[] { + return [this.m00, this.m01, this.m02, this.m10, this.m11, this.m12, 0, 0, 1] + } + + get float32Array(): Float32Array { + return new Float32Array(this.array) + } + + // Static + + static get identity(): AffineTransform { + return new AffineTransform([1, 0, 0, 0, 1, 0]) + } + + static rotate(theta: number, origin?: Point): AffineTransform { + const rotation = new AffineTransform([Math.cos(theta), -Math.sin(theta), 0, Math.sin(theta), Math.cos(theta), 0]) + + if (origin && (origin.x !== 0 || origin.y !== 0)) { + return AffineTransform.multiply( + AffineTransform.translate(origin.x, origin.y), + rotation, + AffineTransform.translate(-origin.x, -origin.y), + ) + } + + return rotation + } + + rotate: (typeof AffineTransform)["rotate"] = (...args) => { + return this.prepend(AffineTransform.rotate(...args)) + } + + static scale(sx: number, sy: number = sx, origin: Point = { x: 0, y: 0 }): AffineTransform { + const scale = new AffineTransform([sx, 0, 0, 0, sy, 0]) + + if (origin.x !== 0 || origin.y !== 0) { + return AffineTransform.multiply( + AffineTransform.translate(origin.x, origin.y), + scale, + AffineTransform.translate(-origin.x, -origin.y), + ) + } + + return scale + } + + scale: (typeof AffineTransform)["scale"] = (...args) => { + return this.prepend(AffineTransform.scale(...args)) + } + + static translate(tx: number, ty: number): AffineTransform { + return new AffineTransform([1, 0, tx, 0, 1, ty]) + } + + translate: (typeof AffineTransform)["translate"] = (...args) => { + return this.prepend(AffineTransform.translate(...args)) + } + + static multiply(...[first, ...rest]: AffineTransform[]): AffineTransform { + if (!first) return AffineTransform.identity + return rest.reduce((result, item) => result.prepend(item), first) + } + + get a() { + return this.m00 + } + + get b() { + return this.m10 + } + + get c() { + return this.m01 + } + + get d() { + return this.m11 + } + + get tx() { + return this.m02 + } + + get ty() { + return this.m12 + } + + get scaleComponents(): Point { + return { x: this.a, y: this.d } + } + + get translationComponents(): Point { + return { x: this.tx, y: this.ty } + } + + get skewComponents(): Point { + return { x: this.c, y: this.b } + } + + toString() { + return `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.tx}, ${this.ty})` + } +} diff --git a/packages/utilities/rect/src/align.ts b/packages/utilities/rect/src/align.ts index a620b6d53c..2562af5dcf 100644 --- a/packages/utilities/rect/src/align.ts +++ b/packages/utilities/rect/src/align.ts @@ -1,46 +1,22 @@ -import type { Rect } from "./rect" +import type { AlignOptions, HAlign, Rect, VAlign } from "./types" function hAlign(a: Rect, ref: Rect, h: HAlign): Rect { let x = ref.minX - - if (h === "left-inside") { - x = ref.minX - } - if (h === "left-outside") { - x = ref.minX - ref.width - } - if (h === "right-inside") { - x = ref.maxX - ref.width - } - if (h === "right-outside") { - x = ref.maxX - } - if (h === "center") { - x = ref.midX - ref.width / 2 - } - + if (h === "left-inside") x = ref.minX + if (h === "left-outside") x = ref.minX - ref.width + if (h === "right-inside") x = ref.maxX - ref.width + if (h === "right-outside") x = ref.maxX + if (h === "center") x = ref.midX - ref.width / 2 return { ...a, x } } function vAlign(a: Rect, ref: Rect, v: VAlign): Rect { let y = ref.minY - - if (v === "top-inside") { - y = ref.minY - } - if (v === "top-outside") { - y = ref.minY - a.height - } - if (v === "bottom-inside") { - y = ref.maxY - a.height - } - if (v === "bottom-outside") { - y = ref.maxY - } - if (v === "center") { - y = ref.midY - a.height / 2 - } - + if (v === "top-inside") y = ref.minY + if (v === "top-outside") y = ref.minY - a.height + if (v === "bottom-inside") y = ref.maxY - a.height + if (v === "bottom-outside") y = ref.maxY + if (v === "center") y = ref.midY - a.height / 2 return { ...a, y } } @@ -48,12 +24,3 @@ export function alignRect(a: Rect, ref: Rect, options: AlignOptions): Rect { const { h, v } = options return vAlign(hAlign(a, ref, h), ref, v) } - -export type AlignOptions = { - h: HAlign - v: VAlign -} - -export type HAlign = "left-inside" | "left-outside" | "center" | "right-inside" | "right-outside" - -export type VAlign = "top-inside" | "top-outside" | "center" | "bottom-inside" | "bottom-outside" diff --git a/packages/utilities/rect/src/clamp.ts b/packages/utilities/rect/src/clamp.ts new file mode 100644 index 0000000000..868733ecf2 --- /dev/null +++ b/packages/utilities/rect/src/clamp.ts @@ -0,0 +1,26 @@ +import type { Point, RectInit, Size } from "./types" + +const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max) + +export const clampPoint = (position: Point, size: Size, boundaryRect: RectInit) => { + const x = clamp(position.x, boundaryRect.x, boundaryRect.x + boundaryRect.width - size.width) + const y = clamp(position.y, boundaryRect.y, boundaryRect.y + boundaryRect.height - size.height) + return { x, y } +} + +const defaultMinSize: Size = { + width: -Infinity, + height: -Infinity, +} + +const defaultMaxSize: Size = { + width: Infinity, + height: Infinity, +} + +export const clampSize = (size: Size, minSize = defaultMinSize, maxSize = defaultMaxSize) => { + return { + width: Math.min(Math.max(size.width, minSize!.width), maxSize!.width), + height: Math.min(Math.max(size.height, minSize!.height), maxSize!.height), + } +} diff --git a/packages/utilities/rect/src/closest.ts b/packages/utilities/rect/src/closest.ts index 753bd30291..deca544874 100644 --- a/packages/utilities/rect/src/closest.ts +++ b/packages/utilities/rect/src/closest.ts @@ -1,6 +1,5 @@ import { distance } from "./distance" -import type { Rect } from "./rect" -import type { Point, RectSide } from "./types" +import type { Point, Rect, RectSide } from "./types" export function closest(...pts: Point[]) { return (a: Point): Point => { @@ -11,18 +10,10 @@ export function closest(...pts: Point[]) { } export function closestSideToRect(ref: Rect, r: Rect): RectSide { - if (r.maxX <= ref.minX) { - return "left" - } - if (r.minX >= ref.maxX) { - return "right" - } - if (r.maxY <= ref.minY) { - return "top" - } - if (r.minY >= ref.maxY) { - return "bottom" - } + if (r.maxX <= ref.minX) return "left" + if (r.minX >= ref.maxX) return "right" + if (r.maxY <= ref.minY) return "top" + if (r.minY >= ref.maxY) return "bottom" return "left" } diff --git a/packages/utilities/rect/src/compass.ts b/packages/utilities/rect/src/compass.ts new file mode 100644 index 0000000000..72d4e37679 --- /dev/null +++ b/packages/utilities/rect/src/compass.ts @@ -0,0 +1,25 @@ +import type { Point } from "./types" + +export type CompassDirection = "n" | "ne" | "e" | "se" | "s" | "sw" | "w" | "nw" + +export const compassDirectionMap: Record = { + n: { x: 0.5, y: 0 }, + ne: { x: 1, y: 0 }, + e: { x: 1, y: 0.5 }, + se: { x: 1, y: 1 }, + s: { x: 0.5, y: 1 }, + sw: { x: 0, y: 1 }, + w: { x: 0, y: 0.5 }, + nw: { x: 0, y: 0 }, +} + +export const oppositeDirectionMap: Record = { + n: "s", + ne: "sw", + e: "w", + se: "nw", + s: "n", + sw: "ne", + w: "e", + nw: "se", +} diff --git a/packages/utilities/rect/src/contains.ts b/packages/utilities/rect/src/contains.ts index 2b60139a16..f84dfed498 100644 --- a/packages/utilities/rect/src/contains.ts +++ b/packages/utilities/rect/src/contains.ts @@ -1,5 +1,5 @@ -import { getRectCorners, isRect, type Rect } from "./rect" -import type { Point } from "./types" +import { getRectCorners, isRect } from "./rect" +import type { Point, Rect } from "./types" export function containsPoint(r: Rect, p: Point): boolean { return r.minX <= p.x && p.x <= r.maxX && r.minY <= p.y && p.y <= r.maxY diff --git a/packages/utilities/rect/src/distance.ts b/packages/utilities/rect/src/distance.ts index 394a7e9adf..f87b9b90b0 100644 --- a/packages/utilities/rect/src/distance.ts +++ b/packages/utilities/rect/src/distance.ts @@ -1,8 +1,7 @@ import { intersects } from "./intersection" -import type { Rect } from "./rect" -import type { Point, RectSide } from "./types" +import type { Point, Rect, RectSide } from "./types" -export type DistanceValue = Point & { +export interface DistanceValue extends Point { value: number } @@ -13,8 +12,10 @@ export function distance(a: Point, b: Point = { x: 0, y: 0 }): number { export function distanceFromPoint(r: Rect, p: Point): DistanceValue { let x = 0 let y = 0 + if (p.x < r.x) x = r.x - p.x else if (p.x > r.maxX) x = p.x - r.maxX + if (p.y < r.y) y = r.y - p.y else if (p.y > r.maxY) y = p.y - r.maxY return { x, y, value: distance({ x, y }) } diff --git a/packages/utilities/rect/src/from-element.ts b/packages/utilities/rect/src/from-element.ts index b55913ef0f..edaa71c5b5 100644 --- a/packages/utilities/rect/src/from-element.ts +++ b/packages/utilities/rect/src/from-element.ts @@ -1,6 +1,8 @@ -import { createRect, type Rect } from "./rect" +import { createRect } from "./rect" +import type { Rect } from "./types" const styleCache = new WeakMap() + function getCacheComputedStyle(el: HTMLElement) { if (!styleCache.has(el)) { const win = el.ownerDocument.defaultView || window diff --git a/packages/utilities/rect/src/from-points.ts b/packages/utilities/rect/src/from-points.ts index 88d66cecee..07c55520c4 100644 --- a/packages/utilities/rect/src/from-points.ts +++ b/packages/utilities/rect/src/from-points.ts @@ -1,5 +1,5 @@ -import { createRect, type Rect } from "./rect" -import type { Point } from "./types" +import { createRect } from "./rect" +import type { Point, Rect } from "./types" export function getRectFromPoints(...pts: Point[]): Rect { const xs = pts.map((p) => p.x) diff --git a/packages/utilities/rect/src/from-range.ts b/packages/utilities/rect/src/from-range.ts index 8851d314d3..d31c814110 100644 --- a/packages/utilities/rect/src/from-range.ts +++ b/packages/utilities/rect/src/from-range.ts @@ -1,6 +1,7 @@ -import { createRect, type Rect } from "./rect" +import { createRect } from "./rect" import { getElementRect } from "./from-element" import { union } from "./union" +import type { Rect } from "./types" export function fromRange(range: Range): Rect { let rs: Rect[] = [] diff --git a/packages/utilities/rect/src/from-rotation.ts b/packages/utilities/rect/src/from-rotation.ts index d7e3de6fb8..b488074175 100644 --- a/packages/utilities/rect/src/from-rotation.ts +++ b/packages/utilities/rect/src/from-rotation.ts @@ -1,5 +1,5 @@ -import { createRect, getRectCorners, type Rect } from "./rect" -import type { Point } from "./types" +import { createRect, getRectCorners } from "./rect" +import type { Point, Rect } from "./types" export function toRad(d: number) { return ((d % 360) * Math.PI) / 180 diff --git a/packages/utilities/rect/src/from-window.ts b/packages/utilities/rect/src/from-window.ts index fca3bfc8a3..05ac478ef8 100644 --- a/packages/utilities/rect/src/from-window.ts +++ b/packages/utilities/rect/src/from-window.ts @@ -1,4 +1,5 @@ -import { createRect, type Rect } from "./rect" +import { createRect } from "./rect" +import type { Rect } from "./types" export type WindowRectOptions = { /** diff --git a/packages/utilities/rect/src/get-polygon.ts b/packages/utilities/rect/src/get-polygon.ts deleted file mode 100644 index 578e18f9da..0000000000 --- a/packages/utilities/rect/src/get-polygon.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createRect, getRectCorners } from "./rect" -import type { RectValue } from "./types" - -export function getElementPolygon(rectValue: RectValue, placement: string) { - const rect = createRect(rectValue) - const { top, right, left, bottom } = getRectCorners(rect) - const [base] = placement.split("-") - - return { - top: [left, top, right, bottom], - right: [top, right, bottom, left], - bottom: [top, left, bottom, right], - left: [right, top, left, bottom], - }[base] -} diff --git a/packages/utilities/rect/src/index.ts b/packages/utilities/rect/src/index.ts index 12654e9c6b..3a8aeaf856 100644 --- a/packages/utilities/rect/src/index.ts +++ b/packages/utilities/rect/src/index.ts @@ -1,4 +1,6 @@ +export * from "./affine-transform" export * from "./align" +export * from "./clamp" export * from "./closest" export * from "./contains" export * from "./distance" @@ -7,10 +9,10 @@ export * from "./from-points" export * from "./from-range" export * from "./from-rotation" export * from "./from-window" -export * from "./get-polygon" export * from "./intersection" export * from "./operations" export * from "./polygon" export * from "./rect" +export * from "./resize" export * from "./types" export * from "./union" diff --git a/packages/utilities/rect/src/intersection.ts b/packages/utilities/rect/src/intersection.ts index d9c8ec5a21..75cc944c57 100644 --- a/packages/utilities/rect/src/intersection.ts +++ b/packages/utilities/rect/src/intersection.ts @@ -1,5 +1,5 @@ -import { createRect, type Rect } from "./rect" -import type { RectSide } from "./types" +import { createRect } from "./rect" +import type { Rect, RectSide } from "./types" /** * Checks if a Rect intersects another Rect diff --git a/packages/utilities/rect/src/operations.ts b/packages/utilities/rect/src/operations.ts index e892768903..8bb010dbcb 100644 --- a/packages/utilities/rect/src/operations.ts +++ b/packages/utilities/rect/src/operations.ts @@ -1,5 +1,5 @@ -import { createRect, type Rect } from "./rect" -import type { Point, RectInset, SymmetricRectInset } from "./types" +import { createRect } from "./rect" +import type { Point, Rect, RectInset, SymmetricRectInset } from "./types" export const isSymmetric = (v: any): v is SymmetricRectInset => "dx" in v || "dy" in v diff --git a/packages/utilities/rect/src/polygon.ts b/packages/utilities/rect/src/polygon.ts index 430716adf6..78af3b2ea6 100644 --- a/packages/utilities/rect/src/polygon.ts +++ b/packages/utilities/rect/src/polygon.ts @@ -1,4 +1,18 @@ -import type { Point } from "./types" +import { createRect, getRectCorners } from "./rect" +import type { Point, RectInit } from "./types" + +export function getElementPolygon(rectValue: RectInit, placement: string) { + const rect = createRect(rectValue) + const { top, right, left, bottom } = getRectCorners(rect) + const [base] = placement.split("-") + + return { + top: [left, top, right, bottom], + right: [top, right, bottom, left], + bottom: [top, left, bottom, right], + left: [right, top, left, bottom], + }[base] +} export function isPointInPolygon(polygon: Point[], point: Point) { const { x, y } = point diff --git a/packages/utilities/rect/src/rect.ts b/packages/utilities/rect/src/rect.ts index 57f9f6687d..1eb2963927 100644 --- a/packages/utilities/rect/src/rect.ts +++ b/packages/utilities/rect/src/rect.ts @@ -1,8 +1,23 @@ -import type { RectEdge, RectValue } from "./types" +import type { Point, Rect, RectEdge, RectInit } from "./types" -const point = (x: number, y: number) => ({ x, y }) +/* ----------------------------------------------------------------------------- + * Point + * -----------------------------------------------------------------------------*/ -export function createRect(r: RectValue) { +export const createPoint = (x: number, y: number) => ({ x, y }) + +export const subtractPoints = (a: Point, b: Point) => createPoint(a.x - b.x, a.y - b.y) +export const addPoints = (a: Point, b: Point) => createPoint(a.x + b.x, a.y + b.y) + +export function isPoint(v: any): v is Point { + return Reflect.has(v, "x") && Reflect.has(v, "y") +} + +/* ----------------------------------------------------------------------------- + * Rect + * -----------------------------------------------------------------------------*/ + +export function createRect(r: RectInit): Rect { const { x, y, width, height } = r const midX = x + width / 2 const midY = y + height / 2 @@ -17,32 +32,27 @@ export function createRect(r: RectValue) { maxY: y + height, midX, midY, - center: point(midX, midY), + center: createPoint(midX, midY), } } -export type Rect = ReturnType - -const hasProp = (obj: any, prop: T): obj is Record => - Object.prototype.hasOwnProperty.call(obj, prop) - export function isRect(v: any): v is Rect { - return hasProp(v, "x") && hasProp(v, "y") && hasProp(v, "width") && hasProp(v, "height") + return Reflect.has(v, "x") && Reflect.has(v, "y") && Reflect.has(v, "width") && Reflect.has(v, "height") } export function getRectCenters(v: Rect) { - const top = point(v.midX, v.minY) - const right = point(v.maxX, v.midY) - const bottom = point(v.midX, v.maxY) - const left = point(v.minX, v.midY) + const top = createPoint(v.midX, v.minY) + const right = createPoint(v.maxX, v.midY) + const bottom = createPoint(v.midX, v.maxY) + const left = createPoint(v.minX, v.midY) return { top, right, bottom, left } } export function getRectCorners(v: Rect) { - const top = point(v.minX, v.minY) - const right = point(v.maxX, v.minY) - const bottom = point(v.maxX, v.maxY) - const left = point(v.minX, v.maxY) + const top = createPoint(v.minX, v.minY) + const right = createPoint(v.maxX, v.minY) + const bottom = createPoint(v.maxX, v.maxY) + const left = createPoint(v.minX, v.maxY) return { top, right, bottom, left } } diff --git a/packages/utilities/rect/src/resize.ts b/packages/utilities/rect/src/resize.ts new file mode 100644 index 0000000000..bdc63b3001 --- /dev/null +++ b/packages/utilities/rect/src/resize.ts @@ -0,0 +1,106 @@ +import { AffineTransform } from "./affine-transform" +import { compassDirectionMap, oppositeDirectionMap, type CompassDirection } from "./compass" +import type { Point, Rect, RectInit, ScalingOptions } from "./types" + +const { sign, abs, min } = Math + +function getRectExtentPoint(rect: Rect, direction: CompassDirection) { + const { minX, minY, maxX, maxY, midX, midY } = rect + const x = direction.includes("w") ? minX : direction.includes("e") ? maxX : midX + const y = direction.includes("n") ? minY : direction.includes("s") ? maxY : midY + return { x, y } +} + +function getOppositeDirection(direction: CompassDirection) { + return oppositeDirectionMap[direction] +} + +export function resizeRect(rect: Rect, offset: Point, direction: CompassDirection, opts: ScalingOptions) { + const { scalingOriginMode, lockAspectRatio } = opts + + const extent = getRectExtentPoint(rect, direction) + + const oppositeDirection = getOppositeDirection(direction) + const oppositeExtent = getRectExtentPoint(rect, oppositeDirection) + + if (scalingOriginMode === "center") { + offset = { x: offset.x * 2, y: offset.y * 2 } + } + + const newExtent = { + x: extent.x + offset.x, + y: extent.y + offset.y, + } + + const multiplier = { + x: compassDirectionMap[direction].x * 2 - 1, + y: compassDirectionMap[direction].y * 2 - 1, + } + + const newSize = { + width: newExtent.x - oppositeExtent.x, + height: newExtent.y - oppositeExtent.y, + } + + const scaleX = (multiplier.x * newSize.width) / rect.width + const scaleY = (multiplier.y * newSize.height) / rect.height + + const largestMagnitude = abs(scaleX) > abs(scaleY) ? scaleX : scaleY + + const scale = lockAspectRatio + ? { x: largestMagnitude, y: largestMagnitude } + : { + x: extent.x === oppositeExtent.x ? 1 : scaleX, + y: extent.y === oppositeExtent.y ? 1 : scaleY, + } + + if (extent.y === oppositeExtent.y) { + scale.y = abs(scale.y) + } else if (sign(scale.y) !== sign(scaleY)) { + scale.y *= -1 + } + + if (extent.x === oppositeExtent.x) { + scale.x = abs(scale.x) + } else if (sign(scale.x) !== sign(scaleX)) { + scale.x *= -1 + } + + switch (scalingOriginMode) { + case "extent": + return transformRect(rect, AffineTransform.scale(scale.x, scale.y, oppositeExtent), false) + case "center": + return transformRect( + rect, + AffineTransform.scale(scale.x, scale.y, { + x: rect.midX, + y: rect.midY, + }), + false, + ) + } +} + +function createRectFromPoints(initialPoint: Point, finalPoint: Point, normalized: boolean = true): RectInit { + if (normalized) { + return { + x: min(finalPoint.x, initialPoint.x), + y: min(finalPoint.y, initialPoint.y), + width: abs(finalPoint.x - initialPoint.x), + height: abs(finalPoint.y - initialPoint.y), + } + } + + return { + x: initialPoint.x, + y: initialPoint.y, + width: finalPoint.x - initialPoint.x, + height: finalPoint.y - initialPoint.y, + } +} + +function transformRect(rect: Rect, transform: AffineTransform, normalized = true): RectInit { + const p1 = transform.applyTo({ x: rect.minX, y: rect.minY }) + const p2 = transform.applyTo({ x: rect.maxX, y: rect.maxY }) + return createRectFromPoints(p1, p2, normalized) +} diff --git a/packages/utilities/rect/src/types.ts b/packages/utilities/rect/src/types.ts index 161c434524..6feb0d2054 100644 --- a/packages/utilities/rect/src/types.ts +++ b/packages/utilities/rect/src/types.ts @@ -1,12 +1,38 @@ -export type Point = { x: number; y: number } +/* ----------------------------------------------------------------------------- + * Basic + * -----------------------------------------------------------------------------*/ -export type RectValue = { +export interface Point { x: number y: number +} + +export interface Size { width: number height: number } +export interface Bounds { + minX: number + midX: number + maxX: number + minY: number + midY: number + maxY: number +} + +export interface CenterPoint { + center: Point +} + +export interface RectInit extends Point, Size {} + +export interface Rect extends Point, Size, Bounds, CenterPoint {} + +/* ----------------------------------------------------------------------------- + * Edge and Side + * -----------------------------------------------------------------------------*/ + export type RectSide = "top" | "right" | "bottom" | "left" export type RectPoint = @@ -42,4 +68,25 @@ export type RectCenters = Record & { export type RectInset = Partial> -export type SymmetricRectInset = { dx?: number; dy?: number } +export interface SymmetricRectInset { + dx?: number + dy?: number +} + +export interface ScalingOptions { + scalingOriginMode: "center" | "extent" + lockAspectRatio: boolean +} + +/* ----------------------------------------------------------------------------- + * Alignment + * -----------------------------------------------------------------------------*/ + +export interface AlignOptions { + h: HAlign + v: VAlign +} + +export type HAlign = "left-inside" | "left-outside" | "center" | "right-inside" | "right-outside" + +export type VAlign = "top-inside" | "top-outside" | "center" | "bottom-inside" | "bottom-outside" diff --git a/packages/utilities/rect/src/union.ts b/packages/utilities/rect/src/union.ts index 298222de91..ede981476c 100644 --- a/packages/utilities/rect/src/union.ts +++ b/packages/utilities/rect/src/union.ts @@ -1,28 +1,16 @@ import { getRectFromPoints } from "./from-points" -import type { Rect } from "./rect" +import type { Rect } from "./types" const { min, max } = Math export function union(...rs: Rect[]): Rect { const pMin = { - x: min.apply( - Math, - rs.map((r) => r.minX), - ), - y: min.apply( - Math, - rs.map((r) => r.minY), - ), + x: min(...rs.map((r) => r.minX)), + y: min(...rs.map((r) => r.minY)), } const pMax = { - x: max.apply( - Math, - rs.map((r) => r.maxX), - ), - y: max.apply( - Math, - rs.map((r) => r.maxY), - ), + x: max(...rs.map((r) => r.maxX)), + y: max(...rs.map((r) => r.maxY)), } return getRectFromPoints(pMin, pMax) } diff --git a/shared/src/style.css b/shared/src/style.css index cfb1d92665..9f23630b4d 100644 --- a/shared/src/style.css +++ b/shared/src/style.css @@ -26,6 +26,10 @@ ul { padding-inline-start: 0; } +[hidden] { + display: none !important; +} + .pre { background: rgba(0, 0, 0, 0.05); padding: 20px; @@ -1986,19 +1990,27 @@ main [data-testid="scrubber"] { [data-scope="floating-panel"][data-part="content"] { border: 1px solid #ebebeb; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + box-shadow: + rgba(0, 0, 0, 0.28) 0px 16px 18px 0px, + rgba(0, 0, 0, 0.12) 0px 4px 16px 0px; outline: 0 !important; background-color: white; + display: flex; + flex-direction: column; } [data-scope="floating-panel"][data-part="body"] { - padding: 24px; + position: relative; + overflow: auto; + flex: 1 1 auto; + padding-block: 16px; + padding-inline: 16px; background-color: white; } [data-scope="floating-panel"][data-part="header"] { - padding-inline: 16px; padding-block: 4px; + padding-inline: 16px; background-color: #f5f5f5; border-bottom: 1px solid #ebebeb; display: flex; @@ -2006,13 +2018,26 @@ main [data-testid="scrubber"] { align-items: center; } -[data-scope="floating-panel"][data-part="close-trigger"] { +[data-scope="floating-panel"][data-part="header"] button { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 14px; + padding: 0; svg { width: 1em; height: 1em; } } +[data-scope="floating-panel"][data-part="trigger-group"] { + display: flex; + align-items: center; + gap: 8px; +} + [data-scope="floating-panel"][data-part="resize-trigger"] { background-color: rgba(154, 18, 18, 0.396); From 7e3ecbbf423490801200bfa9bbb406668fa034d7 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Thu, 21 Mar 2024 22:04:51 +0000 Subject: [PATCH 13/19] refactor: stage types --- .../floating-panel/src/floating-panel.machine.ts | 6 ++++++ .../floating-panel/src/floating-panel.types.ts | 16 +++++++++------- packages/machines/floating-panel/src/index.ts | 11 ++++++++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index 99609a8f7f..4504be68ce 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -275,6 +275,12 @@ export function machine(userContext: UserDefinedContext) { invokeOnResizeEnd(ctx) { ctx.onResizeEnd?.({ size: ctx.size }) }, + invokeOnMinimize(ctx) { + ctx.onStageChange?.({ stage: "minimized" }) + }, + invokeOnMaximize(ctx) { + ctx.onStageChange?.({ stage: "maximized" }) + }, }, }, ) diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index 86437a5109..cc3da8beab 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -14,6 +14,12 @@ export interface OpenChangeDetails { open: boolean } +export type Stage = "minimized" | "maximized" + +export interface StageChangeDetails { + stage: Stage +} + interface PublicContext extends DirectionProperty, CommonProperties { /** * Whether the panel is open @@ -88,13 +94,9 @@ interface PublicContext extends DirectionProperty, CommonProperties { */ gridSize: number /** - * Function called when the panel is minimized - */ - onMinimize?(): void - /** - * Function called when the panel is maximized + * Function called when the stage of the panel changes */ - onMaximize?(): void + onStageChange?(details: StageChangeDetails): void } interface PrivateContext { @@ -117,7 +119,7 @@ interface PrivateContext { /** * The stage of the panel */ - stage?: "minimized" | "maximized" + stage?: Stage } type ComputedContext = Readonly<{}> diff --git a/packages/machines/floating-panel/src/index.ts b/packages/machines/floating-panel/src/index.ts index 4ae58ea0b2..548cbbcb26 100644 --- a/packages/machines/floating-panel/src/index.ts +++ b/packages/machines/floating-panel/src/index.ts @@ -2,4 +2,13 @@ export { anatomy } from "./floating-panel.anatomy" export { connect } from "./floating-panel.connect" export { machine } from "./floating-panel.machine" export * from "./floating-panel.props" -export type { UserDefinedContext as Context } from "./floating-panel.types" +export type { + UserDefinedContext as Context, + DockProps, + DragDetails, + OpenChangeDetails, + ResizeDetails, + ResizeTriggerAxis, + ResizeTriggerProps, + StageChangeDetails, +} from "./floating-panel.types" From fca5f1f2c1c2062dec3168bbd42a69223ef3ed04 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Fri, 22 Mar 2024 14:31:44 +0000 Subject: [PATCH 14/19] feat: track boundary element --- .xstate/floating-panel.js | 6 ++-- .../src/floating-panel.machine.ts | 32 +++++++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.xstate/floating-panel.js b/.xstate/floating-panel.js index 6024abfa0f..59f3eb4250 100644 --- a/.xstate/floating-panel.js +++ b/.xstate/floating-panel.js @@ -34,7 +34,7 @@ const fetchMachine = createMachine({ }, open: { tags: ["open"], - entry: ["setBoundaryRect"], + activities: ["trackBoundaryRect"], on: { DRAG_START: { cond: "!isMaximized", @@ -68,7 +68,7 @@ const fetchMachine = createMachine({ }, "open.dragging": { tags: ["open"], - activities: ["trackPointerMove"], + activities: ["trackPointerMove", "trackBoundaryRect"], exit: ["clearPrevPosition"], on: { DRAG: { @@ -89,7 +89,7 @@ const fetchMachine = createMachine({ }, "open.resizing": { tags: ["open"], - activities: ["trackPointerMove"], + activities: ["trackPointerMove", "trackBoundaryRect"], exit: ["clearPrevSize"], on: { DRAG: { diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index 4504be68ce..495952b7f3 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -1,5 +1,5 @@ import { createMachine, guards } from "@zag-js/core" -import { trackPointerMove } from "@zag-js/dom-event" +import { addDomEvent, trackPointerMove } from "@zag-js/dom-event" import { isHTMLElement } from "@zag-js/dom-query" import { addPoints, @@ -52,7 +52,7 @@ export function machine(userContext: UserDefinedContext) { open: { tags: ["open"], - entry: ["setBoundaryRect"], + activities: ["trackBoundaryRect"], on: { DRAG_START: { guard: not("isMaximized"), @@ -87,7 +87,7 @@ export function machine(userContext: UserDefinedContext) { "open.dragging": { tags: ["open"], - activities: ["trackPointerMove"], + activities: ["trackPointerMove", "trackBoundaryRect"], exit: ["clearPrevPosition"], on: { DRAG: { @@ -109,7 +109,7 @@ export function machine(userContext: UserDefinedContext) { "open.resizing": { tags: ["open"], - activities: ["trackPointerMove"], + activities: ["trackPointerMove", "trackBoundaryRect"], exit: ["clearPrevSize"], on: { DRAG: { @@ -149,14 +149,26 @@ export function machine(userContext: UserDefinedContext) { }, }) }, - }, - actions: { - setBoundaryRect(ctx) { - const el = ctx.getBoundaryEl?.() + trackBoundaryRect(ctx) { const win = dom.getWin(ctx) - const rect = isHTMLElement(el) ? getElementRect(el) : getWindowRect(win) - ctx.boundaryRect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height } + const el = ctx.getBoundaryEl?.() + + if (isHTMLElement(el)) { + ctx.boundaryRect = getElementRect(el) + const obs = new win.ResizeObserver(() => { + ctx.boundaryRect = getElementRect(el) + }) + obs.observe(el) + return () => obs.disconnect() + } + + ctx.boundaryRect = getWindowRect(win) + return addDomEvent(win, "resize", () => { + ctx.boundaryRect = getWindowRect(win) + }) }, + }, + actions: { setPrevPosition(ctx, evt) { ctx.prevPosition = { ...ctx.position } ctx.lastEventPosition = evt.position From de33f65e65f43ecc3614ea3138c68abee51445c1 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Fri, 22 Mar 2024 16:27:40 +0000 Subject: [PATCH 15/19] feat: complete v1 --- .../src/floating-panel.connect.ts | 57 +++----- .../floating-panel/src/floating-panel.dom.ts | 2 + .../src/floating-panel.machine.ts | 138 ++++++++++++------ .../src/floating-panel.props.ts | 11 +- .../src/floating-panel.types.ts | 55 +++++-- .../src/{utils => }/get-resize-axis-style.ts | 2 +- packages/machines/floating-panel/src/index.ts | 6 +- .../src/utils/get-intersection.ts | 32 ---- packages/utilities/core/src/object.ts | 13 ++ packages/utilities/rect/src/clamp.ts | 4 +- packages/utilities/rect/src/constrain.ts | 18 +++ packages/utilities/rect/src/equality.ts | 13 ++ packages/utilities/rect/src/index.ts | 2 + 13 files changed, 223 insertions(+), 130 deletions(-) rename packages/machines/floating-panel/src/{utils => }/get-resize-axis-style.ts (94%) delete mode 100644 packages/machines/floating-panel/src/utils/get-intersection.ts create mode 100644 packages/utilities/rect/src/constrain.ts create mode 100644 packages/utilities/rect/src/equality.ts diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts index 9108bb0fc7..6eb9cad497 100644 --- a/packages/machines/floating-panel/src/floating-panel.connect.ts +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -3,17 +3,14 @@ import { dataAttr, getEventTarget } from "@zag-js/dom-query" import type { NormalizeProps, PropTypes } from "@zag-js/types" import { parts } from "./floating-panel.anatomy" import { dom } from "./floating-panel.dom" -import type { DockProps, ResizeTriggerProps, Send, State } from "./floating-panel.types" -import { getResizeAxisStyle } from "./utils/get-resize-axis-style" +import type { MachineApi, ResizeTriggerProps, Send, State } from "./floating-panel.types" +import { getResizeAxisStyle } from "./get-resize-axis-style" -export function connect(state: State, send: Send, normalize: NormalizeProps) { +export function connect(state: State, send: Send, normalize: NormalizeProps): MachineApi { const isOpen = state.hasTag("open") const isDragging = state.matches("open.dragging") const isResizing = state.matches("open.resizing") - const isMaximized = state.context.stage === "maximized" - const isMinimized = state.context.stage === "minimized" - return { isOpen, isDragging, @@ -22,10 +19,13 @@ export function connect(state: State, send: Send, normalize triggerProps: normalize.button({ ...parts.trigger.attrs, type: "button", + disabled: state.context.isDisabled, id: dom.getTriggerId(state.context), "data-state": isOpen ? "open" : "closed", + "data-dragging": dataAttr(isDragging), "aria-controls": dom.getContentId(state.context), onClick() { + if (state.context.isDisabled) return send({ type: "OPEN" }) }, }), @@ -45,14 +45,15 @@ export function connect(state: State, send: Send, normalize role: "dialog", tabIndex: 0, hidden: !isOpen, - "data-state": isOpen ? "open" : "closed", id: dom.getContentId(state.context), "aria-labelledby": dom.getTitleId(state.context), + "data-state": isOpen ? "open" : "closed", "data-dragging": dataAttr(isDragging), style: { position: "relative", width: "var(--width)", height: "var(--height)", + overflow: state.context.isMinimized ? "hidden" : undefined, }, onKeyDown(event) { const keyMap: EventKeyMap = { @@ -76,7 +77,7 @@ export function connect(state: State, send: Send, normalize closeTriggerProps: normalize.button({ ...parts.closeTrigger.attrs, - disabled: state.context.disabled, + disabled: state.context.isDisabled, "aria-label": "Close Window", type: "button", onClick() { @@ -86,9 +87,9 @@ export function connect(state: State, send: Send, normalize minimizeTriggerProps: normalize.button({ ...parts.minimizeTrigger.attrs, - disabled: state.context.disabled, + disabled: state.context.isDisabled, "aria-label": "Minimize Window", - hidden: isMinimized || isMaximized, + hidden: state.context.isStaged, type: "button", onClick() { send("MINIMIZE") @@ -97,9 +98,9 @@ export function connect(state: State, send: Send, normalize maximizeTriggerProps: normalize.button({ ...parts.maximizeTrigger.attrs, - disabled: state.context.disabled, + disabled: state.context.isDisabled, "aria-label": "Maximize Window", - hidden: isMaximized || isMinimized, + hidden: state.context.isStaged, type: "button", onClick() { send("MAXIMIZE") @@ -108,9 +109,9 @@ export function connect(state: State, send: Send, normalize restoreTriggerProps: normalize.button({ ...parts.restoreTrigger.attrs, - disabled: state.context.disabled, + disabled: state.context.isDisabled, "aria-label": "Restore Window", - hidden: !(isMaximized || isMinimized), + hidden: !state.context.isStaged, type: "button", onClick() { send("RESTORE") @@ -118,16 +119,14 @@ export function connect(state: State, send: Send, normalize }), getResizeTriggerProps(props: ResizeTriggerProps) { - const disabled = !state.context.resizable || state.context.disabled return normalize.element({ ...parts.resizeTrigger.attrs, - disabled, - "data-disabled": dataAttr(disabled), + "data-disabled": dataAttr(!state.context.canResize), "data-axis": props.axis, onPointerDown(event) { - if (disabled || event.button == 2) return - event.currentTarget.setPointerCapture(event.pointerId) + if (!state.context.canResize || event.button == 2) return + event.currentTarget.setPointerCapture(event.pointerId) event.stopPropagation() send({ @@ -146,13 +145,14 @@ export function connect(state: State, send: Send, normalize dragTriggerProps: normalize.element({ ...parts.dragTrigger.attrs, - disabled: state.context.draggable || state.context.disabled, + "data-disabled": dataAttr(!state.context.canDrag), onPointerDown(event) { - if (!state.context.draggable || state.context.disabled || event.button == 2) return + if (!state.context.canDrag || event.button == 2) return const target = getEventTarget(getNativeEvent(event)) if (target?.closest("button")) { + console.log(target?.closest("button")) return } @@ -166,7 +166,7 @@ export function connect(state: State, send: Send, normalize }) }, onDoubleClick() { - send("MAXIMIZE") + send(state.context.isMaximized ? "RESTORE" : "MAXIMIZE") }, style: { userSelect: "none", @@ -175,16 +175,6 @@ export function connect(state: State, send: Send, normalize }, }), - getDockProps(props: DockProps) { - const isIntersecting = true - return normalize.element({ - ...parts.dock.attrs, - "data-ownedby": state.context.id, - "data-dock": props.id, - "data-intersecting": dataAttr(isIntersecting), - }) - }, - titleProps: normalize.element({ ...parts.title.attrs, id: dom.getTitleId(state.context), @@ -192,13 +182,14 @@ export function connect(state: State, send: Send, normalize headerProps: normalize.element({ ...parts.header.attrs, + id: dom.getHeaderId(state.context), "data-dragging": dataAttr(isDragging), }), bodyProps: normalize.element({ ...parts.body.attrs, "data-dragging": dataAttr(isDragging), - hidden: isMinimized, + hidden: state.context.isMinimized, }), } } diff --git a/packages/machines/floating-panel/src/floating-panel.dom.ts b/packages/machines/floating-panel/src/floating-panel.dom.ts index f0d62ed08b..5df0f2bbe1 100644 --- a/packages/machines/floating-panel/src/floating-panel.dom.ts +++ b/packages/machines/floating-panel/src/floating-panel.dom.ts @@ -6,8 +6,10 @@ export const dom = createScope({ getPositionerId: (ctx: Ctx) => `float:${ctx.id}:positioner`, getContentId: (ctx: Ctx) => `float:${ctx.id}:content`, getTitleId: (ctx: Ctx) => `float:${ctx.id}:title`, + getHeaderId: (ctx: Ctx) => `float:${ctx.id}:header`, getTriggerEl: (ctx: Ctx) => dom.getById(ctx, dom.getTriggerId(ctx)), getPositionerEl: (ctx: Ctx) => dom.getById(ctx, dom.getPositionerId(ctx)), getContentEl: (ctx: Ctx) => dom.getById(ctx, dom.getContentId(ctx)), + getHeaderEl: (ctx: Ctx) => dom.getById(ctx, dom.getHeaderId(ctx)), }) diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index 495952b7f3..d69af97a68 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -5,13 +5,19 @@ import { addPoints, clampPoint, clampSize, + constrainRect, createRect, getElementRect, getWindowRect, + isPointEqual, + isSizeEqual, resizeRect, subtractPoints, + type Point, + type Rect, + type Size, } from "@zag-js/rect-utils" -import { compact } from "@zag-js/utils" +import { compact, pick } from "@zag-js/utils" import { dom } from "./floating-panel.dom" import type { MachineContext, MachineState, UserDefinedContext } from "./floating-panel.types" @@ -27,13 +33,26 @@ export function machine(userContext: UserDefinedContext) { size: { width: 320, height: 400 }, position: { x: 300, y: 100 }, gridSize: 1, + disabled: false, + resizable: true, + draggable: true, ...ctx, + stage: undefined, lastEventPosition: null, prevPosition: null, prevSize: null, boundaryRect: null, }, + computed: { + isMaximized: (ctx) => ctx.stage === "maximized", + isMinimized: (ctx) => ctx.stage === "minimized", + isStaged: (ctx) => !!ctx.stage, + isDisabled: (ctx) => !!ctx.disabled, + canResize: (ctx) => (ctx.resizable || !ctx.isDisabled) && !ctx.stage, + canDrag: (ctx) => (ctx.draggable || !ctx.isDisabled) && !ctx.isMaximized, + }, + watch: { position: ["setPositionStyle"], size: ["setSizeStyle"], @@ -133,8 +152,8 @@ export function machine(userContext: UserDefinedContext) { { guards: { closeOnEsc: (ctx) => !!ctx.closeOnEscape, - isMaximized: (ctx) => ctx.stage === "maximized", - isMinimized: (ctx) => ctx.stage === "minimized", + isMaximized: (ctx) => ctx.isMaximized, + isMinimized: (ctx) => ctx.isMinimized, }, activities: { trackPointerMove(ctx, _evt, { send }) { @@ -153,19 +172,35 @@ export function machine(userContext: UserDefinedContext) { const win = dom.getWin(ctx) const el = ctx.getBoundaryEl?.() + const adjust = (boundary: Rect) => { + const boundaryRect = pick(boundary, ["width", "height", "x", "y"]) + ctx.boundaryRect = boundaryRect + + const res = ctx.isMaximized + ? boundaryRect + : constrainRect( + { + ...ctx.position, + ...ctx.size, + }, + boundaryRect, + ) + + set.size(ctx, pick(res, ["width", "height"])) + set.position(ctx, pick(res, ["x", "y"])) + } + if (isHTMLElement(el)) { - ctx.boundaryRect = getElementRect(el) - const obs = new win.ResizeObserver(() => { - ctx.boundaryRect = getElementRect(el) - }) + const exec = () => adjust(getElementRect(el)) + exec() + const obs = new win.ResizeObserver(exec) obs.observe(el) return () => obs.disconnect() } - ctx.boundaryRect = getWindowRect(win) - return addDomEvent(win, "resize", () => { - ctx.boundaryRect = getWindowRect(win) - }) + const exec = () => adjust(getWindowRect(win)) + exec() + return addDomEvent(win, "resize", exec) }, }, actions: { @@ -183,9 +218,10 @@ export function machine(userContext: UserDefinedContext) { diff.x = Math.round(diff.x / ctx.gridSize!) * ctx.gridSize! diff.y = Math.round(diff.y / ctx.gridSize!) * ctx.gridSize! - const position = addPoints(ctx.prevPosition!, diff) - const point = clampPoint(position, ctx.size, ctx.boundaryRect!) - ctx.position = point + let position = addPoints(ctx.prevPosition!, diff) + position = clampPoint(position, ctx.size, ctx.boundaryRect!) + + set.position(ctx, position) }, setPositionStyle(ctx) { const el = dom.getPositionerEl(ctx) @@ -195,8 +231,8 @@ export function machine(userContext: UserDefinedContext) { resetRect(ctx, _evt, { initialContext }) { ctx.stage = undefined if (!ctx.preserveOnClose) { - ctx.position = initialContext.position - ctx.size = initialContext.size + set.position(ctx, initialContext.position) + set.size(ctx, initialContext.size) } }, setPrevSize(ctx, evt) { @@ -220,58 +256,61 @@ export function machine(userContext: UserDefinedContext) { lockAspectRatio: !!ctx.lockAspectRatio || evt.shiftKey, }) - let nextSize = { width: nextRect.width, height: nextRect.height } - let nextPosition = { x: nextRect.x, y: nextRect.y } + let nextSize = pick(nextRect, ["width", "height"]) + let nextPosition = pick(nextRect, ["x", "y"]) nextSize = clampSize(nextSize, ctx.minSize, ctx.maxSize) - ctx.size = nextSize + set.size(ctx, nextSize) if (nextPosition) { const point = clampPoint(nextPosition, nextSize, ctx.boundaryRect!) - ctx.position = point + set.position(ctx, point) } }, setSizeStyle(ctx) { const el = dom.getPositionerEl(ctx) - - if (ctx.size.width != null) { - el?.style.setProperty("--width", `${ctx.size.width}px`) - } else { - el?.style.removeProperty("--width") - } - - if (ctx.size.height != null) { - el?.style.setProperty("--height", `${ctx.size.height}px`) - } else { - el?.style.removeProperty("--height") - } + el?.style.setProperty("--width", `${ctx.size.width}px`) + el?.style.setProperty("--height", `${ctx.size.height}px`) }, setMaximized(ctx) { - // set max size + // set max stage ctx.stage = "maximized" + + // save previous ctx.prevSize = ctx.size ctx.prevPosition = ctx.position + // update size - ctx.position = { x: 0, y: 0 } - ctx.size = { width: ctx.boundaryRect!.width, height: ctx.boundaryRect!.height } + set.position(ctx, { x: 0, y: 0 }) + set.size(ctx, pick(ctx.boundaryRect!, ["height", "width"])) }, setMinimized(ctx) { - // set min size + // set min stage ctx.stage = "minimized" + + // save previous ctx.prevSize = ctx.size ctx.prevPosition = ctx.position + // update size - const size: any = { ...ctx.size } - delete size.height - ctx.size = size + const headerEl = dom.getHeaderEl(ctx) + if (!headerEl) return + const size = { + ...ctx.size, + height: headerEl?.offsetHeight, + } + set.size(ctx, size) }, setRestored(ctx) { + // remove stage ctx.stage = undefined - if (!ctx.prevSize || !ctx.prevPosition) return + // restore size + if (!ctx.prevSize || !ctx.prevPosition) return ctx.size = ctx.prevSize ctx.position = ctx.prevPosition - // clear prev size + + // clear previous ctx.prevSize = null ctx.prevPosition = null }, @@ -282,10 +321,10 @@ export function machine(userContext: UserDefinedContext) { ctx.onOpenChange?.({ open: false }) }, invokeOnDragEnd(ctx) { - ctx.onDragEnd?.({ position: ctx.position }) + ctx.onPositionChangeEnd?.({ position: ctx.position }) }, invokeOnResizeEnd(ctx) { - ctx.onResizeEnd?.({ size: ctx.size }) + ctx.onSizeChangeEnd?.({ size: ctx.size }) }, invokeOnMinimize(ctx) { ctx.onStageChange?.({ stage: "minimized" }) @@ -297,3 +336,16 @@ export function machine(userContext: UserDefinedContext) { }, ) } + +const set = { + size(ctx: MachineContext, value: Size) { + if (isSizeEqual(ctx.size, value)) return + ctx.size = value + ctx.onSizeChange?.({ size: value }) + }, + position(ctx: MachineContext, value: Point) { + if (isPointEqual(ctx.position, value)) return + ctx.position = value + ctx.onPositionChange?.({ position: value }) + }, +} diff --git a/packages/machines/floating-panel/src/floating-panel.props.ts b/packages/machines/floating-panel/src/floating-panel.props.ts index caccc17b3d..3121f202d0 100644 --- a/packages/machines/floating-panel/src/floating-panel.props.ts +++ b/packages/machines/floating-panel/src/floating-panel.props.ts @@ -14,13 +14,12 @@ export const props = createProps()([ "lockAspectRatio", "maxSize", "minSize", - "onDrag", - "onDragEnd", - "onMaximize", - "onMinimize", + "onPositionChange", + "onPositionChangeEnd", "onOpenChange", - "onResize", - "onResizeEnd", + "onStageChange", + "onSizeChange", + "onSizeChangeEnd", "open", "position", "preserveOnClose", diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index cc3da8beab..b48d4ef31f 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -1,12 +1,12 @@ import type { StateMachine as S } from "@zag-js/core" import type { Point, RectInit, Size } from "@zag-js/rect-utils" -import type { CommonProperties, DirectionProperty, RequiredBy } from "@zag-js/types" +import type { CommonProperties, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" -export interface DragDetails { +export interface PositionChangeDetails { position: Point } -export interface ResizeDetails { +export interface SizeChangeDetails { size: Size } @@ -68,11 +68,11 @@ interface PublicContext extends DirectionProperty, CommonProperties { /** * Function called when the position of the panel changes via dragging */ - onDrag?(details: DragDetails): void + onPositionChange?(details: PositionChangeDetails): void /** * Function called when the position of the panel changes via dragging ends */ - onDragEnd?(details: DragDetails): void + onPositionChangeEnd?(details: PositionChangeDetails): void /** * Function called when the panel is opened or closed */ @@ -80,11 +80,11 @@ interface PublicContext extends DirectionProperty, CommonProperties { /** * Function called when the size of the panel changes via resizing */ - onResize?(details: ResizeDetails): void + onSizeChange?(details: SizeChangeDetails): void /** * Function called when the size of the panel changes via resizing ends */ - onResizeEnd?(details: ResizeDetails): void + onSizeChangeEnd?(details: SizeChangeDetails): void /** * Whether the panel size and position should be preserved when it is closed */ @@ -122,7 +122,14 @@ interface PrivateContext { stage?: Stage } -type ComputedContext = Readonly<{}> +type ComputedContext = Readonly<{ + isMaximized: boolean + isMinimized: boolean + isStaged: boolean + isDisabled: boolean + canResize: boolean + canDrag: boolean +}> export type UserDefinedContext = RequiredBy @@ -137,12 +144,40 @@ export type State = S.State export type Send = S.Send +/* ----------------------------------------------------------------------------- + * Component props + * -----------------------------------------------------------------------------*/ + export type ResizeTriggerAxis = "s" | "w" | "e" | "n" | "sw" | "nw" | "se" | "ne" export interface ResizeTriggerProps { axis: ResizeTriggerAxis } -export interface DockProps { - id: string +export interface MachineApi { + /** + * Whether the panel is open + */ + isOpen: boolean + /** + * Whether the panel is being dragged + */ + isDragging: boolean + /** + * Whether the panel is being resized + */ + isResizing: boolean + + dragTriggerProps: T["element"] + getResizeTriggerProps(props: ResizeTriggerProps): T["element"] + triggerProps: T["button"] + positionerProps: T["element"] + contentProps: T["element"] + titleProps: T["element"] + headerProps: T["element"] + bodyProps: T["element"] + closeTriggerProps: T["button"] + minimizeTriggerProps: T["button"] + maximizeTriggerProps: T["button"] + restoreTriggerProps: T["button"] } diff --git a/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts b/packages/machines/floating-panel/src/get-resize-axis-style.ts similarity index 94% rename from packages/machines/floating-panel/src/utils/get-resize-axis-style.ts rename to packages/machines/floating-panel/src/get-resize-axis-style.ts index 51227297a4..61ac70fb21 100644 --- a/packages/machines/floating-panel/src/utils/get-resize-axis-style.ts +++ b/packages/machines/floating-panel/src/get-resize-axis-style.ts @@ -1,5 +1,5 @@ import type { JSX } from "@zag-js/types" -import type { ResizeTriggerAxis } from "../floating-panel.types" +import type { ResizeTriggerAxis } from "./floating-panel.types" export function getResizeAxisStyle(axis: ResizeTriggerAxis): JSX.CSSProperties { switch (axis) { diff --git a/packages/machines/floating-panel/src/index.ts b/packages/machines/floating-panel/src/index.ts index 548cbbcb26..e6d68aaf39 100644 --- a/packages/machines/floating-panel/src/index.ts +++ b/packages/machines/floating-panel/src/index.ts @@ -4,11 +4,11 @@ export { machine } from "./floating-panel.machine" export * from "./floating-panel.props" export type { UserDefinedContext as Context, - DockProps, - DragDetails, + PositionChangeDetails, OpenChangeDetails, - ResizeDetails, + SizeChangeDetails, ResizeTriggerAxis, ResizeTriggerProps, StageChangeDetails, + MachineApi as Api, } from "./floating-panel.types" diff --git a/packages/machines/floating-panel/src/utils/get-intersection.ts b/packages/machines/floating-panel/src/utils/get-intersection.ts deleted file mode 100644 index 124391ef6f..0000000000 --- a/packages/machines/floating-panel/src/utils/get-intersection.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Rect } from "../floating-panel.types" - -export function getIntersection(r1: Rect, r2: Rect) { - const xOverlap = Math.max(0, Math.min(r1.x + r1.width, r2.x + r2.width) - Math.max(r1.x, r2.x)) - const yOverlap = Math.max(0, Math.min(r1.y + r1.height, r2.y + r2.height) - Math.max(r1.y, r2.y)) - - const intersectionRect = - xOverlap > 0 && yOverlap > 0 - ? { - x: Math.max(r1.x, r2.x), - y: Math.max(r1.y, r2.y), - width: xOverlap, - height: yOverlap, - } - : null - - const intersectionArea = intersectionRect ? intersectionRect.width * intersectionRect.height : 0 - - const r1Area = r1.width * r1.height - const r2Area = r2.width * r2.height - - const unionArea = r1Area + r2Area - intersectionArea - - const intersectionRatio = unionArea > 0 ? intersectionArea / unionArea : 0 - const isIntersecting = intersectionRatio > 0 - - return { - isIntersecting, - intersectionRatio, - intersectionRect, - } -} diff --git a/packages/utilities/core/src/object.ts b/packages/utilities/core/src/object.ts index 56fb63845f..90d0329a6f 100644 --- a/packages/utilities/core/src/object.ts +++ b/packages/utilities/core/src/object.ts @@ -21,3 +21,16 @@ export function json(value: any) { const isPlainObject = (value: any) => { return value && typeof value === "object" && value.constructor === Object } + +export function pick, K extends keyof T>(obj: T, keys: K[]): Pick { + const filtered: Partial = {} + + for (const key of keys) { + const value = obj[key] + if (value !== undefined) { + filtered[key] = value + } + } + + return filtered as any +} diff --git a/packages/utilities/rect/src/clamp.ts b/packages/utilities/rect/src/clamp.ts index 868733ecf2..ff6999ef1a 100644 --- a/packages/utilities/rect/src/clamp.ts +++ b/packages/utilities/rect/src/clamp.ts @@ -9,8 +9,8 @@ export const clampPoint = (position: Point, size: Size, boundaryRect: RectInit) } const defaultMinSize: Size = { - width: -Infinity, - height: -Infinity, + width: 0, + height: 0, } const defaultMaxSize: Size = { diff --git a/packages/utilities/rect/src/constrain.ts b/packages/utilities/rect/src/constrain.ts new file mode 100644 index 0000000000..f840ad54bc --- /dev/null +++ b/packages/utilities/rect/src/constrain.ts @@ -0,0 +1,18 @@ +import type { RectInit } from "./types" + +// given a rect and a boundary, return a new rect that is constrained within the boundary +// resize or reposition the rect so that it fits within the boundary +export const constrainRect = (rect: RectInit, boundary: RectInit): RectInit => { + const { x, y, width, height } = rect + const { x: bx, y: by, width: bw, height: bh } = boundary + + const left = Math.max(bx, Math.min(x, bx + bw - width)) + const top = Math.max(by, Math.min(y, by + bh - height)) + + return { + x: left, + y: top, + width: Math.min(width, bw), + height: Math.min(height, bh), + } +} diff --git a/packages/utilities/rect/src/equality.ts b/packages/utilities/rect/src/equality.ts new file mode 100644 index 0000000000..18a3bc27ff --- /dev/null +++ b/packages/utilities/rect/src/equality.ts @@ -0,0 +1,13 @@ +import type { Point, RectInit, Size } from "./types" + +export const isSizeEqual = (a: Size, b: Size) => { + return a.width === b.width && a.height === b.height +} + +export const isPointEqual = (a: Point, b: Point) => { + return a.x === b.x && a.y === b.y +} + +export const isRectEqual = (a: RectInit, b: RectInit) => { + return isPointEqual(a, b) && isSizeEqual(a, b) +} diff --git a/packages/utilities/rect/src/index.ts b/packages/utilities/rect/src/index.ts index 3a8aeaf856..5d24785169 100644 --- a/packages/utilities/rect/src/index.ts +++ b/packages/utilities/rect/src/index.ts @@ -2,8 +2,10 @@ export * from "./affine-transform" export * from "./align" export * from "./clamp" export * from "./closest" +export * from "./constrain" export * from "./contains" export * from "./distance" +export * from "./equality" export * from "./from-element" export * from "./from-points" export * from "./from-range" From 5d50a7980ecff68e8d95a3733aa3c023119e48bb Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Fri, 22 Mar 2024 20:28:58 +0000 Subject: [PATCH 16/19] style: fix --- packages/core/CHANGELOG.md | 8 ++++---- packages/docs/data/api.json | 2 +- packages/frameworks/solid/CHANGELOG.md | 2 +- packages/frameworks/vue/CHANGELOG.md | 7 ++----- packages/machines/checkbox/CHANGELOG.md | 4 ++-- packages/machines/color-picker/CHANGELOG.md | 3 ++- packages/machines/combobox/CHANGELOG.md | 18 +++++++++++------- packages/machines/date-picker/CHANGELOG.md | 6 ++++-- packages/machines/dialog/CHANGELOG.md | 4 ++-- packages/machines/editable/CHANGELOG.md | 2 +- packages/machines/hover-card/CHANGELOG.md | 17 +++++++++-------- packages/machines/menu/CHANGELOG.md | 8 +++++--- packages/machines/number-input/CHANGELOG.md | 2 +- packages/machines/pin-input/CHANGELOG.md | 2 +- packages/machines/popover/CHANGELOG.md | 9 +++++---- packages/machines/progress/CHANGELOG.md | 4 +++- packages/machines/rating-group/CHANGELOG.md | 4 ++-- packages/machines/select/CHANGELOG.md | 14 ++++++++------ packages/machines/slider/CHANGELOG.md | 3 ++- packages/machines/splitter/CHANGELOG.md | 4 +++- packages/machines/switch/CHANGELOG.md | 4 ++-- packages/machines/tabs/CHANGELOG.md | 4 +++- packages/machines/tags-input/CHANGELOG.md | 4 +++- packages/machines/tooltip/CHANGELOG.md | 6 +++--- packages/machines/tour/CHANGELOG.md | 3 ++- packages/utilities/core/CHANGELOG.md | 8 ++++---- packages/utilities/dismissable/CHANGELOG.md | 3 ++- packages/utilities/element-rect/CHANGELOG.md | 8 ++++---- packages/utilities/popper/CHANGELOG.md | 4 +++- packages/utilities/tabbable/CHANGELOG.md | 18 +++++++----------- 30 files changed, 102 insertions(+), 83 deletions(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 93dcf49529..c14f063631 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -522,7 +522,7 @@ date: (a, b) => a.getTime() === b.getTime(), }, }, - ); + ) ``` - [#462](https://github.com/chakra-ui/zag/pull/462) @@ -698,9 +698,9 @@ This helper can be used in inline guards or the gaurds options. ```js - import { guards } from "@zag-js/core"; + import { guards } from "@zag-js/core" - const { isIn } = gaurds; + const { isIn } = gaurds const machine = createMachine({ on: { @@ -712,7 +712,7 @@ open: {}, closed: {}, }, - }); + }) ``` * [`587cbec9`](https://github.com/chakra-ui/zag/commit/587cbec9b32ee9e8faef5ceeefb779231b152018) Thanks diff --git a/packages/docs/data/api.json b/packages/docs/data/api.json index 581c3f0cd2..5ae35c55a9 100644 --- a/packages/docs/data/api.json +++ b/packages/docs/data/api.json @@ -3488,4 +3488,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/frameworks/solid/CHANGELOG.md b/packages/frameworks/solid/CHANGELOG.md index 1b8eb3ba07..6d640058b7 100644 --- a/packages/frameworks/solid/CHANGELOG.md +++ b/packages/frameworks/solid/CHANGELOG.md @@ -566,7 +566,7 @@ max: props.max, min: props.min, })), - }); + }) } ``` diff --git a/packages/frameworks/vue/CHANGELOG.md b/packages/frameworks/vue/CHANGELOG.md index 4dec5e8300..99c6643a5f 100644 --- a/packages/frameworks/vue/CHANGELOG.md +++ b/packages/frameworks/vue/CHANGELOG.md @@ -716,13 +716,10 @@ ```ts // Before - type Ref = - | string - | Vue.Ref - | ((ref: Element | Vue.ComponentPublicInstance | null) => void); + type Ref = string | Vue.Ref | ((ref: Element | Vue.ComponentPublicInstance | null) => void) // After - type Ref = VNodeRef; + type Ref = VNodeRef ``` - Updated dependencies [[`61c11646`](https://github.com/chakra-ui/zag/commit/61c116467c1758bdda7efe1f27d4ed26e7d44624), diff --git a/packages/machines/checkbox/CHANGELOG.md b/packages/machines/checkbox/CHANGELOG.md index 9145cf7289..10fa71cd8e 100644 --- a/packages/machines/checkbox/CHANGELOG.md +++ b/packages/machines/checkbox/CHANGELOG.md @@ -870,7 +870,7 @@ id: "1", checked: true, }), - ); + ) // this will update the checkbox when the `checked` value changes const [state, send] = useMachine(checkbox.machine({ id: "1" }), { @@ -878,7 +878,7 @@ // when this value changes, the checkbox will be checked/unchecked checked: true, }, - }); + }) ``` ### Patch Changes diff --git a/packages/machines/color-picker/CHANGELOG.md b/packages/machines/color-picker/CHANGELOG.md index f1c71195dd..26980a77d6 100644 --- a/packages/machines/color-picker/CHANGELOG.md +++ b/packages/machines/color-picker/CHANGELOG.md @@ -4,7 +4,8 @@ ### Patch Changes -- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: +- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), + [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: - @zag-js/dismissable@0.39.0 - @zag-js/popper@0.39.0 - @zag-js/anatomy@0.39.0 diff --git a/packages/machines/combobox/CHANGELOG.md b/packages/machines/combobox/CHANGELOG.md index 926308dd6e..d2d16a88e1 100644 --- a/packages/machines/combobox/CHANGELOG.md +++ b/packages/machines/combobox/CHANGELOG.md @@ -4,9 +4,13 @@ ### Patch Changes -- [#1324](https://github.com/chakra-ui/zag/pull/1324) [`ce5b7ce`](https://github.com/chakra-ui/zag/commit/ce5b7ce9ab48cd0dfb750a3fc7c0452912625b6d) Thanks [@TylerAPfledderer](https://github.com/TylerAPfledderer)! - Fixes issue where on load -- if the user set initial `value` to context -- `value` should be used to check whether a - trigger button using `clearTriggerProps` should be visible on the page. -- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: +- [#1324](https://github.com/chakra-ui/zag/pull/1324) + [`ce5b7ce`](https://github.com/chakra-ui/zag/commit/ce5b7ce9ab48cd0dfb750a3fc7c0452912625b6d) Thanks + [@TylerAPfledderer](https://github.com/TylerAPfledderer)! - Fixes issue where on load -- if the user set initial + `value` to context -- `value` should be used to check whether a trigger button using `clearTriggerProps` should be + visible on the page. +- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), + [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: - @zag-js/dismissable@0.39.0 - @zag-js/popper@0.39.0 - @zag-js/anatomy@0.39.0 @@ -724,12 +728,12 @@ const collection = select.collection({ items: [], itemToString(item) { - return item.label; + return item.label }, itemToValue(item) { - return item.value; + return item.value }, - }); + }) // Pass the collection to the select machine const [state, send] = useMachine( @@ -737,7 +741,7 @@ collection, id: useId(), }), - ); + ) ``` ### Patch Changes diff --git a/packages/machines/date-picker/CHANGELOG.md b/packages/machines/date-picker/CHANGELOG.md index d896a212c8..df25e4cd20 100644 --- a/packages/machines/date-picker/CHANGELOG.md +++ b/packages/machines/date-picker/CHANGELOG.md @@ -4,11 +4,13 @@ ### Minor Changes -- [`6b93232`](https://github.com/chakra-ui/zag/commit/6b932324a776ab66fc34b9dc8673400b60500f1c) Thanks [@segunadebayo](https://github.com/segunadebayo)! - Remove unused `parse` function +- [`6b93232`](https://github.com/chakra-ui/zag/commit/6b932324a776ab66fc34b9dc8673400b60500f1c) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Remove unused `parse` function ### Patch Changes -- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: +- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), + [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: - @zag-js/dismissable@0.39.0 - @zag-js/popper@0.39.0 - @zag-js/anatomy@0.39.0 diff --git a/packages/machines/dialog/CHANGELOG.md b/packages/machines/dialog/CHANGELOG.md index d896751a7a..664ed9bdf3 100644 --- a/packages/machines/dialog/CHANGELOG.md +++ b/packages/machines/dialog/CHANGELOG.md @@ -888,7 +888,7 @@ ```jsx // this is will open the dialog initially - const [state, send] = useMachine(dialog.machine({ id: "1", open: true })); + const [state, send] = useMachine(dialog.machine({ id: "1", open: true })) // this will open the dialog when the `open` value changes const [state, send] = useMachine(dialog.machine({ id: "1" }), { @@ -896,7 +896,7 @@ // when this value changes, the dialog will open/close open: true, }, - }); + }) ``` ### Patch Changes diff --git a/packages/machines/editable/CHANGELOG.md b/packages/machines/editable/CHANGELOG.md index bd5b70bfd8..0bd0c25aa3 100644 --- a/packages/machines/editable/CHANGELOG.md +++ b/packages/machines/editable/CHANGELOG.md @@ -1283,7 +1283,7 @@ editable.machine({ placeholder: { edit: "Enter...", preview: "Add name..." }, }), - ); + ) ``` ## 0.1.4 diff --git a/packages/machines/hover-card/CHANGELOG.md b/packages/machines/hover-card/CHANGELOG.md index 11cc9c93fc..9acf2dc721 100644 --- a/packages/machines/hover-card/CHANGELOG.md +++ b/packages/machines/hover-card/CHANGELOG.md @@ -4,7 +4,8 @@ ### Patch Changes -- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: +- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), + [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: - @zag-js/dismissable@0.39.0 - @zag-js/popper@0.39.0 - @zag-js/anatomy@0.39.0 @@ -826,7 +827,7 @@ ```jsx // this is will open the dialog initially - const [state, send] = useMachine(dialog.machine({ id: "1", open: true })); + const [state, send] = useMachine(dialog.machine({ id: "1", open: true })) // this will open the dialog when the `open` value changes const [state, send] = useMachine(dialog.machine({ id: "1" }), { @@ -834,7 +835,7 @@ // when this value changes, the dialog will open/close open: true, }, - }); + }) ``` ### Patch Changes @@ -854,7 +855,7 @@ of the popover. This API supports all the positioning options. ```js - api.setPositioning({ placement: "top" }); + api.setPositioning({ placement: "top" }) ``` ### Patch Changes @@ -948,15 +949,15 @@ [@TimKolberger](https://github.com/TimKolberger)! - Add `open` and `close` functions to the connect api: ```ts - import * as hoverCard from "@zag-js/hover-card"; + import * as hoverCard from "@zag-js/hover-card" - const api = hoverCard.connect(state, send, normalizeProps); + const api = hoverCard.connect(state, send, normalizeProps) // call `open` to open the hover card - api.open(); + api.open() // call `close` to close the hover card - api.close(); + api.close() ``` ### Patch Changes diff --git a/packages/machines/menu/CHANGELOG.md b/packages/machines/menu/CHANGELOG.md index a5111a1dab..4aa180289c 100644 --- a/packages/machines/menu/CHANGELOG.md +++ b/packages/machines/menu/CHANGELOG.md @@ -4,11 +4,13 @@ ### Minor Changes -- [`492d152`](https://github.com/chakra-ui/zag/commit/492d152c04ab367e6c4fd80b1a1f1a68bb287e46) Thanks [@segunadebayo](https://github.com/segunadebayo)! - Expose `onEscapeKeyDown` event handler +- [`492d152`](https://github.com/chakra-ui/zag/commit/492d152c04ab367e6c4fd80b1a1f1a68bb287e46) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Expose `onEscapeKeyDown` event handler ### Patch Changes -- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: +- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), + [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: - @zag-js/dismissable@0.39.0 - @zag-js/popper@0.39.0 - @zag-js/anatomy@0.39.0 @@ -1084,7 +1086,7 @@ of the popover. This API supports all the positioning options. ```js - api.setPositioning({ placement: "top" }); + api.setPositioning({ placement: "top" }) ``` ### Patch Changes diff --git a/packages/machines/number-input/CHANGELOG.md b/packages/machines/number-input/CHANGELOG.md index 05cf87f4b2..2e47fba97e 100644 --- a/packages/machines/number-input/CHANGELOG.md +++ b/packages/machines/number-input/CHANGELOG.md @@ -1189,7 +1189,7 @@ // details => { value: string, valueAsNumber: number } }, }), - ); + ) ``` - Add `focus()` and `blur()` methods to the machine's `api` diff --git a/packages/machines/pin-input/CHANGELOG.md b/packages/machines/pin-input/CHANGELOG.md index c50b5e642a..39a0f1743a 100644 --- a/packages/machines/pin-input/CHANGELOG.md +++ b/packages/machines/pin-input/CHANGELOG.md @@ -1140,7 +1140,7 @@ define their own patterns to validate against. ```jsx - const [state, send] = useMachine(pinInput.machine({ pattern: "^[0-9.-]+$" })); + const [state, send] = useMachine(pinInput.machine({ pattern: "^[0-9.-]+$" })) ``` Improve form support by exposing `hiddenInputProps`. When the machine is passed a `name` property: diff --git a/packages/machines/popover/CHANGELOG.md b/packages/machines/popover/CHANGELOG.md index 5f93a4375c..192bf83c0e 100644 --- a/packages/machines/popover/CHANGELOG.md +++ b/packages/machines/popover/CHANGELOG.md @@ -4,7 +4,8 @@ ### Patch Changes -- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: +- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), + [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: - @zag-js/dismissable@0.39.0 - @zag-js/popper@0.39.0 - @zag-js/anatomy@0.39.0 @@ -1027,7 +1028,7 @@ ```jsx // this is will open the dialog initially - const [state, send] = useMachine(dialog.machine({ id: "1", open: true })); + const [state, send] = useMachine(dialog.machine({ id: "1", open: true })) // this will open the dialog when the `open` value changes const [state, send] = useMachine(dialog.machine({ id: "1" }), { @@ -1035,7 +1036,7 @@ // when this value changes, the dialog will open/close open: true, }, - }); + }) ``` ### Patch Changes @@ -1058,7 +1059,7 @@ positioning options. ```js - api.setPositioning({ placement: "top" }); + api.setPositioning({ placement: "top" }) ``` - Updated dependencies [[`fa2ecc8e`](https://github.com/chakra-ui/zag/commit/fa2ecc8ea235b824f45deda10070c321f896886c), diff --git a/packages/machines/progress/CHANGELOG.md b/packages/machines/progress/CHANGELOG.md index 9934682b06..4f344a0807 100644 --- a/packages/machines/progress/CHANGELOG.md +++ b/packages/machines/progress/CHANGELOG.md @@ -4,7 +4,9 @@ ### Patch Changes -- [`a621fe5`](https://github.com/chakra-ui/zag/commit/a621fe5445f938341e761539471e737eaa149da5) Thanks [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where progress throws when value is initially set to `null` +- [`a621fe5`](https://github.com/chakra-ui/zag/commit/a621fe5445f938341e761539471e737eaa149da5) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where progress throws when value is initially set to + `null` - Updated dependencies []: - @zag-js/anatomy@0.39.0 diff --git a/packages/machines/rating-group/CHANGELOG.md b/packages/machines/rating-group/CHANGELOG.md index ea068a11e0..2e062608ab 100644 --- a/packages/machines/rating-group/CHANGELOG.md +++ b/packages/machines/rating-group/CHANGELOG.md @@ -863,10 +863,10 @@ ```js // set the value of the rating-group to 3 - api.setValue(3); + api.setValue(3) // clear the value of the rating-group - api.clearValue(); + api.clearValue() ``` ### Patch Changes diff --git a/packages/machines/select/CHANGELOG.md b/packages/machines/select/CHANGELOG.md index a22b90fa59..4dcd4d439e 100644 --- a/packages/machines/select/CHANGELOG.md +++ b/packages/machines/select/CHANGELOG.md @@ -4,9 +4,11 @@ ### Patch Changes -- [`ce1975a`](https://github.com/chakra-ui/zag/commit/ce1975a7a3335710b8863c370ffc168d108decca) Thanks [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where multiple select doesn't work correctly in forms. +- [`ce1975a`](https://github.com/chakra-ui/zag/commit/ce1975a7a3335710b8863c370ffc168d108decca) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where multiple select doesn't work correctly in forms. -- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: +- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), + [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: - @zag-js/dismissable@0.39.0 - @zag-js/popper@0.39.0 - @zag-js/anatomy@0.39.0 @@ -826,12 +828,12 @@ const collection = select.collection({ items: [], itemToString(item) { - return item.label; + return item.label }, itemToValue(item) { - return item.value; + return item.value }, - }); + }) // Pass the collection to the select machine const [state, send] = useMachine( @@ -839,7 +841,7 @@ collection, id: useId(), }), - ); + ) ``` ### Patch Changes diff --git a/packages/machines/slider/CHANGELOG.md b/packages/machines/slider/CHANGELOG.md index e21e9fc6b0..61debfcd72 100644 --- a/packages/machines/slider/CHANGELOG.md +++ b/packages/machines/slider/CHANGELOG.md @@ -4,7 +4,8 @@ ### Minor Changes -- [`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a) Thanks [@segunadebayo](https://github.com/segunadebayo)! - Support custom `name` for each slider thumb +- [`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Support custom `name` for each slider thumb ### Patch Changes diff --git a/packages/machines/splitter/CHANGELOG.md b/packages/machines/splitter/CHANGELOG.md index 5b43203cc4..19c9e5b505 100644 --- a/packages/machines/splitter/CHANGELOG.md +++ b/packages/machines/splitter/CHANGELOG.md @@ -4,7 +4,9 @@ ### Patch Changes -- [#1333](https://github.com/chakra-ui/zag/pull/1333) [`2b594ce`](https://github.com/chakra-ui/zag/commit/2b594ce0481dba747f0aa41853e09c33caf50d90) Thanks [@anubra266](https://github.com/anubra266)! - Add `data-orientation` attribute to splitter panel part +- [#1333](https://github.com/chakra-ui/zag/pull/1333) + [`2b594ce`](https://github.com/chakra-ui/zag/commit/2b594ce0481dba747f0aa41853e09c33caf50d90) Thanks + [@anubra266](https://github.com/anubra266)! - Add `data-orientation` attribute to splitter panel part - Updated dependencies []: - @zag-js/anatomy@0.39.0 diff --git a/packages/machines/switch/CHANGELOG.md b/packages/machines/switch/CHANGELOG.md index b7e1fb6f09..2326f23747 100644 --- a/packages/machines/switch/CHANGELOG.md +++ b/packages/machines/switch/CHANGELOG.md @@ -814,7 +814,7 @@ id: "1", checked: true, }), - ); + ) // this will update the checkbox when the `checked` value changes const [state, send] = useMachine(checkbox.machine({ id: "1" }), { @@ -822,7 +822,7 @@ // when this value changes, the checkbox will be checked/unchecked checked: true, }, - }); + }) ``` ### Patch Changes diff --git a/packages/machines/tabs/CHANGELOG.md b/packages/machines/tabs/CHANGELOG.md index 83a840bfc2..e0b37225e4 100644 --- a/packages/machines/tabs/CHANGELOG.md +++ b/packages/machines/tabs/CHANGELOG.md @@ -4,7 +4,9 @@ ### Patch Changes -- [#1333](https://github.com/chakra-ui/zag/pull/1333) [`2b594ce`](https://github.com/chakra-ui/zag/commit/2b594ce0481dba747f0aa41853e09c33caf50d90) Thanks [@anubra266](https://github.com/anubra266)! - Tabs: Rename `tablist` part to `list` to match naming convention +- [#1333](https://github.com/chakra-ui/zag/pull/1333) + [`2b594ce`](https://github.com/chakra-ui/zag/commit/2b594ce0481dba747f0aa41853e09c33caf50d90) Thanks + [@anubra266](https://github.com/anubra266)! - Tabs: Rename `tablist` part to `list` to match naming convention - Updated dependencies []: - @zag-js/anatomy@0.39.0 diff --git a/packages/machines/tags-input/CHANGELOG.md b/packages/machines/tags-input/CHANGELOG.md index 960763d87e..6b64911eff 100644 --- a/packages/machines/tags-input/CHANGELOG.md +++ b/packages/machines/tags-input/CHANGELOG.md @@ -4,7 +4,9 @@ ### Patch Changes -- [`409caad`](https://github.com/chakra-ui/zag/commit/409caad0332e8893ed3cf9f44c9bc21aa3308e74) Thanks [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where setting `addOnPaste` to `false` and pasting text prevents subsequent tags from being added +- [`409caad`](https://github.com/chakra-ui/zag/commit/409caad0332e8893ed3cf9f44c9bc21aa3308e74) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where setting `addOnPaste` to `false` and pasting text + prevents subsequent tags from being added - Updated dependencies []: - @zag-js/anatomy@0.39.0 diff --git a/packages/machines/tooltip/CHANGELOG.md b/packages/machines/tooltip/CHANGELOG.md index a6005eda1e..d9053758e3 100644 --- a/packages/machines/tooltip/CHANGELOG.md +++ b/packages/machines/tooltip/CHANGELOG.md @@ -851,7 +851,7 @@ ```jsx // this is will open the dialog initially - const [state, send] = useMachine(dialog.machine({ id: "1", open: true })); + const [state, send] = useMachine(dialog.machine({ id: "1", open: true })) // this will open the dialog when the `open` value changes const [state, send] = useMachine(dialog.machine({ id: "1" }), { @@ -859,7 +859,7 @@ // when this value changes, the dialog will open/close open: true, }, - }); + }) ``` ### Patch Changes @@ -879,7 +879,7 @@ of the popover. This API supports all the positioning options. ```js - api.setPositioning({ placement: "top" }); + api.setPositioning({ placement: "top" }) ``` ### Patch Changes diff --git a/packages/machines/tour/CHANGELOG.md b/packages/machines/tour/CHANGELOG.md index fd95c5a015..c07ebcc4f7 100644 --- a/packages/machines/tour/CHANGELOG.md +++ b/packages/machines/tour/CHANGELOG.md @@ -4,7 +4,8 @@ ### Patch Changes -- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: +- Updated dependencies [[`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a), + [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83)]: - @zag-js/dismissable@0.39.0 - @zag-js/popper@0.39.0 - @zag-js/anatomy@0.39.0 diff --git a/packages/utilities/core/CHANGELOG.md b/packages/utilities/core/CHANGELOG.md index a2f8b542d4..53c877d811 100644 --- a/packages/utilities/core/CHANGELOG.md +++ b/packages/utilities/core/CHANGELOG.md @@ -105,12 +105,12 @@ const collection = select.collection({ items: [], itemToString(item) { - return item.label; + return item.label }, itemToValue(item) { - return item.value; + return item.value }, - }); + }) // Pass the collection to the select machine const [state, send] = useMachine( @@ -118,7 +118,7 @@ collection, id: useId(), }), - ); + ) ``` ## 0.17.0 diff --git a/packages/utilities/dismissable/CHANGELOG.md b/packages/utilities/dismissable/CHANGELOG.md index 96cedec439..00305a30a8 100644 --- a/packages/utilities/dismissable/CHANGELOG.md +++ b/packages/utilities/dismissable/CHANGELOG.md @@ -4,7 +4,8 @@ ### Patch Changes -- [`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a) Thanks [@segunadebayo](https://github.com/segunadebayo)! - Use capture phase for escape keydown dismissing +- [`27f9ec0`](https://github.com/chakra-ui/zag/commit/27f9ec0812f19228921158885107ed43d559544a) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Use capture phase for escape keydown dismissing - Updated dependencies []: - @zag-js/utils@0.39.0 diff --git a/packages/utilities/element-rect/CHANGELOG.md b/packages/utilities/element-rect/CHANGELOG.md index 5b76c2e81a..cde35ca849 100644 --- a/packages/utilities/element-rect/CHANGELOG.md +++ b/packages/utilities/element-rect/CHANGELOG.md @@ -142,11 +142,11 @@ position or rect. ```js - import { trackElementRect } from "@zag-js/element-rect"; + import { trackElementRect } from "@zag-js/element-rect" - trackElementRect(element, update, { scope: "size" }); // only track size - trackElementRect(element, update, { scope: "position" }); // only track position - trackElementRect(element, update, { scope: "rect" }); // track size and position (default) + trackElementRect(element, update, { scope: "size" }) // only track size + trackElementRect(element, update, { scope: "position" }) // only track position + trackElementRect(element, update, { scope: "rect" }) // track size and position (default) ``` ## 0.2.2 diff --git a/packages/utilities/popper/CHANGELOG.md b/packages/utilities/popper/CHANGELOG.md index 1e07d046b2..6b7e58dae4 100644 --- a/packages/utilities/popper/CHANGELOG.md +++ b/packages/utilities/popper/CHANGELOG.md @@ -4,7 +4,9 @@ ### Patch Changes -- [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83) Thanks [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where `crossAxis` positioning property doesn't work in some cases. +- [`565a7e4`](https://github.com/chakra-ui/zag/commit/565a7e46070edb7bb2a39ed9d065dcaee418db83) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where `crossAxis` positioning property doesn't work in + some cases. - Updated dependencies []: - @zag-js/utils@0.39.0 diff --git a/packages/utilities/tabbable/CHANGELOG.md b/packages/utilities/tabbable/CHANGELOG.md index 08ad52b888..53a79d048a 100644 --- a/packages/utilities/tabbable/CHANGELOG.md +++ b/packages/utilities/tabbable/CHANGELOG.md @@ -411,20 +411,16 @@ element in DOM sequence. ```js - import { proxyTabFocus } from "@zag-js/tabbable"; + import { proxyTabFocus } from "@zag-js/tabbable" export function App() { - const referenceRef = useRef(); - const containerRef = useRef(); + const referenceRef = useRef() + const containerRef = useRef() useEffect(() => { - const focusElement = (el) => el.focus({ preventScroll: true }); - return proxyTabFocus( - containerRef.current, - referenceRef.current, - focusElement, - ); - }, []); + const focusElement = (el) => el.focus({ preventScroll: true }) + return proxyTabFocus(containerRef.current, referenceRef.current, focusElement) + }, []) return (
@@ -437,7 +433,7 @@
- ); + ) } ``` From c8adb486796e6eaad15d7567ad2657fc69fac1e1 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Fri, 22 Mar 2024 20:39:12 +0000 Subject: [PATCH 17/19] feat: update examples --- examples/next-ts/pages/floating-panel.tsx | 2 +- examples/nuxt-ts/pages/floating-panel.vue | 55 ++++++++++++++++-- .../solid-ts/src/pages/floating-panel.tsx | 54 +++++++++++++++--- examples/vue-ts/src/pages/floating-panel.tsx | 56 +++++++++++++++---- 4 files changed, 141 insertions(+), 26 deletions(-) diff --git a/examples/next-ts/pages/floating-panel.tsx b/examples/next-ts/pages/floating-panel.tsx index 326f4a16f0..7bd339e874 100644 --- a/examples/next-ts/pages/floating-panel.tsx +++ b/examples/next-ts/pages/floating-panel.tsx @@ -59,7 +59,7 @@ export default function Page() {
- + diff --git a/examples/nuxt-ts/pages/floating-panel.vue b/examples/nuxt-ts/pages/floating-panel.vue index 5c7f30ddc8..43c38fadd7 100644 --- a/examples/nuxt-ts/pages/floating-panel.vue +++ b/examples/nuxt-ts/pages/floating-panel.vue @@ -1,20 +1,63 @@ diff --git a/examples/solid-ts/src/pages/floating-panel.tsx b/examples/solid-ts/src/pages/floating-panel.tsx index ca99770aa6..1fe34d6ed7 100644 --- a/examples/solid-ts/src/pages/floating-panel.tsx +++ b/examples/solid-ts/src/pages/floating-panel.tsx @@ -1,25 +1,61 @@ -import * as floating-panel from "@zag-js/floating-panel" -import { normalizeProps, useMachine, mergeProps } from "@zag-js/solid" +import * as floatingPanel from "@zag-js/floating-panel" +import { floatingPanelControls } from "@zag-js/shared" +import { normalizeProps, useMachine } from "@zag-js/solid" +import { ArrowDownLeft, Maximize2, Minus, XIcon } from "lucide-solid" import { createMemo, createUniqueId } from "solid-js" -import { floating-panelControls, floating-panelData } from "@zag-js/shared" import { StateVisualizer } from "../components/state-visualizer" import { Toolbar } from "../components/toolbar" import { useControls } from "../hooks/use-controls" export default function Page() { - const controls = useControls(floating-panelControls) + const controls = useControls(floatingPanelControls) - const [state, send] = useMachine(floating-panel.machine({ id: createUniqueId() }), { + const [state, send] = useMachine(floatingPanel.machine({ id: createUniqueId() }), { context: controls.context, }) - const api = createMemo(() => floating-panel.connect(state, send, normalizeProps)) + const api = createMemo(() => floatingPanel.connect(state, send, normalizeProps)) return ( <> -
-
- +
+
+ +
+
+
+
+

Floating Panel

+
+ + + + +
+
+
+
+

Some content

+
+ +
+
+
+
+
+
+
+
+
+
diff --git a/examples/vue-ts/src/pages/floating-panel.tsx b/examples/vue-ts/src/pages/floating-panel.tsx index 804ab28003..29f860bbca 100644 --- a/examples/vue-ts/src/pages/floating-panel.tsx +++ b/examples/vue-ts/src/pages/floating-panel.tsx @@ -1,30 +1,66 @@ -import * as floating-panel from "@zag-js/floating-panel" -import { normalizeProps, useMachine, mergeProps } from "@zag-js/vue" +import * as floatingPanel from "@zag-js/floating-panel" +import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, h, Fragment } from "vue" -import { floating-panelControls, floating-panelData } from "@zag-js/shared" +import { floatingPanelControls } from "@zag-js/shared" import { StateVisualizer } from "../components/state-visualizer" import { Toolbar } from "../components/toolbar" +import { ArrowDownLeft, Maximize2, Minus, XIcon } from "lucide-vue-next" import { useControls } from "../hooks/use-controls" export default defineComponent({ - name: "floating-panel", + name: "floatingPanel", setup() { - const controls = useControls(floating-panelControls) + const controls = useControls(floatingPanelControls) - const [state, send] = useMachine(floating-panel.machine({ id: "1" }), { + const [state, send] = useMachine(floatingPanel.machine({ id: "1" }), { context: controls.context, }) - const apiRef = computed(() => floating-panel.connect(state.value, send, normalizeProps)) + const apiRef = computed(() => floatingPanel.connect(state.value, send, normalizeProps)) return () => { const api = apiRef.value return ( <> -
-
- +
+
+ +
+
+
+
+

Floating Panel

+
+ + + + +
+
+
+
+

Some content

+
+ +
+
+
+
+
+
+
+
+
+
From 38c69b965608e2c62442866ee2078a3f3d4b9d92 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Fri, 22 Mar 2024 21:02:13 +0000 Subject: [PATCH 18/19] feat: keyboard support --- .xstate/floating-panel.js | 3 ++ .../src/floating-panel.connect.ts | 22 ++++++++--- .../src/floating-panel.machine.ts | 39 ++++++++++++++----- .../src/floating-panel.types.ts | 2 +- 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/.xstate/floating-panel.js b/.xstate/floating-panel.js index 59f3eb4250..4ab3d32b87 100644 --- a/.xstate/floating-panel.js +++ b/.xstate/floating-panel.js @@ -63,6 +63,9 @@ const fetchMachine = createMachine({ }, RESTORE: { actions: ["setRestored"] + }, + MOVE: { + actions: ["setPositionFromKeybord"] } } }, diff --git a/packages/machines/floating-panel/src/floating-panel.connect.ts b/packages/machines/floating-panel/src/floating-panel.connect.ts index 6eb9cad497..777accf2b8 100644 --- a/packages/machines/floating-panel/src/floating-panel.connect.ts +++ b/packages/machines/floating-panel/src/floating-panel.connect.ts @@ -1,5 +1,5 @@ -import { getEventKey, getNativeEvent, type EventKeyMap } from "@zag-js/dom-event" -import { dataAttr, getEventTarget } from "@zag-js/dom-query" +import { getEventKey, getEventStep, getNativeEvent, type EventKeyMap } from "@zag-js/dom-event" +import { dataAttr, getEventTarget, isSelfEvent } from "@zag-js/dom-query" import type { NormalizeProps, PropTypes } from "@zag-js/types" import { parts } from "./floating-panel.anatomy" import { dom } from "./floating-panel.dom" @@ -56,14 +56,24 @@ export function connect(state: State, send: Send, normalize overflow: state.context.isMinimized ? "hidden" : undefined, }, onKeyDown(event) { + if (!isSelfEvent(getNativeEvent(event))) return + const step = getEventStep(event) * state.context.gridSize const keyMap: EventKeyMap = { Escape() { send("ESCAPE") }, - ArrowLeft() {}, - ArrowRight() {}, - ArrowUp() {}, - ArrowDown() {}, + ArrowLeft() { + send({ type: "MOVE", direction: "left", step }) + }, + ArrowRight() { + send({ type: "MOVE", direction: "right", step }) + }, + ArrowUp() { + send({ type: "MOVE", direction: "up", step }) + }, + ArrowDown() { + send({ type: "MOVE", direction: "down", step }) + }, } const handler = keyMap[getEventKey(event, state.context)] diff --git a/packages/machines/floating-panel/src/floating-panel.machine.ts b/packages/machines/floating-panel/src/floating-panel.machine.ts index d69af97a68..c6362057cb 100644 --- a/packages/machines/floating-panel/src/floating-panel.machine.ts +++ b/packages/machines/floating-panel/src/floating-panel.machine.ts @@ -17,9 +17,9 @@ import { type Rect, type Size, } from "@zag-js/rect-utils" -import { compact, pick } from "@zag-js/utils" +import { compact, invariant, isEqual, match, pick } from "@zag-js/utils" import { dom } from "./floating-panel.dom" -import type { MachineContext, MachineState, UserDefinedContext } from "./floating-panel.types" +import type { MachineContext, MachineState, Stage, UserDefinedContext } from "./floating-panel.types" const { not } = guards @@ -101,6 +101,9 @@ export function machine(userContext: UserDefinedContext) { RESTORE: { actions: ["setRestored"], }, + MOVE: { + actions: ["setPositionFromKeybord"], + }, }, }, @@ -215,8 +218,8 @@ export function machine(userContext: UserDefinedContext) { setPosition(ctx, evt) { let diff = subtractPoints(evt.position, ctx.lastEventPosition!) - diff.x = Math.round(diff.x / ctx.gridSize!) * ctx.gridSize! - diff.y = Math.round(diff.y / ctx.gridSize!) * ctx.gridSize! + diff.x = Math.round(diff.x / ctx.gridSize) * ctx.gridSize + diff.y = Math.round(diff.y / ctx.gridSize) * ctx.gridSize let position = addPoints(ctx.prevPosition!, diff) position = clampPoint(position, ctx.size, ctx.boundaryRect!) @@ -274,7 +277,7 @@ export function machine(userContext: UserDefinedContext) { }, setMaximized(ctx) { // set max stage - ctx.stage = "maximized" + set.stage(ctx, "maximized") // save previous ctx.prevSize = ctx.size @@ -286,7 +289,7 @@ export function machine(userContext: UserDefinedContext) { }, setMinimized(ctx) { // set min stage - ctx.stage = "minimized" + set.stage(ctx, "minimized") // save previous ctx.prevSize = ctx.size @@ -303,17 +306,30 @@ export function machine(userContext: UserDefinedContext) { }, setRestored(ctx) { // remove stage - ctx.stage = undefined + set.stage(ctx, undefined) // restore size if (!ctx.prevSize || !ctx.prevPosition) return - ctx.size = ctx.prevSize - ctx.position = ctx.prevPosition + set.size(ctx, ctx.prevSize) + set.position(ctx, ctx.prevPosition) // clear previous ctx.prevSize = null ctx.prevPosition = null }, + setPositionFromKeybord(ctx, evt) { + invariant(evt.step == null, "step is required") + + let nextPosition = match(evt.direction, { + left: { x: ctx.position.x - evt.step, y: ctx.position.y }, + right: { x: ctx.position.x + evt.step, y: ctx.position.y }, + up: { x: ctx.position.x, y: ctx.position.y - evt.step }, + down: { x: ctx.position.x, y: ctx.position.y + evt.step }, + }) + + nextPosition = clampPoint(nextPosition, ctx.size, ctx.boundaryRect!) + set.position(ctx, nextPosition) + }, invokeOnOpen(ctx) { ctx.onOpenChange?.({ open: true }) }, @@ -348,4 +364,9 @@ const set = { ctx.position = value ctx.onPositionChange?.({ position: value }) }, + stage(ctx: MachineContext, value: Stage | undefined) { + if (isEqual(ctx.stage, value)) return + ctx.stage = value + ctx.onStageChange?.({ stage: value }) + }, } diff --git a/packages/machines/floating-panel/src/floating-panel.types.ts b/packages/machines/floating-panel/src/floating-panel.types.ts index b48d4ef31f..83bb712a8a 100644 --- a/packages/machines/floating-panel/src/floating-panel.types.ts +++ b/packages/machines/floating-panel/src/floating-panel.types.ts @@ -17,7 +17,7 @@ export interface OpenChangeDetails { export type Stage = "minimized" | "maximized" export interface StageChangeDetails { - stage: Stage + stage: Stage | undefined } interface PublicContext extends DirectionProperty, CommonProperties { From 2aa3d002fbcacdb77d89742d008a62721e366ced Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Fri, 22 Mar 2024 21:03:56 +0000 Subject: [PATCH 19/19] docs: add changeset --- .changeset/thin-buses-burn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thin-buses-burn.md diff --git a/.changeset/thin-buses-burn.md b/.changeset/thin-buses-burn.md new file mode 100644 index 0000000000..0cdb226b4f --- /dev/null +++ b/.changeset/thin-buses-burn.md @@ -0,0 +1,5 @@ +--- +"@zag-js/floating-panel": minor +--- + +Introduce new floating panel machine for draggable and resizable panels