Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 4.0.6

- [0796644](https://github.com/bvaughn/react-resizable-panels/commit/0796644): Account for Flex gap when calculating pointer-move delta %

## 4.0.5

- [#535](https://github.com/bvaughn/react-resizable-panels/pull/535): Updated docs to make size and layout formats clearer

## 4.0.4

- [#534](https://github.com/bvaughn/react-resizable-panels/pull/534): Set focus on `Separator` on "pointerdown"
- [e08fe42](https://github.com/bvaughn/react-resizable-panels/commit/e08fe42195d8ace7e4e62205453be4a5245fefb9): Improve iOS/Safari resize UX

## 4.0.3

- Fixed TS type for `defaultLayout` value returned from `useDefaultLayout`
Expand Down Expand Up @@ -57,9 +70,9 @@ import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
import { Group, Panel, Separator } from "react-resizable-panels";

<Group orientation="horizontal">
<Panel defaultSize={30} minSize={20}>left</Panel>
<Panel defaultSize="30%" minSize="20%">left</Panel>
<Separator />
<Panel defaultSize={30} minSize={20}>right</Panel>
<Panel defaultSize="30%" minSize="20%">right</Panel>
</Group>
```

Expand Down
85 changes: 69 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,28 @@ Documentation for this project is available at [react-resizable-panels.vercel.ap

### Group

#### Required props
<!-- Group:description:begin -->
A Group wraps a set of resizable Panel components.
Group content can be resized _horizontally_ or _vertically_.

Group elements always include the following attributes:

```html
<div data-group data-testid="group-id-prop" id="group-id-prop">
```

ℹ️ [Test id](https://testing-library.com/docs/queries/bytestid/) can be used to narrow selection when unit testing.
<!-- Group:description:end -->

<!-- Group:required:begin -->
#### Required props

<!-- Group:required:end -->
<!-- Group:required-props:begin -->
None
<!-- Group:required-props:end -->

#### Optional props

<!-- Group:optional:begin -->
<!-- Group:optional-props:begin -->

<table>
<thead>
Expand Down Expand Up @@ -114,19 +127,43 @@ Use this prop to disable that behavior for Panels and Separators in this group.<
</tbody>
</table>

<!-- Group:optional:end -->
<!-- Group:optional-props:end -->

### Panel

#### Required props
<!-- Panel:description:begin -->
A Panel wraps resizable content and can be configured with min/max size constraints and collapsible behavior.

Panel size props can be in the following formats:
- Percentage of the parent Group (0..100)
- Pixels
- Relative font units (em, rem)
- Viewport relative units (vh, vw)

ℹ️ Numeric values are assumed to be pixels.
Strings without explicit units are assumed to be percentages (0%..100%).
Percentages may also be specified as strings ending with "%" (e.g. "33%")
Pixels may also be specified as strings ending with the unit "px".
Other units should be specified as strings ending with their CSS property units (e.g. 1rem, 50vh)

Panel elements always include the following attributes:

```html
<div data-panel data-testid="panel-id-prop" id="panel-id-prop">
```

ℹ️ [Test id](https://testing-library.com/docs/queries/bytestid/) can be used to narrow selection when unit testing.
<!-- Panel:description:end -->

<!-- Panel:required:begin -->
#### Required props

<!-- Panel:required:end -->
<!-- Panel:required-props:begin -->
None
<!-- Panel:required-props:end -->

#### Optional props

<!-- Panel:optional:begin -->
<!-- Panel:optional-props:begin -->

<table>
<thead>
Expand Down Expand Up @@ -158,7 +195,7 @@ Falls back to <code>useId</code> when not provided.</p>
</tr>
<tr>
<td>collapsedSize</td>
<td><p>Panel size when collapsed; defaults to 0.</p>
<td><p>Panel size when collapsed; defaults to 0%.</p>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -209,19 +246,35 @@ Falls back to <code>useId</code> when not provided.</p>
</tbody>
</table>

<!-- Panel:optional:end -->
<!-- Panel:optional-props:end -->

### Separator

#### Required props
<!-- Separator:description:begin -->
Separators are not _required_ but they are _recommended_ as they improve keyboard accessibility.

Separators should be rendered as the direct child of a Group component.

Separator elements always include the following attributes:

<!-- Separator:required:begin -->
```html
<div data-separator data-testid="separator-id-prop" id="separator-id-prop" role="separator">
```

ℹ️ [Test id](https://testing-library.com/docs/queries/bytestid/) can be used to narrow selection when unit testing.

ℹ️ In addition to the attributes shown above, separator also renders all required [WAI-ARIA properties](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/separator_role#associated_wai-aria_roles_states_and_properties).
<!-- Separator:description:end -->

#### Required props

<!-- Separator:required:end -->
<!-- Separator:required-props:begin -->
None
<!-- Separator:required-props:end -->

#### Optional props

<!-- Separator:optional:begin -->
<!-- Separator:optional-props:begin -->

<table>
<thead>
Expand Down Expand Up @@ -260,4 +313,4 @@ Falls back to <code>useId</code> when not provided.</p>
</tbody>
</table>

<!-- Separator:optional:end -->
<!-- Separator:optional-props:end -->
26 changes: 24 additions & 2 deletions integrations/vite/tests/pointer-interactions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ test.describe("pointer interactions", () => {
});

await expect(page.getByText('"onLayoutCount": 2')).toBeVisible();
await expect(page.getByText('"left": 84')).toBeVisible();
await expect(page.getByText('"left": 87')).toBeVisible();
await expect(page.getByText('"center": 5')).toBeVisible();
await expect(page.getByText('"right": 11')).toBeVisible();
await expect(page.getByText('"right": 8')).toBeVisible();

await page.mouse.move(x - 500, y);

Expand All @@ -169,4 +169,26 @@ test.describe("pointer interactions", () => {
await expect(page.getByText('"center": 33')).toBeVisible();
await expect(page.getByText('"right": 33')).toBeVisible();
});

test("drag should measure delta using the available group size (minus flex gap)", async ({
page
}) => {
await goToUrl(
page,
<Group className="gap-20">
<Panel defaultSize="10%" id="left" minSize="5%" />
<Separator />
<Panel id="right" minSize="5%" />
</Group>
);

await expect(page.getByText('"onLayoutCount": 1')).toBeVisible();
await expect(page.getByText('"left": 10')).toBeVisible();

await resizeHelper(page, ["left", "right"], 800, 0);

// The new layout would be ~91% and 9% if not for flex-gap
await expect(page.getByText('"onLayoutCount": 2')).toBeVisible();
await expect(page.getByText('"left": 95')).toBeVisible();
});
});
9 changes: 6 additions & 3 deletions integrations/vite/tests/utils/serializer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ export type Config = {
};

export type GroupJson = {
type: "Group";
children: (PanelJson | SeparatorJson)[];
className?: string;
props: object;
type: "Group";
};

export type PanelJson = {
children?: GroupJson | undefined;
type: "Panel";
className?: string;
props: object;
type: "Panel";
};

export type SeparatorJson = {
type: "Separator";
className?: string;
props: object;
type: "Separator";
};
4 changes: 2 additions & 2 deletions lib/components/group/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ export interface GroupImperativeHandle {
/**
* Get the Group's current layout as a map of Panel id to percentage (0..100)
*
* @return Map of Panel id to percentage (0..100)
* @return Map of Panel id to percentages (specified as numbers ranging between 0..100)
*/
getLayout: () => { [panelId: string]: number };

/**
* Set a new layout for the Group
*
* @param layout Map of Panel id to percentage (0..100)
* @param layout Map of Panel id to percentage (a number between 0..100)
* @return Applied layout (after validation)
*/
setLayout: (layout: { [panelId: string]: number }) => Layout;
Expand Down
22 changes: 14 additions & 8 deletions lib/components/panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ import { usePanelImperativeHandle } from "./usePanelImperativeHandle";
/**
* A Panel wraps resizable content and can be configured with min/max size constraints and collapsible behavior.
*
* Panel size props can be specified using the following CSS units:
* - Pixels (default if value is of type `number`)
* - Percentages (default if value is of type `string`)
* - Font sizes (em, rem)
* - Viewport sizes (vh, vw)
* Panel size props can be in the following formats:
* - Percentage of the parent Group (0..100)
* - Pixels
* - Relative font units (em, rem)
* - Viewport relative units (vh, vw)
*
* ℹ️ Numeric values are assumed to be pixels.
* Strings without explicit units are assumed to be percentages (0%..100%).
* Percentages may also be specified as strings ending with "%" (e.g. "33%")
* Pixels may also be specified as strings ending with the unit "px".
* Other units should be specified as strings ending with their CSS property units (e.g. 1rem, 50vh)
*
* Panel elements always include the following attributes:
*
Expand All @@ -32,13 +38,13 @@ import { usePanelImperativeHandle } from "./usePanelImperativeHandle";
export function Panel({
children,
className,
collapsedSize = 0,
collapsedSize = "0%",
collapsible = false,
defaultSize,
elementRef,
id: idProp,
maxSize = "100",
minSize = "0",
maxSize = "100%",
minSize = "0%",
onResize: onResizeUnstable,
panelRef,
style,
Expand Down
19 changes: 16 additions & 3 deletions lib/components/panel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ export interface PanelImperativeHandle {
*
* @return Panel size (in pixels and as a percentage of the parent group)
*/
getSize: () => PanelSize;
getSize: () => {
asPercentage: number;
inPixels: number;
};

/**
* The Panel is currently collapsed.
Expand All @@ -63,7 +66,17 @@ export interface PanelImperativeHandle {
/**
* Update the Panel's size.
*
* ℹ️ Size may be specified in pixel format (number) or as a percentage (string).
* Size can be in the following formats:
* - Percentage of the parent Group (0..100)
* - Pixels
* - Relative font units (em, rem)
* - Viewport relative units (vh, vw)
*
* ℹ️ Numeric values are assumed to be pixels.
* Strings without explicit units are assumed to be percentages (0%..100%).
* Percentages may also be specified as strings ending with "%" (e.g. "33%")
* Pixels may also be specified as strings ending with the unit "px".
* Other units should be specified as strings ending with their CSS property units (e.g. 1rem, 50vh)
*
* @param size New panel size
* @return Applied size (after validation)
Expand All @@ -80,7 +93,7 @@ export type PanelProps = HTMLAttributes<HTMLDivElement> & {
className?: string | undefined;

/**
* Panel size when collapsed; defaults to 0.
* Panel size when collapsed; defaults to 0%.
*/
collapsedSize?: number | string | undefined;

Expand Down
18 changes: 9 additions & 9 deletions lib/global/cursor/updateCursorStyle.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { Properties } from "csstype";
import { read } from "../mutableState";
import { getCursorStyle } from "./getCursorStyle";

let prevCursor: Properties["cursor"] | null = null;
let prevStyle: string | undefined = undefined;
let styleSheet: CSSStyleSheet | undefined = undefined;

export function updateCursorStyle() {
Expand All @@ -17,31 +16,32 @@ export function updateCursorStyle() {
switch (interactionState.state) {
case "active":
case "hover": {
const style = getCursorStyle({
const cursorStyle = getCursorStyle({
cursorFlags,
groups: interactionState.hitRegions.map((current) => current.group),
state: interactionState.state
});

if (prevCursor === style) {
const nextStyle = `*{cursor: ${cursorStyle} !important; ${interactionState.state === "active" ? "touch-action: none;" : ""} }`;
if (prevStyle === nextStyle) {
return;
}

prevCursor = style;
prevStyle = nextStyle;

if (style) {
if (cursorStyle) {
if (styleSheet.cssRules.length === 0) {
styleSheet.insertRule(`*{cursor: ${style} !important;}`);
styleSheet.insertRule(nextStyle);
} else {
styleSheet.replaceSync(`*{cursor: ${style} !important;}`);
styleSheet.replaceSync(nextStyle);
}
} else if (styleSheet.cssRules.length === 1) {
styleSheet.deleteRule(0);
}
break;
}
case "inactive": {
prevCursor = null;
prevStyle = undefined;

if (styleSheet.cssRules.length === 1) {
styleSheet.deleteRule(0);
Expand Down
Loading
Loading