Skip to content

Commit 2cd04aa

Browse files
committed
work
1 parent d07bd4a commit 2cd04aa

18 files changed

Lines changed: 3992 additions & 3382 deletions

File tree

packages/frontend/dom/dist/jsenv_dom.js

Lines changed: 3446 additions & 3207 deletions
Large diffs are not rendered by default.

packages/frontend/dom/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export {
99
dispatchPublicCustomEvent,
1010
findEvent,
1111
formatEventSideEffect,
12+
isPressDrivenClick,
1213
isTouchDrivenEvent,
1314
} from "./src/dom_events.js";
1415
export { createIterableWeakSet } from "./src/iterable_weak_set.js";

packages/frontend/dom/src/dom_events.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,25 @@ export const isTouchDrivenEvent = (event) => {
138138
return event.sourceCapabilities?.firesTouchEvents === true;
139139
};
140140

141+
/**
142+
* Whether `event` is a click a pointer press produced, as opposed to one
143+
* dispatched with no press behind it: keyboard activation (Enter or Space on a
144+
* button or a link), `element.click()`, a label forwarding to the control it
145+
* labels.
146+
*
147+
* `detail` is the click count, and it is 0 for every click no press stands
148+
* behind — the reading that tells the two apart, and why nothing here may
149+
* overwrite it (see EVENT_TYPES_WITH_MEANINGFUL_DETAIL below). It survives a
150+
* cancelled `pointerdown`: the compatibility mouse events go, the click and its
151+
* count stay — which is what makes it readable for a press a drag source or a
152+
* control arbitrated.
153+
*
154+
* Asked by whoever armed something on a press and is waiting for the click that
155+
* press owes: a click nothing pressed for is not that one, and taking it would
156+
* cost a keyboard user their activation.
157+
*/
158+
export const isPressDrivenClick = (event) => event.detail > 0;
159+
141160
/**
142161
* Returns true if the event itself or any event in its chain matches the predicate.
143162
*

packages/frontend/dom/src/interaction/click_suppression.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@
2323
* stands aside on its own.
2424
*/
2525

26+
import { isPressDrivenClick } from "../dom_events.js";
27+
2628
let suppressing = false;
2729
let disarmAtNextPress = false;
2830

2931
const suppressClick = (clickEvent) => {
3032
if (!suppressing) {
3133
return;
3234
}
35+
if (!isPressDrivenClick(clickEvent)) {
36+
// A click nothing pressed for — a keyboard activation, an `element.click()`
37+
// — is not the one a gesture left behind, and swallowing it would take away
38+
// an activation no hand ever gave. Left armed rather than lifted: the click
39+
// this is waiting for may still be coming, and the next press lifts it
40+
// either way.
41+
return;
42+
}
3343
suppressing = false;
3444
disarmAtNextPress = false;
3545
clickEvent.preventDefault();
@@ -64,8 +74,11 @@ export const suppressClickAfterGesture = () => {
6474
};
6575

6676
/**
67-
* Whether the click being dispatched is one a gesture left behind — armed by
68-
* `suppressClickAfterGesture`, waiting to be swallowed by this module.
77+
* Whether `clickEvent` is one a gesture left behind — armed by
78+
* `suppressClickAfterGesture`, waiting to be swallowed by this module. The
79+
* click itself is asked for, not just the arming: a keyboard activation
80+
* arriving while a gesture's click is still awaited is nobody's leftover (see
81+
* isPressDrivenClick).
6982
*
7083
* A last resort, not a convenience. The suppressor already swallows the click
7184
* before anyone else sees it; the one listener that legitimately needs to ask
@@ -76,4 +89,5 @@ export const suppressClickAfterGesture = () => {
7689
* dead code. Reach for it only when you are sure that is your situation and
7790
* no other ordering is available.
7891
*/
79-
export const clickIsSuppressed = () => suppressing;
92+
export const clickIsSuppressed = (clickEvent) =>
93+
suppressing && isPressDrivenClick(clickEvent);

packages/frontend/dom/src/interaction/drag/drag_after_intent.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,43 @@ export const markDragSource = (element, axes = "xy") => {
212212
};
213213
};
214214

215+
// Which drag source, on this very press, said it walks no axis after all.
216+
const STOOD_DOWN = Symbol.for("jsenv_drag_source_stood_down");
217+
218+
/**
219+
* Says THIS press carries nothing, from an element that is a drag source
220+
* otherwise.
221+
*
222+
* The mark above is written once and says what the element IS — something a drag
223+
* can start from, along these axes. Whether it is free to be carried right now
224+
* is another question, one render away from changing (a court locked on a plan,
225+
* a row pinned by whoever owns the list), and the element is the only one who
226+
* knows it, at the moment the press lands. So it is said on the press rather
227+
* than in the DOM, and everything that reads a source's axes to know what is
228+
* left for itself — a box that travels, a surface that pans — steps over the one
229+
* that stood down: what walks no axis takes none.
230+
*
231+
* It is legible in time because a press reaches the element it landed on before
232+
* whatever holds it, so the source has stood down while the event is still on
233+
* its way up.
234+
*
235+
* @param {PointerEvent} pressEvent
236+
* @param {Element} element The source itself.
237+
*/
238+
export const standDownFromPress = (pressEvent, element) => {
239+
pressEvent[STOOD_DOWN] = element;
240+
};
241+
242+
/**
243+
* Which drag source stood down from this press, if any — see standDownFromPress.
244+
*
245+
* @param {PointerEvent} pressEvent
246+
* @returns {Element|null}
247+
*/
248+
export const dragSourceThatStoodDown = (pressEvent) => {
249+
return pressEvent[STOOD_DOWN] || null;
250+
};
251+
215252
/**
216253
* Waits for the user to mean it, then starts a drag gesture.
217254
*

packages/frontend/dom/src/interaction/drag/drag_to.js

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
dragAfterIntent,
5757
keepTouchRefusable,
5858
markDragSource,
59+
standDownFromPress,
5960
} from "./drag_after_intent.js";
6061
import { initDragConstraints } from "./drag_constraint.js";
6162
import { createDragElementPositioner } from "./drag_element_positioner.js";
@@ -67,7 +68,7 @@ import {
6768
getDropTargetInfo,
6869
rectangleAreIntersecting,
6970
} from "./drop_target_detection.js";
70-
import { standDownFromPanZoomSurface } from "./pan_zoom.js";
71+
import { findPanZoomSurface } from "./pan_zoom.js";
7172
import { applyStickyFrontiersToAutoScrollArea } from "./sticky_frontiers.js";
7273

7374
const dragStyleController = createStyleController("drag_to_move");
@@ -921,16 +922,21 @@ export const startDragTo = (
921922
* says nothing reads as a screen that is broken, and the hand pulls harder; the
922923
* refusal has to be told where the grab would have been felt.
923924
*
924-
* WHO KEEPS THE PRESS: whoever else it was for. Nothing is carried here, so no
925-
* axis is walked and nothing is disputed — the press is kept only because, over
926-
* a list or a page, nobody else wanted it (and the click it leaves behind has to
927-
* be swallowed, something pulled and told to stay put must not also be clicked).
928-
* Over a surface that pans, somebody does: "I cannot be carried" and "I want to
929-
* look around" are two sentences, and the second is the one the hand says nine
930-
* times out of ten — a thing that cannot be taken hold of is exactly the one a
931-
* finger rests on without thinking. So the surface keeps the press, whole (see
932-
* standDownFromPanZoomSurface), and the refusal is only told: no pointer taken,
933-
* no click swallowed, nothing prevented.
925+
* WHAT IT GIVES UP, ALWAYS: the axes. Nothing is carried, so nothing is walked,
926+
* and everything that reads what a drag source walks to know what is left for
927+
* itself steps over this one (see standDownFromPress) — a swipe that starts on a
928+
* locked row is the swipe of the box it stands in, a drag that starts on a
929+
* pinned object pans the surface under it. A locked thing must not be a hole in
930+
* what holds it.
931+
*
932+
* WHAT IT KEEPS: the press, unless a surface that pans was after it. Over a list
933+
* or a page nobody else was, so the press is settled here like any gesture
934+
* settles one — the pointer is taken (a `longpress` on the same finger must not
935+
* answer afterwards), and the click the release leaves behind is swallowed:
936+
* something pulled and told to stay put must not also be clicked. A surface is
937+
* the one thing that takes it whole and in every direction, so there it is the
938+
* surface's and the refusal takes nothing at all: no pointer, no click, nothing
939+
* prevented.
934940
*
935941
* @param {PointerEvent} event The `pointerdown` that would have become a drag.
936942
* @param {object} [options]
@@ -953,14 +959,21 @@ export const refuseDragTo = (
953959
if (!isPrimaryButtonEvent(event)) {
954960
return;
955961
}
956-
const stoodDown = standDownFromPanZoomSurface(event, draggedElement);
957-
if (!stoodDown) {
962+
// Nothing will be carried, so no axis is walked: whatever reads a drag
963+
// source's axes to know what is left for itself must find none here.
964+
standDownFromPress(event, draggedElement);
965+
// And over a surface that pans, the press was never this element's to keep:
966+
// "this one cannot be carried" and "I want to look around" are two sentences,
967+
// and the hand says the second nine times out of ten — a thing that cannot be
968+
// taken hold of is exactly the one a finger rests on without thinking.
969+
const surfaceKeepsThePress = Boolean(findPanZoomSurface(draggedElement));
970+
if (!surfaceKeepsThePress) {
958971
event.preventDefault();
959972
}
960973
dragAfterIntent(
961974
event,
962975
() => {
963-
if (stoodDown) {
976+
if (surfaceKeepsThePress) {
964977
// The surface is holding the hand: taking the pointer or the click from
965978
// it would be taking the gesture it is answering. The word is all that
966979
// is owed.

packages/frontend/dom/src/interaction/drag/drag_to_travel.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
*/
3939

4040
import { createDragGestureController } from "./drag_gesture.js";
41-
import { dragAfterIntent } from "./drag_after_intent.js";
41+
import {
42+
dragAfterIntent,
43+
dragSourceThatStoodDown,
44+
} from "./drag_after_intent.js";
4245
import {
4346
claimWheelGesture,
4447
releaseWheelGesture,
@@ -334,8 +337,12 @@ const isTopLayer = (element) => {
334337
* A box lifted into the top layer on the way up takes everything: a popover or a
335338
* modal dialog is written inside a slide and painted over the whole screen, so
336339
* the slides are nowhere near the finger and none of the axes are left.
340+
*
341+
* `stoodDown` is the one element on the way up whose axes are not read: a drag
342+
* source that says this press carries nothing (see standDownFromPress in
343+
* drag_after_intent.js).
337344
*/
338-
const axesLeftBy = (axes, fromElement, stopElement, attribute) => {
345+
const axesLeftBy = (axes, fromElement, stopElement, attribute, stoodDown) => {
339346
if (!stopElement.contains(fromElement)) {
340347
// Not a press that came up through this box: a browser view transition
341348
// delivers one to the document root instead, and the caller hands it over
@@ -350,6 +357,14 @@ const axesLeftBy = (axes, fromElement, stopElement, attribute) => {
350357
// there is nothing left of it here, whatever axes are still unclaimed.
351358
return "";
352359
}
360+
if (element === stoodDown) {
361+
// It says which way it would be dragged and it is not being dragged from
362+
// this press (standDownFromPress): a locked object walks no axis, so it
363+
// takes none — a swipe that starts on it is the swipe of whatever it
364+
// stands in.
365+
element = element.parentElement;
366+
continue;
367+
}
353368
const taken = element.getAttribute(attribute);
354369
if (taken) {
355370
let rest = "";
@@ -539,7 +554,13 @@ export const startDragToTravel = (
539554
);
540555
const axesLeft =
541556
axesLeftByTravels &&
542-
axesLeftBy(axesLeftByTravels, target, element, DRAG_SOURCE_AXES_ATTRIBUTE);
557+
axesLeftBy(
558+
axesLeftByTravels,
559+
target,
560+
element,
561+
DRAG_SOURCE_AXES_ATTRIBUTE,
562+
dragSourceThatStoodDown(pointerDownEvent),
563+
);
543564
if (!axesLeft) {
544565
return null;
545566
}

packages/frontend/dom/src/interaction/drag/pan_zoom.js

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
claimWheelGesture,
7373
wheelGestureIsTakenFrom,
7474
} from "../scroll/wheel_gesture.js";
75+
import { dragSourceThatStoodDown } from "./drag_after_intent.js";
7576
import { isPrimaryButtonEvent } from "./drag_gesture.js";
7677
import { DRAG_EXCLUDED_SELECTOR } from "./drag_to_travel.js";
7778

@@ -167,31 +168,16 @@ const pageScrolls = (document) => {
167168
// or on plain content in it finds the surface first.
168169
const YIELDED_SELECTOR = `${DRAG_EXCLUDED_SELECTOR},[data-drag-source],[data-drag-ignore],[${SURFACE_ATTRIBUTE}]`;
169170

170-
// Which element, on this very press, said it walks no axis after all.
171-
const STOOD_DOWN = Symbol.for("jsenv_drag_source_stood_down");
172-
173171
/**
174-
* A drag source standing on a surface says THIS press is not its own after all:
175-
* nothing will be carried from it, so nothing is walked, and what the hand is on
176-
* is the surface. Read by the surface a hair later — a press reaches the element
177-
* it landed on before whatever holds it — which is why it is said on the event
178-
* rather than in the DOM: whether a source is free to be carried is a render
179-
* away from changing, and the mark that makes it a drag source was written long
180-
* before this finger.
181-
*
182-
* Returns whether there is a surface at all: a source that stands on none keeps
183-
* the press, there being nobody else to hand it to.
172+
* The surface this element stands on, if any: what a gesture that gives itself
173+
* up asks, to know whether there is anyone to give it up TO (see refuseDragTo in
174+
* drag_to.js).
184175
*
185-
* @param {PointerEvent} event The `pointerdown` the source is standing down from.
186-
* @param {Element} element The source itself.
176+
* @param {Element} element
177+
* @returns {Element|null}
187178
*/
188-
export const standDownFromPanZoomSurface = (event, element) => {
189-
const surface = element.closest(`[${SURFACE_ATTRIBUTE}]`);
190-
if (!surface) {
191-
return false;
192-
}
193-
event[STOOD_DOWN] = element;
194-
return true;
179+
export const findPanZoomSurface = (element) => {
180+
return element.closest(`[${SURFACE_ATTRIBUTE}]`);
195181
};
196182

197183
/**
@@ -337,11 +323,11 @@ export const installPanZoom = (
337323
return;
338324
}
339325
let yieldedTo = event.target.closest(YIELDED_SELECTOR);
340-
if (yieldedTo && yieldedTo === event[STOOD_DOWN]) {
341-
// It is a drag source and it is taking nothing from this press
342-
// (standDownFromPanZoomSurface), so it is no reason to yield — and the
343-
// walk goes on above it, where a field, a nested surface or something
344-
// that IS being carried would still be.
326+
if (yieldedTo && yieldedTo === dragSourceThatStoodDown(event)) {
327+
// It is a drag source and it carries nothing from this press
328+
// (standDownFromPress), so it is no reason to yield — and the walk goes on
329+
// above it, where a field, a nested surface or something that IS being
330+
// carried would still be.
345331
yieldedTo = yieldedTo.parentElement?.closest(YIELDED_SELECTOR) || null;
346332
}
347333
if (yieldedTo && yieldedTo !== element && element.contains(yieldedTo)) {

0 commit comments

Comments
 (0)