@astryxdesign/core 0.7.8-canary.fbb81c6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/dist/ClickableCard/ClickableCard.d.ts +1 -1
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +1 -5
- package/dist/DateInput/PickerWheel.d.ts.map +1 -1
- package/dist/DateInput/PickerWheel.js +1 -7
- package/dist/DropdownMenu/DropdownMenu.d.ts +1 -9
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +4 -9
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +2 -3
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +2 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +2 -3
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +2 -3
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +1 -6
- package/dist/Layer/preserveAnimations.d.ts +5 -1
- package/dist/Layer/preserveAnimations.d.ts.map +1 -1
- package/dist/Layer/preserveAnimations.js +214 -12
- package/dist/MoreMenu/MoreMenu.d.ts +3 -5
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.d.ts +1 -1
- package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.js +1 -5
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +1 -3
- package/dist/astryx.css +2 -6
- package/dist/hooks/menuPressGesture.d.ts +21 -132
- package/dist/hooks/menuPressGesture.d.ts.map +1 -1
- package/dist/hooks/menuPressGesture.js +30 -155
- package/dist/hooks/useClickableContainer.d.ts +5 -19
- package/dist/hooks/useClickableContainer.d.ts.map +1 -1
- package/dist/hooks/useClickableContainer.js +5 -28
- package/dist/hooks/useMenuPress.d.ts +1 -34
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +5 -93
- package/dist/motion/transientCopy.d.ts +2 -4
- package/dist/motion/transientCopy.d.ts.map +1 -1
- package/dist/motion/transientCopy.js +5 -34
- package/dist/motion/uikit/gestures.d.ts +0 -41
- package/dist/motion/uikit/gestures.d.ts.map +1 -1
- package/dist/motion/uikit/gestures.js +0 -25
- package/dist/motion/uikit/index.d.ts +1 -1
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +1 -1
- package/dist/utils/pressFeedback.d.ts +0 -7
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +1 -16
- package/package.json +5 -5
- package/src/ClickableCard/ClickableCard.tsx +1 -8
- package/src/DateInput/PickerWheel.tsx +2 -7
- package/src/DropdownMenu/DropdownMenu.doc.mjs +2 -1
- package/src/DropdownMenu/DropdownMenu.tsx +5 -29
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -5
- package/src/DropdownMenu/DropdownMenuItem.tsx +4 -8
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -5
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +1 -5
- package/src/Item/Item.tsx +5 -15
- package/src/Layer/preserveAnimations.ts +275 -12
- package/src/MoreMenu/MoreMenu.doc.mjs +4 -2
- package/src/MoreMenu/MoreMenu.tsx +3 -5
- package/src/SelectableCard/SelectableCard.tsx +1 -8
- package/src/TreeList/TreeListItem.tsx +6 -8
- package/src/hooks/menuPressGesture.ts +38 -243
- package/src/hooks/useClickableContainer.doc.mjs +2 -8
- package/src/hooks/useClickableContainer.ts +6 -52
- package/src/hooks/useMenuPress.ts +11 -118
- package/src/motion/MEASUREMENTS.md +0 -11
- package/src/motion/transientCopy.ts +5 -45
- package/src/motion/uikit/gestures.ts +0 -22
- package/src/motion/uikit/index.ts +0 -1
- package/src/utils/pressFeedback.ts +1 -16
- package/dist/hooks/menuPressValues.d.ts +0 -16
- package/dist/hooks/menuPressValues.d.ts.map +0 -1
- package/dist/hooks/menuPressValues.js +0 -18
- package/dist/motion/carryAnimations.d.ts +0 -58
- package/dist/motion/carryAnimations.d.ts.map +0 -1
- package/dist/motion/carryAnimations.js +0 -305
- package/dist/privateAttributes/menuPressLit.stylex.d.ts +0 -52
- package/dist/privateAttributes/menuPressLit.stylex.d.ts.map +0 -1
- package/dist/privateAttributes/menuPressLit.stylex.js +0 -99
- package/src/hooks/menuPressValues.ts +0 -18
- package/src/motion/carryAnimations.ts +0 -389
- package/src/privateAttributes/menuPressLit.stylex.ts +0 -104
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file menuPressGesture.ts
|
|
5
5
|
* @input A menu press gesture and one pointer event, both as plain data
|
|
6
6
|
* @output The pure `menuPressStep` state machine, its state, event and effect
|
|
7
|
-
* types, the settle constant
|
|
7
|
+
* types, and the settle constant
|
|
8
8
|
* @position Pure module under `useMenuPress`. It has no DOM and no React, so
|
|
9
9
|
* a unit test can check each transition without a browser.
|
|
10
10
|
*
|
|
@@ -13,24 +13,12 @@
|
|
|
13
13
|
* - /packages/core/src/hooks/useMenuPress.ts
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* How long the opening press must last before its release may act, unless a
|
|
20
|
-
* finger's handoff applies.
|
|
21
|
-
*/
|
|
16
|
+
/** How long the opening press must last before its release may act. */
|
|
22
17
|
export const MENU_PRESS_SETTLE_MS = 300;
|
|
23
18
|
|
|
24
|
-
/**
|
|
25
|
-
* How far a finger that opened the menu travels, from the first sample the
|
|
26
|
-
* menu sees, before it starts picking (UIMenu's scrub handoff).
|
|
27
|
-
*/
|
|
28
|
-
export const MENU_PRESS_HANDOFF_PX: number = UIKIT_MENU_PRESS_HANDOFF_PX;
|
|
29
|
-
|
|
30
19
|
export type MenuPressPointerType = 'mouse' | 'touch' | 'pen';
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
export type MenuPressState<T> =
|
|
21
|
+
export type MenuPressGesture<T> =
|
|
34
22
|
| {phase: 'idle'}
|
|
35
23
|
| {
|
|
36
24
|
phase: 'triggerPress';
|
|
@@ -41,12 +29,6 @@ export type MenuPressState<T> =
|
|
|
41
29
|
phase: 'open';
|
|
42
30
|
pointerType: MenuPressPointerType;
|
|
43
31
|
openedAt: number;
|
|
44
|
-
/**
|
|
45
|
-
* A finger's handoff: the first position the menu saw after it opened,
|
|
46
|
-
* which the scrub distance is counted from. Unset until that sample.
|
|
47
|
-
* @internal Not public contract.
|
|
48
|
-
*/
|
|
49
|
-
handoffFrom?: {x: number; y: number};
|
|
50
32
|
}
|
|
51
33
|
| {
|
|
52
34
|
phase: 'tracking';
|
|
@@ -57,8 +39,7 @@ export type MenuPressState<T> =
|
|
|
57
39
|
row: T | null;
|
|
58
40
|
};
|
|
59
41
|
|
|
60
|
-
|
|
61
|
-
export type MenuPressInput<T> =
|
|
42
|
+
export type MenuPressEvent<T> =
|
|
62
43
|
| {
|
|
63
44
|
type: 'down';
|
|
64
45
|
target: 'trigger' | 'menu';
|
|
@@ -69,38 +50,18 @@ export type MenuPressInput<T> =
|
|
|
69
50
|
}
|
|
70
51
|
/** The menu opened while the trigger press is still held. */
|
|
71
52
|
| {type: 'opened'; time: number}
|
|
72
|
-
| {
|
|
73
|
-
type: 'move';
|
|
74
|
-
row: T | null;
|
|
75
|
-
isInMenu: boolean;
|
|
76
|
-
time: number;
|
|
77
|
-
/**
|
|
78
|
-
* Viewport position, for a finger's handoff distance.
|
|
79
|
-
* @internal Not public contract.
|
|
80
|
-
*/
|
|
81
|
-
x?: number;
|
|
82
|
-
/** @internal See `x`. */
|
|
83
|
-
y?: number;
|
|
84
|
-
}
|
|
53
|
+
| {type: 'move'; row: T | null; isInMenu: boolean; time: number}
|
|
85
54
|
| {
|
|
86
55
|
type: 'up';
|
|
87
56
|
row: T | null;
|
|
88
57
|
isInMenu: boolean;
|
|
89
58
|
isOnTrigger: boolean;
|
|
90
59
|
time: number;
|
|
91
|
-
/**
|
|
92
|
-
* Viewport position, for a finger's handoff distance.
|
|
93
|
-
* @internal Not public contract.
|
|
94
|
-
*/
|
|
95
|
-
x?: number;
|
|
96
|
-
/** @internal See `x`. */
|
|
97
|
-
y?: number;
|
|
98
60
|
}
|
|
99
61
|
/** The browser took the gesture: pointercancel, a second pointer, a scroll. */
|
|
100
62
|
| {type: 'cancel'};
|
|
101
63
|
|
|
102
|
-
|
|
103
|
-
export type MenuPressOutcome<T> =
|
|
64
|
+
export type MenuPressEffect<T> =
|
|
104
65
|
| {type: 'none'}
|
|
105
66
|
/** A mouse pressed the trigger: open the menu now, under the held pointer. */
|
|
106
67
|
| {type: 'open'}
|
|
@@ -110,52 +71,27 @@ export type MenuPressOutcome<T> =
|
|
|
110
71
|
/**
|
|
111
72
|
* The gesture ended without a row acting. `stray` says whether the browser
|
|
112
73
|
* will still report a `click` for this gesture that must not act; `dismiss`
|
|
113
|
-
* says the menu should close
|
|
74
|
+
* says the menu should close (a mouse released outside it).
|
|
114
75
|
*/
|
|
115
76
|
| {type: 'settle'; stray: boolean; dismiss: boolean};
|
|
116
77
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
effect: MenuPressOutcome<T>;
|
|
78
|
+
export interface MenuPressStep<T> {
|
|
79
|
+
gesture: MenuPressGesture<T>;
|
|
80
|
+
effect: MenuPressEffect<T>;
|
|
121
81
|
}
|
|
122
82
|
|
|
123
|
-
export const IDLE_MENU_PRESS:
|
|
83
|
+
export const IDLE_MENU_PRESS: MenuPressGesture<never> = {phase: 'idle'};
|
|
124
84
|
|
|
125
|
-
const NONE:
|
|
85
|
+
const NONE: MenuPressEffect<never> = {type: 'none'};
|
|
126
86
|
|
|
127
|
-
function idle<T>(effect:
|
|
87
|
+
function idle<T>(effect: MenuPressEffect<T>): MenuPressStep<T> {
|
|
128
88
|
return {gesture: {phase: 'idle'}, effect};
|
|
129
89
|
}
|
|
130
90
|
|
|
131
|
-
function highlightEffect<T>(row: T | null):
|
|
91
|
+
function highlightEffect<T>(row: T | null): MenuPressEffect<T> {
|
|
132
92
|
return row == null ? {type: 'clear'} : {type: 'highlight', row};
|
|
133
93
|
}
|
|
134
94
|
|
|
135
|
-
/** A finger or a pen: UIKit's touch rules apply. */
|
|
136
|
-
function isFinger(pointerType: MenuPressPointerType): boolean {
|
|
137
|
-
return pointerType !== 'mouse';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function hasPosition(event: {
|
|
141
|
-
x?: number;
|
|
142
|
-
y?: number;
|
|
143
|
-
}): event is {x: number; y: number} {
|
|
144
|
-
return event.x != null && event.y != null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Whether a finger has travelled the handoff distance from `from`. The
|
|
149
|
-
* distance is a straight line, as UIMenu measures it: a diagonal 7 pt each
|
|
150
|
-
* way does not hand off, 9 pt each way does.
|
|
151
|
-
*/
|
|
152
|
-
function hasPassedHandoff(
|
|
153
|
-
from: {x: number; y: number},
|
|
154
|
-
to: {x: number; y: number},
|
|
155
|
-
): boolean {
|
|
156
|
-
return Math.hypot(to.x - from.x, to.y - from.y) >= MENU_PRESS_HANDOFF_PX;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
95
|
/**
|
|
160
96
|
* Resolve a release while the highlight is tracking the pointer: over an
|
|
161
97
|
* enabled row it acts, over the menu's chrome it acts on nothing, outside the
|
|
@@ -164,8 +100,8 @@ function hasPassedHandoff(
|
|
|
164
100
|
*/
|
|
165
101
|
function release<T>(
|
|
166
102
|
pointerType: MenuPressPointerType,
|
|
167
|
-
event: Extract<
|
|
168
|
-
):
|
|
103
|
+
event: Extract<MenuPressEvent<T>, {type: 'up'}>,
|
|
104
|
+
): MenuPressStep<T> {
|
|
169
105
|
if (event.row != null) {
|
|
170
106
|
return idle({type: 'act', row: event.row});
|
|
171
107
|
}
|
|
@@ -183,15 +119,10 @@ function release<T>(
|
|
|
183
119
|
* - `idle`: no gesture is live.
|
|
184
120
|
* - `triggerPress`: a finger rests on the trigger, and the menu is not open
|
|
185
121
|
* yet. A mouse press on the trigger skips this state and opens at once.
|
|
186
|
-
* - `open`: the menu opened under a held pointer, and the pointer
|
|
187
|
-
*
|
|
122
|
+
* - `open`: the menu opened under a held pointer, and the pointer has not
|
|
123
|
+
* entered it yet.
|
|
188
124
|
* - `tracking`: the pointer is held, and the highlight follows it.
|
|
189
125
|
*
|
|
190
|
-
* A finger or a pen that opened the menu picks only after it travels
|
|
191
|
-
* `MENU_PRESS_HANDOFF_PX` from the first position the menu sees. UIMenu
|
|
192
|
-
* hands off the same way. Until then nothing lights, and its lift acts on
|
|
193
|
-
* nothing. A host that reports no positions keeps the settle rule.
|
|
194
|
-
*
|
|
195
126
|
* The settle rule: the release of the opening gesture acts only after the
|
|
196
127
|
* pointer entered the menu or the press lasted `MENU_PRESS_SETTLE_MS`. Else a
|
|
197
128
|
* menu that opens under the pointer would pick a row that nobody chose.
|
|
@@ -199,14 +130,11 @@ function release<T>(
|
|
|
199
130
|
* Rows are opaque (`T`). The hook feeds in the enabled row under the
|
|
200
131
|
* pointer, or `null` over a divider, a heading, a disabled row, or outside
|
|
201
132
|
* the menu.
|
|
202
|
-
*
|
|
203
|
-
* @internal The machine's own shape, with the finger's handoff; not public
|
|
204
|
-
* contract.
|
|
205
133
|
*/
|
|
206
|
-
export function
|
|
207
|
-
gesture:
|
|
208
|
-
event:
|
|
209
|
-
):
|
|
134
|
+
export function menuPressStep<T>(
|
|
135
|
+
gesture: MenuPressGesture<T>,
|
|
136
|
+
event: MenuPressEvent<T>,
|
|
137
|
+
): MenuPressStep<T> {
|
|
210
138
|
switch (gesture.phase) {
|
|
211
139
|
case 'idle':
|
|
212
140
|
return stepIdle(gesture, event);
|
|
@@ -220,9 +148,9 @@ export function menuPressTransition<T>(
|
|
|
220
148
|
}
|
|
221
149
|
|
|
222
150
|
function stepIdle<T>(
|
|
223
|
-
gesture: Extract<
|
|
224
|
-
event:
|
|
225
|
-
):
|
|
151
|
+
gesture: Extract<MenuPressGesture<T>, {phase: 'idle'}>,
|
|
152
|
+
event: MenuPressEvent<T>,
|
|
153
|
+
): MenuPressStep<T> {
|
|
226
154
|
if (event.type !== 'down') {
|
|
227
155
|
return {gesture, effect: NONE};
|
|
228
156
|
}
|
|
@@ -258,9 +186,9 @@ function stepIdle<T>(
|
|
|
258
186
|
}
|
|
259
187
|
|
|
260
188
|
function stepTriggerPress<T>(
|
|
261
|
-
gesture: Extract<
|
|
262
|
-
event:
|
|
263
|
-
):
|
|
189
|
+
gesture: Extract<MenuPressGesture<T>, {phase: 'triggerPress'}>,
|
|
190
|
+
event: MenuPressEvent<T>,
|
|
191
|
+
): MenuPressStep<T> {
|
|
264
192
|
switch (event.type) {
|
|
265
193
|
case 'opened':
|
|
266
194
|
return {
|
|
@@ -288,34 +216,11 @@ function stepTriggerPress<T>(
|
|
|
288
216
|
}
|
|
289
217
|
|
|
290
218
|
function stepOpen<T>(
|
|
291
|
-
gesture: Extract<
|
|
292
|
-
event:
|
|
293
|
-
):
|
|
219
|
+
gesture: Extract<MenuPressGesture<T>, {phase: 'open'}>,
|
|
220
|
+
event: MenuPressEvent<T>,
|
|
221
|
+
): MenuPressStep<T> {
|
|
294
222
|
switch (event.type) {
|
|
295
|
-
case 'move':
|
|
296
|
-
if (isFinger(gesture.pointerType) && hasPosition(event)) {
|
|
297
|
-
// UIMenu counts from the first sample after the open, not from the
|
|
298
|
-
// open point.
|
|
299
|
-
const from = gesture.handoffFrom;
|
|
300
|
-
if (from == null) {
|
|
301
|
-
return {
|
|
302
|
-
gesture: {...gesture, handoffFrom: {x: event.x, y: event.y}},
|
|
303
|
-
effect: NONE,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
if (!hasPassedHandoff(from, event)) {
|
|
307
|
-
return {gesture, effect: NONE};
|
|
308
|
-
}
|
|
309
|
-
return {
|
|
310
|
-
gesture: {
|
|
311
|
-
phase: 'tracking',
|
|
312
|
-
pointerType: gesture.pointerType,
|
|
313
|
-
origin: 'trigger',
|
|
314
|
-
row: event.row,
|
|
315
|
-
},
|
|
316
|
-
effect: highlightEffect(event.row),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
223
|
+
case 'move':
|
|
319
224
|
if (!event.isInMenu) {
|
|
320
225
|
return {gesture, effect: NONE};
|
|
321
226
|
}
|
|
@@ -328,14 +233,7 @@ function stepOpen<T>(
|
|
|
328
233
|
},
|
|
329
234
|
effect: highlightEffect(event.row),
|
|
330
235
|
};
|
|
331
|
-
}
|
|
332
236
|
case 'up': {
|
|
333
|
-
if (isFinger(gesture.pointerType) && hasPosition(event)) {
|
|
334
|
-
// The finger never travelled the handoff distance, so the row under it
|
|
335
|
-
// appeared under a press nobody moved. Nothing acts, the menu stays,
|
|
336
|
-
// and the trigger's click for this press is spent.
|
|
337
|
-
return idle({type: 'settle', stray: true, dismiss: false});
|
|
338
|
-
}
|
|
339
237
|
const hasSettled = event.time - gesture.openedAt >= MENU_PRESS_SETTLE_MS;
|
|
340
238
|
if (!hasSettled) {
|
|
341
239
|
// The pointer never entered the menu and the press was short: the
|
|
@@ -347,7 +245,7 @@ function stepOpen<T>(
|
|
|
347
245
|
}
|
|
348
246
|
case 'cancel':
|
|
349
247
|
// As above: the gesture ends, so its document listeners come off. The
|
|
350
|
-
// menu stays open
|
|
248
|
+
// menu stays open — a cancelled pointer did not ask to close it.
|
|
351
249
|
return idle({type: 'settle', stray: false, dismiss: false});
|
|
352
250
|
case 'down':
|
|
353
251
|
case 'opened':
|
|
@@ -356,11 +254,11 @@ function stepOpen<T>(
|
|
|
356
254
|
}
|
|
357
255
|
|
|
358
256
|
function stepTracking<T>(
|
|
359
|
-
gesture: Extract<
|
|
360
|
-
event:
|
|
361
|
-
):
|
|
257
|
+
gesture: Extract<MenuPressGesture<T>, {phase: 'tracking'}>,
|
|
258
|
+
event: MenuPressEvent<T>,
|
|
259
|
+
): MenuPressStep<T> {
|
|
362
260
|
switch (event.type) {
|
|
363
|
-
case 'move':
|
|
261
|
+
case 'move': {
|
|
364
262
|
if (event.row === gesture.row) {
|
|
365
263
|
return {gesture, effect: NONE};
|
|
366
264
|
}
|
|
@@ -368,6 +266,7 @@ function stepTracking<T>(
|
|
|
368
266
|
gesture: {...gesture, row: event.row},
|
|
369
267
|
effect: highlightEffect(event.row),
|
|
370
268
|
};
|
|
269
|
+
}
|
|
371
270
|
case 'up':
|
|
372
271
|
return release(gesture.pointerType, event);
|
|
373
272
|
case 'cancel':
|
|
@@ -379,107 +278,3 @@ function stepTracking<T>(
|
|
|
379
278
|
return {gesture, effect: NONE};
|
|
380
279
|
}
|
|
381
280
|
}
|
|
382
|
-
|
|
383
|
-
// ---------------------------------------------------------------------------
|
|
384
|
-
// The released public shape (`@astryxdesign/core/hooks`). The handoff
|
|
385
|
-
// positions and `handoffFrom` stay internal: a caller of
|
|
386
|
-
// `menuPressStep` keeps the settle rule, as released.
|
|
387
|
-
// ---------------------------------------------------------------------------
|
|
388
|
-
|
|
389
|
-
export type MenuPressGesture<T> =
|
|
390
|
-
| {phase: 'idle'}
|
|
391
|
-
| {
|
|
392
|
-
phase: 'triggerPress';
|
|
393
|
-
pointerType: MenuPressPointerType;
|
|
394
|
-
startedAt: number;
|
|
395
|
-
}
|
|
396
|
-
| {
|
|
397
|
-
phase: 'open';
|
|
398
|
-
pointerType: MenuPressPointerType;
|
|
399
|
-
openedAt: number;
|
|
400
|
-
}
|
|
401
|
-
| {
|
|
402
|
-
phase: 'tracking';
|
|
403
|
-
pointerType: MenuPressPointerType;
|
|
404
|
-
/** Where the press began. */
|
|
405
|
-
origin: 'trigger' | 'menu';
|
|
406
|
-
/** The enabled row currently under the pointer, if any. */
|
|
407
|
-
row: T | null;
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
export type MenuPressEvent<T> =
|
|
411
|
-
| {
|
|
412
|
-
type: 'down';
|
|
413
|
-
target: 'trigger' | 'menu';
|
|
414
|
-
pointerType: MenuPressPointerType;
|
|
415
|
-
/** The enabled row under the press when it lands inside the menu. */
|
|
416
|
-
row: T | null;
|
|
417
|
-
time: number;
|
|
418
|
-
}
|
|
419
|
-
/** The menu opened while the trigger press is still held. */
|
|
420
|
-
| {type: 'opened'; time: number}
|
|
421
|
-
| {type: 'move'; row: T | null; isInMenu: boolean; time: number}
|
|
422
|
-
| {
|
|
423
|
-
type: 'up';
|
|
424
|
-
row: T | null;
|
|
425
|
-
isInMenu: boolean;
|
|
426
|
-
isOnTrigger: boolean;
|
|
427
|
-
time: number;
|
|
428
|
-
}
|
|
429
|
-
/** The browser took the gesture: pointercancel, a second pointer, a scroll. */
|
|
430
|
-
| {type: 'cancel'};
|
|
431
|
-
|
|
432
|
-
export type MenuPressEffect<T> =
|
|
433
|
-
| {type: 'none'}
|
|
434
|
-
/** A mouse pressed the trigger: open the menu now, under the held pointer. */
|
|
435
|
-
| {type: 'open'}
|
|
436
|
-
| {type: 'highlight'; row: T}
|
|
437
|
-
| {type: 'clear'}
|
|
438
|
-
| {type: 'act'; row: T}
|
|
439
|
-
/**
|
|
440
|
-
* The gesture ended without a row acting. `stray` says whether the browser
|
|
441
|
-
* will still report a `click` for this gesture that must not act; `dismiss`
|
|
442
|
-
* says the menu should close (a mouse released outside it).
|
|
443
|
-
*/
|
|
444
|
-
| {type: 'settle'; stray: boolean; dismiss: boolean};
|
|
445
|
-
|
|
446
|
-
export interface MenuPressStep<T> {
|
|
447
|
-
gesture: MenuPressGesture<T>;
|
|
448
|
-
effect: MenuPressEffect<T>;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* The menu press machine, as released: one step from a gesture and an event
|
|
453
|
-
* to the next gesture and the effect to apply. Pure.
|
|
454
|
-
*/
|
|
455
|
-
export function menuPressStep<T>(
|
|
456
|
-
gesture: MenuPressGesture<T>,
|
|
457
|
-
event: MenuPressEvent<T>,
|
|
458
|
-
): MenuPressStep<T> {
|
|
459
|
-
// The released machine keeps the settle rule. Positions start the handoff,
|
|
460
|
-
// so a caller's event reaches the transition without them.
|
|
461
|
-
const {
|
|
462
|
-
x: _x,
|
|
463
|
-
y: _y,
|
|
464
|
-
...released
|
|
465
|
-
} = event as MenuPressEvent<T> & {x?: number; y?: number};
|
|
466
|
-
const {gesture: next, effect} = menuPressTransition(
|
|
467
|
-
gesture,
|
|
468
|
-
released as MenuPressEvent<T>,
|
|
469
|
-
);
|
|
470
|
-
return {
|
|
471
|
-
gesture: publicGesture(next),
|
|
472
|
-
effect,
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function publicGesture<T>(gesture: MenuPressState<T>): MenuPressGesture<T> {
|
|
477
|
-
if (gesture.phase !== 'open') {
|
|
478
|
-
return gesture;
|
|
479
|
-
}
|
|
480
|
-
return {
|
|
481
|
-
phase: 'open',
|
|
482
|
-
pointerType: gesture.pointerType,
|
|
483
|
-
openedAt: gesture.openedAt,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
@@ -61,17 +61,12 @@ export const docs = {
|
|
|
61
61
|
type: '(event: MouseEvent<HTMLElement>) => void',
|
|
62
62
|
description: 'Mouse up handler to attach to the container (handles middle-click navigation for href).',
|
|
63
63
|
},
|
|
64
|
-
{
|
|
65
|
-
name: 'onPointerDown',
|
|
66
|
-
type: '(event: PointerEvent<HTMLElement>) => void',
|
|
67
|
-
description: 'Pointer down handler to attach to the container. On a container that paints its press with usePressFeedback, it drops a touch press that starts on a nested interactive element, which does not activate the container. A mouse :active press paint is unchanged. Without it, the container keeps its click rules and its pressed state for every press.',
|
|
68
|
-
},
|
|
69
64
|
],
|
|
70
65
|
usage: {
|
|
71
66
|
description:
|
|
72
67
|
'Makes a container element clickable while preserving nested interactive element behavior. Solves the "nested interactive elements" problem: when a card is clickable but contains buttons/links, clicking those should NOT trigger the card\'s action. Detects interactive ancestors between the click target and the container, and ignores text selections. Supports href navigation (including middle-click and Ctrl/Cmd+click for new tabs).',
|
|
73
68
|
bestPractices: [
|
|
74
|
-
{ guidance: true, description: 'Attach onClick
|
|
69
|
+
{ guidance: true, description: 'Attach both onClick and onMouseUp to the container element for full click handling including middle-click.' },
|
|
75
70
|
{ guidance: true, description: 'Use inside ClickableCard or SelectableCard for the standard card interaction pattern.' },
|
|
76
71
|
{ guidance: false, description: 'Use when the entire container is a single interactive element; just use a <button> or <a> directly.' },
|
|
77
72
|
],
|
|
@@ -98,13 +93,12 @@ export const docsDense = {
|
|
|
98
93
|
returnDescriptions: {
|
|
99
94
|
onClick: 'click handler for container element.',
|
|
100
95
|
onMouseUp: 'mouse up handler for container (handles middle-click navigation for href).',
|
|
101
|
-
onPointerDown: 'pointer down handler for container: drops usePressFeedback touch press starting on nested interactive element; mouse :active paint unchanged.',
|
|
102
96
|
},
|
|
103
97
|
usage: {
|
|
104
98
|
description:
|
|
105
99
|
'Makes container element clickable while preserving nested interactive element behavior. Solves "nested interactive elements" problem: when card is clickable but contains buttons/links, clicking those must NOT trigger card\'s action. Detects interactive ancestors between click target + container, ignores text selections. Supports href navigation (incl. middle-click + Ctrl/Cmd+click for new tabs).',
|
|
106
100
|
bestPractices: [
|
|
107
|
-
{ guidance: true, description: 'Attach onClick + onMouseUp
|
|
101
|
+
{ guidance: true, description: 'Attach both onClick + onMouseUp to container element for full click handling incl. middle-click.' },
|
|
108
102
|
{ guidance: true, description: 'Use inside ClickableCard / SelectableCard for standard card interaction pattern.' },
|
|
109
103
|
{ guidance: false, description: 'Use when entire container is single interactive element; just use <button> / <a> directly.' },
|
|
110
104
|
],
|
|
@@ -5,28 +5,19 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useClickableContainer.ts
|
|
7
7
|
* @input Container ref, interactive element ref, click/href handlers
|
|
8
|
-
* @output onClick
|
|
9
|
-
* INTERACTIVE_SELECTORS list;
|
|
8
|
+
* @output onClick and onMouseUp handlers for the container; INTERACTIVE_SELECTORS list;
|
|
10
9
|
* hasInteractiveAncestor and the shared innermostInteractive walk
|
|
11
10
|
* @position Core hook for clickable containers with nested interactive
|
|
12
11
|
* elements. A click on a nested button or link does not run the card's
|
|
13
|
-
* action
|
|
14
|
-
* passes the rule in utils/safeUrl.ts first.
|
|
12
|
+
* action. Each navigation passes the rule in utils/safeUrl.ts first.
|
|
15
13
|
*
|
|
16
14
|
* SYNC: When modified, update:
|
|
17
15
|
* - /packages/core/src/hooks/index.ts (export)
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
|
-
import {
|
|
21
|
-
useCallback,
|
|
22
|
-
useEffect,
|
|
23
|
-
type RefObject,
|
|
24
|
-
type MouseEvent,
|
|
25
|
-
type PointerEvent,
|
|
26
|
-
} from 'react';
|
|
18
|
+
import {useCallback, useEffect, type RefObject, type MouseEvent} from 'react';
|
|
27
19
|
import {isLayerSurface} from '../Layer/layerSurfacePress';
|
|
28
20
|
import {isSafeUrl} from '../utils/safeUrl';
|
|
29
|
-
import {cancelPress} from '../utils/pressFeedback';
|
|
30
21
|
|
|
31
22
|
/**
|
|
32
23
|
* The attribute this hook sets on the container it makes clickable. A click
|
|
@@ -168,14 +159,6 @@ export interface UseClickableContainerOptions {
|
|
|
168
159
|
export interface ClickableContainerResult {
|
|
169
160
|
onClick: (event: MouseEvent<HTMLElement>) => void;
|
|
170
161
|
onMouseUp: (event: MouseEvent<HTMLElement>) => void;
|
|
171
|
-
/**
|
|
172
|
-
* Spread on the container with `onClick`. A press that starts on a nested
|
|
173
|
-
* interactive element does not activate the container. On a container that
|
|
174
|
-
* paints its press with `usePressFeedback`, this drops that touch press.
|
|
175
|
-
* A mouse `:active` press paint is unchanged. A container without it keeps
|
|
176
|
-
* its activation rule and shows its pressed state for every press.
|
|
177
|
-
*/
|
|
178
|
-
onPointerDown: (event: PointerEvent<HTMLElement>) => void;
|
|
179
162
|
}
|
|
180
163
|
|
|
181
164
|
/**
|
|
@@ -193,16 +176,12 @@ export interface ClickableContainerResult {
|
|
|
193
176
|
* @example
|
|
194
177
|
* ```
|
|
195
178
|
* const containerRef = useRef<HTMLDivElement>(null);
|
|
196
|
-
* const { onClick, onMouseUp
|
|
179
|
+
* const { onClick, onMouseUp } = useClickableContainer({
|
|
197
180
|
* containerRef,
|
|
198
181
|
* onClick: () => console.log('card clicked'),
|
|
199
182
|
* });
|
|
200
183
|
* return (
|
|
201
|
-
* <div
|
|
202
|
-
* ref={containerRef}
|
|
203
|
-
* onClick={onClick}
|
|
204
|
-
* onMouseUp={onMouseUp}
|
|
205
|
-
* onPointerDown={onPointerDown}>
|
|
184
|
+
* <div ref={containerRef} onClick={onClick} onMouseUp={onMouseUp}>
|
|
206
185
|
* <p>Click anywhere on this card</p>
|
|
207
186
|
* <button onClick={() => alert('button')}>Nested button</button>
|
|
208
187
|
* </div>
|
|
@@ -326,30 +305,5 @@ export function useClickableContainer({
|
|
|
326
305
|
[containerRef, href, disabled],
|
|
327
306
|
);
|
|
328
307
|
|
|
329
|
-
|
|
330
|
-
// that would not activate the container shows no press on it. A mouse or
|
|
331
|
-
// pen press keeps its `:active` paint.
|
|
332
|
-
const onPointerDown = useCallback(
|
|
333
|
-
(event: PointerEvent<HTMLElement>) => {
|
|
334
|
-
const containerEl = containerRef.current;
|
|
335
|
-
const eventTarget = event.target;
|
|
336
|
-
if (
|
|
337
|
-
event.pointerType !== 'touch' ||
|
|
338
|
-
disabled ||
|
|
339
|
-
containerEl == null ||
|
|
340
|
-
!(eventTarget instanceof Element) ||
|
|
341
|
-
eventTarget === event.currentTarget ||
|
|
342
|
-
!hasInteractiveAncestor(eventTarget, containerEl) ||
|
|
343
|
-
// The control the container delegates to is the container's own
|
|
344
|
-
// action: a press on it activates the container.
|
|
345
|
-
interactiveRef?.current?.contains(eventTarget) === true
|
|
346
|
-
) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
cancelPress(containerEl);
|
|
350
|
-
},
|
|
351
|
-
[containerRef, interactiveRef, disabled],
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
return {onClick, onMouseUp, onPointerDown};
|
|
308
|
+
return {onClick, onMouseUp};
|
|
355
309
|
}
|