@zag-js/splitter
Version:
Core logic for the splitter widget implemented as a state machine
263 lines (260 loc) • 7.91 kB
text/typescript
import { Machine, EventObject, Service } from '@zag-js/core';
import { PropTypes, RequiredBy, DirectionProperty, CommonProperties } from '@zag-js/types';
import { SplitterRegistry } from './utils/registry.mjs';
type ResizeEvent = PointerEvent | KeyboardEvent;
type PanelId = string;
type ResizeTriggerId = `${PanelId}:${PanelId}` | `${PanelId}:` | `:${PanelId}`;
type PanelSize = number | string;
type PanelResizeBehavior = "preserve-relative-size" | "preserve-pixel-size";
interface PanelData {
/**
* The id of the panel.
*/
id: PanelId;
/**
* The order of the panel. useful of you intend to conditionally render the panel.
*/
order?: number | undefined;
/**
* The minimum size of the panel.
*/
minSize?: PanelSize | undefined;
/**
* The maximum size of the panel.
*/
maxSize?: PanelSize | undefined;
/**
* Whether the panel is collapsible.
*/
collapsible?: boolean | undefined;
/**
* How the panel should behave when the parent group is resized.
*/
resizeBehavior?: PanelResizeBehavior | undefined;
/**
* The size of the panel when collapsed.
*/
collapsedSize?: PanelSize | undefined;
}
type NormalizedPanelData = Omit<PanelData, "minSize" | "maxSize" | "collapsedSize"> & {
minSize?: number | undefined;
maxSize?: number | undefined;
collapsedSize?: number | undefined;
};
interface CursorState {
isAtMin: boolean;
isAtMax: boolean;
}
interface ResizeDetails {
size: number[];
resizeTriggerId: string | null;
layout: string;
expandToSizes: Record<string, number>;
}
interface ResizeEndDetails {
size: number[];
resizeTriggerId: string | null;
}
interface ExpandCollapseDetails {
panelId: string;
size: number;
}
type ElementIds = Partial<{
root: string;
resizeTrigger: (id: string) => string;
label: (id: string) => string;
panel: (id: string | number) => string;
}>;
interface SplitterProps extends DirectionProperty, CommonProperties {
/**
* The orientation of the splitter. Can be `horizontal` or `vertical`
* @default "horizontal"
*/
orientation?: "horizontal" | "vertical" | undefined;
/**
* The controlled size data of the panels
*/
size?: PanelSize[] | undefined;
/**
* The initial size of the panels when rendered.
* Use when you don't need to control the size of the panels.
*/
defaultSize?: PanelSize[] | undefined;
/**
* The size constraints of the panels.
*/
panels: PanelData[];
/**
* Function called when the splitter is resized.
*/
onResize?: ((details: ResizeDetails) => void) | undefined;
/**
* Function called when the splitter resize starts.
*/
onResizeStart?: (() => void) | undefined;
/**
* Function called when the splitter resize ends.
*/
onResizeEnd?: ((details: ResizeEndDetails) => void) | undefined;
/**
* The ids of the elements in the splitter. Useful for composition.
*/
ids?: ElementIds | undefined;
/**
* The number of pixels to resize the panel by when the keyboard is used.
*/
keyboardResizeBy?: number | null | undefined;
/**
* The nonce for the injected splitter cursor stylesheet.
*/
nonce?: string | undefined;
/**
* Function called when a panel is collapsed.
*/
onCollapse?: ((details: ExpandCollapseDetails) => void) | undefined;
/**
* Function called when a panel is expanded.
*/
onExpand?: ((details: ExpandCollapseDetails) => void) | undefined;
/**
* The splitter registry to use for multi-drag support.
* When provided, enables dragging at the intersection of multiple splitters.
*/
registry?: SplitterRegistry | undefined;
}
type PropsWithDefault = "orientation" | "panels";
interface DragState {
resizeTriggerId: string;
resolvedResizeTriggerId: `${PanelId}:${PanelId}`;
resizeTriggerRect: DOMRect;
initialCursorPosition: number;
initialSize: number[];
}
interface KeyboardState {
resizeTriggerId: string;
resolvedResizeTriggerId: `${PanelId}:${PanelId}` | null;
}
interface Context {
dragState: DragState | null;
keyboardState: KeyboardState | null;
size: number[];
panels: NormalizedPanelData[];
}
interface Refs {
panelSizeBeforeCollapse: Map<string, number>;
panelIdToLastNotifiedSizeMap: Map<string, number>;
prevDelta: number;
initialSize: number[] | null;
prevInitialLayout: string | null;
prevGroupSize: number | null;
lastRequestedSize: number[] | null;
suppressOnResize: boolean;
}
interface SplitterSchema {
state: "idle" | "hover:temp" | "hover" | "dragging" | "focused";
tag: "focus";
props: RequiredBy<SplitterProps, PropsWithDefault>;
context: Context;
computed: {
horizontal: boolean;
};
refs: Refs;
action: string;
event: EventObject;
effect: string;
guard: string;
}
type SplitterService = Service<SplitterSchema>;
type SplitterMachine = Machine<SplitterSchema>;
interface PanelProps {
id: PanelId;
}
interface ResizeTriggerProps {
id: ResizeTriggerId;
disabled?: boolean | undefined;
}
interface ResizeTriggerState {
dragging: boolean;
focused: boolean;
disabled: boolean;
}
interface PanelItem {
type: "panel";
id: PanelId;
}
interface ResizeTriggerItem {
type: "handle";
id: ResizeTriggerId;
}
type SplitterItem = PanelItem | ResizeTriggerItem;
interface SplitterApi<T extends PropTypes = PropTypes> {
/**
* Whether the splitter is currently being resized.
*/
dragging: boolean;
/**
* The orientation of the splitter.
*/
orientation: "horizontal" | "vertical";
/**
* Returns the current sizes of the panels.
*/
getSizes: () => number[];
/**
* Sets the sizes of the panels.
*/
setSizes: (size: PanelSize[]) => void;
/**
* Returns the items of the splitter.
*/
getItems: () => SplitterItem[];
/**
* Returns the panels of the splitter.
*/
getPanels: () => PanelData[];
/**
* Returns the panel with the specified id.
*/
getPanelById: (id: PanelId) => PanelData;
/**
* Returns the size of the specified panel.
*/
getPanelSize: (id: PanelId) => number;
/**
* Returns whether the specified panel is collapsed.
*/
isPanelCollapsed: (id: PanelId) => boolean;
/**
* Returns whether the specified panel is expanded.
*/
isPanelExpanded: (id: PanelId) => boolean;
/**
* Collapses the specified panel.
*/
collapsePanel: (id: PanelId) => void;
/**
* Expands the specified panel.
*/
expandPanel: (id: PanelId, minSize?: number) => void;
/**
* Resizes the specified panel.
*/
resizePanel: (id: PanelId, unsafePanelSize: number) => void;
/**
* Returns the layout of the splitter.
*/
getLayout: () => string;
/**
* Resets the splitter to its initial state.
*/
resetSizes: VoidFunction;
/**
* Returns the state of the resize trigger.
*/
getResizeTriggerState: (props: ResizeTriggerProps) => ResizeTriggerState;
getRootProps: () => T["element"];
getPanelProps: (props: PanelProps) => T["element"];
getResizeTriggerProps: (props: ResizeTriggerProps) => T["element"];
getResizeTriggerIndicator: (props: ResizeTriggerProps) => T["element"];
}
export type { CursorState, DragState, ElementIds, ExpandCollapseDetails, KeyboardState, NormalizedPanelData, PanelData, PanelId, PanelItem, PanelProps, PanelResizeBehavior, PanelSize, ResizeDetails, ResizeEndDetails, ResizeEvent, ResizeTriggerId, ResizeTriggerItem, ResizeTriggerProps, ResizeTriggerState, SplitterApi, SplitterItem, SplitterMachine, SplitterProps, SplitterSchema, SplitterService };