main repo

This commit is contained in:
Basilosaurusrex
2025-11-24 18:09:40 +01:00
parent b636ee5e70
commit f027651f9b
34146 changed files with 4436636 additions and 0 deletions

27
node_modules/react-resizable-panels/.eslintrc.cjs generated vendored Normal file
View File

@@ -0,0 +1,27 @@
/* eslint-env node */
module.exports = {
ignorePatterns: [".parcel-cache", "dist", "node_modules"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: "../../tsconfig.json",
tsconfigRootDir: __dirname,
},
plugins: ["@typescript-eslint", "no-restricted-imports", "react-hooks"],
root: true,
rules: {
"@typescript-eslint/no-non-null-assertion": "error",
"no-restricted-imports": [
"error",
{
paths: ["react"],
},
],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": [
"warn",
{
additionalHooks: "(useIsomorphicLayoutEffect)",
},
],
},
};

565
node_modules/react-resizable-panels/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,565 @@
# Changelog
## 2.1.4
- Improve TypeScript HTML tag type generics (#407)
- Edge case check to make sure resize handle hasn't been unmounted while dragging (#410)
## 2.1.3
- Edge case bug fix for a resize handle unmounting while being dragged (#402)
## 2.1.2
- Suppress invalid layout warning for empty panel groups (#396)
## 2.1.1
- Fix `onDragging` regression (#391)
- Fix cursor icon behavior in nested panels (#390)
## 2.1.0
- Add opt-in support for setting the [`"nonce"` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) for the global cursor style (#386)
- Support disabling global cursor styles (#387)
## 2.0.23
- Improve obfuscation for `React.useId` references (#382)
## 2.0.22
- Force eager layout re-calculation after panel added/removed (#375)
## 2.0.21
- Handle pointer event edge case with different origin iframes (#374)
## 2.0.20
- Reset global cursor if an active resize handle is unmounted (#313)
- Resize handle supports (optional) `onFocus` or `onBlur` props (#370)
## 2.0.19
- Add optional `minSize` override param to panel `expand` imperative API
## 2.0.18
- Inline object `hitAreaMargins` will not trigger re-initialization logic unless inner values change (#342)
## 2.0.17
- Prevent pointer events handled by resize handles from triggering elements behind/underneath (#338)
## 2.0.16
- Replaced .toPrecision() with .toFixed() to avoid undesirable layout shift (#323)
## 2.0.15
- Better account for high-precision sizes with `onCollapse` and `onExpand` callbacks (#325)
## 2.0.14
- Better account for high-precision `collapsedSize` values (#325)
## 2.0.13
- Fix potential cycle in stacking-order logic for an unmounted node (#317)
## 2.0.12
- Improve resize for edge cases with collapsed panels; intermediate resize states should now fall back to the most recent valid layout rather than the initial layout (#311)
## 2.0.11
- Fix resize handle cursor hit detection when when viewport is scrolled (#305)
## 2.0.10
- Fix conditional layout edge case (#309)
## 2.0.9
- Fix Flex stacking context bug (#301)
- Fix case where pointer event listeners were sometimes added to the document unnecessarily
## 2.0.8
- `Panel`/`PanelGroup`/`PanelResizeHandle`` pass "id" prop through to DOM (#299)
- `Panel` attributes `data-panel-collapsible` and `data-panel-size` are no longer DEV-only (#297)
## 2.0.7
- Group default layouts use `toPrecision` to avoid small layout shifts due to floating point precision differences between initial server rendering and client hydration (#295)
## 2.0.6
- Replace `useLayoutEffect` usage with SSR-safe wrapper hook (#294)
## 2.0.5
- Resize handle hit detection considers [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context) when determining hit detection (#291)
## 2.0.4
- Fixed `PanelResizeHandle` `onDragging` prop to only be called for the handle being dragged (#289)
## 2.0.3
- Fix resize handle onDragging callback (#278)
## 2.0.2
- Fixed an issue where size might not be re-initialized correctly after a panel was hidden by the `unstable_Activity` (previously "Offscreen") API.
## 2.0.1
- Fixed a regression introduced in 2.0.0 that caused React `onClick` and `onMouseUp` handlers not to fire.
## 2.0.0
- Support resizing multiple (intersecting) panels at once (#274)
This behavior can be customized using a new `hitAreaMargins` prop; defaults to a 15 pixel margin for _coarse_ inputs and a 5 pixel margin for _fine_ inputs.
## 1.0.10
- Fixed edge case constraints check bug that could cause a collapsed panel to re-expand unnecessarily (#273)
## 1.0.9
- DOM util methods scope param defaults to `document` (#262)
- Updating a `Panel`'s pixel constraints will trigger revalidation of the `Panel`'s size (#266)
## 1.0.8
- Update component signature to declare `ReactElement` return type (rather than `ReactNode`) (#256)
- Update `Panel` dev warning to avoid warning when `defaultSize === collapsedSize` for collapsible panels (#257)
- Support shadow dom by removing direct references to / dependencies on the root `document` (#204)
## 1.0.7
- Narrow `tagName` prop to only allow `HTMLElement` names (rather than the broader `Element` type) (#251)
## 1.0.6
- Export internal DOM helper methods.
## 1.0.5
- Fix server rendering regression (#240); Panels will now render with their `defaultSize` during initial mount (if one is specified). This allows server-rendered components to store the most recent size in a cookie and use that value as the default for subsequent page visits.
## 1.0.4
- Edge case bug fix for `isCollapsed` panel method; previously an uninitialized `collapsedSize` value was not being initialized to `0`, which caused `isCollapsed` to incorrectly report `false` in some cases.
## 1.0.3
- Remember most recently expanded panel size in local storage (#234)
## 1.0.2
- Change local storage key for persisted sizes to avoid restoring pixel-based sizes (#233)
## 1.0.1
- Small bug fix to guard against saving an incorrect panel layout to local storage
# 1.0.0
- Remove support for pixel-based Panel constraints; (props like `defaultSizePercentage` should now be `defaultSize`)
- Replaced `dataAttributes` prop with `...rest` prop that supports all HTML attributes
## 0.0.63
- Change default (not-yet-registered) Panel flex-grow style from 0 to 1
## 0.0.62
- Edge case expand/collapse invalid size guard (#220)
## 0.0.61
- Better unstable Offscreen/Activity API.
## 0.0.60
- Better support imperative API usage from mount effects.
- Better support strict effects mode.
- Better checks not to call `onResize` or `onLayout` more than once.
## 0.0.59
- Support imperative panel API usage on-mount.
- Made PanelGroup bailout condition smarter (don't bailout for empty groups unless pixel constraints are used).
- Improved window splitter compatibility by better handling "Enter" key.
## 0.0.58
- Change group layout to more thoroughly distribute resize delta to support more flexible group size configurations.
- Add data attribute support to `Panel`, `PanelGroup`, and `PanelResizeHandle`.
- Update API documentation to reflect changed imperative API method names.
- `PanelOnResize` TypeScript def updated to reflect that previous size param is `undefined` the first time it is called.
## 0.0.57
- [#207](https://github.com/bvaughn/react-resizable-panels/pull/207): Fix DEV conditional error that broke data attributes (and selectors).
## 0.0.56
Support a mix of percentage and pixel based units at the `Panel` level:
```jsx
<Panel defaultSizePixels={100} minSizePercentage={20} maxSizePercentage={50} />
```
> **Note**: Pixel units require the use of a `ResizeObserver` to validate. Percentage based units are recommended when possible.
### Example migrating panels with percentage units
<table>
<thead>
<tr>
<th>v55</th>
<th>v56</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<pre lang="jsx">
&lt;Panel
defaultSize={25}
minSize={10}
maxSize={50}
/&gt;
</pre>
</td>
<td>
<pre lang="jsx">
&lt;Panel
defaultSizePercentage={25}
minSizePercentage={10}
maxSizePercentage={50}
/&gt;
</pre>
</td>
</tr>
</tbody>
</table>
### Example migrating panels with pixel units
<table>
<thead>
<tr>
<th>v55</th>
<th>v56</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<pre lang="jsx">
&lt;PanelGroup
direction="horizontal"
units="pixels"
&gt;
&lt;Panel minSize={100} maxSize={200} /&gt;
&lt;PanelResizeHandle /&gt;
&lt;Panel /&gt;
&lt;/PanelGroup&gt;
</pre>
</td>
<td>
<pre lang="jsx">
&lt;PanelGroup direction="horizontal"&gt;
&lt;Panel
minSizePixels={100}
maxSizePixels={200}
/&gt;
&lt;PanelResizeHandle /&gt;
&lt;Panel /&gt;
&lt;/PanelGroup&gt;
</pre>
</td>
</tr>
</tbody>
</table>
For a complete list of supported properties and example usage, refer to the docs.
## 0.0.55
- New `units` prop added to `PanelGroup` to support pixel-based panel size constraints.
This prop defaults to "percentage" but can be set to "pixels" for static, pixel based layout constraints.
This can be used to add enable pixel-based min/max and default size values, e.g.:
```jsx
<PanelGroup direction="horizontal" units="pixels">
{/* Will be constrained to 100-200 pixels (assuming group is large enough to permit this) */}
<Panel minSize={100} maxSize={200} />
<PanelResizeHandle />
<Panel />
<PanelResizeHandle />
<Panel />
</PanelGroup>
```
Imperative API methods are also able to work with either pixels or percentages now. They default to whatever units the group has been configured to use, but can be overridden with an additional, optional parameter, e.g.
```ts
panelRef.resize(100, "pixels");
panelGroupRef.setLayout([25, 50, 25], "percentages");
// Works for getters too, e.g.
const percentage = panelRef.getSize("percentages");
const pixels = panelRef.getSize("pixels");
const layout = panelGroupRef.getLayout("pixels");
```
## 0.0.54
- [172](https://github.com/bvaughn/react-resizable-panels/issues/172): Development warning added to `PanelGroup` for conditionally-rendered `Panel`(s) that don't have `id` and `order` props
- [156](https://github.com/bvaughn/react-resizable-panels/pull/156): Package exports now used to select between node (server-rendering) and browser (client-rendering) bundles
## 0.0.53
- Fix edge case race condition for `onResize` callbacks during initial mount
## 0.0.52
- [162](https://github.com/bvaughn/react-resizable-panels/issues/162): Add `Panel.collapsedSize` property to allow panels to be collapsed to custom, non-0 sizes
- [161](https://github.com/bvaughn/react-resizable-panels/pull/161): Bug fix: `onResize` should be called for the initial `Panel` size regardless of the `onLayout` prop
## 0.0.51
- [154](https://github.com/bvaughn/react-resizable-panels/issues/154): `onResize` and `onCollapse` props are called in response to `PanelGroup.setLayout`
- [123](https://github.com/bvaughn/react-resizable-panels/issues/123): `onResize` called when number of panels in a group change due to conditional rendering
## 0.0.50
- Improved panel size validation in `PanelGroup`.
## 0.0.49
- Improved development warnings and props validation checks in `PanelGroup`.
## 0.0.48
- [148](https://github.com/bvaughn/react-resizable-panels/pull/148): Build release bundle with Preconstruct
## 0.0.47
- Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
## 0.0.46
- SSR: Avoid accessing default storage (`localStorage`) during initialization; avoid throwing error in browsers that have 3rd party cookies/storage disabled.
## 0.0.45
- SSR: Avoid layout shift by using `defaultSize` to set initial `flex-grow` style
- SSR: Warn if `Panel` is server-rendered without a `defaultSize` prop
- [#135](https://github.com/bvaughn/react-resizable-panels/issues/135): Support RTL layouts
## 0.0.44
- [#142](https://github.com/bvaughn/react-resizable-panels/pull/142): Avoid re-registering Panel when props change; this should reduce the number of scenarios requiring the `order` prop
## 0.0.43
- Add imperative `getLayout` API to `PanelGroup`
- [#139](https://github.com/bvaughn/react-resizable-panels/pull/139): Fix edge case bug where simultaneous `localStorage` updates to multiple saved groups would drop some values
## 0.0.42
- Change cursor style from `col-resize`/`row-resize` to `ew-resize`/`ns-resize` to better match cursor style at edges of a panel.
## 0.0.41
- Add imperative `setLayout` API for `PanelGroup`.
## 0.0.40
- README changes
## 0.0.39
- [#118](https://github.com/bvaughn/react-resizable-panels/issues/118): Fix import regression from 0.0.38.
## 0.0.38
- [#117](https://github.com/bvaughn/react-resizable-panels/issues/117): `Panel` collapse behavior works better near viewport edges.
- [#115](https://github.com/bvaughn/react-resizable-panels/pull/115): `PanelResizeHandle` logic calls `event.preventDefault` for events it handles.
- [#82](https://github.com/bvaughn/react-resizable-panels/issues/82): `useId` import changed to avoid triggering errors with older versions of React. (Note this may have an impact on tree-shaking though it is presumed to be minimal, given the small `"react"` package size.)
## 0.0.37
- [#94](https://github.com/bvaughn/react-resizable-panels/issues/94): Add `onDragging` prop to `PanelResizeHandle` to be notified of when dragging starts/stops.
## 0.0.36
- [#96](https://github.com/bvaughn/react-resizable-panels/issues/96): No longer disable `pointer-events` during resize by default. This behavior can be re-enabled using the newly added `PanelGroup` prop `disablePointerEventsDuringResize`.
## 0.0.35
- [#92](https://github.com/bvaughn/react-resizable-panels/pull/92): Change `browserslist` so compiled module works with CRA 4.0.3 Babel config out of the box.
## 0.0.34
- [#85](https://github.com/bvaughn/react-resizable-panels/issues/85): Add optional `storage` prop to `PanelGroup` to make it easier to persist layouts somewhere other than `localStorage` (e.g. like a Cookie).
- [#70](https://github.com/bvaughn/react-resizable-panels/issues/70): When resizing is done via mouse/touch event some initial state is stored so that any panels that contract will also expand if drag direction is reversed.
- [#86](https://github.com/bvaughn/react-resizable-panels/issues/86): Layout changes triggered by keyboard no longer affect the global cursor.
- Fixed small cursor regression introduced in 0.0.33.
## 0.0.33
- Collapsible `Panel`s will always call `onCollapse` on-mount regardless of their collapsed state.
- Fixed regression in b5d3ec1 where arrow keys may fail to expand a collapsed panel.
## 0.0.32
- [#75](https://github.com/bvaughn/react-resizable-panels/issues/75): Ensure `Panel` and `PanelGroup` callbacks are always called after mounting.
## 0.0.31
- [#71](https://github.com/bvaughn/react-resizable-panels/issues/71): Added `getSize` and `getCollapsed` to imperative API exposed by `Panel`.
- [#67](https://github.com/bvaughn/react-resizable-panels/issues/67), [#72](https://github.com/bvaughn/react-resizable-panels/issues/72): Removed nullish coalescing operator (`??`) because it caused problems with default create-react-app configuration.
- Fix edge case when expanding a panel via imperative API that was collapsed by user drag
## 0.0.30
- [#68](https://github.com/bvaughn/react-resizable-panels/pull/68): Reduce volume/frequency of local storage writes for `PanelGroup`s configured to _auto-save_.
- Added `onLayout` prop to `PanelGroup` to be called when group layout changes. Note that some form of debouncing is recommended before processing these values (e.g. saving to a database).
## 0.0.29
- [#58](https://github.com/bvaughn/react-resizable-panels/pull/58): Add imperative `collapse`, `expand`, and `resize` methods to `Panel`.
- [#64](https://github.com/bvaughn/react-resizable-panels/pull/64): Disable `pointer-events` inside of `Panel`s during resize. This avoid edge cases like nested iframes.
- [#57](https://github.com/bvaughn/react-resizable-panels/pull/57): Improve server rendering check to include `window.document`. This more closely matches React's own check and avoids false positives for environments that alias `window` to some global object.
## 0.0.28
- [#53](https://github.com/bvaughn/react-resizable-panels/issues/53): Avoid `useLayoutEffect` warning when server rendering. Render panels with default style of `flex: 1 1 auto` during initial render.
## 0.0.27
- [#4](https://github.com/bvaughn/react-resizable-panels/issues/4): Add `collapsible` and `onCollapse` props to `Panel` to support auto-collapsing panels that resize beyond their `minSize` value (similar to VS Code's panel UX).
## 0.0.26
- Reduce style re-calc from resize-in-progress cursor style.
## 0.0.25
- While a resize is active, the global cursor style now reliably overrides per-element styles (to avoid flickering if you drag over e.g. an anchor element).
## 0.0.24
- [#49](https://github.com/bvaughn/react-resizable-panels/issues/49): Change cursor based on min/max boundaries.
## 0.0.23
- [#40](https://github.com/bvaughn/react-resizable-panels/issues/40): Add optional `maxSize` prop to `Panel`.
- [#41](https://github.com/bvaughn/react-resizable-panels/issues/41): Add optional `onResize` prop to `Panel`. This prop can be used (along with `defaultSize`) to persistence layouts somewhere externally.
- [#42](https://github.com/bvaughn/react-resizable-panels/issues/42): Don't cancel resize operations when exiting the window. Only cancel when a `"mouseup"` (or `"touchend"`) event is fired.
## 0.0.22
- Replaced the `"ew-resize"` and `"ns-resize"` cursor style with `"col-resize"` and `"row-resize"`.
## 0.0.21
- [#39](https://github.com/bvaughn/react-resizable-panels/issues/39): Fixed regression in TypeScript defs introduced in `0.0.20`
## 0.0.20
- Add `displayName` to `Panel`, `PanelGroup`, `PanelGroupContext`, and `PanelResizeHandle` to work around ParcelJS scope hoisting renaming.
## 0.0.19
- Add optional `style` and `tagName` props to `Panel`, `PanelGroup`, and `PanelResizeHandle` to simplify custom styling.
- Add `data-panel-group-direction` attribute to `PanelGroup` and `PanelResizeHandle` to simplify custom drag handle styling.
## 0.0.18
- `Panel` and `PanelGroup` now use `overflow: hidden` style by default to avoid potential scrollbar flickers while resizing.
## 0.0.17
- Bug fix: `Panel` styles include `flex-basis`, `flex-shrink`, and `overflow` so that their sizes are not unintentionally impacted by their content.
## 0.0.16
- Bug fix: Resize handle ARIA attributes now rendering proper min/max/now values for Window Splitter.
- Bug fix: Up/down arrows are ignored for _horizontal_ layouts and left/right arrows are ignored for _vertical_ layouts as per Window Splitter spec.
- [#36](https://github.com/bvaughn/react-resizable-panels/issues/36): Removed `PanelContext` in favor of adding `data-resize-handle-active` attribute to active resize handles. This attribute can be used to update the style for active handles.
## 0.0.15
- [#30](https://github.com/bvaughn/react-resizable-panels/issues/30): `PanelGroup` uses `display: flex` rather than absolute positioning. This provides several benefits: (a) more responsive resizing for nested groups, (b) no explicit `width`/`height` props, and (c) `PanelResizeHandle` components can now be rendered directly within `PanelGroup` (rather than as children of `Panel`s).
## 0.0.14
- [#23](https://github.com/bvaughn/react-resizable-panels/issues/23): Fix small regression with `autoSaveId` that was introduced with non-deterministic `useId` ids.
## 0.0.13
- [#18](https://github.com/bvaughn/react-resizable-panels/issues/18): Support server-side rendering (e.g. Next JS) by using `useId` (when available). `Panel` components no longer _require_ a user-provided `id` prop and will also fall back to using `useId` when none is provided.
- `PanelGroup` component now sets `position: relative` style by default, as well as an explicit `height` and `width` style.
## 0.0.12
- Bug fix: [#19](https://github.com/bvaughn/react-resizable-panels/issues/19): Fix initial "jump" that could occur when dragging started.
- Bug fix: [#20](https://github.com/bvaughn/react-resizable-panels/issues/20): Stop resize/drag operation on "contextmenu" event.
- Bug fix: [#21](https://github.com/bvaughn/react-resizable-panels/issues/21): Disable text selection while dragging active (Firefox only)
## 0.0.11
- Drag UX change: Reversing drag after dragging past the min/max size of a panel will no longer have an effect until the pointer overlaps with the resize handle. (Thanks @davidkpiano for the suggestion!)
- Bug fix: Resize handles are no longer left in a "focused" state after a touch/mouse event.
## 0.0.10
- Corrupt build artifact. Don't use this version.
## 0.0.9
- [#13](https://github.com/bvaughn/react-resizable-panels/issues/13): `PanelResizeHandle` should declare "separator" role and implement the recommended ["Window Splitter" pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)
## 0.0.8
- [#7](https://github.com/bvaughn/react-resizable-panels/issues/7): Support "touch" events for mobile compatibility.
## 0.0.7
- Add `PanelContext` with `activeHandleId` property identifying the resize handle currently being dragged (or `null`). This enables more customized UI/UX when resizing is in progress.
## 0.0.6
- [#5](https://github.com/bvaughn/react-resizable-panels/issues/5): Removed `panelBefore` and `panelAfter` props from `PanelResizeHandle`. `PanelGroup` now infers this based on position within the group.
## 0.0.5
- TypeScript props type fix for `PanelGroup`'s `children` prop.
## 0.0.4
- [#8](https://github.com/bvaughn/react-resizable-panels/issues/8): Added optional `order` prop to `Panel` to improve conditional rendering.
## 0.0.3
- [#3](https://github.com/bvaughn/react-resizable-panels/issues/3): `Panel`s can be conditionally rendered within a group. `PanelGroup` will persist separate layouts for each combination of visible panels.
## 0.0.2
- Documentation-only update.
## 0.0.1
- Initial release.

245
node_modules/react-resizable-panels/README.md generated vendored Normal file
View File

@@ -0,0 +1,245 @@
# react-resizable-panels
React components for resizable panel groups/layouts
```jsx
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
<PanelGroup autoSaveId="example" direction="horizontal">
<Panel defaultSize={25}>
<SourcesExplorer />
</Panel>
<PanelResizeHandle />
<Panel>
<SourceViewer />
</Panel>
<PanelResizeHandle />
<Panel defaultSize={25}>
<Console />
</Panel>
</PanelGroup>;
```
## If you like this project, 🎉 [become a sponsor](https://github.com/sponsors/bvaughn/) or ☕ [buy me a coffee](http://givebrian.coffee/)
## Props
### `PanelGroup`
| prop | type | description |
| :----------- | :--------------------------- | :--------------------------------------------------------------- |
| `autoSaveId` | `?string` | Unique id used to auto-save group arrangement via `localStorage` |
| `children` | `ReactNode` | Arbitrary React element(s) |
| `className` | `?string` | Class name to attach to root element |
| `direction` | `"horizontal" \| "vertical"` | Group orientation |
| `id` | `?string` | Group id; falls back to `useId` when not provided |
| `onLayout` | `?(sizes: number[]) => void` | Called when group layout changes |
| `storage` | `?PanelGroupStorage` | Custom storage API; defaults to `localStorage` <sup>1</sup> |
| `style` | `?CSSProperties` | CSS style to attach to root element |
| `tagName` | `?string = "div"` | HTML element tag name for root element |
<sup>1</sup>: Storage API must define the following _synchronous_ methods:
- `getItem: (name:string) => string`
- `setItem: (name: string, value: string) => void`
`PanelGroup` components also expose an imperative API for manual resizing:
| method | description |
| :---------------------------- | :--------------------------------------------------------------- |
| `getId(): string` | Gets the panel group's ID. |
| `getLayout(): number[]` | Gets the panel group's current _layout_ (`[1 - 100, ...]`). |
| `setLayout(layout: number[])` | Resize panel group to the specified _layout_ (`[1 - 100, ...]`). |
### `Panel`
| prop | type | description |
| :-------------- | :------------------------ | :-------------------------------------------------------------------------------------------- |
| `children` | `ReactNode` | Arbitrary React element(s) |
| `className` | `?string` | Class name to attach to root element |
| `collapsedSize` | `?number=0` | Panel should collapse to this size |
| `collapsible` | `?boolean=false` | Panel should collapse when resized beyond its `minSize` |
| `defaultSize` | `?number` | Initial size of panel (numeric value between 1-100) |
| `id` | `?string` | Panel id (unique within group); falls back to `useId` when not provided |
| `maxSize` | `?number = 100` | Maximum allowable size of panel (numeric value between 1-100); defaults to `100` |
| `minSize` | `?number = 10` | Minimum allowable size of panel (numeric value between 1-100); defaults to `10` |
| `onCollapse` | `?() => void` | Called when panel is collapsed |
| `onExpand` | `?() => void` | Called when panel is expanded |
| `onResize` | `?(size: number) => void` | Called when panel is resized; `size` parameter is a numeric value between 1-100. <sup>1</sup> |
| `order` | `?number` | Order of panel within group; required for groups with conditionally rendered panels |
| `style` | `?CSSProperties` | CSS style to attach to root element |
| `tagName` | `?string = "div"` | HTML element tag name for root element |
<sup>1</sup>: If any `Panel` has an `onResize` callback, the `order` prop should be provided for all `Panel`s.
`Panel` components also expose an imperative API for manual resizing:
| method | description |
| :----------------------- | :--------------------------------------------------------------------------------- |
| `collapse()` | If panel is `collapsible`, collapse it fully. |
| `expand()` | If panel is currently _collapsed_, expand it to its most recent size. |
| `getId(): string` | Gets the ID of the panel. |
| `getSize(): number` | Gets the current size of the panel as a percentage (`1 - 100`). |
| `isCollapsed(): boolean` | Returns `true` if the panel is currently _collapsed_ (`size === 0`). |
| `isExpanded(): boolean` | Returns `true` if the panel is currently _not collapsed_ (`!isCollapsed()`). |
| `getSize(): number` | Returns the most recently committed size of the panel as a percentage (`1 - 100`). |
| `resize(size: number)` | Resize panel to the specified _percentage_ (`1 - 100`). |
### `PanelResizeHandle`
| prop | type | description |
| :--------------- | :-------------------------------------------- | :------------------------------------------------------------------------------ |
| `children` | `?ReactNode` | Custom drag UI; can be any arbitrary React element(s) |
| `className` | `?string` | Class name to attach to root element |
| `hitAreaMargins` | `?{ coarse: number = 15; fine: number = 5; }` | Allow this much margin when determining resizable handle hit detection |
| `disabled` | `?boolean` | Disable drag handle |
| `id` | `?string` | Resize handle id (unique within group); falls back to `useId` when not provided |
| `onDragging` | `?(isDragging: boolean) => void` | Called when group layout changes |
| `style` | `?CSSProperties` | CSS style to attach to root element |
| `tagName` | `?string = "div"` | HTML element tag name for root element |
---
## FAQ
### Can panel sizes be specified in pixels?
No. Pixel-based constraints [added significant complexity](https://github.com/bvaughn/react-resizable-panels/pull/176) to the initialization and validation logic and so I've decided not to support them. You may be able to implement a version of this yourself following [a pattern like this](https://github.com/bvaughn/react-resizable-panels/issues/46#issuecomment-1368108416) but it is not officially supported by this library.
### How can I fix layout/sizing problems with conditionally rendered panels?
The `Panel` API doesn't _require_ `id` and `order` props because they aren't necessary for static layouts. When panels are conditionally rendered though, it's best to supply these values.
```tsx
<PanelGroup direction="horizontal">
{renderSideBar && (
<>
<Panel id="sidebar" minSize={25} order={1}>
<Sidebar />
</Panel>
<PanelResizeHandle />
</>
)}
<Panel minSize={25} order={2}>
<Main />
</Panel>
</PanelGroup>
```
### Can a attach a ref to the DOM elements?
No. I think exposing two refs (one for the component's imperative API and one for a DOM element) would be awkward. This library does export several utility methods for accessing the underlying DOM elements though. For example:
```tsx
import {
getPanelElement,
getPanelGroupElement,
getResizeHandleElement,
Panel,
PanelGroup,
PanelResizeHandle,
} from "react-resizable-panels";
export function Example() {
const refs = useRef();
useEffect(() => {
const groupElement = getPanelGroupElement("group");
const leftPanelElement = getPanelElement("left-panel");
const rightPanelElement = getPanelElement("right-panel");
const resizeHandleElement = getResizeHandleElement("resize-handle");
// If you want to, you can store them in a ref to pass around
refs.current = {
groupElement,
leftPanelElement,
rightPanelElement,
resizeHandleElement,
};
}, []);
return (
<PanelGroup direction="horizontal" id="group">
<Panel id="left-panel">{/* ... */}</Panel>
<PanelResizeHandle id="resize-handle" />
<Panel id="right-panel">{/* ... */}</Panel>
</PanelGroup>
);
}
```
### Why don't I see any resize UI?
This likely means that you haven't applied any CSS to style the resize handles. By default, a resize handle is just an empty DOM element. To add styling, use the `className` or `style` props:
```tsx
// Tailwind example
<PanelResizeHandle className="w-2 bg-blue-800" />
```
### How can I use persistent layouts with SSR?
By default, this library uses `localStorage` to persist layouts. With server rendering, this can cause a flicker when the default layout (rendered on the server) is replaced with the persisted layout (in `localStorage`). The way to avoid this flicker is to also persist the layout with a cookie like so:
#### Server component
```tsx
import ResizablePanels from "@/app/ResizablePanels";
import { cookies } from "next/headers";
export function ServerComponent() {
const layout = cookies().get("react-resizable-panels:layout");
let defaultLayout;
if (layout) {
defaultLayout = JSON.parse(layout.value);
}
return <ClientComponent defaultLayout={defaultLayout} />;
}
```
#### Client component
```tsx
"use client";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
export function ClientComponent({
defaultLayout = [33, 67],
}: {
defaultLayout: number[] | undefined;
}) {
const onLayout = (sizes: number[]) => {
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`;
};
return (
<PanelGroup direction="horizontal" onLayout={onLayout}>
<Panel defaultSize={defaultLayout[0]}>{/* ... */}</Panel>
<PanelResizeHandle className="w-2 bg-blue-800" />
<Panel defaultSize={defaultLayout[1]}>{/* ... */}</Panel>
</PanelGroup>
);
}
```
> [!NOTE]
> Be sure to specify a `defaultSize` prop for **every** `Panel` component to avoid layout flicker.
A demo of this is available [here](https://github.com/bvaughn/react-resizable-panels-demo-ssr).
#### How can I set the [CSP `"nonce"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) attribute?
```js
import { setNonce } from "react-resizable-panels";
setNonce("your-nonce-value-here");
```
#### How can I disable global cursor styles?
```js
import { disableGlobalCursorStyles } from "react-resizable-panels";
disableGlobalCursorStyles();
```

View File

@@ -0,0 +1,70 @@
import { ForwardedRef, HTMLAttributes, PropsWithChildren, ReactElement } from "./vendor/react.js";
export type PanelOnCollapse = () => void;
export type PanelOnExpand = () => void;
export type PanelOnResize = (size: number, prevSize: number | undefined) => void;
export type PanelCallbacks = {
onCollapse?: PanelOnCollapse;
onExpand?: PanelOnExpand;
onResize?: PanelOnResize;
};
export type PanelConstraints = {
collapsedSize?: number | undefined;
collapsible?: boolean | undefined;
defaultSize?: number | undefined;
maxSize?: number | undefined;
minSize?: number | undefined;
};
export type PanelData = {
callbacks: PanelCallbacks;
constraints: PanelConstraints;
id: string;
idIsFromProps: boolean;
order: number | undefined;
};
export type ImperativePanelHandle = {
collapse: () => void;
expand: (minSize?: number) => void;
getId(): string;
getSize(): number;
isCollapsed: () => boolean;
isExpanded: () => boolean;
resize: (size: number) => void;
};
export type PanelProps<T extends keyof HTMLElementTagNameMap = keyof HTMLElementTagNameMap> = Omit<HTMLAttributes<HTMLElementTagNameMap[T]>, "id" | "onResize"> & PropsWithChildren<{
className?: string;
collapsedSize?: number | undefined;
collapsible?: boolean | undefined;
defaultSize?: number | undefined;
id?: string;
maxSize?: number | undefined;
minSize?: number | undefined;
onCollapse?: PanelOnCollapse;
onExpand?: PanelOnExpand;
onResize?: PanelOnResize;
order?: number;
style?: object;
tagName?: T;
}>;
export declare function PanelWithForwardedRef({ children, className: classNameFromProps, collapsedSize, collapsible, defaultSize, forwardedRef, id: idFromProps, maxSize, minSize, onCollapse, onExpand, onResize, order, style: styleFromProps, tagName: Type, ...rest }: PanelProps & {
forwardedRef: ForwardedRef<ImperativePanelHandle>;
}): ReactElement;
export declare namespace PanelWithForwardedRef {
var displayName: string;
}
export declare const Panel: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLObjectElement | HTMLElement | HTMLSlotElement | HTMLStyleElement | HTMLTitleElement | HTMLAnchorElement | HTMLAreaElement | HTMLAudioElement | HTMLBaseElement | HTMLQuoteElement | HTMLBodyElement | HTMLBRElement | HTMLButtonElement | HTMLCanvasElement | HTMLTableCaptionElement | HTMLTableColElement | HTMLDataElement | HTMLDataListElement | HTMLModElement | HTMLDetailsElement | HTMLDialogElement | HTMLDivElement | HTMLDListElement | HTMLEmbedElement | HTMLFieldSetElement | HTMLFormElement | HTMLHeadingElement | HTMLHeadElement | HTMLHRElement | HTMLHtmlElement | HTMLIFrameElement | HTMLImageElement | HTMLInputElement | HTMLLabelElement | HTMLLegendElement | HTMLLIElement | HTMLLinkElement | HTMLMapElement | HTMLMenuElement | HTMLMetaElement | HTMLMeterElement | HTMLOListElement | HTMLOptGroupElement | HTMLOptionElement | HTMLOutputElement | HTMLParagraphElement | HTMLPictureElement | HTMLPreElement | HTMLProgressElement | HTMLScriptElement | HTMLSelectElement | HTMLSourceElement | HTMLSpanElement | HTMLTableElement | HTMLTableSectionElement | HTMLTableCellElement | HTMLTemplateElement | HTMLTextAreaElement | HTMLTimeElement | HTMLTableRowElement | HTMLTrackElement | HTMLUListElement | HTMLVideoElement>, "id" | "onResize"> & {
className?: string | undefined;
collapsedSize?: number | undefined;
collapsible?: boolean | undefined;
defaultSize?: number | undefined;
id?: string | undefined;
maxSize?: number | undefined;
minSize?: number | undefined;
onCollapse?: PanelOnCollapse | undefined;
onExpand?: PanelOnExpand | undefined;
onResize?: PanelOnResize | undefined;
order?: number | undefined;
style?: object | undefined;
tagName?: keyof HTMLElementTagNameMap | undefined;
} & {
children?: import("react").ReactNode;
} & import("react").RefAttributes<ImperativePanelHandle>>;

View File

@@ -0,0 +1,36 @@
import { Direction } from "./types.js";
import { CSSProperties, HTMLAttributes, PropsWithChildren } from "./vendor/react.js";
export type ImperativePanelGroupHandle = {
getId: () => string;
getLayout: () => number[];
setLayout: (layout: number[]) => void;
};
export type PanelGroupStorage = {
getItem(name: string): string | null;
setItem(name: string, value: string): void;
};
export type PanelGroupOnLayout = (layout: number[]) => void;
export type PanelGroupProps = Omit<HTMLAttributes<keyof HTMLElementTagNameMap>, "id"> & PropsWithChildren<{
autoSaveId?: string | null;
className?: string;
direction: Direction;
id?: string | null;
keyboardResizeBy?: number | null;
onLayout?: PanelGroupOnLayout | null;
storage?: PanelGroupStorage;
style?: CSSProperties;
tagName?: keyof HTMLElementTagNameMap;
}>;
export declare const PanelGroup: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<keyof HTMLElementTagNameMap>, "id"> & {
autoSaveId?: string | null | undefined;
className?: string | undefined;
direction: Direction;
id?: string | null | undefined;
keyboardResizeBy?: number | null | undefined;
onLayout?: PanelGroupOnLayout | null | undefined;
storage?: PanelGroupStorage | undefined;
style?: CSSProperties | undefined;
tagName?: keyof HTMLElementTagNameMap | undefined;
} & {
children?: import("react").ReactNode;
} & import("react").RefAttributes<ImperativePanelGroupHandle>>;

View File

@@ -0,0 +1,20 @@
import { PointerHitAreaMargins } from "./PanelResizeHandleRegistry.js";
import { CSSProperties, HTMLAttributes, PropsWithChildren, ReactElement } from "./vendor/react.js";
export type PanelResizeHandleOnDragging = (isDragging: boolean) => void;
export type ResizeHandlerState = "drag" | "hover" | "inactive";
export type PanelResizeHandleProps = Omit<HTMLAttributes<keyof HTMLElementTagNameMap>, "id" | "onBlur" | "onFocus"> & PropsWithChildren<{
className?: string;
disabled?: boolean;
hitAreaMargins?: PointerHitAreaMargins;
id?: string | null;
onBlur?: () => void;
onDragging?: PanelResizeHandleOnDragging;
onFocus?: () => void;
style?: CSSProperties;
tabIndex?: number;
tagName?: keyof HTMLElementTagNameMap;
}>;
export declare function PanelResizeHandle({ children, className: classNameFromProps, disabled, hitAreaMargins, id: idFromProps, onBlur, onDragging, onFocus, style: styleFromProps, tabIndex, tagName: Type, ...rest }: PanelResizeHandleProps): ReactElement;
export declare namespace PanelResizeHandle {
var displayName: string;
}

View File

@@ -0,0 +1,19 @@
import { Direction, ResizeEvent } from "./types.js";
export type ResizeHandlerAction = "down" | "move" | "up";
export type SetResizeHandlerState = (action: ResizeHandlerAction, isActive: boolean, event: ResizeEvent | null) => void;
export type PointerHitAreaMargins = {
coarse: number;
fine: number;
};
export type ResizeHandlerData = {
direction: Direction;
element: HTMLElement;
hitAreaMargins: PointerHitAreaMargins;
setResizeHandlerState: SetResizeHandlerState;
};
export declare const EXCEEDED_HORIZONTAL_MIN = 1;
export declare const EXCEEDED_HORIZONTAL_MAX = 2;
export declare const EXCEEDED_VERTICAL_MIN = 4;
export declare const EXCEEDED_VERTICAL_MAX = 8;
export declare function registerResizeHandle(resizeHandleId: string, element: HTMLElement, direction: Direction, hitAreaMargins: PointerHitAreaMargins, setResizeHandlerState: SetResizeHandlerState): () => void;
export declare function reportConstraintsViolation(resizeHandleId: string, flag: number): void;

View File

@@ -0,0 +1,20 @@
import { Panel } from "./Panel.js";
import { PanelGroup } from "./PanelGroup.js";
import { PanelResizeHandle } from "./PanelResizeHandle.js";
import { assert } from "./utils/assert.js";
import { setNonce } from "./utils/csp.js";
import { enableGlobalCursorStyles, disableGlobalCursorStyles } from "./utils/cursor.js";
import { getPanelElement } from "./utils/dom/getPanelElement.js";
import { getPanelElementsForGroup } from "./utils/dom/getPanelElementsForGroup.js";
import { getPanelGroupElement } from "./utils/dom/getPanelGroupElement.js";
import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement.js";
import { getResizeHandleElementIndex } from "./utils/dom/getResizeHandleElementIndex.js";
import { getResizeHandleElementsForGroup } from "./utils/dom/getResizeHandleElementsForGroup.js";
import { getResizeHandlePanelIds } from "./utils/dom/getResizeHandlePanelIds.js";
import { getIntersectingRectangle } from "./utils/rects/getIntersectingRectangle.js";
import { intersects } from "./utils/rects/intersects.js";
import type { ImperativePanelHandle, PanelOnCollapse, PanelOnExpand, PanelOnResize, PanelProps } from "./Panel.js";
import type { ImperativePanelGroupHandle, PanelGroupOnLayout, PanelGroupProps, PanelGroupStorage } from "./PanelGroup.js";
import type { PanelResizeHandleOnDragging, PanelResizeHandleProps } from "./PanelResizeHandle.js";
import type { PointerHitAreaMargins } from "./PanelResizeHandleRegistry.js";
export { ImperativePanelGroupHandle, ImperativePanelHandle, PanelGroupOnLayout, PanelGroupProps, PanelGroupStorage, PanelOnCollapse, PanelOnExpand, PanelOnResize, PanelProps, PanelResizeHandleOnDragging, PanelResizeHandleProps, PointerHitAreaMargins, Panel, PanelGroup, PanelResizeHandle, assert, getIntersectingRectangle, intersects, getPanelElement, getPanelElementsForGroup, getPanelGroupElement, getResizeHandleElement, getResizeHandleElementIndex, getResizeHandleElementsForGroup, getResizeHandlePanelIds, enableGlobalCursorStyles, disableGlobalCursorStyles, setNonce, };

View File

@@ -0,0 +1,3 @@
export type Direction = "horizontal" | "vertical";
export type ResizeEvent = KeyboardEvent | PointerEvent | MouseEvent;
export type ResizeHandler = (event: ResizeEvent) => void;

View File

@@ -0,0 +1 @@
export declare function assert(expectedCondition: any, message: string): asserts expectedCondition;

View File

@@ -0,0 +1,2 @@
export declare function getNonce(): string | null;
export declare function setNonce(value: string | null): void;

View File

@@ -0,0 +1,7 @@
type CursorState = "horizontal" | "intersection" | "vertical";
export declare function disableGlobalCursorStyles(): void;
export declare function enableGlobalCursorStyles(): void;
export declare function getCursorStyle(state: CursorState, constraintFlags: number): string;
export declare function resetGlobalCursorStyle(): void;
export declare function setGlobalCursorStyle(state: CursorState, constraintFlags: number): void;
export {};

View File

@@ -0,0 +1 @@
export declare function getPanelElement(id: string, scope?: ParentNode | HTMLElement): HTMLElement | null;

View File

@@ -0,0 +1 @@
export declare function getPanelElementsForGroup(groupId: string, scope?: ParentNode | HTMLElement): HTMLElement[];

View File

@@ -0,0 +1 @@
export declare function getPanelGroupElement(id: string, rootElement?: ParentNode | HTMLElement): HTMLElement | null;

View File

@@ -0,0 +1 @@
export declare function getResizeHandleElement(id: string, scope?: ParentNode | HTMLElement): HTMLElement | null;

View File

@@ -0,0 +1 @@
export declare function getResizeHandleElementIndex(groupId: string, id: string, scope?: ParentNode | HTMLElement): number | null;

View File

@@ -0,0 +1 @@
export declare function getResizeHandleElementsForGroup(groupId: string, scope?: ParentNode | HTMLElement): HTMLElement[];

View File

@@ -0,0 +1,2 @@
import { PanelData } from "../../Panel.js";
export declare function getResizeHandlePanelIds(groupId: string, handleId: string, panelsArray: PanelData[], scope?: ParentNode | HTMLElement): [idBefore: string | null, idAfter: string | null];

View File

@@ -0,0 +1,2 @@
import { Rectangle } from "./types.js";
export declare function getIntersectingRectangle(rectOne: Rectangle, rectTwo: Rectangle, strict: boolean): Rectangle;

View File

@@ -0,0 +1,2 @@
import { Rectangle } from "./types.js";
export declare function intersects(rectOne: Rectangle, rectTwo: Rectangle, strict: boolean): boolean;

View File

@@ -0,0 +1,6 @@
export interface Rectangle {
x: number;
y: number;
width: number;
height: number;
}

View File

@@ -0,0 +1,7 @@
import * as React from "react";
import type { CSSProperties, ElementType, ForwardedRef, HTMLAttributes, MouseEvent, PropsWithChildren, ReactElement, ReactNode, RefObject, TouchEvent } from "react";
declare const createElement: typeof React.createElement, createContext: typeof React.createContext, createRef: typeof React.createRef, forwardRef: typeof React.forwardRef, useCallback: typeof React.useCallback, useContext: typeof React.useContext, useEffect: typeof React.useEffect, useImperativeHandle: typeof React.useImperativeHandle, useMemo: typeof React.useMemo, useRef: typeof React.useRef, useState: typeof React.useState;
declare const useId: () => string;
declare const useLayoutEffect_do_not_use_directly: typeof React.useLayoutEffect;
export { createElement, createContext, createRef, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect_do_not_use_directly, useMemo, useRef, useState, };
export type { CSSProperties, ElementType, ForwardedRef, HTMLAttributes, MouseEvent, PropsWithChildren, ReactElement, ReactNode, RefObject, TouchEvent, };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
export {
Panel,
PanelGroup,
PanelResizeHandle,
assert,
disableGlobalCursorStyles,
enableGlobalCursorStyles,
getIntersectingRectangle,
getPanelElement,
getPanelElementsForGroup,
getPanelGroupElement,
getResizeHandleElement,
getResizeHandleElementIndex,
getResizeHandleElementsForGroup,
getResizeHandlePanelIds,
intersects,
setNonce
} from "./react-resizable-panels.browser.cjs.js";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
export {
Panel,
PanelGroup,
PanelResizeHandle,
assert,
disableGlobalCursorStyles,
enableGlobalCursorStyles,
getIntersectingRectangle,
getPanelElement,
getPanelElementsForGroup,
getPanelGroupElement,
getResizeHandleElement,
getResizeHandleElementIndex,
getResizeHandleElementsForGroup,
getResizeHandlePanelIds,
intersects,
setNonce
} from "./react-resizable-panels.browser.development.cjs.js";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
export * from "./declarations/src/index.js";
//# sourceMappingURL=react-resizable-panels.cjs.d.mts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"react-resizable-panels.cjs.d.mts","sourceRoot":"","sources":["./declarations/src/index.d.ts"],"names":[],"mappings":"AAAA"}

View File

@@ -0,0 +1,2 @@
export * from "./declarations/src/index";
//# sourceMappingURL=react-resizable-panels.cjs.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"react-resizable-panels.cjs.d.ts","sourceRoot":"","sources":["./declarations/src/index.d.ts"],"names":[],"mappings":"AAAA"}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
export {
Panel,
PanelGroup,
PanelResizeHandle,
assert,
disableGlobalCursorStyles,
enableGlobalCursorStyles,
getIntersectingRectangle,
getPanelElement,
getPanelElementsForGroup,
getPanelGroupElement,
getResizeHandleElement,
getResizeHandleElementIndex,
getResizeHandleElementsForGroup,
getResizeHandlePanelIds,
intersects,
setNonce
} from "./react-resizable-panels.cjs.js";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
export {
Panel,
PanelGroup,
PanelResizeHandle,
assert,
disableGlobalCursorStyles,
enableGlobalCursorStyles,
getIntersectingRectangle,
getPanelElement,
getPanelElementsForGroup,
getPanelGroupElement,
getResizeHandleElement,
getResizeHandleElementIndex,
getResizeHandleElementsForGroup,
getResizeHandlePanelIds,
intersects,
setNonce
} from "./react-resizable-panels.development.cjs.js";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
export {
Panel,
PanelGroup,
PanelResizeHandle,
assert,
disableGlobalCursorStyles,
enableGlobalCursorStyles,
getIntersectingRectangle,
getPanelElement,
getPanelElementsForGroup,
getPanelGroupElement,
getResizeHandleElement,
getResizeHandleElementIndex,
getResizeHandleElementsForGroup,
getResizeHandlePanelIds,
intersects,
setNonce
} from "./react-resizable-panels.development.node.cjs.js";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
export {
Panel,
PanelGroup,
PanelResizeHandle,
assert,
disableGlobalCursorStyles,
enableGlobalCursorStyles,
getIntersectingRectangle,
getPanelElement,
getPanelElementsForGroup,
getPanelGroupElement,
getResizeHandleElement,
getResizeHandleElementIndex,
getResizeHandleElementsForGroup,
getResizeHandlePanelIds,
intersects,
setNonce
} from "./react-resizable-panels.node.cjs.js";

File diff suppressed because it is too large Load Diff

10
node_modules/react-resizable-panels/jest.config.js generated vendored Normal file
View File

@@ -0,0 +1,10 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testEnvironment: "jsdom",
preset: "ts-jest",
prettierPath: null,
testEnvironmentOptions: {
customExportConditions: ["development"],
},
testMatch: ["**/*.test.{ts,tsx}"],
};

90
node_modules/react-resizable-panels/package.json generated vendored Normal file
View File

@@ -0,0 +1,90 @@
{
"name": "react-resizable-panels",
"version": "2.1.4",
"description": "React components for resizable panel groups/layouts",
"author": "Brian Vaughn <brian.david.vaughn@gmail.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/bvaughn/react-resizable-panels.git"
},
"source": "src/index.ts",
"main": "dist/react-resizable-panels.cjs.js",
"module": "dist/react-resizable-panels.esm.js",
"exports": {
".": {
"types": {
"import": "./dist/react-resizable-panels.cjs.mjs",
"default": "./dist/react-resizable-panels.cjs.js"
},
"development": {
"browser": {
"module": "./dist/react-resizable-panels.browser.development.esm.js",
"import": "./dist/react-resizable-panels.browser.development.cjs.mjs",
"default": "./dist/react-resizable-panels.browser.development.cjs.js"
},
"node": {
"module": "./dist/react-resizable-panels.development.node.esm.js",
"import": "./dist/react-resizable-panels.development.node.cjs.mjs",
"default": "./dist/react-resizable-panels.development.node.cjs.js"
},
"module": "./dist/react-resizable-panels.development.esm.js",
"import": "./dist/react-resizable-panels.development.cjs.mjs",
"default": "./dist/react-resizable-panels.development.cjs.js"
},
"browser": {
"module": "./dist/react-resizable-panels.browser.esm.js",
"import": "./dist/react-resizable-panels.browser.cjs.mjs",
"default": "./dist/react-resizable-panels.browser.cjs.js"
},
"node": {
"module": "./dist/react-resizable-panels.node.esm.js",
"import": "./dist/react-resizable-panels.node.cjs.mjs",
"default": "./dist/react-resizable-panels.node.cjs.js"
},
"module": "./dist/react-resizable-panels.esm.js",
"import": "./dist/react-resizable-panels.cjs.mjs",
"default": "./dist/react-resizable-panels.cjs.js"
},
"./package.json": "./package.json"
},
"imports": {
"#is-development": {
"development": "./src/env-conditions/development.ts",
"default": "./src/env-conditions/production.ts"
},
"#is-browser": {
"browser": "./src/env-conditions/browser.ts",
"node": "./src/env-conditions/node.ts",
"default": "./src/env-conditions/unknown.ts"
}
},
"types": "dist/react-resizable-panels.cjs.d.ts",
"scripts": {
"clear": "pnpm run clear:builds & pnpm run clear:node_modules",
"clear:builds": "rm -rf ./packages/*/dist",
"clear:node_modules": "rm -rf ./node_modules",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"test": "jest --config=jest.config.js",
"test:watch": "jest --config=jest.config.js --watch",
"watch": "parcel watch --port=2345"
},
"devDependencies": {
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"eslint": "^8.37.0",
"eslint-plugin-no-restricted-imports": "^0.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"react": "experimental",
"react-dom": "experimental"
},
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
},
"browserslist": [
"Chrome 79"
]
}

1084
node_modules/react-resizable-panels/src/Panel.test.tsx generated vendored Normal file

File diff suppressed because it is too large Load Diff

259
node_modules/react-resizable-panels/src/Panel.ts generated vendored Normal file
View File

@@ -0,0 +1,259 @@
import { isBrowser } from "#is-browser";
import { isDevelopment } from "#is-development";
import { PanelGroupContext } from "./PanelGroupContext";
import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect";
import useUniqueId from "./hooks/useUniqueId";
import {
ForwardedRef,
HTMLAttributes,
PropsWithChildren,
ReactElement,
createElement,
forwardRef,
useContext,
useImperativeHandle,
useRef,
} from "./vendor/react";
export type PanelOnCollapse = () => void;
export type PanelOnExpand = () => void;
export type PanelOnResize = (
size: number,
prevSize: number | undefined
) => void;
export type PanelCallbacks = {
onCollapse?: PanelOnCollapse;
onExpand?: PanelOnExpand;
onResize?: PanelOnResize;
};
export type PanelConstraints = {
collapsedSize?: number | undefined;
collapsible?: boolean | undefined;
defaultSize?: number | undefined;
maxSize?: number | undefined;
minSize?: number | undefined;
};
export type PanelData = {
callbacks: PanelCallbacks;
constraints: PanelConstraints;
id: string;
idIsFromProps: boolean;
order: number | undefined;
};
export type ImperativePanelHandle = {
collapse: () => void;
expand: (minSize?: number) => void;
getId(): string;
getSize(): number;
isCollapsed: () => boolean;
isExpanded: () => boolean;
resize: (size: number) => void;
};
export type PanelProps<
T extends keyof HTMLElementTagNameMap = keyof HTMLElementTagNameMap,
> = Omit<HTMLAttributes<HTMLElementTagNameMap[T]>, "id" | "onResize"> &
PropsWithChildren<{
className?: string;
collapsedSize?: number | undefined;
collapsible?: boolean | undefined;
defaultSize?: number | undefined;
id?: string;
maxSize?: number | undefined;
minSize?: number | undefined;
onCollapse?: PanelOnCollapse;
onExpand?: PanelOnExpand;
onResize?: PanelOnResize;
order?: number;
style?: object;
tagName?: T;
}>;
export function PanelWithForwardedRef({
children,
className: classNameFromProps = "",
collapsedSize,
collapsible,
defaultSize,
forwardedRef,
id: idFromProps,
maxSize,
minSize,
onCollapse,
onExpand,
onResize,
order,
style: styleFromProps,
tagName: Type = "div",
...rest
}: PanelProps & {
forwardedRef: ForwardedRef<ImperativePanelHandle>;
}): ReactElement {
const context = useContext(PanelGroupContext);
if (context === null) {
throw Error(
`Panel components must be rendered within a PanelGroup container`
);
}
const {
collapsePanel,
expandPanel,
getPanelSize,
getPanelStyle,
groupId,
isPanelCollapsed,
reevaluatePanelConstraints,
registerPanel,
resizePanel,
unregisterPanel,
} = context;
const panelId = useUniqueId(idFromProps);
const panelDataRef = useRef<PanelData>({
callbacks: {
onCollapse,
onExpand,
onResize,
},
constraints: {
collapsedSize,
collapsible,
defaultSize,
maxSize,
minSize,
},
id: panelId,
idIsFromProps: idFromProps !== undefined,
order,
});
const devWarningsRef = useRef<{
didLogMissingDefaultSizeWarning: boolean;
}>({
didLogMissingDefaultSizeWarning: false,
});
// Normally we wouldn't log a warning during render,
// but effects don't run on the server, so we can't do it there
if (isDevelopment) {
if (!devWarningsRef.current.didLogMissingDefaultSizeWarning) {
if (!isBrowser && defaultSize == null) {
devWarningsRef.current.didLogMissingDefaultSizeWarning = true;
console.warn(
`WARNING: Panel defaultSize prop recommended to avoid layout shift after server rendering`
);
}
}
}
useIsomorphicLayoutEffect(() => {
const { callbacks, constraints } = panelDataRef.current;
const prevConstraints = { ...constraints };
panelDataRef.current.id = panelId;
panelDataRef.current.idIsFromProps = idFromProps !== undefined;
panelDataRef.current.order = order;
callbacks.onCollapse = onCollapse;
callbacks.onExpand = onExpand;
callbacks.onResize = onResize;
constraints.collapsedSize = collapsedSize;
constraints.collapsible = collapsible;
constraints.defaultSize = defaultSize;
constraints.maxSize = maxSize;
constraints.minSize = minSize;
// If constraints have changed, we should revisit panel sizes.
// This is uncommon but may happen if people are trying to implement pixel based constraints.
if (
prevConstraints.collapsedSize !== constraints.collapsedSize ||
prevConstraints.collapsible !== constraints.collapsible ||
prevConstraints.maxSize !== constraints.maxSize ||
prevConstraints.minSize !== constraints.minSize
) {
reevaluatePanelConstraints(panelDataRef.current, prevConstraints);
}
});
useIsomorphicLayoutEffect(() => {
const panelData = panelDataRef.current;
registerPanel(panelData);
return () => {
unregisterPanel(panelData);
};
}, [order, panelId, registerPanel, unregisterPanel]);
useImperativeHandle(
forwardedRef,
() => ({
collapse: () => {
collapsePanel(panelDataRef.current);
},
expand: (minSize?: number) => {
expandPanel(panelDataRef.current, minSize);
},
getId() {
return panelId;
},
getSize() {
return getPanelSize(panelDataRef.current);
},
isCollapsed() {
return isPanelCollapsed(panelDataRef.current);
},
isExpanded() {
return !isPanelCollapsed(panelDataRef.current);
},
resize: (size: number) => {
resizePanel(panelDataRef.current, size);
},
}),
[
collapsePanel,
expandPanel,
getPanelSize,
isPanelCollapsed,
panelId,
resizePanel,
]
);
const style = getPanelStyle(panelDataRef.current, defaultSize);
return createElement(Type, {
...rest,
children,
className: classNameFromProps,
id: idFromProps,
style: {
...style,
...styleFromProps,
},
// CSS selectors
"data-panel": "",
"data-panel-collapsible": collapsible || undefined,
"data-panel-group-id": groupId,
"data-panel-id": panelId,
"data-panel-size": parseFloat("" + style.flexGrow).toFixed(1),
});
}
export const Panel = forwardRef<ImperativePanelHandle, PanelProps>(
(props: PanelProps, ref: ForwardedRef<ImperativePanelHandle>) =>
createElement(PanelWithForwardedRef, { ...props, forwardedRef: ref })
);
PanelWithForwardedRef.displayName = "Panel";
Panel.displayName = "forwardRef(Panel)";

View File

@@ -0,0 +1,443 @@
// @ts-expect-error This is an experimental API
// eslint-disable-next-line no-restricted-imports
import { unstable_Activity as Activity, Fragment } from "react";
import { Root, createRoot } from "react-dom/client";
import { act } from "react-dom/test-utils";
import {
ImperativePanelGroupHandle,
ImperativePanelHandle,
Panel,
PanelGroup,
PanelResizeHandle,
getPanelElement,
} from ".";
import { assert } from "./utils/assert";
import { getPanelGroupElement } from "./utils/dom/getPanelGroupElement";
import {
mockPanelGroupOffsetWidthAndHeight,
verifyAttribute,
} from "./utils/test-utils";
import { createRef } from "./vendor/react";
describe("PanelGroup", () => {
let expectedWarnings: string[] = [];
let root: Root;
let container: HTMLElement;
let uninstallMockOffsetWidthAndHeight: () => void;
function expectWarning(expectedMessage: string) {
expectedWarnings.push(expectedMessage);
}
beforeEach(() => {
// @ts-expect-error
global.IS_REACT_ACT_ENVIRONMENT = true;
// JSDom doesn't support element sizes
uninstallMockOffsetWidthAndHeight = mockPanelGroupOffsetWidthAndHeight();
container = document.createElement("div");
document.body.appendChild(container);
expectedWarnings = [];
root = createRoot(container);
jest.spyOn(console, "warn").mockImplementation((actualMessage: string) => {
const match = expectedWarnings.findIndex((expectedMessage) => {
return actualMessage.includes(expectedMessage);
});
if (match >= 0) {
expectedWarnings.splice(match, 1);
return;
}
throw Error(`Unexpected warning: ${actualMessage}`);
});
});
afterEach(() => {
uninstallMockOffsetWidthAndHeight();
jest.clearAllMocks();
jest.resetModules();
act(() => {
root.unmount();
});
expect(expectedWarnings).toHaveLength(0);
});
it("should recalculate layout after being hidden by Activity", () => {
const panelRef = createRef<ImperativePanelHandle>();
let mostRecentLayout: number[] | null = null;
const onLayout = (layout: number[]) => {
mostRecentLayout = layout;
};
act(() => {
root.render(
<Activity mode="visible">
<PanelGroup direction="horizontal" onLayout={onLayout}>
<Panel id="left" ref={panelRef} />
<PanelResizeHandle />
<Panel defaultSize={40} id="right" />
</PanelGroup>
</Activity>
);
});
expect(mostRecentLayout).toEqual([60, 40]);
expect(panelRef.current?.getSize()).toEqual(60);
const leftPanelElement = getPanelElement("left");
const rightPanelElement = getPanelElement("right");
expect(leftPanelElement?.getAttribute("data-panel-size")).toBe("60.0");
expect(rightPanelElement?.getAttribute("data-panel-size")).toBe("40.0");
act(() => {
root.render(
<Activity mode="hidden">
<PanelGroup direction="horizontal" onLayout={onLayout}>
<Panel id="left" ref={panelRef} />
<PanelResizeHandle />
<Panel defaultSize={40} id="right" />
</PanelGroup>
</Activity>
);
});
act(() => {
root.render(
<Activity mode="visible">
<PanelGroup direction="horizontal" onLayout={onLayout}>
<Panel id="left" ref={panelRef} />
<PanelResizeHandle />
<Panel defaultSize={40} id="right" />
</PanelGroup>
</Activity>
);
});
expect(mostRecentLayout).toEqual([60, 40]);
expect(panelRef.current?.getSize()).toEqual(60);
// This bug is only observable in the DOM; callbacks will not re-fire
expect(leftPanelElement?.getAttribute("data-panel-size")).toBe("60.0");
expect(rightPanelElement?.getAttribute("data-panel-size")).toBe("40.0");
});
// github.com/bvaughn/react-resizable-panels/issues/303
it("should recalculate layout after panels are changed", () => {
let mostRecentLayout: number[] | null = null;
const onLayout = (layout: number[]) => {
mostRecentLayout = layout;
};
act(() => {
root.render(
<PanelGroup direction="vertical" onLayout={onLayout}>
<Panel id="foo" minSize={30} order={0} />
<PanelResizeHandle />
<Panel id="bar" minSize={70} order={1} />
</PanelGroup>
);
});
expect(mostRecentLayout).toEqual([30, 70]);
act(() => {
root.render(
<PanelGroup direction="vertical" onLayout={onLayout}>
<Panel id="bar" minSize={70} order={0} />
</PanelGroup>
);
});
expect(mostRecentLayout).toEqual([100]);
});
describe("imperative handle API", () => {
it("should report the most recently rendered group id", () => {
const ref = createRef<ImperativePanelGroupHandle>();
act(() => {
root.render(<PanelGroup direction="horizontal" id="one" ref={ref} />);
});
expect(ref.current?.getId()).toBe("one");
act(() => {
root.render(<PanelGroup direction="horizontal" id="two" ref={ref} />);
});
expect(ref.current?.getId()).toBe("two");
});
it("should get and set layouts", () => {
const ref = createRef<ImperativePanelGroupHandle>();
let mostRecentLayout: number[] | null = null;
const onLayout = (layout: number[]) => {
mostRecentLayout = layout;
};
act(() => {
root.render(
<PanelGroup direction="horizontal" onLayout={onLayout} ref={ref}>
<Panel defaultSize={50} id="a" />
<PanelResizeHandle />
<Panel defaultSize={50} id="b" />
</PanelGroup>
);
});
expect(mostRecentLayout).toEqual([50, 50]);
act(() => {
ref.current?.setLayout([25, 75]);
});
expect(mostRecentLayout).toEqual([25, 75]);
});
});
it("should support ...rest attributes", () => {
act(() => {
root.render(
<PanelGroup
data-test-name="foo"
direction="horizontal"
id="group"
tabIndex={123}
title="bar"
>
<Panel />
<PanelResizeHandle />
<Panel />
</PanelGroup>
);
});
const element = getPanelGroupElement("group", container);
assert(element, "");
expect(element.tabIndex).toBe(123);
expect(element.getAttribute("data-test-name")).toBe("foo");
expect(element.title).toBe("bar");
});
describe("callbacks", () => {
describe("onLayout", () => {
it("should be called with the initial group layout on mount", () => {
let onLayout = jest.fn();
act(() => {
root.render(
<PanelGroup direction="horizontal" onLayout={onLayout}>
<Panel defaultSize={35} />
<PanelResizeHandle />
<Panel defaultSize={65} />
</PanelGroup>
);
});
expect(onLayout).toHaveBeenCalledTimes(1);
expect(onLayout).toHaveBeenCalledWith([35, 65]);
});
it("should be called any time the group layout changes", () => {
let onLayout = jest.fn();
let panelGroupRef = createRef<ImperativePanelGroupHandle>();
let panelRef = createRef<ImperativePanelHandle>();
act(() => {
root.render(
<PanelGroup
direction="horizontal"
onLayout={onLayout}
ref={panelGroupRef}
>
<Panel defaultSize={35} ref={panelRef} />
<PanelResizeHandle />
<Panel defaultSize={65} />
</PanelGroup>
);
});
onLayout.mockReset();
act(() => {
panelGroupRef.current?.setLayout([25, 75]);
});
expect(onLayout).toHaveBeenCalledTimes(1);
expect(onLayout).toHaveBeenCalledWith([25, 75]);
onLayout.mockReset();
act(() => {
panelRef.current?.resize(50);
});
expect(onLayout).toHaveBeenCalledTimes(1);
expect(onLayout).toHaveBeenCalledWith([50, 50]);
});
});
});
describe("data attributes", () => {
it("should initialize with the correct props based attributes", () => {
act(() => {
root.render(
<PanelGroup direction="horizontal" id="test-group"></PanelGroup>
);
});
const element = getPanelGroupElement("test-group", container);
assert(element, "");
verifyAttribute(element, "data-panel-group", "");
verifyAttribute(element, "data-panel-group-direction", "horizontal");
verifyAttribute(element, "data-panel-group-id", "test-group");
});
});
describe("a11y", () => {
it("should pass explicit id prop to DOM", () => {
act(() => {
root.render(
<PanelGroup direction="horizontal" id="explicit-id">
<Panel />
<PanelResizeHandle />
<Panel />
</PanelGroup>
);
});
const element = container.querySelector("[data-panel-group]");
expect(element).not.toBeNull();
expect(element?.getAttribute("id")).toBe("explicit-id");
});
it("should not pass auto-generated id prop to DOM", () => {
act(() => {
root.render(
<PanelGroup direction="horizontal">
<Panel />
<PanelResizeHandle />
<Panel />
</PanelGroup>
);
});
const element = container.querySelector("[data-panel-group]");
expect(element).not.toBeNull();
expect(element?.getAttribute("id")).toBeNull();
});
});
describe("DEV warnings", () => {
it("should warn about unstable layouts without id and order props", () => {
act(() => {
root.render(
<PanelGroup direction="horizontal">
<Panel defaultSize={100} id="a" />
</PanelGroup>
);
});
expectWarning(
"Panel id and order props recommended when panels are dynamically rendered"
);
act(() => {
root.render(
<PanelGroup direction="horizontal">
<Panel defaultSize={50} id="a" />
<PanelResizeHandle />
<Panel defaultSize={50} id="b" />
</PanelGroup>
);
});
});
it("should warn about missing resize handles", () => {
expectWarning(
'Missing resize handle for PanelGroup "group-without-handle"'
);
act(() => {
root.render(
<PanelGroup direction="horizontal" id="group-without-handle">
<Panel />
<Panel />
</PanelGroup>
);
});
});
it("should warn about an invalid declarative layout", () => {
expectWarning("Invalid layout total size: 60%, 80%");
act(() => {
root.render(
<PanelGroup direction="horizontal" id="group-without-handle">
<Panel defaultSize={60} />
<PanelResizeHandle />
<Panel defaultSize={80} />
</PanelGroup>
);
});
});
it("should warn about an invalid layout set via the imperative api", () => {
const ref = createRef<ImperativePanelGroupHandle>();
act(() => {
root.render(
<PanelGroup
direction="horizontal"
id="group-without-handle"
ref={ref}
>
<Panel defaultSize={30} />
<PanelResizeHandle />
<Panel defaultSize={70} />
</PanelGroup>
);
});
expectWarning("Invalid layout total size: 60%, 80%");
act(() => {
ref.current?.setLayout([60, 80]);
});
});
it("should warn about an empty layout", () => {
act(() => {
root.render(
<PanelGroup direction="horizontal" id="group-without-handle">
<Panel />
</PanelGroup>
);
});
// Since the layout is empty, no warning is expected (even though the sizes won't total 100%)
act(() => {
root.render(
<PanelGroup
direction="horizontal"
id="group-without-handle"
></PanelGroup>
);
});
});
});
});

985
node_modules/react-resizable-panels/src/PanelGroup.ts generated vendored Normal file
View File

@@ -0,0 +1,985 @@
import { isDevelopment } from "#is-development";
import { PanelConstraints, PanelData } from "./Panel";
import {
DragState,
PanelGroupContext,
ResizeEvent,
TPanelGroupContext,
} from "./PanelGroupContext";
import {
EXCEEDED_HORIZONTAL_MAX,
EXCEEDED_HORIZONTAL_MIN,
EXCEEDED_VERTICAL_MAX,
EXCEEDED_VERTICAL_MIN,
reportConstraintsViolation,
} from "./PanelResizeHandleRegistry";
import { useForceUpdate } from "./hooks/useForceUpdate";
import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect";
import useUniqueId from "./hooks/useUniqueId";
import { useWindowSplitterPanelGroupBehavior } from "./hooks/useWindowSplitterPanelGroupBehavior";
import { Direction } from "./types";
import { adjustLayoutByDelta } from "./utils/adjustLayoutByDelta";
import { areEqual } from "./utils/arrays";
import { assert } from "./utils/assert";
import { calculateDeltaPercentage } from "./utils/calculateDeltaPercentage";
import { calculateUnsafeDefaultLayout } from "./utils/calculateUnsafeDefaultLayout";
import { callPanelCallbacks } from "./utils/callPanelCallbacks";
import { compareLayouts } from "./utils/compareLayouts";
import { computePanelFlexBoxStyle } from "./utils/computePanelFlexBoxStyle";
import debounce from "./utils/debounce";
import { determinePivotIndices } from "./utils/determinePivotIndices";
import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
import { isKeyDown, isMouseEvent, isPointerEvent } from "./utils/events";
import { getResizeEventCursorPosition } from "./utils/events/getResizeEventCursorPosition";
import { initializeDefaultStorage } from "./utils/initializeDefaultStorage";
import {
fuzzyCompareNumbers,
fuzzyNumbersEqual,
} from "./utils/numbers/fuzzyCompareNumbers";
import {
loadPanelGroupState,
savePanelGroupState,
} from "./utils/serialization";
import { validatePanelConstraints } from "./utils/validatePanelConstraints";
import { validatePanelGroupLayout } from "./utils/validatePanelGroupLayout";
import {
CSSProperties,
ForwardedRef,
HTMLAttributes,
PropsWithChildren,
ReactElement,
createElement,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "./vendor/react";
const LOCAL_STORAGE_DEBOUNCE_INTERVAL = 100;
export type ImperativePanelGroupHandle = {
getId: () => string;
getLayout: () => number[];
setLayout: (layout: number[]) => void;
};
export type PanelGroupStorage = {
getItem(name: string): string | null;
setItem(name: string, value: string): void;
};
export type PanelGroupOnLayout = (layout: number[]) => void;
const defaultStorage: PanelGroupStorage = {
getItem: (name: string) => {
initializeDefaultStorage(defaultStorage);
return defaultStorage.getItem(name);
},
setItem: (name: string, value: string) => {
initializeDefaultStorage(defaultStorage);
defaultStorage.setItem(name, value);
},
};
export type PanelGroupProps = Omit<
HTMLAttributes<keyof HTMLElementTagNameMap>,
"id"
> &
PropsWithChildren<{
autoSaveId?: string | null;
className?: string;
direction: Direction;
id?: string | null;
keyboardResizeBy?: number | null;
onLayout?: PanelGroupOnLayout | null;
storage?: PanelGroupStorage;
style?: CSSProperties;
tagName?: keyof HTMLElementTagNameMap;
}>;
const debounceMap: {
[key: string]: typeof savePanelGroupState;
} = {};
function PanelGroupWithForwardedRef({
autoSaveId = null,
children,
className: classNameFromProps = "",
direction,
forwardedRef,
id: idFromProps = null,
onLayout = null,
keyboardResizeBy = null,
storage = defaultStorage,
style: styleFromProps,
tagName: Type = "div",
...rest
}: PanelGroupProps & {
forwardedRef: ForwardedRef<ImperativePanelGroupHandle>;
}): ReactElement {
const groupId = useUniqueId(idFromProps);
const panelGroupElementRef = useRef<HTMLDivElement | null>(null);
const [dragState, setDragState] = useState<DragState | null>(null);
const [layout, setLayout] = useState<number[]>([]);
const forceUpdate = useForceUpdate();
const panelIdToLastNotifiedSizeMapRef = useRef<Record<string, number>>({});
const panelSizeBeforeCollapseRef = useRef<Map<string, number>>(new Map());
const prevDeltaRef = useRef<number>(0);
const committedValuesRef = useRef<{
autoSaveId: string | null;
direction: Direction;
dragState: DragState | null;
id: string;
keyboardResizeBy: number | null;
onLayout: PanelGroupOnLayout | null;
storage: PanelGroupStorage;
}>({
autoSaveId,
direction,
dragState,
id: groupId,
keyboardResizeBy,
onLayout,
storage,
});
const eagerValuesRef = useRef<{
layout: number[];
panelDataArray: PanelData[];
panelDataArrayChanged: boolean;
}>({
layout,
panelDataArray: [],
panelDataArrayChanged: false,
});
const devWarningsRef = useRef<{
didLogIdAndOrderWarning: boolean;
didLogPanelConstraintsWarning: boolean;
prevPanelIds: string[];
}>({
didLogIdAndOrderWarning: false,
didLogPanelConstraintsWarning: false,
prevPanelIds: [],
});
useImperativeHandle(
forwardedRef,
() => ({
getId: () => committedValuesRef.current.id,
getLayout: () => {
const { layout } = eagerValuesRef.current;
return layout;
},
setLayout: (unsafeLayout: number[]) => {
const { onLayout } = committedValuesRef.current;
const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
const safeLayout = validatePanelGroupLayout({
layout: unsafeLayout,
panelConstraints: panelDataArray.map(
(panelData) => panelData.constraints
),
});
if (!areEqual(prevLayout, safeLayout)) {
setLayout(safeLayout);
eagerValuesRef.current.layout = safeLayout;
if (onLayout) {
onLayout(safeLayout);
}
callPanelCallbacks(
panelDataArray,
safeLayout,
panelIdToLastNotifiedSizeMapRef.current
);
}
},
}),
[]
);
useIsomorphicLayoutEffect(() => {
committedValuesRef.current.autoSaveId = autoSaveId;
committedValuesRef.current.direction = direction;
committedValuesRef.current.dragState = dragState;
committedValuesRef.current.id = groupId;
committedValuesRef.current.onLayout = onLayout;
committedValuesRef.current.storage = storage;
});
useWindowSplitterPanelGroupBehavior({
committedValuesRef,
eagerValuesRef,
groupId,
layout,
panelDataArray: eagerValuesRef.current.panelDataArray,
setLayout,
panelGroupElement: panelGroupElementRef.current,
});
useEffect(() => {
const { panelDataArray } = eagerValuesRef.current;
// If this panel has been configured to persist sizing information, save sizes to local storage.
if (autoSaveId) {
if (layout.length === 0 || layout.length !== panelDataArray.length) {
return;
}
let debouncedSave = debounceMap[autoSaveId];
// Limit the frequency of localStorage updates.
if (debouncedSave == null) {
debouncedSave = debounce(
savePanelGroupState,
LOCAL_STORAGE_DEBOUNCE_INTERVAL
);
debounceMap[autoSaveId] = debouncedSave;
}
// Clone mutable data before passing to the debounced function,
// else we run the risk of saving an incorrect combination of mutable and immutable values to state.
const clonedPanelDataArray = [...panelDataArray];
const clonedPanelSizesBeforeCollapse = new Map(
panelSizeBeforeCollapseRef.current
);
debouncedSave(
autoSaveId,
clonedPanelDataArray,
clonedPanelSizesBeforeCollapse,
layout,
storage
);
}
}, [autoSaveId, layout, storage]);
// DEV warnings
useEffect(() => {
if (isDevelopment) {
const { panelDataArray } = eagerValuesRef.current;
const {
didLogIdAndOrderWarning,
didLogPanelConstraintsWarning,
prevPanelIds,
} = devWarningsRef.current;
if (!didLogIdAndOrderWarning) {
const panelIds = panelDataArray.map(({ id }) => id);
devWarningsRef.current.prevPanelIds = panelIds;
const panelsHaveChanged =
prevPanelIds.length > 0 && !areEqual(prevPanelIds, panelIds);
if (panelsHaveChanged) {
if (
panelDataArray.find(
({ idIsFromProps, order }) => !idIsFromProps || order == null
)
) {
devWarningsRef.current.didLogIdAndOrderWarning = true;
console.warn(
`WARNING: Panel id and order props recommended when panels are dynamically rendered`
);
}
}
}
if (!didLogPanelConstraintsWarning) {
const panelConstraints = panelDataArray.map(
(panelData) => panelData.constraints
);
for (
let panelIndex = 0;
panelIndex < panelConstraints.length;
panelIndex++
) {
const panelData = panelDataArray[panelIndex];
assert(panelData, `Panel data not found for index ${panelIndex}`);
const isValid = validatePanelConstraints({
panelConstraints,
panelId: panelData.id,
panelIndex,
});
if (!isValid) {
devWarningsRef.current.didLogPanelConstraintsWarning = true;
break;
}
}
}
}
});
// External APIs are safe to memoize via committed values ref
const collapsePanel = useCallback((panelData: PanelData) => {
const { onLayout } = committedValuesRef.current;
const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
if (panelData.constraints.collapsible) {
const panelConstraintsArray = panelDataArray.map(
(panelData) => panelData.constraints
);
const {
collapsedSize = 0,
panelSize,
pivotIndices,
} = panelDataHelper(panelDataArray, panelData, prevLayout);
assert(
panelSize != null,
`Panel size not found for panel "${panelData.id}"`
);
if (!fuzzyNumbersEqual(panelSize, collapsedSize)) {
// Store size before collapse;
// This is the size that gets restored if the expand() API is used.
panelSizeBeforeCollapseRef.current.set(panelData.id, panelSize);
const isLastPanel =
findPanelDataIndex(panelDataArray, panelData) ===
panelDataArray.length - 1;
const delta = isLastPanel
? panelSize - collapsedSize
: collapsedSize - panelSize;
const nextLayout = adjustLayoutByDelta({
delta,
initialLayout: prevLayout,
panelConstraints: panelConstraintsArray,
pivotIndices,
prevLayout,
trigger: "imperative-api",
});
if (!compareLayouts(prevLayout, nextLayout)) {
setLayout(nextLayout);
eagerValuesRef.current.layout = nextLayout;
if (onLayout) {
onLayout(nextLayout);
}
callPanelCallbacks(
panelDataArray,
nextLayout,
panelIdToLastNotifiedSizeMapRef.current
);
}
}
}
}, []);
// External APIs are safe to memoize via committed values ref
const expandPanel = useCallback(
(panelData: PanelData, minSizeOverride?: number) => {
const { onLayout } = committedValuesRef.current;
const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
if (panelData.constraints.collapsible) {
const panelConstraintsArray = panelDataArray.map(
(panelData) => panelData.constraints
);
const {
collapsedSize = 0,
panelSize = 0,
minSize: minSizeFromProps = 0,
pivotIndices,
} = panelDataHelper(panelDataArray, panelData, prevLayout);
const minSize = minSizeOverride ?? minSizeFromProps;
if (fuzzyNumbersEqual(panelSize, collapsedSize)) {
// Restore this panel to the size it was before it was collapsed, if possible.
const prevPanelSize = panelSizeBeforeCollapseRef.current.get(
panelData.id
);
const baseSize =
prevPanelSize != null && prevPanelSize >= minSize
? prevPanelSize
: minSize;
const isLastPanel =
findPanelDataIndex(panelDataArray, panelData) ===
panelDataArray.length - 1;
const delta = isLastPanel
? panelSize - baseSize
: baseSize - panelSize;
const nextLayout = adjustLayoutByDelta({
delta,
initialLayout: prevLayout,
panelConstraints: panelConstraintsArray,
pivotIndices,
prevLayout,
trigger: "imperative-api",
});
if (!compareLayouts(prevLayout, nextLayout)) {
setLayout(nextLayout);
eagerValuesRef.current.layout = nextLayout;
if (onLayout) {
onLayout(nextLayout);
}
callPanelCallbacks(
panelDataArray,
nextLayout,
panelIdToLastNotifiedSizeMapRef.current
);
}
}
}
},
[]
);
// External APIs are safe to memoize via committed values ref
const getPanelSize = useCallback((panelData: PanelData) => {
const { layout, panelDataArray } = eagerValuesRef.current;
const { panelSize } = panelDataHelper(panelDataArray, panelData, layout);
assert(
panelSize != null,
`Panel size not found for panel "${panelData.id}"`
);
return panelSize;
}, []);
// This API should never read from committedValuesRef
const getPanelStyle = useCallback(
(panelData: PanelData, defaultSize: number | undefined) => {
const { panelDataArray } = eagerValuesRef.current;
const panelIndex = findPanelDataIndex(panelDataArray, panelData);
return computePanelFlexBoxStyle({
defaultSize,
dragState,
layout,
panelData: panelDataArray,
panelIndex,
});
},
[dragState, layout]
);
// External APIs are safe to memoize via committed values ref
const isPanelCollapsed = useCallback((panelData: PanelData) => {
const { layout, panelDataArray } = eagerValuesRef.current;
const {
collapsedSize = 0,
collapsible,
panelSize,
} = panelDataHelper(panelDataArray, panelData, layout);
assert(
panelSize != null,
`Panel size not found for panel "${panelData.id}"`
);
return collapsible === true && fuzzyNumbersEqual(panelSize, collapsedSize);
}, []);
// External APIs are safe to memoize via committed values ref
const isPanelExpanded = useCallback((panelData: PanelData) => {
const { layout, panelDataArray } = eagerValuesRef.current;
const {
collapsedSize = 0,
collapsible,
panelSize,
} = panelDataHelper(panelDataArray, panelData, layout);
assert(
panelSize != null,
`Panel size not found for panel "${panelData.id}"`
);
return !collapsible || fuzzyCompareNumbers(panelSize, collapsedSize) > 0;
}, []);
const registerPanel = useCallback(
(panelData: PanelData) => {
const { panelDataArray } = eagerValuesRef.current;
panelDataArray.push(panelData);
panelDataArray.sort((panelA, panelB) => {
const orderA = panelA.order;
const orderB = panelB.order;
if (orderA == null && orderB == null) {
return 0;
} else if (orderA == null) {
return -1;
} else if (orderB == null) {
return 1;
} else {
return orderA - orderB;
}
});
eagerValuesRef.current.panelDataArrayChanged = true;
forceUpdate();
},
[forceUpdate]
);
// (Re)calculate group layout whenever panels are registered or unregistered.
// eslint-disable-next-line react-hooks/exhaustive-deps
useIsomorphicLayoutEffect(() => {
if (eagerValuesRef.current.panelDataArrayChanged) {
eagerValuesRef.current.panelDataArrayChanged = false;
const { autoSaveId, onLayout, storage } = committedValuesRef.current;
const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
// If this panel has been configured to persist sizing information,
// default size should be restored from local storage if possible.
let unsafeLayout: number[] | null = null;
if (autoSaveId) {
const state = loadPanelGroupState(autoSaveId, panelDataArray, storage);
if (state) {
panelSizeBeforeCollapseRef.current = new Map(
Object.entries(state.expandToSizes)
);
unsafeLayout = state.layout;
}
}
if (unsafeLayout == null) {
unsafeLayout = calculateUnsafeDefaultLayout({
panelDataArray,
});
}
// Validate even saved layouts in case something has changed since last render
// e.g. for pixel groups, this could be the size of the window
const nextLayout = validatePanelGroupLayout({
layout: unsafeLayout,
panelConstraints: panelDataArray.map(
(panelData) => panelData.constraints
),
});
if (!areEqual(prevLayout, nextLayout)) {
setLayout(nextLayout);
eagerValuesRef.current.layout = nextLayout;
if (onLayout) {
onLayout(nextLayout);
}
callPanelCallbacks(
panelDataArray,
nextLayout,
panelIdToLastNotifiedSizeMapRef.current
);
}
}
});
// Reset the cached layout if hidden by the Activity/Offscreen API
useIsomorphicLayoutEffect(() => {
const eagerValues = eagerValuesRef.current;
return () => {
eagerValues.layout = [];
};
}, []);
const registerResizeHandle = useCallback((dragHandleId: string) => {
return function resizeHandler(event: ResizeEvent) {
event.preventDefault();
const panelGroupElement = panelGroupElementRef.current;
if (!panelGroupElement) {
return () => null;
}
const {
direction,
dragState,
id: groupId,
keyboardResizeBy,
onLayout,
} = committedValuesRef.current;
const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
const { initialLayout } = dragState ?? {};
const pivotIndices = determinePivotIndices(
groupId,
dragHandleId,
panelGroupElement
);
let delta = calculateDeltaPercentage(
event,
dragHandleId,
direction,
dragState,
keyboardResizeBy,
panelGroupElement
);
// Support RTL layouts
const isHorizontal = direction === "horizontal";
if (document.dir === "rtl" && isHorizontal) {
delta = -delta;
}
const panelConstraints = panelDataArray.map(
(panelData) => panelData.constraints
);
const nextLayout = adjustLayoutByDelta({
delta,
initialLayout: initialLayout ?? prevLayout,
panelConstraints,
pivotIndices,
prevLayout,
trigger: isKeyDown(event) ? "keyboard" : "mouse-or-touch",
});
const layoutChanged = !compareLayouts(prevLayout, nextLayout);
// Only update the cursor for layout changes triggered by touch/mouse events (not keyboard)
// Update the cursor even if the layout hasn't changed (we may need to show an invalid cursor state)
if (isPointerEvent(event) || isMouseEvent(event)) {
// Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
// In this case, Panel sizes might not change
// but updating cursor in this scenario would cause a flicker.
if (prevDeltaRef.current != delta) {
prevDeltaRef.current = delta;
if (!layoutChanged && delta !== 0) {
// If the pointer has moved too far to resize the panel any further, note this so we can update the cursor.
// This mimics VS Code behavior.
if (isHorizontal) {
reportConstraintsViolation(
dragHandleId,
delta < 0 ? EXCEEDED_HORIZONTAL_MIN : EXCEEDED_HORIZONTAL_MAX
);
} else {
reportConstraintsViolation(
dragHandleId,
delta < 0 ? EXCEEDED_VERTICAL_MIN : EXCEEDED_VERTICAL_MAX
);
}
} else {
reportConstraintsViolation(dragHandleId, 0);
}
}
}
if (layoutChanged) {
setLayout(nextLayout);
eagerValuesRef.current.layout = nextLayout;
if (onLayout) {
onLayout(nextLayout);
}
callPanelCallbacks(
panelDataArray,
nextLayout,
panelIdToLastNotifiedSizeMapRef.current
);
}
};
}, []);
// External APIs are safe to memoize via committed values ref
const resizePanel = useCallback(
(panelData: PanelData, unsafePanelSize: number) => {
const { onLayout } = committedValuesRef.current;
const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
const panelConstraintsArray = panelDataArray.map(
(panelData) => panelData.constraints
);
const { panelSize, pivotIndices } = panelDataHelper(
panelDataArray,
panelData,
prevLayout
);
assert(
panelSize != null,
`Panel size not found for panel "${panelData.id}"`
);
const isLastPanel =
findPanelDataIndex(panelDataArray, panelData) ===
panelDataArray.length - 1;
const delta = isLastPanel
? panelSize - unsafePanelSize
: unsafePanelSize - panelSize;
const nextLayout = adjustLayoutByDelta({
delta,
initialLayout: prevLayout,
panelConstraints: panelConstraintsArray,
pivotIndices,
prevLayout,
trigger: "imperative-api",
});
if (!compareLayouts(prevLayout, nextLayout)) {
setLayout(nextLayout);
eagerValuesRef.current.layout = nextLayout;
if (onLayout) {
onLayout(nextLayout);
}
callPanelCallbacks(
panelDataArray,
nextLayout,
panelIdToLastNotifiedSizeMapRef.current
);
}
},
[]
);
const reevaluatePanelConstraints = useCallback(
(panelData: PanelData, prevConstraints: PanelConstraints) => {
const { layout, panelDataArray } = eagerValuesRef.current;
const {
collapsedSize: prevCollapsedSize = 0,
collapsible: prevCollapsible,
} = prevConstraints;
const {
collapsedSize: nextCollapsedSize = 0,
collapsible: nextCollapsible,
maxSize: nextMaxSize = 100,
minSize: nextMinSize = 0,
} = panelData.constraints;
const { panelSize: prevPanelSize } = panelDataHelper(
panelDataArray,
panelData,
layout
);
if (prevPanelSize == null) {
// It's possible that the panels in this group have changed since the last render
return;
}
if (
prevCollapsible &&
nextCollapsible &&
fuzzyNumbersEqual(prevPanelSize, prevCollapsedSize)
) {
if (!fuzzyNumbersEqual(prevCollapsedSize, nextCollapsedSize)) {
resizePanel(panelData, nextCollapsedSize);
} else {
// Stay collapsed
}
} else if (prevPanelSize < nextMinSize) {
resizePanel(panelData, nextMinSize);
} else if (prevPanelSize > nextMaxSize) {
resizePanel(panelData, nextMaxSize);
}
},
[resizePanel]
);
// TODO Multiple drag handles can be active at the same time so this API is a bit awkward now
const startDragging = useCallback(
(dragHandleId: string, event: ResizeEvent) => {
const { direction } = committedValuesRef.current;
const { layout } = eagerValuesRef.current;
if (!panelGroupElementRef.current) {
return;
}
const handleElement = getResizeHandleElement(
dragHandleId,
panelGroupElementRef.current
);
assert(
handleElement,
`Drag handle element not found for id "${dragHandleId}"`
);
const initialCursorPosition = getResizeEventCursorPosition(
direction,
event
);
setDragState({
dragHandleId,
dragHandleRect: handleElement.getBoundingClientRect(),
initialCursorPosition,
initialLayout: layout,
});
},
[]
);
const stopDragging = useCallback(() => {
setDragState(null);
}, []);
const unregisterPanel = useCallback(
(panelData: PanelData) => {
const { panelDataArray } = eagerValuesRef.current;
const index = findPanelDataIndex(panelDataArray, panelData);
if (index >= 0) {
panelDataArray.splice(index, 1);
// TRICKY
// When a panel is removed from the group, we should delete the most recent prev-size entry for it.
// If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
// Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
delete panelIdToLastNotifiedSizeMapRef.current[panelData.id];
eagerValuesRef.current.panelDataArrayChanged = true;
forceUpdate();
}
},
[forceUpdate]
);
const context = useMemo(
() =>
({
collapsePanel,
direction,
dragState,
expandPanel,
getPanelSize,
getPanelStyle,
groupId,
isPanelCollapsed,
isPanelExpanded,
reevaluatePanelConstraints,
registerPanel,
registerResizeHandle,
resizePanel,
startDragging,
stopDragging,
unregisterPanel,
panelGroupElement: panelGroupElementRef.current,
}) satisfies TPanelGroupContext,
[
collapsePanel,
dragState,
direction,
expandPanel,
getPanelSize,
getPanelStyle,
groupId,
isPanelCollapsed,
isPanelExpanded,
reevaluatePanelConstraints,
registerPanel,
registerResizeHandle,
resizePanel,
startDragging,
stopDragging,
unregisterPanel,
]
);
const style: CSSProperties = {
display: "flex",
flexDirection: direction === "horizontal" ? "row" : "column",
height: "100%",
overflow: "hidden",
width: "100%",
};
return createElement(
PanelGroupContext.Provider,
{ value: context },
createElement(Type, {
...rest,
children,
className: classNameFromProps,
id: idFromProps,
ref: panelGroupElementRef,
style: {
...style,
...styleFromProps,
},
// CSS selectors
"data-panel-group": "",
"data-panel-group-direction": direction,
"data-panel-group-id": groupId,
})
);
}
export const PanelGroup = forwardRef<
ImperativePanelGroupHandle,
PanelGroupProps
>((props: PanelGroupProps, ref: ForwardedRef<ImperativePanelGroupHandle>) =>
createElement(PanelGroupWithForwardedRef, { ...props, forwardedRef: ref })
);
PanelGroupWithForwardedRef.displayName = "PanelGroup";
PanelGroup.displayName = "forwardRef(PanelGroup)";
function findPanelDataIndex(panelDataArray: PanelData[], panelData: PanelData) {
return panelDataArray.findIndex(
(prevPanelData) =>
prevPanelData === panelData || prevPanelData.id === panelData.id
);
}
function panelDataHelper(
panelDataArray: PanelData[],
panelData: PanelData,
layout: number[]
) {
const panelIndex = findPanelDataIndex(panelDataArray, panelData);
const isLastPanel = panelIndex === panelDataArray.length - 1;
const pivotIndices = isLastPanel
? [panelIndex - 1, panelIndex]
: [panelIndex, panelIndex + 1];
const panelSize = layout[panelIndex];
return {
...panelData.constraints,
panelSize,
pivotIndices,
};
}

View File

@@ -0,0 +1,42 @@
import { PanelConstraints, PanelData } from "./Panel";
import { CSSProperties, createContext } from "./vendor/react";
// The "contextmenu" event is not supported as a PointerEvent in all browsers yet, so MouseEvent still need to be handled
export type ResizeEvent = KeyboardEvent | PointerEvent | MouseEvent;
export type ResizeHandler = (event: ResizeEvent) => void;
export type DragState = {
dragHandleId: string;
dragHandleRect: DOMRect;
initialCursorPosition: number;
initialLayout: number[];
};
export type TPanelGroupContext = {
collapsePanel: (panelData: PanelData) => void;
direction: "horizontal" | "vertical";
dragState: DragState | null;
expandPanel: (panelData: PanelData, minSizeOverride?: number) => void;
getPanelSize: (panelData: PanelData) => number;
getPanelStyle: (
panelData: PanelData,
defaultSize: number | undefined
) => CSSProperties;
groupId: string;
isPanelCollapsed: (panelData: PanelData) => boolean;
isPanelExpanded: (panelData: PanelData) => boolean;
reevaluatePanelConstraints: (
panelData: PanelData,
prevConstraints: PanelConstraints
) => void;
registerPanel: (panelData: PanelData) => void;
registerResizeHandle: (dragHandleId: string) => ResizeHandler;
resizePanel: (panelData: PanelData, size: number) => void;
startDragging: (dragHandleId: string, event: ResizeEvent) => void;
stopDragging: () => void;
unregisterPanel: (panelData: PanelData) => void;
panelGroupElement: ParentNode | null;
};
export const PanelGroupContext = createContext<TPanelGroupContext | null>(null);
PanelGroupContext.displayName = "PanelGroupContext";

View File

@@ -0,0 +1,367 @@
import { Root, createRoot } from "react-dom/client";
import { act } from "react-dom/test-utils";
import {
Panel,
PanelGroup,
PanelResizeHandle,
type PanelResizeHandleProps,
} from ".";
import { assert } from "./utils/assert";
import * as cursorUtils from "./utils/cursor";
import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
import {
dispatchPointerEvent,
mockBoundingClientRect,
verifyAttribute,
} from "./utils/test-utils";
jest.mock("./utils/cursor", () => ({
getCursorStyle: jest.fn(),
resetGlobalCursorStyle: jest.fn(),
setGlobalCursorStyle: jest.fn(),
}));
describe("PanelResizeHandle", () => {
let expectedWarnings: string[] = [];
let root: Root;
let container: HTMLElement;
beforeEach(() => {
// @ts-expect-error
global.IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement("div");
document.body.appendChild(container);
expectedWarnings = [];
root = createRoot(container);
jest.spyOn(console, "warn").mockImplementation((actualMessage: string) => {
const match = expectedWarnings.findIndex((expectedMessage) => {
return actualMessage.includes(expectedMessage);
});
if (match >= 0) {
expectedWarnings.splice(match, 1);
return;
}
throw Error(`Unexpected warning: ${actualMessage}`);
});
});
afterEach(() => {
jest.clearAllMocks();
jest.resetModules();
act(() => {
root.unmount();
});
expect(expectedWarnings).toHaveLength(0);
});
it("should support ...rest attributes", () => {
act(() => {
root.render(
<PanelGroup direction="horizontal">
<Panel />
<PanelResizeHandle
data-test-name="foo"
id="handle"
tabIndex={123}
title="bar"
/>
<Panel />
</PanelGroup>
);
});
const element = getResizeHandleElement("handle", container);
assert(element, "");
expect(element.tabIndex).toBe(123);
expect(element.getAttribute("data-test-name")).toBe("foo");
expect(element.title).toBe("bar");
});
function setupMockedGroup({
leftProps = {},
rightProps = {},
}: {
leftProps?: Partial<PanelResizeHandleProps>;
rightProps?: Partial<PanelResizeHandleProps>;
} = {}) {
act(() => {
root.render(
<PanelGroup direction="horizontal" id="test-group">
<Panel />
<PanelResizeHandle id="handle-left" tabIndex={1} {...leftProps} />
<Panel />
<PanelResizeHandle id="handle-right" tabIndex={2} {...rightProps} />
<Panel />
</PanelGroup>
);
});
const leftElement = getResizeHandleElement("handle-left", container);
const rightElement = getResizeHandleElement("handle-right", container);
assert(leftElement, "");
assert(rightElement, "");
// JSDom doesn't properly handle bounding rects
mockBoundingClientRect(leftElement, {
x: 50,
y: 0,
height: 50,
width: 2,
});
mockBoundingClientRect(rightElement, {
x: 100,
y: 0,
height: 50,
width: 2,
});
return {
leftElement,
rightElement,
};
}
describe("callbacks", () => {
describe("onDragging", () => {
it("should fire when dragging starts/stops", () => {
const onDragging = jest.fn();
const { leftElement } = setupMockedGroup({
leftProps: { onDragging },
});
act(() => {
dispatchPointerEvent("pointermove", leftElement);
});
expect(onDragging).not.toHaveBeenCalled();
act(() => {
dispatchPointerEvent("pointerdown", leftElement);
});
expect(onDragging).toHaveBeenCalledTimes(1);
expect(onDragging).toHaveBeenCalledWith(true);
act(() => {
dispatchPointerEvent("pointerup", leftElement);
});
expect(onDragging).toHaveBeenCalledTimes(2);
expect(onDragging).toHaveBeenCalledWith(false);
});
it("should only fire for the handle that has been dragged", () => {
const onDraggingLeft = jest.fn();
const onDraggingRight = jest.fn();
const { leftElement } = setupMockedGroup({
leftProps: { onDragging: onDraggingLeft },
rightProps: { onDragging: onDraggingRight },
});
act(() => {
dispatchPointerEvent("pointermove", leftElement);
});
expect(onDraggingLeft).not.toHaveBeenCalled();
expect(onDraggingRight).not.toHaveBeenCalled();
act(() => {
dispatchPointerEvent("pointerdown", leftElement);
});
expect(onDraggingLeft).toHaveBeenCalledTimes(1);
expect(onDraggingLeft).toHaveBeenCalledWith(true);
expect(onDraggingRight).not.toHaveBeenCalled();
act(() => {
dispatchPointerEvent("pointerup", leftElement);
});
expect(onDraggingLeft).toHaveBeenCalledTimes(2);
expect(onDraggingLeft).toHaveBeenCalledWith(false);
expect(onDraggingRight).not.toHaveBeenCalled();
});
});
});
describe("data attributes", () => {
it("should initialize with the correct props based attributes", () => {
const { leftElement, rightElement } = setupMockedGroup();
verifyAttribute(leftElement, "data-panel-group-id", "test-group");
verifyAttribute(leftElement, "data-resize-handle", "");
verifyAttribute(leftElement, "data-panel-group-direction", "horizontal");
verifyAttribute(leftElement, "data-panel-resize-handle-enabled", "true");
verifyAttribute(
leftElement,
"data-panel-resize-handle-id",
"handle-left"
);
verifyAttribute(rightElement, "data-panel-group-id", "test-group");
verifyAttribute(rightElement, "data-resize-handle", "");
verifyAttribute(rightElement, "data-panel-group-direction", "horizontal");
verifyAttribute(rightElement, "data-panel-resize-handle-enabled", "true");
verifyAttribute(
rightElement,
"data-panel-resize-handle-id",
"handle-right"
);
});
it("should update data-resize-handle-active and data-resize-handle-state when dragging starts/stops", () => {
const { leftElement, rightElement } = setupMockedGroup();
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "inactive");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
act(() => {
dispatchPointerEvent("pointermove", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "hover");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
act(() => {
dispatchPointerEvent("pointerdown", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", "pointer");
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "drag");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
act(() => {
dispatchPointerEvent("pointermove", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", "pointer");
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "drag");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
act(() => {
dispatchPointerEvent("pointerup", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "hover");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
act(() => {
dispatchPointerEvent("pointermove", rightElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "inactive");
verifyAttribute(rightElement, "data-resize-handle-state", "hover");
});
it("should update data-resize-handle-active when focused", () => {
const { leftElement, rightElement } = setupMockedGroup();
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
act(() => {
leftElement.focus();
});
expect(document.activeElement).toBe(leftElement);
verifyAttribute(leftElement, "data-resize-handle-active", "keyboard");
verifyAttribute(rightElement, "data-resize-handle-active", null);
act(() => {
leftElement.blur();
});
expect(document.activeElement).not.toBe(leftElement);
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
});
});
describe("a11y", () => {
it("should pass explicit id prop to DOM", () => {
act(() => {
root.render(
<PanelGroup direction="horizontal">
<Panel />
<PanelResizeHandle id="explicit-id" />
<Panel />
</PanelGroup>
);
});
const element = container.querySelector("[data-resize-handle]");
expect(element).not.toBeNull();
expect(element?.getAttribute("id")).toBe("explicit-id");
});
it("should not pass auto-generated id prop to DOM", () => {
act(() => {
root.render(
<PanelGroup direction="horizontal">
<Panel />
<PanelResizeHandle />
<Panel />
</PanelGroup>
);
});
const element = container.querySelector("[data-resize-handle]");
expect(element).not.toBeNull();
expect(element?.getAttribute("id")).toBeNull();
});
});
it("resets the global cursor style on unmount", () => {
const onDraggingLeft = jest.fn();
const { leftElement } = setupMockedGroup({
leftProps: { onDragging: onDraggingLeft },
rightProps: {},
});
act(() => {
dispatchPointerEvent("pointermove", leftElement);
});
act(() => {
dispatchPointerEvent("pointerdown", leftElement);
});
expect(onDraggingLeft).toHaveBeenCalledTimes(1);
expect(onDraggingLeft).toHaveBeenCalledWith(true);
expect(cursorUtils.resetGlobalCursorStyle).not.toHaveBeenCalled();
expect(cursorUtils.setGlobalCursorStyle).toHaveBeenCalled();
onDraggingLeft.mockReset();
act(() => {
dispatchPointerEvent("pointermove", leftElement);
});
expect(onDraggingLeft).not.toHaveBeenCalled();
act(() => {
dispatchPointerEvent("pointerup", leftElement);
});
expect(onDraggingLeft).toHaveBeenCalledTimes(1);
expect(onDraggingLeft).toHaveBeenCalledWith(false);
onDraggingLeft.mockReset();
act(() => {
dispatchPointerEvent("pointermove", leftElement);
});
expect(onDraggingLeft).not.toHaveBeenCalled();
act(() => {
root.unmount();
});
expect(cursorUtils.resetGlobalCursorStyle).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,246 @@
import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect";
import useUniqueId from "./hooks/useUniqueId";
import { useWindowSplitterResizeHandlerBehavior } from "./hooks/useWindowSplitterBehavior";
import {
PanelGroupContext,
ResizeEvent,
ResizeHandler,
} from "./PanelGroupContext";
import {
PointerHitAreaMargins,
registerResizeHandle,
ResizeHandlerAction,
} from "./PanelResizeHandleRegistry";
import { assert } from "./utils/assert";
import {
createElement,
CSSProperties,
HTMLAttributes,
PropsWithChildren,
ReactElement,
useContext,
useEffect,
useRef,
useState,
} from "./vendor/react";
export type PanelResizeHandleOnDragging = (isDragging: boolean) => void;
export type ResizeHandlerState = "drag" | "hover" | "inactive";
export type PanelResizeHandleProps = Omit<
HTMLAttributes<keyof HTMLElementTagNameMap>,
"id" | "onBlur" | "onFocus"
> &
PropsWithChildren<{
className?: string;
disabled?: boolean;
hitAreaMargins?: PointerHitAreaMargins;
id?: string | null;
onBlur?: () => void;
onDragging?: PanelResizeHandleOnDragging;
onFocus?: () => void;
style?: CSSProperties;
tabIndex?: number;
tagName?: keyof HTMLElementTagNameMap;
}>;
export function PanelResizeHandle({
children = null,
className: classNameFromProps = "",
disabled = false,
hitAreaMargins,
id: idFromProps,
onBlur,
onDragging,
onFocus,
style: styleFromProps = {},
tabIndex = 0,
tagName: Type = "div",
...rest
}: PanelResizeHandleProps): ReactElement {
const elementRef = useRef<HTMLElement>(null);
// Use a ref to guard against users passing inline props
const callbacksRef = useRef<{
onDragging: PanelResizeHandleOnDragging | undefined;
}>({ onDragging });
useEffect(() => {
callbacksRef.current.onDragging = onDragging;
});
const panelGroupContext = useContext(PanelGroupContext);
if (panelGroupContext === null) {
throw Error(
`PanelResizeHandle components must be rendered within a PanelGroup container`
);
}
const {
direction,
groupId,
registerResizeHandle: registerResizeHandleWithParentGroup,
startDragging,
stopDragging,
panelGroupElement,
} = panelGroupContext;
const resizeHandleId = useUniqueId(idFromProps);
const [state, setState] = useState<ResizeHandlerState>("inactive");
const [isFocused, setIsFocused] = useState(false);
const [resizeHandler, setResizeHandler] = useState<ResizeHandler | null>(
null
);
const committedValuesRef = useRef<{
state: ResizeHandlerState;
}>({
state,
});
useIsomorphicLayoutEffect(() => {
committedValuesRef.current.state = state;
});
useEffect(() => {
if (disabled) {
setResizeHandler(null);
} else {
const resizeHandler = registerResizeHandleWithParentGroup(resizeHandleId);
setResizeHandler(() => resizeHandler);
}
}, [disabled, resizeHandleId, registerResizeHandleWithParentGroup]);
// Extract hit area margins before passing them to the effect's dependency array
// so that inline object values won't trigger re-renders
const coarseHitAreaMargins = hitAreaMargins?.coarse ?? 15;
const fineHitAreaMargins = hitAreaMargins?.fine ?? 5;
useEffect(() => {
if (disabled || resizeHandler == null) {
return;
}
const element = elementRef.current;
assert(element, "Element ref not attached");
const setResizeHandlerState = (
action: ResizeHandlerAction,
isActive: boolean,
event: ResizeEvent | null
) => {
if (isActive) {
switch (action) {
case "down": {
setState("drag");
assert(event, 'Expected event to be defined for "down" action');
startDragging(resizeHandleId, event);
const { onDragging } = callbacksRef.current;
if (onDragging) {
onDragging(true);
}
break;
}
case "move": {
const { state } = committedValuesRef.current;
if (state !== "drag") {
setState("hover");
}
assert(event, 'Expected event to be defined for "move" action');
resizeHandler(event);
break;
}
case "up": {
setState("hover");
stopDragging();
const { onDragging } = callbacksRef.current;
if (onDragging) {
onDragging(false);
}
break;
}
}
} else {
setState("inactive");
}
};
return registerResizeHandle(
resizeHandleId,
element,
direction,
{
coarse: coarseHitAreaMargins,
fine: fineHitAreaMargins,
},
setResizeHandlerState
);
}, [
coarseHitAreaMargins,
direction,
disabled,
fineHitAreaMargins,
registerResizeHandleWithParentGroup,
resizeHandleId,
resizeHandler,
startDragging,
stopDragging,
]);
useWindowSplitterResizeHandlerBehavior({
disabled,
handleId: resizeHandleId,
resizeHandler,
panelGroupElement,
});
const style: CSSProperties = {
touchAction: "none",
userSelect: "none",
};
return createElement(Type, {
...rest,
children,
className: classNameFromProps,
id: idFromProps,
onBlur: () => {
setIsFocused(false);
onBlur?.();
},
onFocus: () => {
setIsFocused(true);
onFocus?.();
},
ref: elementRef,
role: "separator",
style: {
...style,
...styleFromProps,
},
tabIndex,
// CSS selectors
"data-panel-group-direction": direction,
"data-panel-group-id": groupId,
"data-resize-handle": "",
"data-resize-handle-active":
state === "drag" ? "pointer" : isFocused ? "keyboard" : undefined,
"data-resize-handle-state": state,
"data-panel-resize-handle-enabled": !disabled,
"data-panel-resize-handle-id": resizeHandleId,
});
}
PanelResizeHandle.displayName = "PanelResizeHandle";

View File

@@ -0,0 +1,336 @@
import { Direction, ResizeEvent } from "./types";
import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor";
import { getResizeEventCoordinates } from "./utils/events/getResizeEventCoordinates";
import { getInputType } from "./utils/getInputType";
import { intersects } from "./utils/rects/intersects";
import { compare } from "./vendor/stacking-order";
export type ResizeHandlerAction = "down" | "move" | "up";
export type SetResizeHandlerState = (
action: ResizeHandlerAction,
isActive: boolean,
event: ResizeEvent | null
) => void;
export type PointerHitAreaMargins = {
coarse: number;
fine: number;
};
export type ResizeHandlerData = {
direction: Direction;
element: HTMLElement;
hitAreaMargins: PointerHitAreaMargins;
setResizeHandlerState: SetResizeHandlerState;
};
export const EXCEEDED_HORIZONTAL_MIN = 0b0001;
export const EXCEEDED_HORIZONTAL_MAX = 0b0010;
export const EXCEEDED_VERTICAL_MIN = 0b0100;
export const EXCEEDED_VERTICAL_MAX = 0b1000;
const isCoarsePointer = getInputType() === "coarse";
let intersectingHandles: ResizeHandlerData[] = [];
let isPointerDown = false;
let ownerDocumentCounts: Map<Document, number> = new Map();
let panelConstraintFlags: Map<string, number> = new Map();
const registeredResizeHandlers = new Set<ResizeHandlerData>();
export function registerResizeHandle(
resizeHandleId: string,
element: HTMLElement,
direction: Direction,
hitAreaMargins: PointerHitAreaMargins,
setResizeHandlerState: SetResizeHandlerState
) {
const { ownerDocument } = element;
const data: ResizeHandlerData = {
direction,
element,
hitAreaMargins,
setResizeHandlerState,
};
const count = ownerDocumentCounts.get(ownerDocument) ?? 0;
ownerDocumentCounts.set(ownerDocument, count + 1);
registeredResizeHandlers.add(data);
updateListeners();
return function unregisterResizeHandle() {
panelConstraintFlags.delete(resizeHandleId);
registeredResizeHandlers.delete(data);
const count = ownerDocumentCounts.get(ownerDocument) ?? 1;
ownerDocumentCounts.set(ownerDocument, count - 1);
updateListeners();
if (count === 1) {
ownerDocumentCounts.delete(ownerDocument);
}
// If the resize handle that is currently unmounting is intersecting with the pointer,
// update the global pointer to account for the change
if (intersectingHandles.includes(data)) {
const index = intersectingHandles.indexOf(data);
if (index >= 0) {
intersectingHandles.splice(index, 1);
}
updateCursor();
// Also instruct the handle to stop dragging; this prevents the parent group from being left in an inconsistent state
// See github.com/bvaughn/react-resizable-panels/issues/402
setResizeHandlerState("up", true, null);
}
};
}
function handlePointerDown(event: PointerEvent) {
const { target } = event;
const { x, y } = getResizeEventCoordinates(event);
isPointerDown = true;
recalculateIntersectingHandles({ target, x, y });
updateListeners();
if (intersectingHandles.length > 0) {
updateResizeHandlerStates("down", event);
event.preventDefault();
event.stopPropagation();
}
}
function handlePointerMove(event: PointerEvent) {
const { x, y } = getResizeEventCoordinates(event);
// Edge case (see #340)
// Detect when the pointer has been released outside an iframe on a different domain
if (isPointerDown && event.buttons === 0) {
isPointerDown = false;
updateResizeHandlerStates("up", event);
}
if (!isPointerDown) {
const { target } = event;
// Recalculate intersecting handles whenever the pointer moves, except if it has already been pressed
// at that point, the handles may not move with the pointer (depending on constraints)
// but the same set of active handles should be locked until the pointer is released
recalculateIntersectingHandles({ target, x, y });
}
updateResizeHandlerStates("move", event);
// Update cursor based on return value(s) from active handles
updateCursor();
if (intersectingHandles.length > 0) {
event.preventDefault();
}
}
function handlePointerUp(event: ResizeEvent) {
const { target } = event;
const { x, y } = getResizeEventCoordinates(event);
panelConstraintFlags.clear();
isPointerDown = false;
if (intersectingHandles.length > 0) {
event.preventDefault();
}
updateResizeHandlerStates("up", event);
recalculateIntersectingHandles({ target, x, y });
updateCursor();
updateListeners();
}
function recalculateIntersectingHandles({
target,
x,
y,
}: {
target: EventTarget | null;
x: number;
y: number;
}) {
intersectingHandles.splice(0);
let targetElement: HTMLElement | null = null;
if (target instanceof HTMLElement) {
targetElement = target;
}
registeredResizeHandlers.forEach((data) => {
const { element: dragHandleElement, hitAreaMargins } = data;
const dragHandleRect = dragHandleElement.getBoundingClientRect();
const { bottom, left, right, top } = dragHandleRect;
const margin = isCoarsePointer
? hitAreaMargins.coarse
: hitAreaMargins.fine;
const eventIntersects =
x >= left - margin &&
x <= right + margin &&
y >= top - margin &&
y <= bottom + margin;
if (eventIntersects) {
// TRICKY
// We listen for pointers events at the root in order to support hit area margins
// (determining when the pointer is close enough to an element to be considered a "hit")
// Clicking on an element "above" a handle (e.g. a modal) should prevent a hit though
// so at this point we need to compare stacking order of a potentially intersecting drag handle,
// and the element that was actually clicked/touched
if (
targetElement !== null &&
document.contains(targetElement) &&
dragHandleElement !== targetElement &&
!dragHandleElement.contains(targetElement) &&
!targetElement.contains(dragHandleElement) &&
// Calculating stacking order has a cost, so we should avoid it if possible
// That is why we only check potentially intersecting handles,
// and why we skip if the event target is within the handle's DOM
compare(targetElement, dragHandleElement) > 0
) {
// If the target is above the drag handle, then we also need to confirm they overlap
// If they are beside each other (e.g. a panel and its drag handle) then the handle is still interactive
//
// It's not enough to compare only the target
// The target might be a small element inside of a larger container
// (For example, a SPAN or a DIV inside of a larger modal dialog)
let currentElement: HTMLElement | null = targetElement;
let didIntersect = false;
while (currentElement) {
if (currentElement.contains(dragHandleElement)) {
break;
} else if (
intersects(
currentElement.getBoundingClientRect(),
dragHandleRect,
true
)
) {
didIntersect = true;
break;
}
currentElement = currentElement.parentElement;
}
if (didIntersect) {
return;
}
}
intersectingHandles.push(data);
}
});
}
export function reportConstraintsViolation(
resizeHandleId: string,
flag: number
) {
panelConstraintFlags.set(resizeHandleId, flag);
}
function updateCursor() {
let intersectsHorizontal = false;
let intersectsVertical = false;
intersectingHandles.forEach((data) => {
const { direction } = data;
if (direction === "horizontal") {
intersectsHorizontal = true;
} else {
intersectsVertical = true;
}
});
let constraintFlags = 0;
panelConstraintFlags.forEach((flag) => {
constraintFlags |= flag;
});
if (intersectsHorizontal && intersectsVertical) {
setGlobalCursorStyle("intersection", constraintFlags);
} else if (intersectsHorizontal) {
setGlobalCursorStyle("horizontal", constraintFlags);
} else if (intersectsVertical) {
setGlobalCursorStyle("vertical", constraintFlags);
} else {
resetGlobalCursorStyle();
}
}
function updateListeners() {
ownerDocumentCounts.forEach((_, ownerDocument) => {
const { body } = ownerDocument;
body.removeEventListener("contextmenu", handlePointerUp);
body.removeEventListener("pointerdown", handlePointerDown);
body.removeEventListener("pointerleave", handlePointerMove);
body.removeEventListener("pointermove", handlePointerMove);
});
window.removeEventListener("pointerup", handlePointerUp);
window.removeEventListener("pointercancel", handlePointerUp);
if (registeredResizeHandlers.size > 0) {
if (isPointerDown) {
if (intersectingHandles.length > 0) {
ownerDocumentCounts.forEach((count, ownerDocument) => {
const { body } = ownerDocument;
if (count > 0) {
body.addEventListener("contextmenu", handlePointerUp);
body.addEventListener("pointerleave", handlePointerMove);
body.addEventListener("pointermove", handlePointerMove);
}
});
}
window.addEventListener("pointerup", handlePointerUp);
window.addEventListener("pointercancel", handlePointerUp);
} else {
ownerDocumentCounts.forEach((count, ownerDocument) => {
const { body } = ownerDocument;
if (count > 0) {
body.addEventListener("pointerdown", handlePointerDown, {
capture: true,
});
body.addEventListener("pointermove", handlePointerMove);
}
});
}
}
}
function updateResizeHandlerStates(
action: ResizeHandlerAction,
event: ResizeEvent
) {
registeredResizeHandlers.forEach((data) => {
const { setResizeHandlerState } = data;
const isActive = intersectingHandles.includes(data);
setResizeHandlerState(action, isActive, event);
});
}

1
node_modules/react-resizable-panels/src/constants.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export const PRECISION = 10;

View File

@@ -0,0 +1 @@
export const isBrowser = true;

View File

@@ -0,0 +1 @@
export const isDevelopment = true;

View File

@@ -0,0 +1 @@
export const isBrowser = false;

View File

@@ -0,0 +1 @@
export const isDevelopment = false;

View File

@@ -0,0 +1 @@
export const isBrowser = typeof window !== "undefined";

View File

@@ -0,0 +1,7 @@
import { useCallback, useState } from "../vendor/react";
export function useForceUpdate() {
const [_, setCount] = useState(0);
return useCallback(() => setCount((prevCount) => prevCount + 1), []);
}

View File

@@ -0,0 +1,8 @@
import { isBrowser } from "#is-browser";
import { useLayoutEffect_do_not_use_directly } from "../vendor/react";
const useIsomorphicLayoutEffect = isBrowser
? useLayoutEffect_do_not_use_directly
: () => {};
export default useIsomorphicLayoutEffect;

View File

@@ -0,0 +1,19 @@
import { useId, useRef } from "../vendor/react";
const wrappedUseId: () => string | null =
typeof useId === "function" ? useId : (): null => null;
let counter = 0;
export default function useUniqueId(
idFromParams: string | null = null
): string {
const idFromUseId = wrappedUseId();
const idRef = useRef<string | null>(idFromParams || idFromUseId || null);
if (idRef.current === null) {
idRef.current = "" + counter++;
}
return idFromParams ?? idRef.current;
}

View File

@@ -0,0 +1,90 @@
import { ResizeHandler } from "../types";
import { assert } from "../utils/assert";
import { getResizeHandleElement } from "../utils/dom/getResizeHandleElement";
import { getResizeHandleElementIndex } from "../utils/dom/getResizeHandleElementIndex";
import { getResizeHandleElementsForGroup } from "../utils/dom/getResizeHandleElementsForGroup";
import { useEffect } from "../vendor/react";
// https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
export function useWindowSplitterResizeHandlerBehavior({
disabled,
handleId,
resizeHandler,
panelGroupElement,
}: {
disabled: boolean;
handleId: string;
resizeHandler: ResizeHandler | null;
panelGroupElement: ParentNode | null;
}): void {
useEffect(() => {
if (disabled || resizeHandler == null || panelGroupElement == null) {
return;
}
const handleElement = getResizeHandleElement(handleId, panelGroupElement);
if (handleElement == null) {
return;
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented) {
return;
}
switch (event.key) {
case "ArrowDown":
case "ArrowLeft":
case "ArrowRight":
case "ArrowUp":
case "End":
case "Home": {
event.preventDefault();
resizeHandler(event);
break;
}
case "F6": {
event.preventDefault();
const groupId = handleElement.getAttribute("data-panel-group-id");
assert(groupId, `No group element found for id "${groupId}"`);
const handles = getResizeHandleElementsForGroup(
groupId,
panelGroupElement
);
const index = getResizeHandleElementIndex(
groupId,
handleId,
panelGroupElement
);
assert(
index !== null,
`No resize element found for id "${handleId}"`
);
const nextIndex = event.shiftKey
? index > 0
? index - 1
: handles.length - 1
: index + 1 < handles.length
? index + 1
: 0;
const nextHandle = handles[nextIndex] as HTMLElement;
nextHandle.focus();
break;
}
}
};
handleElement.addEventListener("keydown", onKeyDown);
return () => {
handleElement.removeEventListener("keydown", onKeyDown);
};
}, [panelGroupElement, disabled, handleId, resizeHandler]);
}

View File

@@ -0,0 +1,201 @@
import { isDevelopment } from "#is-development";
import { PanelData } from "../Panel";
import { Direction } from "../types";
import { adjustLayoutByDelta } from "../utils/adjustLayoutByDelta";
import { assert } from "../utils/assert";
import { calculateAriaValues } from "../utils/calculateAriaValues";
import { determinePivotIndices } from "../utils/determinePivotIndices";
import { getPanelGroupElement } from "../utils/dom/getPanelGroupElement";
import { getResizeHandleElementsForGroup } from "../utils/dom/getResizeHandleElementsForGroup";
import { getResizeHandlePanelIds } from "../utils/dom/getResizeHandlePanelIds";
import { fuzzyNumbersEqual } from "../utils/numbers/fuzzyNumbersEqual";
import { RefObject, useEffect, useRef } from "../vendor/react";
import useIsomorphicLayoutEffect from "./useIsomorphicEffect";
// https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
export function useWindowSplitterPanelGroupBehavior({
committedValuesRef,
eagerValuesRef,
groupId,
layout,
panelDataArray,
panelGroupElement,
setLayout,
}: {
committedValuesRef: RefObject<{
direction: Direction;
}>;
eagerValuesRef: RefObject<{
panelDataArray: PanelData[];
}>;
groupId: string;
layout: number[];
panelDataArray: PanelData[];
panelGroupElement: ParentNode | null;
setLayout: (sizes: number[]) => void;
}): void {
const devWarningsRef = useRef<{
didWarnAboutMissingResizeHandle: boolean;
}>({
didWarnAboutMissingResizeHandle: false,
});
useIsomorphicLayoutEffect(() => {
if (!panelGroupElement) {
return;
}
const resizeHandleElements = getResizeHandleElementsForGroup(
groupId,
panelGroupElement
);
for (let index = 0; index < panelDataArray.length - 1; index++) {
const { valueMax, valueMin, valueNow } = calculateAriaValues({
layout,
panelsArray: panelDataArray,
pivotIndices: [index, index + 1],
});
const resizeHandleElement = resizeHandleElements[index];
if (resizeHandleElement == null) {
if (isDevelopment) {
const { didWarnAboutMissingResizeHandle } = devWarningsRef.current;
if (!didWarnAboutMissingResizeHandle) {
devWarningsRef.current.didWarnAboutMissingResizeHandle = true;
console.warn(
`WARNING: Missing resize handle for PanelGroup "${groupId}"`
);
}
}
} else {
const panelData = panelDataArray[index];
assert(panelData, `No panel data found for index "${index}"`);
resizeHandleElement.setAttribute("aria-controls", panelData.id);
resizeHandleElement.setAttribute(
"aria-valuemax",
"" + Math.round(valueMax)
);
resizeHandleElement.setAttribute(
"aria-valuemin",
"" + Math.round(valueMin)
);
resizeHandleElement.setAttribute(
"aria-valuenow",
valueNow != null ? "" + Math.round(valueNow) : ""
);
}
}
return () => {
resizeHandleElements.forEach((resizeHandleElement, index) => {
resizeHandleElement.removeAttribute("aria-controls");
resizeHandleElement.removeAttribute("aria-valuemax");
resizeHandleElement.removeAttribute("aria-valuemin");
resizeHandleElement.removeAttribute("aria-valuenow");
});
};
}, [groupId, layout, panelDataArray, panelGroupElement]);
useEffect(() => {
if (!panelGroupElement) {
return;
}
const eagerValues = eagerValuesRef.current;
assert(eagerValues, `Eager values not found`);
const { panelDataArray } = eagerValues;
const groupElement = getPanelGroupElement(groupId, panelGroupElement);
assert(groupElement != null, `No group found for id "${groupId}"`);
const handles = getResizeHandleElementsForGroup(groupId, panelGroupElement);
assert(handles, `No resize handles found for group id "${groupId}"`);
const cleanupFunctions = handles.map((handle) => {
const handleId = handle.getAttribute("data-panel-resize-handle-id");
assert(handleId, `Resize handle element has no handle id attribute`);
const [idBefore, idAfter] = getResizeHandlePanelIds(
groupId,
handleId,
panelDataArray,
panelGroupElement
);
if (idBefore == null || idAfter == null) {
return () => {};
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented) {
return;
}
switch (event.key) {
case "Enter": {
event.preventDefault();
const index = panelDataArray.findIndex(
(panelData) => panelData.id === idBefore
);
if (index >= 0) {
const panelData = panelDataArray[index];
assert(panelData, `No panel data found for index ${index}`);
const size = layout[index];
const {
collapsedSize = 0,
collapsible,
minSize = 0,
} = panelData.constraints;
if (size != null && collapsible) {
const nextLayout = adjustLayoutByDelta({
delta: fuzzyNumbersEqual(size, collapsedSize)
? minSize - collapsedSize
: collapsedSize - size,
initialLayout: layout,
panelConstraints: panelDataArray.map(
(panelData) => panelData.constraints
),
pivotIndices: determinePivotIndices(
groupId,
handleId,
panelGroupElement
),
prevLayout: layout,
trigger: "keyboard",
});
if (layout !== nextLayout) {
setLayout(nextLayout);
}
}
}
break;
}
}
};
handle.addEventListener("keydown", onKeyDown);
return () => {
handle.removeEventListener("keydown", onKeyDown);
};
});
return () => {
cleanupFunctions.forEach((cleanupFunction) => cleanupFunction());
};
}, [
panelGroupElement,
committedValuesRef,
eagerValuesRef,
groupId,
layout,
panelDataArray,
setLayout,
]);
}

77
node_modules/react-resizable-panels/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,77 @@
import { Panel } from "./Panel";
import { PanelGroup } from "./PanelGroup";
import { PanelResizeHandle } from "./PanelResizeHandle";
import { assert } from "./utils/assert";
import { setNonce } from "./utils/csp";
import {
enableGlobalCursorStyles,
disableGlobalCursorStyles,
} from "./utils/cursor";
import { getPanelElement } from "./utils/dom/getPanelElement";
import { getPanelElementsForGroup } from "./utils/dom/getPanelElementsForGroup";
import { getPanelGroupElement } from "./utils/dom/getPanelGroupElement";
import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
import { getResizeHandleElementIndex } from "./utils/dom/getResizeHandleElementIndex";
import { getResizeHandleElementsForGroup } from "./utils/dom/getResizeHandleElementsForGroup";
import { getResizeHandlePanelIds } from "./utils/dom/getResizeHandlePanelIds";
import { getIntersectingRectangle } from "./utils/rects/getIntersectingRectangle";
import { intersects } from "./utils/rects/intersects";
import type {
ImperativePanelHandle,
PanelOnCollapse,
PanelOnExpand,
PanelOnResize,
PanelProps,
} from "./Panel";
import type {
ImperativePanelGroupHandle,
PanelGroupOnLayout,
PanelGroupProps,
PanelGroupStorage,
} from "./PanelGroup";
import type {
PanelResizeHandleOnDragging,
PanelResizeHandleProps,
} from "./PanelResizeHandle";
import type { PointerHitAreaMargins } from "./PanelResizeHandleRegistry";
export {
// TypeScript types
ImperativePanelGroupHandle,
ImperativePanelHandle,
PanelGroupOnLayout,
PanelGroupProps,
PanelGroupStorage,
PanelOnCollapse,
PanelOnExpand,
PanelOnResize,
PanelProps,
PanelResizeHandleOnDragging,
PanelResizeHandleProps,
PointerHitAreaMargins,
// React components
Panel,
PanelGroup,
PanelResizeHandle,
// Utility methods
assert,
getIntersectingRectangle,
intersects,
// DOM helpers
getPanelElement,
getPanelElementsForGroup,
getPanelGroupElement,
getResizeHandleElement,
getResizeHandleElementIndex,
getResizeHandleElementsForGroup,
getResizeHandlePanelIds,
// Styles and CSP (see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce)
enableGlobalCursorStyles,
disableGlobalCursorStyles,
setNonce,
};

5
node_modules/react-resizable-panels/src/types.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
export type Direction = "horizontal" | "vertical";
// The "contextmenu" event is not supported as a PointerEvent in all browsers yet, so MouseEvent still need to be handled
export type ResizeEvent = KeyboardEvent | PointerEvent | MouseEvent;
export type ResizeHandler = (event: ResizeEvent) => void;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,308 @@
import { PanelConstraints } from "../Panel";
import { assert } from "./assert";
import { fuzzyCompareNumbers } from "./numbers/fuzzyCompareNumbers";
import { fuzzyLayoutsEqual } from "./numbers/fuzzyLayoutsEqual";
import { fuzzyNumbersEqual } from "./numbers/fuzzyNumbersEqual";
import { resizePanel } from "./resizePanel";
// All units must be in percentages; pixel values should be pre-converted
export function adjustLayoutByDelta({
delta,
initialLayout,
panelConstraints: panelConstraintsArray,
pivotIndices,
prevLayout,
trigger,
}: {
delta: number;
initialLayout: number[];
panelConstraints: PanelConstraints[];
pivotIndices: number[];
prevLayout: number[];
trigger: "imperative-api" | "keyboard" | "mouse-or-touch";
}): number[] {
if (fuzzyNumbersEqual(delta, 0)) {
return initialLayout;
}
const nextLayout = [...initialLayout];
const [firstPivotIndex, secondPivotIndex] = pivotIndices;
assert(firstPivotIndex != null, "Invalid first pivot index");
assert(secondPivotIndex != null, "Invalid second pivot index");
let deltaApplied = 0;
// const DEBUG = [];
// DEBUG.push(`adjustLayoutByDelta()`);
// DEBUG.push(` initialLayout: ${initialLayout.join(", ")}`);
// DEBUG.push(` prevLayout: ${prevLayout.join(", ")}`);
// DEBUG.push(` delta: ${delta}`);
// DEBUG.push(` pivotIndices: ${pivotIndices.join(", ")}`);
// DEBUG.push(` trigger: ${trigger}`);
// DEBUG.push("");
// A resizing panel affects the panels before or after it.
//
// A negative delta means the panel(s) immediately after the resize handle should grow/expand by decreasing its offset.
// Other panels may also need to shrink/contract (and shift) to make room, depending on the min weights.
//
// A positive delta means the panel(s) immediately before the resize handle should "expand".
// This is accomplished by shrinking/contracting (and shifting) one or more of the panels after the resize handle.
{
// If this is a resize triggered by a keyboard event, our logic for expanding/collapsing is different.
// We no longer check the halfway threshold because this may prevent the panel from expanding at all.
if (trigger === "keyboard") {
{
// Check if we should expand a collapsed panel
const index = delta < 0 ? secondPivotIndex : firstPivotIndex;
const panelConstraints = panelConstraintsArray[index];
assert(
panelConstraints,
`Panel constraints not found for index ${index}`
);
const {
collapsedSize = 0,
collapsible,
minSize = 0,
} = panelConstraints;
// DEBUG.push(`edge case check 1: ${index}`);
// DEBUG.push(` -> collapsible? ${collapsible}`);
if (collapsible) {
const prevSize = initialLayout[index];
assert(
prevSize != null,
`Previous layout not found for panel index ${index}`
);
if (fuzzyNumbersEqual(prevSize, collapsedSize)) {
const localDelta = minSize - prevSize;
// DEBUG.push(` -> expand delta: ${localDelta}`);
if (fuzzyCompareNumbers(localDelta, Math.abs(delta)) > 0) {
delta = delta < 0 ? 0 - localDelta : localDelta;
// DEBUG.push(` -> delta: ${delta}`);
}
}
}
}
{
// Check if we should collapse a panel at its minimum size
const index = delta < 0 ? firstPivotIndex : secondPivotIndex;
const panelConstraints = panelConstraintsArray[index];
assert(
panelConstraints,
`No panel constraints found for index ${index}`
);
const {
collapsedSize = 0,
collapsible,
minSize = 0,
} = panelConstraints;
// DEBUG.push(`edge case check 2: ${index}`);
// DEBUG.push(` -> collapsible? ${collapsible}`);
if (collapsible) {
const prevSize = initialLayout[index];
assert(
prevSize != null,
`Previous layout not found for panel index ${index}`
);
if (fuzzyNumbersEqual(prevSize, minSize)) {
const localDelta = prevSize - collapsedSize;
// DEBUG.push(` -> expand delta: ${localDelta}`);
if (fuzzyCompareNumbers(localDelta, Math.abs(delta)) > 0) {
delta = delta < 0 ? 0 - localDelta : localDelta;
// DEBUG.push(` -> delta: ${delta}`);
}
}
}
}
}
// DEBUG.push("");
}
{
// Pre-calculate max available delta in the opposite direction of our pivot.
// This will be the maximum amount we're allowed to expand/contract the panels in the primary direction.
// If this amount is less than the requested delta, adjust the requested delta.
// If this amount is greater than the requested delta, that's useful information too
// as an expanding panel might change from collapsed to min size.
const increment = delta < 0 ? 1 : -1;
let index = delta < 0 ? secondPivotIndex : firstPivotIndex;
let maxAvailableDelta = 0;
// DEBUG.push("pre calc...");
while (true) {
const prevSize = initialLayout[index];
assert(
prevSize != null,
`Previous layout not found for panel index ${index}`
);
const maxSafeSize = resizePanel({
panelConstraints: panelConstraintsArray,
panelIndex: index,
size: 100,
});
const delta = maxSafeSize - prevSize;
// DEBUG.push(` ${index}: ${prevSize} -> ${maxSafeSize}`);
maxAvailableDelta += delta;
index += increment;
if (index < 0 || index >= panelConstraintsArray.length) {
break;
}
}
// DEBUG.push(` -> max available delta: ${maxAvailableDelta}`);
const minAbsDelta = Math.min(Math.abs(delta), Math.abs(maxAvailableDelta));
delta = delta < 0 ? 0 - minAbsDelta : minAbsDelta;
// DEBUG.push(` -> adjusted delta: ${delta}`);
// DEBUG.push("");
}
{
// Delta added to a panel needs to be subtracted from other panels (within the constraints that those panels allow).
const pivotIndex = delta < 0 ? firstPivotIndex : secondPivotIndex;
let index = pivotIndex;
while (index >= 0 && index < panelConstraintsArray.length) {
const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
const prevSize = initialLayout[index];
assert(
prevSize != null,
`Previous layout not found for panel index ${index}`
);
const unsafeSize = prevSize - deltaRemaining;
const safeSize = resizePanel({
panelConstraints: panelConstraintsArray,
panelIndex: index,
size: unsafeSize,
});
if (!fuzzyNumbersEqual(prevSize, safeSize)) {
deltaApplied += prevSize - safeSize;
nextLayout[index] = safeSize;
if (
deltaApplied
.toPrecision(3)
.localeCompare(Math.abs(delta).toPrecision(3), undefined, {
numeric: true,
}) >= 0
) {
break;
}
}
if (delta < 0) {
index--;
} else {
index++;
}
}
}
// DEBUG.push(`after 1: ${nextLayout.join(", ")}`);
// DEBUG.push(` deltaApplied: ${deltaApplied}`);
// DEBUG.push("");
// If we were unable to resize any of the panels panels, return the previous state.
// This will essentially bailout and ignore e.g. drags past a panel's boundaries
if (fuzzyLayoutsEqual(prevLayout, nextLayout)) {
// DEBUG.push(`bailout to previous layout: ${prevLayout.join(", ")}`);
// console.log(DEBUG.join("\n"));
return prevLayout;
}
{
// Now distribute the applied delta to the panels in the other direction
const pivotIndex = delta < 0 ? secondPivotIndex : firstPivotIndex;
const prevSize = initialLayout[pivotIndex];
assert(
prevSize != null,
`Previous layout not found for panel index ${pivotIndex}`
);
const unsafeSize = prevSize + deltaApplied;
const safeSize = resizePanel({
panelConstraints: panelConstraintsArray,
panelIndex: pivotIndex,
size: unsafeSize,
});
// Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
nextLayout[pivotIndex] = safeSize;
// Edge case where expanding or contracting one panel caused another one to change collapsed state
if (!fuzzyNumbersEqual(safeSize, unsafeSize)) {
let deltaRemaining = unsafeSize - safeSize;
const pivotIndex = delta < 0 ? secondPivotIndex : firstPivotIndex;
let index = pivotIndex;
while (index >= 0 && index < panelConstraintsArray.length) {
const prevSize = nextLayout[index];
assert(
prevSize != null,
`Previous layout not found for panel index ${index}`
);
const unsafeSize = prevSize + deltaRemaining;
const safeSize = resizePanel({
panelConstraints: panelConstraintsArray,
panelIndex: index,
size: unsafeSize,
});
if (!fuzzyNumbersEqual(prevSize, safeSize)) {
deltaRemaining -= safeSize - prevSize;
nextLayout[index] = safeSize;
}
if (fuzzyNumbersEqual(deltaRemaining, 0)) {
break;
}
if (delta > 0) {
index--;
} else {
index++;
}
}
}
}
// DEBUG.push(`after 2: ${nextLayout.join(", ")}`);
// DEBUG.push(` deltaApplied: ${deltaApplied}`);
// DEBUG.push("");
const totalSize = nextLayout.reduce((total, size) => size + total, 0);
// DEBUG.push(`total size: ${totalSize}`);
// If our new layout doesn't add up to 100%, that means the requested delta can't be applied
// In that case, fall back to our most recent valid layout
if (!fuzzyNumbersEqual(totalSize, 100)) {
// DEBUG.push(`bailout to previous layout: ${prevLayout.join(", ")}`);
// console.log(DEBUG.join("\n"));
return prevLayout;
}
// console.log(DEBUG.join("\n"));
return nextLayout;
}

View File

@@ -0,0 +1,13 @@
export function areEqual(arrayA: any[], arrayB: any[]): boolean {
if (arrayA.length !== arrayB.length) {
return false;
}
for (let index = 0; index < arrayA.length; index++) {
if (arrayA[index] !== arrayB[index]) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,10 @@
export function assert(
expectedCondition: any,
message: string
): asserts expectedCondition {
if (!expectedCondition) {
console.error(message);
throw Error(message);
}
}

View File

@@ -0,0 +1,106 @@
import { PanelConstraints, PanelData } from "../Panel";
import { calculateAriaValues } from "./calculateAriaValues";
describe("calculateAriaValues", () => {
let idCounter = 0;
let orderCounter = 0;
function createPanelData(constraints: PanelConstraints = {}): PanelData {
return {
callbacks: {
onCollapse: undefined,
onExpand: undefined,
onResize: undefined,
},
constraints,
id: `${idCounter++}`,
idIsFromProps: false,
order: orderCounter++,
};
}
beforeEach(() => {
idCounter = 0;
orderCounter = 0;
});
it("should work correctly for panels with no min/max constraints", () => {
expect(
calculateAriaValues({
layout: [50, 50],
panelsArray: [createPanelData(), createPanelData()],
pivotIndices: [0, 1],
})
).toEqual({
valueMax: 100,
valueMin: 0,
valueNow: 50,
});
expect(
calculateAriaValues({
layout: [20, 50, 30],
panelsArray: [createPanelData(), createPanelData(), createPanelData()],
pivotIndices: [0, 1],
})
).toEqual({
valueMax: 100,
valueMin: 0,
valueNow: 20,
});
expect(
calculateAriaValues({
layout: [20, 50, 30],
panelsArray: [createPanelData(), createPanelData(), createPanelData()],
pivotIndices: [1, 2],
})
).toEqual({
valueMax: 100,
valueMin: 0,
valueNow: 50,
});
});
it("should work correctly for panels with min/max constraints", () => {
expect(
calculateAriaValues({
layout: [25, 75],
panelsArray: [
createPanelData({
maxSize: 35,
minSize: 10,
}),
createPanelData(),
],
pivotIndices: [0, 1],
})
).toEqual({
valueMax: 35,
valueMin: 10,
valueNow: 25,
});
expect(
calculateAriaValues({
layout: [25, 50, 25],
panelsArray: [
createPanelData({
maxSize: 35,
minSize: 10,
}),
createPanelData(),
createPanelData({
maxSize: 35,
minSize: 10,
}),
],
pivotIndices: [1, 2],
})
).toEqual({
valueMax: 80,
valueMin: 30,
valueNow: 50,
});
});
});

View File

@@ -0,0 +1,45 @@
import { PanelData } from "../Panel";
import { assert } from "./assert";
export function calculateAriaValues({
layout,
panelsArray,
pivotIndices,
}: {
layout: number[];
panelsArray: PanelData[];
pivotIndices: number[];
}) {
let currentMinSize = 0;
let currentMaxSize = 100;
let totalMinSize = 0;
let totalMaxSize = 0;
const firstIndex = pivotIndices[0];
assert(firstIndex != null, "No pivot index found");
// A panel's effective min/max sizes also need to account for other panel's sizes.
panelsArray.forEach((panelData, index) => {
const { constraints } = panelData;
const { maxSize = 100, minSize = 0 } = constraints;
if (index === firstIndex) {
currentMinSize = minSize;
currentMaxSize = maxSize;
} else {
totalMinSize += minSize;
totalMaxSize += maxSize;
}
});
const valueMax = Math.min(currentMaxSize, 100 - totalMinSize);
const valueMin = Math.max(currentMinSize, 100 - totalMaxSize);
const valueNow = layout[firstIndex];
return {
valueMax,
valueMin,
valueNow,
};
}

View File

@@ -0,0 +1,63 @@
import { DragState, ResizeEvent } from "../PanelGroupContext";
import { Direction } from "../types";
import { calculateDragOffsetPercentage } from "./calculateDragOffsetPercentage";
import { isKeyDown } from "./events";
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX
export function calculateDeltaPercentage(
event: ResizeEvent,
dragHandleId: string,
direction: Direction,
initialDragState: DragState | null,
keyboardResizeBy: number | null,
panelGroupElement: HTMLElement
): number {
if (isKeyDown(event)) {
const isHorizontal = direction === "horizontal";
let delta = 0;
if (event.shiftKey) {
delta = 100;
} else if (keyboardResizeBy != null) {
delta = keyboardResizeBy;
} else {
delta = 10;
}
let movement = 0;
switch (event.key) {
case "ArrowDown":
movement = isHorizontal ? 0 : delta;
break;
case "ArrowLeft":
movement = isHorizontal ? -delta : 0;
break;
case "ArrowRight":
movement = isHorizontal ? delta : 0;
break;
case "ArrowUp":
movement = isHorizontal ? 0 : -delta;
break;
case "End":
movement = 100;
break;
case "Home":
movement = -100;
break;
}
return movement;
} else {
if (initialDragState == null) {
return 0;
}
return calculateDragOffsetPercentage(
event,
dragHandleId,
direction,
initialDragState,
panelGroupElement
);
}
}

View File

@@ -0,0 +1,40 @@
import { DragState, ResizeEvent } from "../PanelGroupContext";
import { Direction } from "../types";
import { assert } from "./assert";
import { getPanelGroupElement } from "./dom/getPanelGroupElement";
import { getResizeHandleElement } from "./dom/getResizeHandleElement";
import { getResizeEventCursorPosition } from "./events/getResizeEventCursorPosition";
export function calculateDragOffsetPercentage(
event: ResizeEvent,
dragHandleId: string,
direction: Direction,
initialDragState: DragState,
panelGroupElement: HTMLElement
): number {
const isHorizontal = direction === "horizontal";
const handleElement = getResizeHandleElement(dragHandleId, panelGroupElement);
assert(
handleElement,
`No resize handle element found for id "${dragHandleId}"`
);
const groupId = handleElement.getAttribute("data-panel-group-id");
assert(groupId, `Resize handle element has no group id attribute`);
let { initialCursorPosition } = initialDragState;
const cursorPosition = getResizeEventCursorPosition(direction, event);
const groupElement = getPanelGroupElement(groupId, panelGroupElement);
assert(groupElement, `No group element found for id "${groupId}"`);
const groupRect = groupElement.getBoundingClientRect();
const groupSizeInPixels = isHorizontal ? groupRect.width : groupRect.height;
const offsetPixels = cursorPosition - initialCursorPosition;
const offsetPercentage = (offsetPixels / groupSizeInPixels) * 100;
return offsetPercentage;
}

View File

@@ -0,0 +1,87 @@
import { PanelConstraints, PanelData } from "../Panel";
import { calculateUnsafeDefaultLayout } from "./calculateUnsafeDefaultLayout";
import { expectToBeCloseToArray } from "./test-utils";
describe("calculateUnsafeDefaultLayout", () => {
let idCounter = 0;
let orderCounter = 0;
function createPanelData(constraints: PanelConstraints = {}): PanelData {
return {
callbacks: {
onCollapse: undefined,
onExpand: undefined,
onResize: undefined,
},
constraints,
id: `${idCounter++}`,
idIsFromProps: false,
order: orderCounter++,
};
}
beforeEach(() => {
idCounter = 0;
orderCounter = 0;
});
it("should assign even sizes for every panel by default", () => {
expectToBeCloseToArray(
calculateUnsafeDefaultLayout({
panelDataArray: [createPanelData()],
}),
[100]
);
expectToBeCloseToArray(
calculateUnsafeDefaultLayout({
panelDataArray: [createPanelData(), createPanelData()],
}),
[50, 50]
);
expectToBeCloseToArray(
calculateUnsafeDefaultLayout({
panelDataArray: [
createPanelData(),
createPanelData(),
createPanelData(),
],
}),
[33.3, 33.3, 33.3]
);
});
it("should respect default panel size constraints", () => {
expectToBeCloseToArray(
calculateUnsafeDefaultLayout({
panelDataArray: [
createPanelData({
defaultSize: 15,
}),
createPanelData({
defaultSize: 85,
}),
],
}),
[15, 85]
);
});
it("should ignore min and max panel size constraints", () => {
expectToBeCloseToArray(
calculateUnsafeDefaultLayout({
panelDataArray: [
createPanelData({
minSize: 40,
}),
createPanelData(),
createPanelData({
maxSize: 10,
}),
],
}),
[33.3, 33.3, 33.3]
);
});
});

View File

@@ -0,0 +1,50 @@
import { PanelData } from "../Panel";
import { assert } from "./assert";
export function calculateUnsafeDefaultLayout({
panelDataArray,
}: {
panelDataArray: PanelData[];
}): number[] {
const layout = Array<number>(panelDataArray.length);
const panelConstraintsArray = panelDataArray.map(
(panelData) => panelData.constraints
);
let numPanelsWithSizes = 0;
let remainingSize = 100;
// Distribute default sizes first
for (let index = 0; index < panelDataArray.length; index++) {
const panelConstraints = panelConstraintsArray[index];
assert(panelConstraints, `Panel constraints not found for index ${index}`);
const { defaultSize } = panelConstraints;
if (defaultSize != null) {
numPanelsWithSizes++;
layout[index] = defaultSize;
remainingSize -= defaultSize;
}
}
// Remaining size should be distributed evenly between panels without default sizes
for (let index = 0; index < panelDataArray.length; index++) {
const panelConstraints = panelConstraintsArray[index];
assert(panelConstraints, `Panel constraints not found for index ${index}`);
const { defaultSize } = panelConstraints;
if (defaultSize != null) {
continue;
}
const numRemainingPanels = panelDataArray.length - numPanelsWithSizes;
const size = remainingSize / numRemainingPanels;
numPanelsWithSizes++;
layout[index] = size;
remainingSize -= size;
}
return layout;
}

View File

@@ -0,0 +1,49 @@
import { PanelData } from "../Panel";
import { assert } from "./assert";
import { fuzzyNumbersEqual } from "./numbers/fuzzyCompareNumbers";
// Layout should be pre-converted into percentages
export function callPanelCallbacks(
panelsArray: PanelData[],
layout: number[],
panelIdToLastNotifiedSizeMap: Record<string, number>
) {
layout.forEach((size, index) => {
const panelData = panelsArray[index];
assert(panelData, `Panel data not found for index ${index}`);
const { callbacks, constraints, id: panelId } = panelData;
const { collapsedSize = 0, collapsible } = constraints;
const lastNotifiedSize = panelIdToLastNotifiedSizeMap[panelId];
if (lastNotifiedSize == null || size !== lastNotifiedSize) {
panelIdToLastNotifiedSizeMap[panelId] = size;
const { onCollapse, onExpand, onResize } = callbacks;
if (onResize) {
onResize(size, lastNotifiedSize);
}
if (collapsible && (onCollapse || onExpand)) {
if (
onExpand &&
(lastNotifiedSize == null ||
fuzzyNumbersEqual(lastNotifiedSize, collapsedSize)) &&
!fuzzyNumbersEqual(size, collapsedSize)
) {
onExpand();
}
if (
onCollapse &&
(lastNotifiedSize == null ||
!fuzzyNumbersEqual(lastNotifiedSize, collapsedSize)) &&
fuzzyNumbersEqual(size, collapsedSize)
) {
onCollapse();
}
}
}
});
}

View File

@@ -0,0 +1,9 @@
import { compareLayouts } from "./compareLayouts";
describe("compareLayouts", () => {
it("should work", () => {
expect(compareLayouts([1, 2], [1])).toBe(false);
expect(compareLayouts([1], [1, 2])).toBe(false);
expect(compareLayouts([1, 2, 3], [1, 2, 3])).toBe(true);
});
});

View File

@@ -0,0 +1,12 @@
export function compareLayouts(a: number[], b: number[]) {
if (a.length !== b.length) {
return false;
} else {
for (let index = 0; index < a.length; index++) {
if (a[index] != b[index]) {
return false;
}
}
}
return true;
}

View File

@@ -0,0 +1,123 @@
import { PanelConstraints, PanelData } from "../Panel";
import { computePanelFlexBoxStyle } from "./computePanelFlexBoxStyle";
describe("computePanelFlexBoxStyle", () => {
function createPanelData(constraints: PanelConstraints = {}): PanelData {
return {
callbacks: {},
constraints,
id: "fake",
idIsFromProps: false,
order: undefined,
};
}
it("should observe a panel's default size if group layout has not yet been computed", () => {
expect(
computePanelFlexBoxStyle({
defaultSize: 0.1233456789,
dragState: null,
layout: [],
panelData: [
createPanelData({
defaultSize: 0.1233456789,
}),
createPanelData(),
],
panelIndex: 0,
precision: 2,
})
).toMatchInlineSnapshot(`
{
"flexBasis": 0,
"flexGrow": "0.12",
"flexShrink": 1,
"overflow": "hidden",
"pointerEvents": undefined,
}
`);
});
it("should always fill the full width for single-panel groups", () => {
expect(
computePanelFlexBoxStyle({
defaultSize: undefined,
dragState: null,
layout: [],
panelData: [createPanelData()],
panelIndex: 0,
precision: 2,
})
).toMatchInlineSnapshot(`
{
"flexBasis": 0,
"flexGrow": "1",
"flexShrink": 1,
"overflow": "hidden",
"pointerEvents": undefined,
}
`);
});
it("should round sizes to avoid floating point precision errors", () => {
const layout = [0.25435, 0.5758, 0.1698];
const panelData = [createPanelData(), createPanelData(), createPanelData()];
expect(
computePanelFlexBoxStyle({
defaultSize: undefined,
dragState: null,
layout,
panelData,
panelIndex: 0,
precision: 2,
})
).toMatchInlineSnapshot(`
{
"flexBasis": 0,
"flexGrow": "0.25",
"flexShrink": 1,
"overflow": "hidden",
"pointerEvents": undefined,
}
`);
expect(
computePanelFlexBoxStyle({
defaultSize: undefined,
dragState: null,
layout,
panelData,
panelIndex: 1,
precision: 2,
})
).toMatchInlineSnapshot(`
{
"flexBasis": 0,
"flexGrow": "0.58",
"flexShrink": 1,
"overflow": "hidden",
"pointerEvents": undefined,
}
`);
expect(
computePanelFlexBoxStyle({
defaultSize: undefined,
dragState: null,
layout,
panelData,
panelIndex: 2,
precision: 2,
})
).toMatchInlineSnapshot(`
{
"flexBasis": 0,
"flexGrow": "0.17",
"flexShrink": 1,
"overflow": "hidden",
"pointerEvents": undefined,
}
`);
});
});

View File

@@ -0,0 +1,50 @@
// This method returns a number between 1 and 100 representing
import { PanelData } from "../Panel";
import { DragState } from "../PanelGroupContext";
import { CSSProperties } from "../vendor/react";
// the % of the group's overall space this panel should occupy.
export function computePanelFlexBoxStyle({
defaultSize,
dragState,
layout,
panelData,
panelIndex,
precision = 3,
}: {
defaultSize: number | undefined;
layout: number[];
dragState: DragState | null;
panelData: PanelData[];
panelIndex: number;
precision?: number;
}): CSSProperties {
const size = layout[panelIndex];
let flexGrow;
if (size == null) {
// Initial render (before panels have registered themselves)
// In order to support server rendering, fall back to default size if provided
flexGrow =
defaultSize != undefined ? defaultSize.toPrecision(precision) : "1";
} else if (panelData.length === 1) {
// Special case: Single panel group should always fill full width/height
flexGrow = "1";
} else {
flexGrow = size.toPrecision(precision);
}
return {
flexBasis: 0,
flexGrow,
flexShrink: 1,
// Without this, Panel sizes may be unintentionally overridden by their content
overflow: "hidden",
// Disable pointer events inside of a panel during resize
// This avoid edge cases like nested iframes
pointerEvents: dragState !== null ? "none" : undefined,
};
}

9
node_modules/react-resizable-panels/src/utils/csp.ts generated vendored Normal file
View File

@@ -0,0 +1,9 @@
let nonce: string | null;
export function getNonce(): string | null {
return nonce;
}
export function setNonce(value: string | null) {
nonce = value;
}

103
node_modules/react-resizable-panels/src/utils/cursor.ts generated vendored Normal file
View File

@@ -0,0 +1,103 @@
import {
EXCEEDED_HORIZONTAL_MAX,
EXCEEDED_HORIZONTAL_MIN,
EXCEEDED_VERTICAL_MAX,
EXCEEDED_VERTICAL_MIN,
} from "../PanelResizeHandleRegistry";
import { getNonce } from "./csp";
type CursorState = "horizontal" | "intersection" | "vertical";
let currentCursorStyle: string | null = null;
let enabled: boolean = true;
let styleElement: HTMLStyleElement | null = null;
export function disableGlobalCursorStyles() {
enabled = false;
}
export function enableGlobalCursorStyles() {
enabled = true;
}
export function getCursorStyle(
state: CursorState,
constraintFlags: number
): string {
if (constraintFlags) {
const horizontalMin = (constraintFlags & EXCEEDED_HORIZONTAL_MIN) !== 0;
const horizontalMax = (constraintFlags & EXCEEDED_HORIZONTAL_MAX) !== 0;
const verticalMin = (constraintFlags & EXCEEDED_VERTICAL_MIN) !== 0;
const verticalMax = (constraintFlags & EXCEEDED_VERTICAL_MAX) !== 0;
if (horizontalMin) {
if (verticalMin) {
return "se-resize";
} else if (verticalMax) {
return "ne-resize";
} else {
return "e-resize";
}
} else if (horizontalMax) {
if (verticalMin) {
return "sw-resize";
} else if (verticalMax) {
return "nw-resize";
} else {
return "w-resize";
}
} else if (verticalMin) {
return "s-resize";
} else if (verticalMax) {
return "n-resize";
}
}
switch (state) {
case "horizontal":
return "ew-resize";
case "intersection":
return "move";
case "vertical":
return "ns-resize";
}
}
export function resetGlobalCursorStyle() {
if (styleElement !== null) {
document.head.removeChild(styleElement);
currentCursorStyle = null;
styleElement = null;
}
}
export function setGlobalCursorStyle(
state: CursorState,
constraintFlags: number
) {
if (!enabled) {
return;
}
const style = getCursorStyle(state, constraintFlags);
if (currentCursorStyle === style) {
return;
}
currentCursorStyle = style;
if (styleElement === null) {
styleElement = document.createElement("style");
const nonce = getNonce();
if (nonce) {
styleElement.setAttribute("nonce", nonce);
}
document.head.appendChild(styleElement);
}
styleElement.innerHTML = `*{cursor: ${style}!important;}`;
}

View File

@@ -0,0 +1,18 @@
export default function debounce<T extends Function>(
callback: T,
durationMs: number = 10
) {
let timeoutId: NodeJS.Timeout | null = null;
let callable = (...args: any) => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
callback(...args);
}, durationMs);
};
return callable as unknown as T;
}

View File

@@ -0,0 +1,15 @@
import { getResizeHandleElementIndex } from "../utils/dom/getResizeHandleElementIndex";
export function determinePivotIndices(
groupId: string,
dragHandleId: string,
panelGroupElement: ParentNode
): [indexBefore: number, indexAfter: number] {
const index = getResizeHandleElementIndex(
groupId,
dragHandleId,
panelGroupElement
);
return index != null ? [index, index + 1] : [-1, -1];
}

View File

@@ -0,0 +1,10 @@
export function getPanelElement(
id: string,
scope: ParentNode | HTMLElement = document
): HTMLElement | null {
const element = scope.querySelector(`[data-panel-id="${id}"]`);
if (element) {
return element as HTMLElement;
}
return null;
}

View File

@@ -0,0 +1,8 @@
export function getPanelElementsForGroup(
groupId: string,
scope: ParentNode | HTMLElement = document
): HTMLElement[] {
return Array.from(
scope.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`)
);
}

View File

@@ -0,0 +1,21 @@
export function getPanelGroupElement(
id: string,
rootElement: ParentNode | HTMLElement = document
): HTMLElement | null {
//If the root element is the PanelGroup
if (
rootElement instanceof HTMLElement &&
(rootElement as HTMLElement)?.dataset?.panelGroupId == id
) {
return rootElement as HTMLElement;
}
//Else query children
const element = rootElement.querySelector(
`[data-panel-group][data-panel-group-id="${id}"]`
);
if (element) {
return element as HTMLElement;
}
return null;
}

View File

@@ -0,0 +1,10 @@
export function getResizeHandleElement(
id: string,
scope: ParentNode | HTMLElement = document
): HTMLElement | null {
const element = scope.querySelector(`[data-panel-resize-handle-id="${id}"]`);
if (element) {
return element as HTMLElement;
}
return null;
}

View File

@@ -0,0 +1,13 @@
import { getResizeHandleElementsForGroup } from "./getResizeHandleElementsForGroup";
export function getResizeHandleElementIndex(
groupId: string,
id: string,
scope: ParentNode | HTMLElement = document
): number | null {
const handles = getResizeHandleElementsForGroup(groupId, scope);
const index = handles.findIndex(
(handle) => handle.getAttribute("data-panel-resize-handle-id") === id
);
return index ?? null;
}

View File

@@ -0,0 +1,10 @@
export function getResizeHandleElementsForGroup(
groupId: string,
scope: ParentNode | HTMLElement = document
): HTMLElement[] {
return Array.from(
scope.querySelectorAll(
`[data-panel-resize-handle-id][data-panel-group-id="${groupId}"]`
)
);
}

View File

@@ -0,0 +1,19 @@
import { PanelData } from "../../Panel";
import { getResizeHandleElement } from "./getResizeHandleElement";
import { getResizeHandleElementsForGroup } from "./getResizeHandleElementsForGroup";
export function getResizeHandlePanelIds(
groupId: string,
handleId: string,
panelsArray: PanelData[],
scope: ParentNode | HTMLElement = document
): [idBefore: string | null, idAfter: string | null] {
const handle = getResizeHandleElement(handleId, scope);
const handles = getResizeHandleElementsForGroup(groupId, scope);
const index = handle ? handles.indexOf(handle) : -1;
const idBefore: string | null = panelsArray[index]?.id ?? null;
const idAfter: string | null = panelsArray[index + 1]?.id ?? null;
return [idBefore, idAfter];
}

View File

@@ -0,0 +1,23 @@
import { ResizeEvent } from "../../types";
import { isMouseEvent, isPointerEvent } from ".";
export function getResizeEventCoordinates(event: ResizeEvent) {
if (isPointerEvent(event)) {
if (event.isPrimary) {
return {
x: event.clientX,
y: event.clientY,
};
}
} else if (isMouseEvent(event)) {
return {
x: event.clientX,
y: event.clientY,
};
}
return {
x: Infinity,
y: Infinity,
};
}

View File

@@ -0,0 +1,14 @@
import { ResizeEvent } from "../../PanelGroupContext";
import { Direction } from "../../types";
import { getResizeEventCoordinates } from "./getResizeEventCoordinates";
export function getResizeEventCursorPosition(
direction: Direction,
event: ResizeEvent
): number {
const isHorizontal = direction === "horizontal";
const { x, y } = getResizeEventCoordinates(event);
return isHorizontal ? x : y;
}

View File

@@ -0,0 +1,13 @@
import { ResizeEvent } from "../../PanelGroupContext";
export function isKeyDown(event: ResizeEvent): event is KeyboardEvent {
return event.type === "keydown";
}
export function isPointerEvent(event: ResizeEvent): event is PointerEvent {
return event.type.startsWith("pointer");
}
export function isMouseEvent(event: ResizeEvent): event is MouseEvent {
return event.type.startsWith("mouse");
}

View File

@@ -0,0 +1,5 @@
export function getInputType(): "coarse" | "fine" | undefined {
if (typeof matchMedia === "function") {
return matchMedia("(pointer:coarse)").matches ? "coarse" : "fine";
}
}

View File

@@ -0,0 +1,26 @@
import { PanelGroupStorage } from "../PanelGroup";
// PanelGroup might be rendering in a server-side environment where localStorage is not available
// or on a browser with cookies/storage disabled.
// In either case, this function avoids accessing localStorage until needed,
// and avoids throwing user-visible errors.
export function initializeDefaultStorage(storageObject: PanelGroupStorage) {
try {
if (typeof localStorage !== "undefined") {
// Bypass this check for future calls
storageObject.getItem = (name: string) => {
return localStorage.getItem(name);
};
storageObject.setItem = (name: string, value: string) => {
localStorage.setItem(name, value);
};
} else {
throw new Error("localStorage not supported in this environment");
}
} catch (error) {
console.error(error);
storageObject.getItem = () => null;
storageObject.setItem = () => {};
}
}

View File

@@ -0,0 +1,16 @@
import { fuzzyCompareNumbers } from "./fuzzyCompareNumbers";
describe("fuzzyCompareNumbers", () => {
it("should return 0 when numbers are equal", () => {
expect(fuzzyCompareNumbers(10.123, 10.123, 5)).toBe(0);
});
it("should return 0 when numbers are fuzzy equal", () => {
expect(fuzzyCompareNumbers(0.000001, 0.000002, 5)).toBe(0);
});
it("should return a delta when numbers are not unequal", () => {
expect(fuzzyCompareNumbers(0.000001, 0.000002, 6)).toBe(-1);
expect(fuzzyCompareNumbers(0.000005, 0.000002, 6)).toBe(1);
});
});

Some files were not shown because too many files have changed in this diff Show More