Skip to content

Commit

Permalink
feat: add imperative reset method (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnwalley committed Dec 3, 2021
1 parent b0377b1 commit aceb88f
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 156 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,30 @@ You can customize the following default variables.
}
```

### Programmatic control

You can use a ref to get access to the Allotment component instance and call its reset method manually:

```jsx
const ref = React.useRef(ref);

return (
<div>
<button
onClick={() => {
ref.current.reset();
}}
>
Reset
</button>
<Allotment ref={ref}>
<div />
<div />
</Allotment>
</div>
);
```

## Frequently asked questions

### Next.js
Expand Down
326 changes: 172 additions & 154 deletions src/allotment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import classNames from "classnames";
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
Expand Down Expand Up @@ -41,6 +42,8 @@ export const Pane = forwardRef<HTMLDivElement, PaneProps>(

Pane.displayName = "Allotment.Pane";

export type AllotmentHandle = { reset: () => void };

export type AllotmentProps = {
children: React.ReactNode;
/**
Expand All @@ -58,182 +61,197 @@ export type AllotmentProps = {
onChange?: (sizes: number[]) => void;
} & CommonProps;

const Allotment = ({
children,
maxSize = Infinity,
minSize = 30,
sizes,
defaultSizes = sizes,
snap = false,
vertical = false,
onChange,
}: AllotmentProps) => {
const containerRef = useRef<HTMLDivElement>(null!);
const previousKeys = useRef<string[]>([]);
const splitViewPropsRef = useRef(new Map<React.Key, CommonProps>());
const splitViewRef = useRef<SplitView | null>(null);
const splitViewViewRef = useRef(new Map<React.Key, HTMLElement>());

if (process.env.NODE_ENV !== "production" && sizes) {
console.warn(`Prop sizes is deprecated. Please use defaultSizes instead.`);
}

const childrenArray = useMemo(
() => React.Children.toArray(children).filter(React.isValidElement),
[children]
);

useLayoutEffect(() => {
let initializeSizes = true;

if (defaultSizes && splitViewViewRef.current.size !== defaultSizes.length) {
initializeSizes = false;

const Allotment = forwardRef<AllotmentHandle, AllotmentProps>(
(
{
children,
maxSize = Infinity,
minSize = 30,
sizes,
defaultSizes = sizes,
snap = false,
vertical = false,
onChange,
},
ref
) => {
const containerRef = useRef<HTMLDivElement>(null!);
const previousKeys = useRef<string[]>([]);
const splitViewPropsRef = useRef(new Map<React.Key, CommonProps>());
const splitViewRef = useRef<SplitView | null>(null);
const splitViewViewRef = useRef(new Map<React.Key, HTMLElement>());

if (process.env.NODE_ENV !== "production" && sizes) {
console.warn(
`Expected ${defaultSizes.length} children based on sizes but found ${splitViewViewRef.current.size}`
`Prop sizes is deprecated. Please use defaultSizes instead.`
);
}

if (initializeSizes && defaultSizes) {
previousKeys.current = childrenArray.map((child) => child.key as string);
}

const options: SplitViewOptions = {
orientation: vertical ? Orientation.Vertical : Orientation.Horizontal,
...(initializeSizes &&
defaultSizes && {
descriptor: {
size: defaultSizes.reduce((a, b) => a + b, 0),
views: defaultSizes.map((size, index) => ({
container: [...splitViewViewRef.current.values()][index],
size: size,
view: {
element: document.createElement("div"),
minimumSize: minSize,
maximumSize: maxSize,
snap: snap,
layout: () => {},
},
})),
},
}),
};

splitViewRef.current = new SplitView(
containerRef.current,
options,
onChange
const childrenArray = useMemo(
() => React.Children.toArray(children).filter(React.isValidElement),
[children]
);

splitViewRef.current.on("sashreset", (_index: number) => {
splitViewRef.current?.distributeViewSizes();
});

const that = splitViewRef.current;
useImperativeHandle(ref, () => ({
reset: () => {
splitViewRef.current?.distributeViewSizes();
},
}));

return () => {
that.dispose();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useLayoutEffect(() => {
let initializeSizes = true;

/**
* Add or remove views as number of children changes
*/
useEffect(() => {
const keys = childrenArray.map((child) => child.key as string);
if (sizes && splitViewViewRef.current.size !== sizes.length) {
initializeSizes = false;

const enter = keys.filter((key) => !previousKeys.current.includes(key));
const exit = previousKeys.current.map((key) => !keys.includes(key));
console.warn(
`Expected ${sizes.length} children based on sizes but found ${splitViewViewRef.current.size}`
);
}

exit.forEach((flag, index) => {
if (flag) {
splitViewRef.current?.removeView(index);
if (initializeSizes && sizes) {
previousKeys.current = childrenArray.map(
(child) => child.key as string
);
}
});

for (const key of enter) {
const props = splitViewPropsRef.current.get(key);

splitViewRef.current?.addView(
splitViewViewRef.current.get(key)!,
{
element: document.createElement("div"),
minimumSize: props?.minSize ?? minSize,
maximumSize: props?.maxSize ?? maxSize,
snap: props?.snap ?? snap,
layout: () => {},
},
Sizing.Distribute
const options: SplitViewOptions = {
orientation: vertical ? Orientation.Vertical : Orientation.Horizontal,
...(initializeSizes &&
sizes && {
descriptor: {
size: sizes.reduce((a, b) => a + b, 0),
views: sizes.map((size, index) => ({
container: [...splitViewViewRef.current.values()][index],
size: size,
view: {
element: document.createElement("div"),
minimumSize: minSize,
maximumSize: maxSize,
snap: snap,
layout: () => {},
},
})),
},
}),
};

splitViewRef.current = new SplitView(
containerRef.current,
options,
onChange
);
}

if (enter.length > 0 || exit.length > 0) {
previousKeys.current = keys;
}
}, [childrenArray, maxSize, minSize, snap]);
splitViewRef.current.on("sashreset", (_index: number) => {
splitViewRef.current?.distributeViewSizes();
});

const that = splitViewRef.current;

return () => {
that.dispose();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

/**
* Add or remove views as number of children changes
*/
useEffect(() => {
const keys = childrenArray.map((child) => child.key as string);

const enter = keys.filter((key) => !previousKeys.current.includes(key));
const exit = previousKeys.current.map((key) => !keys.includes(key));

exit.forEach((flag, index) => {
if (flag) {
splitViewRef.current?.removeView(index);
}
});

for (const key of enter) {
const props = splitViewPropsRef.current.get(key);

splitViewRef.current?.addView(
splitViewViewRef.current.get(key)!,
{
element: document.createElement("div"),
minimumSize: props?.minSize ?? minSize,
maximumSize: props?.maxSize ?? maxSize,
snap: props?.snap ?? snap,
layout: () => {},
},
Sizing.Distribute
);
}

useResizeObserver({
ref: containerRef,
onResize: ({ width, height }) => {
if (width && height) {
splitViewRef.current?.layout(vertical ? height : width);
if (enter.length > 0 || exit.length > 0) {
previousKeys.current = keys;
}
},
});

return (
<div
ref={containerRef}
className={classNames(
styles.splitView,
vertical ? styles.vertical : styles.horizontal,
styles.separatorBorder
)}
>
<div className={styles.splitViewContainer}>
{React.Children.toArray(children).map((child, index) => {
if (!React.isValidElement(child)) {
return null;
}

// toArray flattens and converts nulls to non-null keys
const key = child.key!;

if (isPane(child)) {
splitViewPropsRef.current.set(key, child.props);

return React.cloneElement(child, {
key: key,
ref: (el: HTMLElement | null) => {
if (el) {
splitViewViewRef.current.set(key, el);
} else {
splitViewViewRef.current.delete(key);
}
},
});
} else {
return (
<Pane
key={key}
ref={(el: HTMLElement | null) => {
}, [childrenArray, maxSize, minSize, snap]);

useResizeObserver({
ref: containerRef,
onResize: ({ width, height }) => {
if (width && height) {
splitViewRef.current?.layout(vertical ? height : width);
}
},
});

return (
<div
ref={containerRef}
className={classNames(
styles.splitView,
vertical ? styles.vertical : styles.horizontal,
styles.separatorBorder
)}
>
<div className={styles.splitViewContainer}>
{React.Children.toArray(children).map((child, index) => {
if (!React.isValidElement(child)) {
return null;
}

// toArray flattens and converts nulls to non-null keys
const key = child.key!;

if (isPane(child)) {
splitViewPropsRef.current.set(key, child.props);

return React.cloneElement(child, {
key: key,
ref: (el: HTMLElement | null) => {
if (el) {
splitViewViewRef.current.set(key, el);
} else {
splitViewViewRef.current.delete(key);
}
}}
>
{child}
</Pane>
);
}
})}
},
});
} else {
return (
<Pane
key={key}
ref={(el: HTMLElement | null) => {
if (el) {
splitViewViewRef.current.set(key, el);
} else {
splitViewViewRef.current.delete(key);
}
}}
>
{child}
</Pane>
);
}
})}
</div>
</div>
</div>
);
};
);
}
);

Allotment.displayName = "Allotment";

Expand Down
Loading

0 comments on commit aceb88f

Please sign in to comment.