From 5a11ea6562cbbea7ff5a42885d64e7969a1f38c4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 25 Jan 2026 09:23:25 -0500 Subject: [PATCH 1/2] Decrease default hit target size; make configurable (#626) Pre-release build: [react-resizable-panels-4.5.1.tgz](https://github.com/user-attachments/files/24845991/react-resizable-panels-4.5.1.tgz) --- CHANGELOG.md | 4 ++ README.md | 13 +++++ lib/components/group/Group.tsx | 14 ++++- lib/components/group/types.ts | 22 +++++++ lib/global/dom/calculateHitRegions.test.ts | 58 +++++++++---------- lib/global/dom/calculateHitRegions.ts | 7 +-- .../event-handlers/onDocumentDoubleClick.ts | 11 ---- lib/global/test/mockGroup.ts | 4 ++ .../utils/findMatchingHitRegions.test.ts | 8 +-- public/generated/docs/Group.json | 17 ++++++ 10 files changed, 109 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f502b7ea..11ebacb81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.5.2 + +- [626](https://github.com/bvaughn/react-resizable-panels/pull/626): Decrease default hit target size for `Separator` and `Panel` edges; make configurable via a new `Group` prop. + ## 4.5.1 - [624](https://github.com/bvaughn/react-resizable-panels/pull/624): **Bugfix**: Fallback to alternate CSS cursor styles for Safari diff --git a/README.md b/README.md index d646b98e2..eb6cca08c 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,19 @@ For most cases, it is recommended to use the onLayoutChanged callba

Called after the Group's layout has been changed.

ℹ️ For layout changes caused by pointer events, this method is not called until the pointer has been released. This method is recommended when saving layouts to some storage api.

+ + + + resizeTargetMinimumSize +

Minimum size of the resizable hit target area (either Separator or Panel edge) +This threshold ensures are large enough to avoid mis-clicks.

+ +

ℹ️ Apple interface guidelines suggest 20pt (27px) on desktops and 28pt (37px) for touch devices +In practice this seems to be much larger than many of their own applications use though.

diff --git a/lib/components/group/Group.tsx b/lib/components/group/Group.tsx index 65753b82f..dd16ce7a0 100644 --- a/lib/components/group/Group.tsx +++ b/lib/components/group/Group.tsx @@ -14,7 +14,12 @@ import type { RegisteredPanel } from "../panel/types"; import type { RegisteredSeparator } from "../separator/types"; import { GroupContext } from "./GroupContext"; import { sortByElementOffset } from "./sortByElementOffset"; -import type { GroupProps, Layout, RegisteredGroup } from "./types"; +import type { + GroupProps, + Layout, + RegisteredGroup, + ResizeTargetMinimumSize +} from "./types"; import { useGroupImperativeHandle } from "./useGroupImperativeHandle"; /** @@ -41,6 +46,10 @@ export function Group({ onLayoutChange: onLayoutChangeUnstable, onLayoutChanged: onLayoutChangedUnstable, orientation = "horizontal", + resizeTargetMinimumSize = { + coarse: 20, + fine: 10 + }, style, ...rest }: GroupProps) { @@ -82,11 +91,13 @@ export function Group({ lastExpandedPanelSizes: { [panelIds: string]: number }; layouts: { [panelIds: string]: Layout }; panels: RegisteredPanel[]; + resizeTargetMinimumSize: ResizeTargetMinimumSize; separators: RegisteredSeparator[]; }>({ lastExpandedPanelSizes: {}, layouts: {}, panels: [], + resizeTargetMinimumSize, separators: [] }); @@ -199,6 +210,7 @@ export function Group({ inMemoryLayouts: inMemoryValuesRef.current.layouts, orientation, panels: inMemoryValues.panels, + resizeTargetMinimumSize: inMemoryValues.resizeTargetMinimumSize, separators: inMemoryValues.separators }; diff --git a/lib/components/group/types.ts b/lib/components/group/types.ts index 02b7cefc3..288702bc9 100644 --- a/lib/components/group/types.ts +++ b/lib/components/group/types.ts @@ -22,6 +22,11 @@ export type DragState = { separatorId: string | undefined; }; +export type ResizeTargetMinimumSize = { + coarse: number; + fine: number; +}; + export type RegisteredGroup = { defaultLayout: Layout | undefined; disableCursor: boolean; @@ -38,6 +43,7 @@ export type RegisteredGroup = { }; orientation: Orientation; panels: RegisteredPanel[]; + resizeTargetMinimumSize: ResizeTargetMinimumSize; separators: RegisteredSeparator[]; }; @@ -140,6 +146,22 @@ export type GroupProps = HTMLAttributes & { */ onLayoutChanged?: (layout: Layout) => void | undefined; + /** + * Minimum size of the resizable hit target area (either `Separator` or `Panel` edge) + * This threshold ensures are large enough to avoid mis-clicks. + * + * - Coarse inputs (typically a finger on a touchscreen) have reduced accuracy; + * to ensure accessibility and ease of use, hit targets should be larger to prevent mis-clicks. + * - Fine inputs (typically a mouse) can be smaller + * + * ℹ️ [Apple interface guidelines](https://developer.apple.com/design/human-interface-guidelines/accessibility) suggest `20pt` (`27px`) on desktops and `28pt` (`37px`) for touch devices + * In practice this seems to be much larger than many of their own applications use though. + */ + resizeTargetMinimumSize?: { + coarse: number; + fine: number; + }; + /** * Specifies the resizable orientation ("horizontal" or "vertical"); defaults to "horizontal" */ diff --git a/lib/global/dom/calculateHitRegions.test.ts b/lib/global/dom/calculateHitRegions.test.ts index ea8318307..47dbe68a6 100644 --- a/lib/global/dom/calculateHitRegions.test.ts +++ b/lib/global/dom/calculateHitRegions.test.ts @@ -41,7 +41,7 @@ describe("calculateHitRegions", () => { "group-1-left", "group-1-right" ], - "rect": "36.5,0 27 x 50" + "rect": "45,0 10 x 50" } ]" `); @@ -60,14 +60,14 @@ describe("calculateHitRegions", () => { "group-1-left", "group-1-center" ], - "rect": "26.5,0 27 x 50" + "rect": "35,0 10 x 50" }, { "panels": [ "group-1-center", "group-1-right" ], - "rect": "66.5,0 27 x 50" + "rect": "75,0 10 x 50" } ]" `); @@ -88,7 +88,7 @@ describe("calculateHitRegions", () => { "group-1-left", "group-1-center" ], - "rect": "31.5,0 27 x 50", + "rect": "40,0 10 x 50", "separator": "group-1-left" }, { @@ -96,7 +96,7 @@ describe("calculateHitRegions", () => { "group-1-center", "group-1-right" ], - "rect": "81.5,0 27 x 50", + "rect": "90,0 10 x 50", "separator": "group-1-right" } ]" @@ -117,14 +117,14 @@ describe("calculateHitRegions", () => { "group-1-a", "group-1-b" ], - "rect": "26.5,0 27 x 50" + "rect": "35,0 10 x 50" }, { "panels": [ "group-1-b", "group-1-c" ], - "rect": "69,0 27 x 50", + "rect": "77.5,0 10 x 50", "separator": "group-1-separator" } ]" @@ -148,38 +148,38 @@ describe("calculateHitRegions", () => { "group-1-a", "group-1-b" ], - "rect": "46.5,0 27 x 50" + "rect": "55,0 10 x 50" }, { "panels": [ "group-1-b", "group-1-c" ], - "rect": "96.5,0 27 x 50" + "rect": "105,0 10 x 50" }, { "panels": [ "group-1-b", "group-1-c" ], - "rect": "106.5,0 27 x 50" + "rect": "115,0 10 x 50" }, { "panels": [ "group-1-c", "group-1-d" ], - "rect": "156.5,0 27 x 50" + "rect": "165,0 10 x 50" } ]" `); }); test("CSS styles (e.g. padding and flex gap)", () => { - const group = mockGroup(new DOMRect(0, 0, 155, 50)); - group.addPanel(new DOMRect(5, 5, 45, 40), "left"); - group.addPanel(new DOMRect(55, 5, 45, 40), "center"); - group.addPanel(new DOMRect(105, 5, 45, 40), "right"); + const group = mockGroup(new DOMRect(0, 0, 190, 70)); + group.addPanel(new DOMRect(10, 10, 50, 40), "left"); + group.addPanel(new DOMRect(70, 10, 50, 40), "center"); + group.addPanel(new DOMRect(130, 10, 50, 40), "right"); expect(serialize(group)).toMatchInlineSnapshot(` "[ @@ -188,14 +188,14 @@ describe("calculateHitRegions", () => { "group-1-left", "group-1-center" ], - "rect": "39,5 27 x 40" + "rect": "60,10 10 x 40" }, { "panels": [ "group-1-center", "group-1-right" ], - "rect": "89,5 27 x 40" + "rect": "120,10 10 x 40" } ]" `); @@ -216,14 +216,14 @@ describe("calculateHitRegions", () => { "group-1-left", "group-1-center" ], - "rect": "36.5,0 27 x 50" + "rect": "45,0 10 x 50" }, { "panels": [ "group-1-center", "group-1-right" ], - "rect": "86.5,0 27 x 50" + "rect": "95,0 10 x 50" } ]" `); @@ -231,13 +231,13 @@ describe("calculateHitRegions", () => { // Test covers conditionally rendered panels and separators test("should sort elements and separators by offset", () => { - const group = mockGroup(new DOMRect(0, 0, 270, 50)); - group.addPanel(new DOMRect(205, 0, 65, 50), "d"); - group.addPanel(new DOMRect(70, 0, 65, 50), "b"); - group.addPanel(new DOMRect(0, 0, 65, 50), "a"); - group.addPanel(new DOMRect(135, 0, 65, 50), "c"); - group.addSeparator(new DOMRect(200, 0, 5, 50), "right"); - group.addSeparator(new DOMRect(65, 0, 5, 50), "left"); + const group = mockGroup(new DOMRect(0, 0, 260, 50)); + group.addPanel(new DOMRect(200, 0, 60, 50), "d"); + group.addPanel(new DOMRect(70, 0, 60, 50), "b"); + group.addPanel(new DOMRect(0, 0, 60, 50), "a"); + group.addPanel(new DOMRect(130, 0, 60, 50), "c"); + group.addSeparator(new DOMRect(190, 0, 10, 50), "right"); + group.addSeparator(new DOMRect(60, 0, 10, 50), "left"); expect(serialize(group)).toMatchInlineSnapshot(` "[ @@ -246,7 +246,7 @@ describe("calculateHitRegions", () => { "group-1-a", "group-1-b" ], - "rect": "54,0 27 x 50", + "rect": "60,0 10 x 50", "separator": "group-1-left" }, { @@ -254,14 +254,14 @@ describe("calculateHitRegions", () => { "group-1-b", "group-1-c" ], - "rect": "121.5,0 27 x 50" + "rect": "125,0 10 x 50" }, { "panels": [ "group-1-c", "group-1-d" ], - "rect": "189,0 27 x 50", + "rect": "190,0 10 x 50", "separator": "group-1-right" } ]" diff --git a/lib/global/dom/calculateHitRegions.ts b/lib/global/dom/calculateHitRegions.ts index dc0b367d9..6de090215 100644 --- a/lib/global/dom/calculateHitRegions.ts +++ b/lib/global/dom/calculateHitRegions.ts @@ -129,10 +129,9 @@ export function calculateHitRegions(group: RegisteredGroup) { ? rectOrSeparator : rectOrSeparator.element.getBoundingClientRect(); - // Ensure that Separators or Panel "edges" have large enough hit areas to be interacted with easily - // Apple interface guidelines suggest 20pt (27) on desktops and 28pt (37px) for touch devices - // https://developer.apple.com/design/human-interface-guidelines/accessibility - const minHitTargetSize = isCoarsePointer() ? 37 : 27; + const minHitTargetSize = isCoarsePointer() + ? group.resizeTargetMinimumSize.coarse + : group.resizeTargetMinimumSize.fine; if (rect.width < minHitTargetSize) { const delta = minHitTargetSize - rect.width; rect = new DOMRect( diff --git a/lib/global/event-handlers/onDocumentDoubleClick.ts b/lib/global/event-handlers/onDocumentDoubleClick.ts index 744fc682d..a539437d8 100644 --- a/lib/global/event-handlers/onDocumentDoubleClick.ts +++ b/lib/global/event-handlers/onDocumentDoubleClick.ts @@ -1,5 +1,3 @@ -import type { RegisteredGroup } from "../../components/group/types"; -import type { RegisteredPanel } from "../../components/panel/types"; import { read } from "../mutableState"; import { findMatchingHitRegions } from "../utils/findMatchingHitRegions"; import { getImperativePanelMethods } from "../utils/getImperativePanelMethods"; @@ -12,16 +10,7 @@ export function onDocumentDoubleClick(event: MouseEvent) { const { mountedGroups } = read(); const hitRegions = findMatchingHitRegions(event, mountedGroups); - - const groups = new Set(); - const panels = new Set(); - hitRegions.forEach((current) => { - groups.add(current.group); - current.panels.forEach((panel) => { - panels.add(panel); - }); - if (current.separator) { const panelWithDefaultSize = current.panels.find( (panel) => panel.panelConstraints.defaultSize !== undefined diff --git a/lib/global/test/mockGroup.ts b/lib/global/test/mockGroup.ts index cfd8ff1ea..76f618673 100644 --- a/lib/global/test/mockGroup.ts +++ b/lib/global/test/mockGroup.ts @@ -57,6 +57,10 @@ export function mockGroup( inMemoryLastExpandedPanelSizes: {}, inMemoryLayouts: {}, orientation, + resizeTargetMinimumSize: { + coarse: 20, + fine: 10 + }, get panels() { return Array.from(mockPanels.values()); diff --git a/lib/global/utils/findMatchingHitRegions.test.ts b/lib/global/utils/findMatchingHitRegions.test.ts index 24c46a3ff..748593a94 100644 --- a/lib/global/utils/findMatchingHitRegions.test.ts +++ b/lib/global/utils/findMatchingHitRegions.test.ts @@ -42,7 +42,7 @@ describe("findMatchingHitRegions", () => { "group-1-left", "group-1-right" ], - "rect": "36.5,0 27 x 50" + "rect": "45,0 10 x 50" } ]" `); @@ -63,7 +63,7 @@ describe("findMatchingHitRegions", () => { "group-1-left", "group-1-right" ], - "rect": "46.5,0 27 x 50", + "rect": "50,0 20 x 50", "separator": "group-1-separator" } ]" @@ -93,14 +93,14 @@ describe("findMatchingHitRegions", () => { "group-1-left", "group-1-right" ], - "rect": "36.5,0 27 x 50" + "rect": "45,0 10 x 50" }, { "panels": [ "group-2-top", "group-2-bottom" ], - "rect": "0,11.5 50 x 27" + "rect": "0,20 50 x 10" } ]" `); diff --git a/public/generated/docs/Group.json b/public/generated/docs/Group.json index a6dddaf2c..4db38c255 100644 --- a/public/generated/docs/Group.json +++ b/public/generated/docs/Group.json @@ -155,6 +155,23 @@ "name": "onLayoutChanged", "required": false }, + "resizeTargetMinimumSize": { + "description": [ + { + "content": "

Minimum size of the resizable hit target area (either Separator or Panel edge)\nThis threshold ensures are large enough to avoid mis-clicks.

\n" + }, + { + "content": "
    \n
  • Coarse inputs (typically a finger on a touchscreen) have reduced accuracy;\nto ensure accessibility and ease of use, hit targets should be larger to prevent mis-clicks.
  • \n
  • Fine inputs (typically a mouse) can be smaller
  • \n
\n" + }, + { + "content": "

Apple interface guidelines suggest 20pt (27px) on desktops and 28pt (37px) for touch devices\nIn practice this seems to be much larger than many of their own applications use though.

\n", + "intent": "primary" + } + ], + "html": "
resizeTargetMinimumSize?: { coarse: number; fine: number; } = {
\n
coarse: 20,
\n
fine: 10
\n
}
", + "name": "resizeTargetMinimumSize", + "required": false + }, "orientation": { "description": [ { From 77af2895656f1e3262f7c43a9762d4001182ae4d Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 25 Jan 2026 09:24:05 -0500 Subject: [PATCH 2/2] 4.5.1 -> 4.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a7e4295b9..d6ee42183 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-resizable-panels", - "version": "4.5.1", + "version": "4.5.2", "type": "module", "author": "Brian Vaughn (https://github.com/bvaughn/)", "contributors": [