Skip to content

Commit 38ab959

Browse files
committed
Decrease default hit target size; make configurable
1 parent d1d2ddf commit 38ab959

File tree

9 files changed

+107
-50
lines changed

9 files changed

+107
-50
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,19 @@ For most cases, it is recommended to use the <code>onLayoutChanged</code> callba
130130
<td><p>Called after the Group&#39;s layout has been changed.</p>
131131
<p>ℹ️ For layout changes caused by pointer events, this method is not called until the pointer has been released.
132132
This method is recommended when saving layouts to some storage api.</p>
133+
</td>
134+
</tr>
135+
<tr>
136+
<td>resizeTargetMinimumSize</td>
137+
<td><p>Minimum size of the resizable hit target area (either <code>Separator</code> or <code>Panel</code> edge)
138+
This threshold ensures are large enough to avoid mis-clicks.</p>
139+
<ul>
140+
<li>Coarse inputs (typically a finger on a touchscreen) have reduced accuracy;
141+
to ensure accessibility and ease of use, hit targets should be larger to prevent mis-clicks.</li>
142+
<li>Fine inputs (typically a mouse) can be smaller</li>
143+
</ul>
144+
<p>ℹ️ <a href="https://developer.apple.com/design/human-interface-guidelines/accessibility">Apple interface guidelines</a> suggest <code>20pt</code> (<code>27px</code>) on desktops and <code>28pt</code> (<code>37px</code>) for touch devices
145+
In practice this seems to be much larger than many of their own applications use though.</p>
133146
</td>
134147
</tr>
135148
<tr>

lib/components/group/Group.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import type { RegisteredPanel } from "../panel/types";
1414
import type { RegisteredSeparator } from "../separator/types";
1515
import { GroupContext } from "./GroupContext";
1616
import { sortByElementOffset } from "./sortByElementOffset";
17-
import type { GroupProps, Layout, RegisteredGroup } from "./types";
17+
import type {
18+
GroupProps,
19+
Layout,
20+
RegisteredGroup,
21+
ResizeTargetMinimumSize
22+
} from "./types";
1823
import { useGroupImperativeHandle } from "./useGroupImperativeHandle";
1924

2025
/**
@@ -41,6 +46,10 @@ export function Group({
4146
onLayoutChange: onLayoutChangeUnstable,
4247
onLayoutChanged: onLayoutChangedUnstable,
4348
orientation = "horizontal",
49+
resizeTargetMinimumSize = {
50+
coarse: 20,
51+
fine: 10
52+
},
4453
style,
4554
...rest
4655
}: GroupProps) {
@@ -82,11 +91,13 @@ export function Group({
8291
lastExpandedPanelSizes: { [panelIds: string]: number };
8392
layouts: { [panelIds: string]: Layout };
8493
panels: RegisteredPanel[];
94+
resizeTargetMinimumSize: ResizeTargetMinimumSize;
8595
separators: RegisteredSeparator[];
8696
}>({
8797
lastExpandedPanelSizes: {},
8898
layouts: {},
8999
panels: [],
100+
resizeTargetMinimumSize,
90101
separators: []
91102
});
92103

@@ -199,6 +210,7 @@ export function Group({
199210
inMemoryLayouts: inMemoryValuesRef.current.layouts,
200211
orientation,
201212
panels: inMemoryValues.panels,
213+
resizeTargetMinimumSize: inMemoryValues.resizeTargetMinimumSize,
202214
separators: inMemoryValues.separators
203215
};
204216

lib/components/group/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export type DragState = {
2222
separatorId: string | undefined;
2323
};
2424

25+
export type ResizeTargetMinimumSize = {
26+
coarse: 20;
27+
fine: 10;
28+
};
29+
2530
export type RegisteredGroup = {
2631
defaultLayout: Layout | undefined;
2732
disableCursor: boolean;
@@ -38,6 +43,7 @@ export type RegisteredGroup = {
3843
};
3944
orientation: Orientation;
4045
panels: RegisteredPanel[];
46+
resizeTargetMinimumSize: ResizeTargetMinimumSize;
4147
separators: RegisteredSeparator[];
4248
};
4349

@@ -140,6 +146,22 @@ export type GroupProps = HTMLAttributes<HTMLDivElement> & {
140146
*/
141147
onLayoutChanged?: (layout: Layout) => void | undefined;
142148

149+
/**
150+
* Minimum size of the resizable hit target area (either `Separator` or `Panel` edge)
151+
* This threshold ensures are large enough to avoid mis-clicks.
152+
*
153+
* - Coarse inputs (typically a finger on a touchscreen) have reduced accuracy;
154+
* to ensure accessibility and ease of use, hit targets should be larger to prevent mis-clicks.
155+
* - Fine inputs (typically a mouse) can be smaller
156+
*
157+
* ℹ️ [Apple interface guidelines](https://developer.apple.com/design/human-interface-guidelines/accessibility) suggest `20pt` (`27px`) on desktops and `28pt` (`37px`) for touch devices
158+
* In practice this seems to be much larger than many of their own applications use though.
159+
*/
160+
resizeTargetMinimumSize?: {
161+
coarse: 20;
162+
fine: 10;
163+
};
164+
143165
/**
144166
* Specifies the resizable orientation ("horizontal" or "vertical"); defaults to "horizontal"
145167
*/

lib/global/dom/calculateHitRegions.test.ts

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe("calculateHitRegions", () => {
4141
"group-1-left",
4242
"group-1-right"
4343
],
44-
"rect": "36.5,0 27 x 50"
44+
"rect": "45,0 10 x 50"
4545
}
4646
]"
4747
`);
@@ -60,14 +60,14 @@ describe("calculateHitRegions", () => {
6060
"group-1-left",
6161
"group-1-center"
6262
],
63-
"rect": "26.5,0 27 x 50"
63+
"rect": "35,0 10 x 50"
6464
},
6565
{
6666
"panels": [
6767
"group-1-center",
6868
"group-1-right"
6969
],
70-
"rect": "66.5,0 27 x 50"
70+
"rect": "75,0 10 x 50"
7171
}
7272
]"
7373
`);
@@ -88,15 +88,15 @@ describe("calculateHitRegions", () => {
8888
"group-1-left",
8989
"group-1-center"
9090
],
91-
"rect": "31.5,0 27 x 50",
91+
"rect": "40,0 10 x 50",
9292
"separator": "group-1-left"
9393
},
9494
{
9595
"panels": [
9696
"group-1-center",
9797
"group-1-right"
9898
],
99-
"rect": "81.5,0 27 x 50",
99+
"rect": "90,0 10 x 50",
100100
"separator": "group-1-right"
101101
}
102102
]"
@@ -117,14 +117,14 @@ describe("calculateHitRegions", () => {
117117
"group-1-a",
118118
"group-1-b"
119119
],
120-
"rect": "26.5,0 27 x 50"
120+
"rect": "35,0 10 x 50"
121121
},
122122
{
123123
"panels": [
124124
"group-1-b",
125125
"group-1-c"
126126
],
127-
"rect": "69,0 27 x 50",
127+
"rect": "77.5,0 10 x 50",
128128
"separator": "group-1-separator"
129129
}
130130
]"
@@ -148,38 +148,38 @@ describe("calculateHitRegions", () => {
148148
"group-1-a",
149149
"group-1-b"
150150
],
151-
"rect": "46.5,0 27 x 50"
151+
"rect": "55,0 10 x 50"
152152
},
153153
{
154154
"panels": [
155155
"group-1-b",
156156
"group-1-c"
157157
],
158-
"rect": "96.5,0 27 x 50"
158+
"rect": "105,0 10 x 50"
159159
},
160160
{
161161
"panels": [
162162
"group-1-b",
163163
"group-1-c"
164164
],
165-
"rect": "106.5,0 27 x 50"
165+
"rect": "115,0 10 x 50"
166166
},
167167
{
168168
"panels": [
169169
"group-1-c",
170170
"group-1-d"
171171
],
172-
"rect": "156.5,0 27 x 50"
172+
"rect": "165,0 10 x 50"
173173
}
174174
]"
175175
`);
176176
});
177177

178178
test("CSS styles (e.g. padding and flex gap)", () => {
179-
const group = mockGroup(new DOMRect(0, 0, 155, 50));
180-
group.addPanel(new DOMRect(5, 5, 45, 40), "left");
181-
group.addPanel(new DOMRect(55, 5, 45, 40), "center");
182-
group.addPanel(new DOMRect(105, 5, 45, 40), "right");
179+
const group = mockGroup(new DOMRect(0, 0, 190, 70));
180+
group.addPanel(new DOMRect(10, 10, 50, 40), "left");
181+
group.addPanel(new DOMRect(70, 10, 50, 40), "center");
182+
group.addPanel(new DOMRect(130, 10, 50, 40), "right");
183183

184184
expect(serialize(group)).toMatchInlineSnapshot(`
185185
"[
@@ -188,14 +188,14 @@ describe("calculateHitRegions", () => {
188188
"group-1-left",
189189
"group-1-center"
190190
],
191-
"rect": "39,5 27 x 40"
191+
"rect": "60,10 10 x 40"
192192
},
193193
{
194194
"panels": [
195195
"group-1-center",
196196
"group-1-right"
197197
],
198-
"rect": "89,5 27 x 40"
198+
"rect": "120,10 10 x 40"
199199
}
200200
]"
201201
`);
@@ -216,28 +216,28 @@ describe("calculateHitRegions", () => {
216216
"group-1-left",
217217
"group-1-center"
218218
],
219-
"rect": "36.5,0 27 x 50"
219+
"rect": "45,0 10 x 50"
220220
},
221221
{
222222
"panels": [
223223
"group-1-center",
224224
"group-1-right"
225225
],
226-
"rect": "86.5,0 27 x 50"
226+
"rect": "95,0 10 x 50"
227227
}
228228
]"
229229
`);
230230
});
231231

232232
// Test covers conditionally rendered panels and separators
233233
test("should sort elements and separators by offset", () => {
234-
const group = mockGroup(new DOMRect(0, 0, 270, 50));
235-
group.addPanel(new DOMRect(205, 0, 65, 50), "d");
236-
group.addPanel(new DOMRect(70, 0, 65, 50), "b");
237-
group.addPanel(new DOMRect(0, 0, 65, 50), "a");
238-
group.addPanel(new DOMRect(135, 0, 65, 50), "c");
239-
group.addSeparator(new DOMRect(200, 0, 5, 50), "right");
240-
group.addSeparator(new DOMRect(65, 0, 5, 50), "left");
234+
const group = mockGroup(new DOMRect(0, 0, 260, 50));
235+
group.addPanel(new DOMRect(200, 0, 60, 50), "d");
236+
group.addPanel(new DOMRect(70, 0, 60, 50), "b");
237+
group.addPanel(new DOMRect(0, 0, 60, 50), "a");
238+
group.addPanel(new DOMRect(130, 0, 60, 50), "c");
239+
group.addSeparator(new DOMRect(190, 0, 10, 50), "right");
240+
group.addSeparator(new DOMRect(60, 0, 10, 50), "left");
241241

242242
expect(serialize(group)).toMatchInlineSnapshot(`
243243
"[
@@ -246,22 +246,22 @@ describe("calculateHitRegions", () => {
246246
"group-1-a",
247247
"group-1-b"
248248
],
249-
"rect": "54,0 27 x 50",
249+
"rect": "60,0 10 x 50",
250250
"separator": "group-1-left"
251251
},
252252
{
253253
"panels": [
254254
"group-1-b",
255255
"group-1-c"
256256
],
257-
"rect": "121.5,0 27 x 50"
257+
"rect": "125,0 10 x 50"
258258
},
259259
{
260260
"panels": [
261261
"group-1-c",
262262
"group-1-d"
263263
],
264-
"rect": "189,0 27 x 50",
264+
"rect": "190,0 10 x 50",
265265
"separator": "group-1-right"
266266
}
267267
]"

lib/global/dom/calculateHitRegions.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,9 @@ export function calculateHitRegions(group: RegisteredGroup) {
129129
? rectOrSeparator
130130
: rectOrSeparator.element.getBoundingClientRect();
131131

132-
// Ensure that Separators or Panel "edges" have large enough hit areas to be interacted with easily
133-
// Apple interface guidelines suggest 20pt (27) on desktops and 28pt (37px) for touch devices
134-
// https://developer.apple.com/design/human-interface-guidelines/accessibility
135-
const minHitTargetSize = isCoarsePointer() ? 37 : 27;
132+
const minHitTargetSize = isCoarsePointer()
133+
? group.resizeTargetMinimumSize.coarse
134+
: group.resizeTargetMinimumSize.fine;
136135
if (rect.width < minHitTargetSize) {
137136
const delta = minHitTargetSize - rect.width;
138137
rect = new DOMRect(

lib/global/event-handlers/onDocumentDoubleClick.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { RegisteredGroup } from "../../components/group/types";
2-
import type { RegisteredPanel } from "../../components/panel/types";
31
import { read } from "../mutableState";
42
import { findMatchingHitRegions } from "../utils/findMatchingHitRegions";
53
import { getImperativePanelMethods } from "../utils/getImperativePanelMethods";
@@ -12,16 +10,7 @@ export function onDocumentDoubleClick(event: MouseEvent) {
1210
const { mountedGroups } = read();
1311

1412
const hitRegions = findMatchingHitRegions(event, mountedGroups);
15-
16-
const groups = new Set<RegisteredGroup>();
17-
const panels = new Set<RegisteredPanel>();
18-
1913
hitRegions.forEach((current) => {
20-
groups.add(current.group);
21-
current.panels.forEach((panel) => {
22-
panels.add(panel);
23-
});
24-
2514
if (current.separator) {
2615
const panelWithDefaultSize = current.panels.find(
2716
(panel) => panel.panelConstraints.defaultSize !== undefined

lib/global/test/mockGroup.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { vi } from "vitest";
22
import type {
33
Orientation,
4-
RegisteredGroup
4+
RegisteredGroup,
5+
ResizeTargetMinimumSize
56
} from "../../components/group/types";
67
import type {
78
PanelConstraintProps,
@@ -57,6 +58,10 @@ export function mockGroup(
5758
inMemoryLastExpandedPanelSizes: {},
5859
inMemoryLayouts: {},
5960
orientation,
61+
resizeTargetMinimumSize: {
62+
coarse: 20,
63+
fine: 10
64+
} satisfies ResizeTargetMinimumSize,
6065

6166
get panels() {
6267
return Array.from(mockPanels.values());

lib/global/utils/findMatchingHitRegions.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe("findMatchingHitRegions", () => {
4242
"group-1-left",
4343
"group-1-right"
4444
],
45-
"rect": "36.5,0 27 x 50"
45+
"rect": "45,0 10 x 50"
4646
}
4747
]"
4848
`);
@@ -63,7 +63,7 @@ describe("findMatchingHitRegions", () => {
6363
"group-1-left",
6464
"group-1-right"
6565
],
66-
"rect": "46.5,0 27 x 50",
66+
"rect": "50,0 20 x 50",
6767
"separator": "group-1-separator"
6868
}
6969
]"
@@ -93,14 +93,14 @@ describe("findMatchingHitRegions", () => {
9393
"group-1-left",
9494
"group-1-right"
9595
],
96-
"rect": "36.5,0 27 x 50"
96+
"rect": "45,0 10 x 50"
9797
},
9898
{
9999
"panels": [
100100
"group-2-top",
101101
"group-2-bottom"
102102
],
103-
"rect": "0,11.5 50 x 27"
103+
"rect": "0,20 50 x 10"
104104
}
105105
]"
106106
`);

0 commit comments

Comments
 (0)