@jsenv/dom 0.17.89 → 0.17.91
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/dist/jsenv_dom.js +386 -50
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -4520,11 +4520,12 @@ const clickIsSuppressed = (clickEvent) =>
|
|
|
4520
4520
|
*
|
|
4521
4521
|
* The wait also has to hold off the system's own answer to the same gesture: a
|
|
4522
4522
|
* FINGER held long enough IS the context-menu gesture, and Android's menu (around
|
|
4523
|
-
* 500ms) or iOS's callout lands a tenth
|
|
4524
|
-
* here.
|
|
4525
|
-
*
|
|
4526
|
-
*
|
|
4527
|
-
*
|
|
4523
|
+
* 500ms) with the word under the finger selected, or iOS's callout, lands a tenth
|
|
4524
|
+
* of a second after the press was answered here. What of that is an event — the
|
|
4525
|
+
* menu, and the selection outside iOS — is refused below; what is not (iOS
|
|
4526
|
+
* selecting the word under the finger) is a stylesheet the caller writes on its
|
|
4527
|
+
* own elements — `-webkit-touch-callout: none` has to be true before the finger
|
|
4528
|
+
* lands, so it cannot be set from here.
|
|
4528
4529
|
*
|
|
4529
4530
|
* THE REFUSAL LASTS THE PRESS, NOT THE WAIT. The system's delay is the longer of
|
|
4530
4531
|
* the two, so between the moment this wait gives up and the moment the finger
|
|
@@ -4645,18 +4646,21 @@ const waitForPressHeld = (
|
|
|
4645
4646
|
};
|
|
4646
4647
|
|
|
4647
4648
|
/* A FINGER held down is the system's own context-menu gesture, and the menu it
|
|
4648
|
-
raises lands on top of the answer this press was
|
|
4649
|
-
not: its context menu comes from the other button,
|
|
4650
|
-
this press, and is the user asking for the browser's
|
|
4651
|
-
alone, and only a touch press refuses it.
|
|
4652
|
-
The
|
|
4653
|
-
page (a drag backdrop, a popup), and
|
|
4654
|
-
|
|
4649
|
+
raises, with the word it selects, lands on top of the answer this press was
|
|
4650
|
+
already given. A MOUSE is not: its context menu comes from the other button,
|
|
4651
|
+
has nothing to do with this press, and is the user asking for the browser's
|
|
4652
|
+
menu — so it is left alone, and only a touch press refuses it.
|
|
4653
|
+
The listeners go on window, in capture: what answers the press may cover the
|
|
4654
|
+
page (a drag backdrop, a popup), and both events are then aimed at that
|
|
4655
|
+
instead of at the element pressed. Which is also why the pressed element's
|
|
4656
|
+
own `user-select: none` does not cover the selection: the word selected is
|
|
4657
|
+
the one under the finger when the system answers, in what the hold opened. */
|
|
4655
4658
|
if (pressEvent.pointerType === "touch") {
|
|
4656
|
-
const
|
|
4657
|
-
|
|
4659
|
+
const preventSystemAnswer = (event) => {
|
|
4660
|
+
event.preventDefault();
|
|
4658
4661
|
};
|
|
4659
|
-
window.addEventListener("contextmenu",
|
|
4662
|
+
window.addEventListener("contextmenu", preventSystemAnswer, true);
|
|
4663
|
+
window.addEventListener("selectstart", preventSystemAnswer, true);
|
|
4660
4664
|
/* The finger letting go, watched for the length of the press rather than for
|
|
4661
4665
|
the length of the wait: the wait can be over long before the hand is.
|
|
4662
4666
|
A pointerCANCEL is not the hand letting go, it is the browser saying it is
|
|
@@ -4683,7 +4687,8 @@ const waitForPressHeld = (
|
|
|
4683
4687
|
window.addEventListener("pointercancel", onPressPointerEnd, true);
|
|
4684
4688
|
pressCleanupCallbacks.push(() => {
|
|
4685
4689
|
clearTimeout(menuGraceTimeout);
|
|
4686
|
-
window.removeEventListener("contextmenu",
|
|
4690
|
+
window.removeEventListener("contextmenu", preventSystemAnswer, true);
|
|
4691
|
+
window.removeEventListener("selectstart", preventSystemAnswer, true);
|
|
4687
4692
|
window.removeEventListener("pointerup", onPressPointerEnd, true);
|
|
4688
4693
|
window.removeEventListener("pointercancel", onPressPointerEnd, true);
|
|
4689
4694
|
});
|
|
@@ -4982,7 +4987,9 @@ const getFocusVisibilityInfo = (node, { excludeAriaHidden } = {}) => {
|
|
|
4982
4987
|
if (
|
|
4983
4988
|
elementIsDialog(nodeOrAncestor) &&
|
|
4984
4989
|
!nodeOrAncestor.open &&
|
|
4985
|
-
!isOpenPopover
|
|
4990
|
+
!isOpenPopover &&
|
|
4991
|
+
// A closed PopupGroup is displayed in the page (see ancestor_open.js).
|
|
4992
|
+
!nodeOrAncestor.hasAttribute("navi-popup-group")
|
|
4986
4993
|
) {
|
|
4987
4994
|
return { visible: false, reason: "inside closed dialog element" };
|
|
4988
4995
|
}
|
|
@@ -5097,6 +5104,11 @@ const elementIsFocusable = (node, { excludeAriaHidden } = {}) => {
|
|
|
5097
5104
|
if (node.hasAttribute("navi-focus-delegate")) {
|
|
5098
5105
|
return false;
|
|
5099
5106
|
}
|
|
5107
|
+
// A <dialog> because it is shown modal while one of its popups is open, not
|
|
5108
|
+
// a surface of its own to focus (see ancestor_open.js).
|
|
5109
|
+
if (node.hasAttribute("navi-popup-group")) {
|
|
5110
|
+
return false;
|
|
5111
|
+
}
|
|
5100
5112
|
if (!canInteract(node)) {
|
|
5101
5113
|
return false;
|
|
5102
5114
|
}
|
|
@@ -5511,6 +5523,20 @@ const getKeyboardEventDefaultAction = (keyboardEvent) => {
|
|
|
5511
5523
|
return "";
|
|
5512
5524
|
};
|
|
5513
5525
|
|
|
5526
|
+
// Whether the keyboard follows macOS's conventions: there Home and End scroll
|
|
5527
|
+
// the page even from a text field, and a chord with Cmd or Option scrolls it to
|
|
5528
|
+
// an end or a page away.
|
|
5529
|
+
let isMac;
|
|
5530
|
+
const isMacPlatform = () => {
|
|
5531
|
+
if (isMac === undefined) {
|
|
5532
|
+
const { userAgentData, userAgent } = window.navigator;
|
|
5533
|
+
isMac = userAgentData
|
|
5534
|
+
? userAgentData.platform === "macOS"
|
|
5535
|
+
: /Mac|iPhone|iPad|iPod/.test(userAgent);
|
|
5536
|
+
}
|
|
5537
|
+
return isMac;
|
|
5538
|
+
};
|
|
5539
|
+
|
|
5514
5540
|
const isTypingIntent = (e) => {
|
|
5515
5541
|
// Modifier keys used for shortcuts: skip
|
|
5516
5542
|
if (e.metaKey || e.ctrlKey) {
|
|
@@ -5552,10 +5578,11 @@ const DEFAULT_BEHAVIORS = [
|
|
|
5552
5578
|
return "activate";
|
|
5553
5579
|
},
|
|
5554
5580
|
enter: (e) => (e.target.form ? "form_submit" : ""),
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5581
|
+
// Moving through a radio group; a checkbox lets them scroll.
|
|
5582
|
+
arrowleft: (e) => (e.target.type === "radio" ? "focus_nav" : undefined),
|
|
5583
|
+
arrowright: (e) => (e.target.type === "radio" ? "focus_nav" : undefined),
|
|
5584
|
+
arrowup: (e) => (e.target.type === "radio" ? "focus_nav" : undefined),
|
|
5585
|
+
arrowdown: (e) => (e.target.type === "radio" ? "focus_nav" : undefined),
|
|
5559
5586
|
},
|
|
5560
5587
|
},
|
|
5561
5588
|
{
|
|
@@ -5578,8 +5605,10 @@ const DEFAULT_BEHAVIORS = [
|
|
|
5578
5605
|
arrowright: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
|
|
5579
5606
|
arrowup: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
|
|
5580
5607
|
arrowdown: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
|
|
5581
|
-
home: (e) =>
|
|
5582
|
-
|
|
5608
|
+
home: (e) =>
|
|
5609
|
+
e.target.readOnly || isMacPlatform() ? "scroll" : "cursor_move",
|
|
5610
|
+
end: (e) =>
|
|
5611
|
+
e.target.readOnly || isMacPlatform() ? "scroll" : "cursor_move",
|
|
5583
5612
|
},
|
|
5584
5613
|
fallback: (e) => (isTypingIntent(e) ? "type" : undefined),
|
|
5585
5614
|
},
|
|
@@ -5664,13 +5693,17 @@ const DEFAULT_BEHAVIORS = [
|
|
|
5664
5693
|
fallback: (e) => (isTypingIntent(e) ? "type" : undefined),
|
|
5665
5694
|
},
|
|
5666
5695
|
{
|
|
5667
|
-
// Buttons and links:
|
|
5696
|
+
// Buttons and links: Enter triggers the element's default action, Space
|
|
5697
|
+
// only a button's — on a link it scrolls the page.
|
|
5668
5698
|
test: (el) =>
|
|
5669
5699
|
el.tagName === "BUTTON" ||
|
|
5670
5700
|
el.tagName === "A" ||
|
|
5671
5701
|
el.getAttribute("role") === "button",
|
|
5672
5702
|
keys: {
|
|
5673
|
-
space:
|
|
5703
|
+
space: (e) =>
|
|
5704
|
+
e.target.tagName === "A" && e.target.getAttribute("role") !== "button"
|
|
5705
|
+
? undefined
|
|
5706
|
+
: "activate",
|
|
5674
5707
|
enter: "activate",
|
|
5675
5708
|
},
|
|
5676
5709
|
},
|
|
@@ -7199,6 +7232,10 @@ const getScrollContainer = (arg, { includeHidden } = {}) => {
|
|
|
7199
7232
|
return null;
|
|
7200
7233
|
}
|
|
7201
7234
|
if (isScrollBoundary(element)) {
|
|
7235
|
+
const placeholder = findEngagedPopupGroupPlaceholder(element);
|
|
7236
|
+
if (placeholder) {
|
|
7237
|
+
return getScrollContainer(placeholder, { includeHidden });
|
|
7238
|
+
}
|
|
7202
7239
|
return getScrollingElement(element.ownerDocument);
|
|
7203
7240
|
}
|
|
7204
7241
|
return (
|
|
@@ -7229,12 +7266,18 @@ const findScrollContainer = (element, { includeHidden } = {}) => {
|
|
|
7229
7266
|
if (parent === document) {
|
|
7230
7267
|
return null;
|
|
7231
7268
|
}
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
}
|
|
7269
|
+
// A boundary that scrolls is the scroll container of what it holds — a
|
|
7270
|
+
// popup whose own box scrolls, a PopupGroup shown modal where it stands.
|
|
7235
7271
|
if (isScrollable(parent, { includeHidden })) {
|
|
7236
7272
|
return parent;
|
|
7237
7273
|
}
|
|
7274
|
+
if (isScrollBoundary(parent)) {
|
|
7275
|
+
const placeholder = findEngagedPopupGroupPlaceholder(parent);
|
|
7276
|
+
if (placeholder) {
|
|
7277
|
+
return findScrollContainer(placeholder, { includeHidden });
|
|
7278
|
+
}
|
|
7279
|
+
return null;
|
|
7280
|
+
}
|
|
7238
7281
|
parent = parent.parentNode;
|
|
7239
7282
|
}
|
|
7240
7283
|
return null;
|
|
@@ -7258,6 +7301,29 @@ const isScrollBoundary = (node) => {
|
|
|
7258
7301
|
return getStyle(node, "position") === "fixed";
|
|
7259
7302
|
};
|
|
7260
7303
|
|
|
7304
|
+
// Except a PopupGroup shown modal (@jsenv/navi's popup_group.jsx): fixed in the
|
|
7305
|
+
// top layer at the place a placeholder holds for it in the page, it follows
|
|
7306
|
+
// that place, so what scrolls the placeholder moves the group and what it
|
|
7307
|
+
// holds. The placeholder is inserted just before it; something rendered
|
|
7308
|
+
// before the group meanwhile lands in between.
|
|
7309
|
+
const findEngagedPopupGroupPlaceholder = (node) => {
|
|
7310
|
+
if (
|
|
7311
|
+
node.tagName !== "DIALOG" ||
|
|
7312
|
+
!node.hasAttribute("navi-popup-group") ||
|
|
7313
|
+
!node.matches(":modal")
|
|
7314
|
+
) {
|
|
7315
|
+
return null;
|
|
7316
|
+
}
|
|
7317
|
+
let previous = node.previousElementSibling;
|
|
7318
|
+
while (previous) {
|
|
7319
|
+
if (previous.hasAttribute("navi-popup-group-placeholder")) {
|
|
7320
|
+
return previous;
|
|
7321
|
+
}
|
|
7322
|
+
previous = previous.previousElementSibling;
|
|
7323
|
+
}
|
|
7324
|
+
return null;
|
|
7325
|
+
};
|
|
7326
|
+
|
|
7261
7327
|
const getSelfAndAncestorScrolls = (element, startOnParent) => {
|
|
7262
7328
|
let scrollX = 0;
|
|
7263
7329
|
let scrollY = 0;
|
|
@@ -8192,18 +8258,23 @@ const TOP_LAYER_SELECTOR$1 = [
|
|
|
8192
8258
|
":fullscreen",
|
|
8193
8259
|
].join(",");
|
|
8194
8260
|
|
|
8195
|
-
// The keys that scroll the document when nothing focusable holds them
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
"
|
|
8199
|
-
"
|
|
8200
|
-
"
|
|
8201
|
-
"
|
|
8202
|
-
"
|
|
8203
|
-
"
|
|
8204
|
-
"
|
|
8205
|
-
"
|
|
8261
|
+
// The keys that scroll the document when nothing focusable holds them, and
|
|
8262
|
+
// the way each one goes, as a wheel would: [deltaX, deltaY].
|
|
8263
|
+
const SCROLL_KEY_DELTA_MAP = new Map([
|
|
8264
|
+
["ArrowUp", [0, -1]],
|
|
8265
|
+
["ArrowDown", [0, 1]],
|
|
8266
|
+
["ArrowLeft", [-1, 0]],
|
|
8267
|
+
["ArrowRight", [1, 0]],
|
|
8268
|
+
["PageUp", [0, -1]],
|
|
8269
|
+
["PageDown", [0, 1]],
|
|
8270
|
+
["Home", [0, -1]],
|
|
8271
|
+
["End", [0, 1]],
|
|
8272
|
+
[" ", [0, 1]],
|
|
8206
8273
|
]);
|
|
8274
|
+
// What a scroll key does by itself on these, whatever it would scroll: a
|
|
8275
|
+
// closed <select> changes its value with the arrows, a media element's
|
|
8276
|
+
// controls seek with them.
|
|
8277
|
+
const KEY_OWNER_SELECTOR = "select, audio, video";
|
|
8207
8278
|
|
|
8208
8279
|
/**
|
|
8209
8280
|
* Stops the background from scrolling by cancelling the scroll gestures that
|
|
@@ -8237,14 +8308,33 @@ const SCROLL_KEY_SET = new Set([
|
|
|
8237
8308
|
* background, when it is one of ours (a modal dialog covers it with its own
|
|
8238
8309
|
* `::backdrop` and there is nothing to pass). It is in the top layer like the
|
|
8239
8310
|
* overlay itself, yet a gesture on it aims at the background behind it.
|
|
8311
|
+
* @param {() => {left: number, top: number, right: number, bottom: number}} [options.getElementBox]
|
|
8312
|
+
* - Where `element` is on screen, when that is less than its border box (an
|
|
8313
|
+
* overlay clipped by a clip-path): a gesture outside that box but reported on
|
|
8314
|
+
* `element` landed on its `::backdrop`.
|
|
8315
|
+
* @param {boolean} [options.containInside] - Also cancel a gesture landing on
|
|
8316
|
+
* or inside `element` that nothing between it and `element` can scroll in
|
|
8317
|
+
* its direction, and a scroll key pressed there the same way. For an overlay
|
|
8318
|
+
* that is not a scroll container itself (a box of the page shown in the top
|
|
8319
|
+
* layer where it stood): its scroll chain goes straight to the viewport, and
|
|
8320
|
+
* no `overscroll-behavior` of its own can stop it there. Decided once what
|
|
8321
|
+
* the gesture landed on has had its say: what takes it from a listener of
|
|
8322
|
+
* its own (a control moving by transform, a drag) cancels it there, and a
|
|
8323
|
+
* gesture cancelled before it gets there is one it refuses.
|
|
8240
8324
|
* @returns {() => void} Cleanup function removing the listeners.
|
|
8241
8325
|
*/
|
|
8242
8326
|
|
|
8243
8327
|
const trapScrollGestureInside = (
|
|
8244
8328
|
element,
|
|
8245
|
-
{
|
|
8329
|
+
{
|
|
8330
|
+
boundaryElement,
|
|
8331
|
+
backdropElement,
|
|
8332
|
+
getElementBox = () => element.getBoundingClientRect(),
|
|
8333
|
+
containInside = false,
|
|
8334
|
+
} = {},
|
|
8246
8335
|
) => {
|
|
8247
8336
|
const { ownerDocument } = element;
|
|
8337
|
+
const { defaultView } = ownerDocument;
|
|
8248
8338
|
const lockedRegion = boundaryElement || ownerDocument.documentElement;
|
|
8249
8339
|
|
|
8250
8340
|
const isBackgroundGesture = (target, clientX, clientY) => {
|
|
@@ -8259,7 +8349,7 @@ const trapScrollGestureInside = (
|
|
|
8259
8349
|
if (target !== element) {
|
|
8260
8350
|
return false;
|
|
8261
8351
|
}
|
|
8262
|
-
const { left, right, top, bottom } =
|
|
8352
|
+
const { left, right, top, bottom } = getElementBox();
|
|
8263
8353
|
return (
|
|
8264
8354
|
clientX < left || clientX > right || clientY < top || clientY > bottom
|
|
8265
8355
|
);
|
|
@@ -8277,6 +8367,42 @@ const trapScrollGestureInside = (
|
|
|
8277
8367
|
return true;
|
|
8278
8368
|
};
|
|
8279
8369
|
|
|
8370
|
+
// Whether something on the event's way from what it landed on up to
|
|
8371
|
+
// `element` scrolls the way the gesture mostly goes, with room left to go
|
|
8372
|
+
// there. The composed path, not the parents: a scroller inside a shadow
|
|
8373
|
+
// root, or the shadow scroller a slotted element shows in, is on it. A box
|
|
8374
|
+
// in the top layer, or fixed, ends the walk: its scroll chain goes on to the
|
|
8375
|
+
// viewport, not to its parent in the DOM.
|
|
8376
|
+
const isTakenInside = (event, deltaX, deltaY) => {
|
|
8377
|
+
const axis = Math.abs(deltaY) >= Math.abs(deltaX) ? "y" : "x";
|
|
8378
|
+
const delta = axis === "y" ? deltaY : deltaX;
|
|
8379
|
+
for (const node of event.composedPath()) {
|
|
8380
|
+
if (node.nodeType !== 1) {
|
|
8381
|
+
continue;
|
|
8382
|
+
}
|
|
8383
|
+
if (canScroll(node, axis) && hasRoomToScroll(node, axis, delta)) {
|
|
8384
|
+
return true;
|
|
8385
|
+
}
|
|
8386
|
+
if (node === element) {
|
|
8387
|
+
return false;
|
|
8388
|
+
}
|
|
8389
|
+
if (
|
|
8390
|
+
node.matches(TOP_LAYER_SELECTOR$1) ||
|
|
8391
|
+
getComputedStyle(node).position === "fixed"
|
|
8392
|
+
) {
|
|
8393
|
+
return false;
|
|
8394
|
+
}
|
|
8395
|
+
}
|
|
8396
|
+
return false;
|
|
8397
|
+
};
|
|
8398
|
+
// On `element` itself too, inside its box: on its padding, between what it
|
|
8399
|
+
// holds. Outside that box it is the wall, decided first (isBackgroundGesture).
|
|
8400
|
+
const leaksFromInside = (event, deltaX, deltaY) =>
|
|
8401
|
+
// A move with no direction scrolls nothing.
|
|
8402
|
+
(deltaX !== 0 || deltaY !== 0) &&
|
|
8403
|
+
element.contains(event.target) &&
|
|
8404
|
+
!isTakenInside(event, deltaX, deltaY);
|
|
8405
|
+
|
|
8280
8406
|
const onWheel = (wheelEvent) => {
|
|
8281
8407
|
if (wheelEvent.ctrlKey) {
|
|
8282
8408
|
// Browser zoom, not a scroll — cancelling it would take zoom away from
|
|
@@ -8293,25 +8419,71 @@ const trapScrollGestureInside = (
|
|
|
8293
8419
|
wheelEvent.preventDefault();
|
|
8294
8420
|
}
|
|
8295
8421
|
};
|
|
8422
|
+
const onWheelInside = (wheelEvent) => {
|
|
8423
|
+
if (
|
|
8424
|
+
wheelEvent.ctrlKey ||
|
|
8425
|
+
wheelEvent.defaultPrevented ||
|
|
8426
|
+
!wheelEvent.cancelable
|
|
8427
|
+
) {
|
|
8428
|
+
return;
|
|
8429
|
+
}
|
|
8430
|
+
if (leaksFromInside(wheelEvent, wheelEvent.deltaX, wheelEvent.deltaY)) {
|
|
8431
|
+
wheelEvent.preventDefault();
|
|
8432
|
+
}
|
|
8433
|
+
};
|
|
8434
|
+
// Where the finger was at the previous touch event: a touchmove carries no
|
|
8435
|
+
// delta of its own. Read in the capture phase, which every touchmove goes
|
|
8436
|
+
// through, for the decision taken once it has been through its target.
|
|
8437
|
+
let previousTouch = null;
|
|
8438
|
+
let touchMoveDelta = null;
|
|
8439
|
+
const onTouchStart = (touchStartEvent) => {
|
|
8440
|
+
const { touches } = touchStartEvent;
|
|
8441
|
+
previousTouch =
|
|
8442
|
+
touches.length === 1
|
|
8443
|
+
? { x: touches[0].clientX, y: touches[0].clientY }
|
|
8444
|
+
: null;
|
|
8445
|
+
};
|
|
8296
8446
|
const onTouchMove = (touchMoveEvent) => {
|
|
8297
8447
|
const { touches } = touchMoveEvent;
|
|
8298
8448
|
if (touches.length !== 1) {
|
|
8299
8449
|
return; // pinch
|
|
8300
8450
|
}
|
|
8301
8451
|
const [touch] = touches;
|
|
8452
|
+
// The content moves against the finger.
|
|
8453
|
+
touchMoveDelta = {
|
|
8454
|
+
touchMoveEvent,
|
|
8455
|
+
deltaX: previousTouch ? previousTouch.x - touch.clientX : 0,
|
|
8456
|
+
deltaY: previousTouch ? previousTouch.y - touch.clientY : 0,
|
|
8457
|
+
};
|
|
8458
|
+
previousTouch = { x: touch.clientX, y: touch.clientY };
|
|
8302
8459
|
if (
|
|
8303
8460
|
isBackgroundGesture(touchMoveEvent.target, touch.clientX, touch.clientY)
|
|
8304
8461
|
) {
|
|
8305
8462
|
touchMoveEvent.preventDefault();
|
|
8306
8463
|
}
|
|
8307
8464
|
};
|
|
8465
|
+
const onTouchMoveInside = (touchMoveEvent) => {
|
|
8466
|
+
if (touchMoveDelta?.touchMoveEvent !== touchMoveEvent) {
|
|
8467
|
+
return; // pinch
|
|
8468
|
+
}
|
|
8469
|
+
const { deltaX, deltaY } = touchMoveDelta;
|
|
8470
|
+
touchMoveDelta = null;
|
|
8471
|
+
if (touchMoveEvent.defaultPrevented || !touchMoveEvent.cancelable) {
|
|
8472
|
+
return;
|
|
8473
|
+
}
|
|
8474
|
+
if (leaksFromInside(touchMoveEvent, deltaX, deltaY)) {
|
|
8475
|
+
touchMoveEvent.preventDefault();
|
|
8476
|
+
}
|
|
8477
|
+
};
|
|
8308
8478
|
const onKeyDown = (keydownEvent) => {
|
|
8309
|
-
if (!
|
|
8479
|
+
if (!SCROLL_KEY_DELTA_MAP.has(keydownEvent.key)) {
|
|
8310
8480
|
return;
|
|
8311
8481
|
}
|
|
8312
8482
|
const { target } = keydownEvent;
|
|
8313
8483
|
// Anything else focused (a field, a scrollable box, a button) owns the key
|
|
8314
|
-
// press; only a document with nothing focused scrolls on it.
|
|
8484
|
+
// press; only a document with nothing focused scrolls on it. Inside
|
|
8485
|
+
// `element`, with containInside, what really owns it is asked
|
|
8486
|
+
// (onKeyDownInside).
|
|
8315
8487
|
if (
|
|
8316
8488
|
target !== ownerDocument.body &&
|
|
8317
8489
|
target !== ownerDocument.documentElement
|
|
@@ -8322,6 +8494,37 @@ const trapScrollGestureInside = (
|
|
|
8322
8494
|
keydownEvent.preventDefault();
|
|
8323
8495
|
}
|
|
8324
8496
|
};
|
|
8497
|
+
// A key whose default would scroll, pressed where nothing between there and
|
|
8498
|
+
// `element` scrolls that way.
|
|
8499
|
+
const onKeyDownInside = (keydownEvent) => {
|
|
8500
|
+
if (keydownEvent.defaultPrevented) {
|
|
8501
|
+
return;
|
|
8502
|
+
}
|
|
8503
|
+
const scrollDelta = readKeyScrollDelta(keydownEvent);
|
|
8504
|
+
if (!scrollDelta) {
|
|
8505
|
+
return;
|
|
8506
|
+
}
|
|
8507
|
+
const { target } = keydownEvent;
|
|
8508
|
+
if (!element.contains(target)) {
|
|
8509
|
+
return;
|
|
8510
|
+
}
|
|
8511
|
+
if (keydownEvent.composedPath()[0] !== target) {
|
|
8512
|
+
// Pressed in a shadow tree, whose host the target is: what the key does
|
|
8513
|
+
// in there is not known from here.
|
|
8514
|
+
return;
|
|
8515
|
+
}
|
|
8516
|
+
if (target.matches(KEY_OWNER_SELECTOR)) {
|
|
8517
|
+
return;
|
|
8518
|
+
}
|
|
8519
|
+
const defaultAction = getKeyboardEventDefaultAction(keydownEvent);
|
|
8520
|
+
if (defaultAction !== "scroll" && defaultAction !== "scroll_self") {
|
|
8521
|
+
return;
|
|
8522
|
+
}
|
|
8523
|
+
const [deltaX, deltaY] = scrollDelta;
|
|
8524
|
+
if (!isTakenInside(keydownEvent, deltaX, deltaY)) {
|
|
8525
|
+
keydownEvent.preventDefault();
|
|
8526
|
+
}
|
|
8527
|
+
};
|
|
8325
8528
|
|
|
8326
8529
|
// Capture phase: the gesture must be cancelled before whatever it landed on
|
|
8327
8530
|
// gets a chance to act on it. passive: false is what makes preventDefault
|
|
@@ -8330,7 +8533,24 @@ const trapScrollGestureInside = (
|
|
|
8330
8533
|
ownerDocument.addEventListener("wheel", onWheel, listenerOptions);
|
|
8331
8534
|
ownerDocument.addEventListener("touchmove", onTouchMove, listenerOptions);
|
|
8332
8535
|
ownerDocument.addEventListener("keydown", onKeyDown, listenerOptions);
|
|
8536
|
+
if (containInside) {
|
|
8537
|
+
ownerDocument.addEventListener("touchstart", onTouchStart, {
|
|
8538
|
+
capture: true,
|
|
8539
|
+
passive: true,
|
|
8540
|
+
});
|
|
8541
|
+
// On the window, after every listener on the way: what is inside has had
|
|
8542
|
+
// its say. passive: false said out loud — a wheel or touch listener on the
|
|
8543
|
+
// window is passive unless told otherwise.
|
|
8544
|
+
defaultView.addEventListener("wheel", onWheelInside, { passive: false });
|
|
8545
|
+
defaultView.addEventListener("touchmove", onTouchMoveInside, {
|
|
8546
|
+
passive: false,
|
|
8547
|
+
});
|
|
8548
|
+
defaultView.addEventListener("keydown", onKeyDownInside);
|
|
8549
|
+
}
|
|
8333
8550
|
return () => {
|
|
8551
|
+
ownerDocument.removeEventListener("touchstart", onTouchStart, {
|
|
8552
|
+
capture: true,
|
|
8553
|
+
});
|
|
8334
8554
|
ownerDocument.removeEventListener("wheel", onWheel, listenerOptions);
|
|
8335
8555
|
ownerDocument.removeEventListener(
|
|
8336
8556
|
"touchmove",
|
|
@@ -8338,9 +8558,97 @@ const trapScrollGestureInside = (
|
|
|
8338
8558
|
listenerOptions,
|
|
8339
8559
|
);
|
|
8340
8560
|
ownerDocument.removeEventListener("keydown", onKeyDown, listenerOptions);
|
|
8561
|
+
defaultView.removeEventListener("wheel", onWheelInside);
|
|
8562
|
+
defaultView.removeEventListener("touchmove", onTouchMoveInside);
|
|
8563
|
+
defaultView.removeEventListener("keydown", onKeyDownInside);
|
|
8341
8564
|
};
|
|
8342
8565
|
};
|
|
8343
8566
|
|
|
8567
|
+
// Whether `scroller` has room left on `axis` the way `delta` goes, with a
|
|
8568
|
+
// pixel of slack against rounded sizes. Its scroll position counts from its
|
|
8569
|
+
// scroll origin, which is on the right of a right-to-left box and at the bottom
|
|
8570
|
+
// of some vertical writing modes: from there it reads 0, then negative.
|
|
8571
|
+
const hasRoomToScroll = (scroller, axis, delta) => {
|
|
8572
|
+
const range =
|
|
8573
|
+
axis === "y"
|
|
8574
|
+
? scroller.scrollHeight - scroller.clientHeight
|
|
8575
|
+
: scroller.scrollWidth - scroller.clientWidth;
|
|
8576
|
+
const position = axis === "y" ? scroller.scrollTop : scroller.scrollLeft;
|
|
8577
|
+
// From the top, or the left.
|
|
8578
|
+
const fromStart = isScrollOriginAtTheEnd(getComputedStyle(scroller), axis)
|
|
8579
|
+
? range + position
|
|
8580
|
+
: position;
|
|
8581
|
+
return delta < 0 ? fromStart > 1 : fromStart < range - 1;
|
|
8582
|
+
};
|
|
8583
|
+
const isScrollOriginAtTheEnd = (
|
|
8584
|
+
{ writingMode, direction, display, flexDirection, flexWrap },
|
|
8585
|
+
axis,
|
|
8586
|
+
) => {
|
|
8587
|
+
const vertical =
|
|
8588
|
+
writingMode.startsWith("vertical") || writingMode.startsWith("sideways");
|
|
8589
|
+
let atTheEnd;
|
|
8590
|
+
if (!vertical) {
|
|
8591
|
+
atTheEnd = axis === "x" && direction === "rtl";
|
|
8592
|
+
} else if (axis === "x") {
|
|
8593
|
+
// Lines stacked from the right.
|
|
8594
|
+
atTheEnd = writingMode.endsWith("-rl");
|
|
8595
|
+
} else {
|
|
8596
|
+
// sideways-lr runs its text bottom to top.
|
|
8597
|
+
atTheEnd =
|
|
8598
|
+
writingMode === "sideways-lr" ? direction !== "rtl" : direction === "rtl";
|
|
8599
|
+
}
|
|
8600
|
+
// A reversed flex container starts at the other end of that axis: its main
|
|
8601
|
+
// axis for a reversed direction (a log resting on its newest item), its
|
|
8602
|
+
// cross axis for wrap-reverse.
|
|
8603
|
+
if (display === "flex" || display === "inline-flex") {
|
|
8604
|
+
const inlineAxis = vertical ? "y" : "x";
|
|
8605
|
+
const blockAxis = vertical ? "x" : "y";
|
|
8606
|
+
const mainAxis = flexDirection.startsWith("row") ? inlineAxis : blockAxis;
|
|
8607
|
+
const reversed =
|
|
8608
|
+
axis === mainAxis
|
|
8609
|
+
? flexDirection.endsWith("-reverse")
|
|
8610
|
+
: flexWrap === "wrap-reverse";
|
|
8611
|
+
if (reversed) {
|
|
8612
|
+
atTheEnd = !atTheEnd;
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
8615
|
+
return atTheEnd;
|
|
8616
|
+
};
|
|
8617
|
+
|
|
8618
|
+
// The way a key scrolls, as a wheel would: [deltaX, deltaY], or null for a key
|
|
8619
|
+
// that scrolls nothing. With a modifier, only the platform's own chords to the
|
|
8620
|
+
// top, the bottom or a page away; never the arrows sideways, which go back and
|
|
8621
|
+
// forward in the history.
|
|
8622
|
+
const readKeyScrollDelta = (keydownEvent) => {
|
|
8623
|
+
const { key, altKey, ctrlKey, metaKey, shiftKey } = keydownEvent;
|
|
8624
|
+
if (!altKey && !ctrlKey && !metaKey) {
|
|
8625
|
+
if (key === " " && shiftKey) {
|
|
8626
|
+
return [0, -1];
|
|
8627
|
+
}
|
|
8628
|
+
return SCROLL_KEY_DELTA_MAP.get(key) || null;
|
|
8629
|
+
}
|
|
8630
|
+
if (isMacPlatform()) {
|
|
8631
|
+
if (!ctrlKey && (metaKey || altKey)) {
|
|
8632
|
+
if (key === "ArrowUp") {
|
|
8633
|
+
return [0, -1];
|
|
8634
|
+
}
|
|
8635
|
+
if (key === "ArrowDown") {
|
|
8636
|
+
return [0, 1];
|
|
8637
|
+
}
|
|
8638
|
+
}
|
|
8639
|
+
return null;
|
|
8640
|
+
}
|
|
8641
|
+
if (ctrlKey && !altKey && !metaKey) {
|
|
8642
|
+
if (key === "Home") {
|
|
8643
|
+
return [0, -1];
|
|
8644
|
+
}
|
|
8645
|
+
if (key === "End") {
|
|
8646
|
+
return [0, 1];
|
|
8647
|
+
}
|
|
8648
|
+
}
|
|
8649
|
+
return null;
|
|
8650
|
+
};
|
|
8651
|
+
|
|
8344
8652
|
// "gesture": cancel the scroll gestures aimed at the background, keeping it
|
|
8345
8653
|
// scrollable (see scroll_gesture_trap.js) — only possible when a backdrop
|
|
8346
8654
|
// covers it.
|
|
@@ -8390,16 +8698,24 @@ const SCROLL_LOCK_STRATEGY = "gesture";
|
|
|
8390
8698
|
* while the overlay is open: the backdrop element, or `true` when the browser
|
|
8391
8699
|
* paints it itself (a modal dialog's `::backdrop`). Enables the gesture
|
|
8392
8700
|
* strategy described above.
|
|
8701
|
+
* @param {Function} [options.getElementBox] - Forwarded to
|
|
8702
|
+
* trapScrollGestureInside: where `element` is on screen when that is less
|
|
8703
|
+
* than its border box.
|
|
8704
|
+
* @param {boolean} [options.containInside] - Forwarded to
|
|
8705
|
+
* trapScrollGestureInside: a gesture or a scroll key inside `element` that
|
|
8706
|
+
* nothing in it can scroll does not reach the background either.
|
|
8393
8707
|
* @returns {() => void} Cleanup function that restores all modified styles.
|
|
8394
8708
|
*/
|
|
8395
8709
|
const trapScrollInside = (
|
|
8396
8710
|
element,
|
|
8397
|
-
{ boundaryElement, backdrop } = {},
|
|
8711
|
+
{ boundaryElement, backdrop, getElementBox, containInside } = {},
|
|
8398
8712
|
) => {
|
|
8399
8713
|
if (backdrop && SCROLL_LOCK_STRATEGY === "gesture") {
|
|
8400
8714
|
return trapScrollGestureInside(element, {
|
|
8401
8715
|
boundaryElement,
|
|
8402
8716
|
backdropElement: backdrop === true ? null : backdrop,
|
|
8717
|
+
getElementBox,
|
|
8718
|
+
containInside,
|
|
8403
8719
|
});
|
|
8404
8720
|
}
|
|
8405
8721
|
|
|
@@ -10042,11 +10358,20 @@ const css$5 = /* css */ `[data-drag-handle], [data-drag-source] {
|
|
|
10042
10358
|
user-select: none;
|
|
10043
10359
|
}
|
|
10044
10360
|
|
|
10045
|
-
[data-drag-on-contact] [data-drag-ignore], [data-drag-on-contact] [popover], [data-drag-on-contact]
|
|
10361
|
+
[data-drag-on-contact] [data-drag-ignore], [data-drag-on-contact] [popover], [data-drag-on-contact] :is(input:not([data-press-only]), textarea), [data-drag-on-contact] :is([contenteditable=""], [contenteditable="true"]) {
|
|
10046
10362
|
user-select: text;
|
|
10047
10363
|
}
|
|
10048
10364
|
|
|
10049
|
-
[data-drag-
|
|
10365
|
+
[data-drag-on-contact] dialog:not([navi-popup-group]:not(:modal)) {
|
|
10366
|
+
user-select: text;
|
|
10367
|
+
}
|
|
10368
|
+
|
|
10369
|
+
[data-drag-ignore], [data-self-interactions~="drag"], [data-self-interactions~="*"], [data-drag-source] [popover] {
|
|
10370
|
+
-webkit-touch-callout: default;
|
|
10371
|
+
touch-action: auto;
|
|
10372
|
+
}
|
|
10373
|
+
|
|
10374
|
+
[data-drag-source] dialog:not([navi-popup-group]:not(:modal)) {
|
|
10050
10375
|
-webkit-touch-callout: default;
|
|
10051
10376
|
touch-action: auto;
|
|
10052
10377
|
}
|
|
@@ -12607,7 +12932,8 @@ const DRAG_EXCLUDED_SELECTOR = [
|
|
|
12607
12932
|
'[data-self-interactions~="drag"]',
|
|
12608
12933
|
'[data-self-interactions~="*"]',
|
|
12609
12934
|
"[popover]",
|
|
12610
|
-
|
|
12935
|
+
// An idle PopupGroup is part of the page (see DRAG_IGNORED_SELECTOR).
|
|
12936
|
+
"dialog:not([navi-popup-group]:not(:modal))",
|
|
12611
12937
|
].join(",");
|
|
12612
12938
|
|
|
12613
12939
|
// Which axes a box travels on, one attribute per gesture, said in the DOM by
|
|
@@ -14374,7 +14700,11 @@ const css$1 = /* css */ `.navi_drop_hint {
|
|
|
14374
14700
|
cursor: default;
|
|
14375
14701
|
}
|
|
14376
14702
|
|
|
14377
|
-
[data-drag-ignore], [data-self-interactions~="drag"], [data-self-interactions~="*"], [data-drag-source] [popover]
|
|
14703
|
+
[data-drag-ignore], [data-self-interactions~="drag"], [data-self-interactions~="*"], [data-drag-source] [popover] {
|
|
14704
|
+
cursor: auto;
|
|
14705
|
+
}
|
|
14706
|
+
|
|
14707
|
+
[data-drag-source] dialog:not([navi-popup-group]:not(:modal)) {
|
|
14378
14708
|
cursor: auto;
|
|
14379
14709
|
}
|
|
14380
14710
|
|
|
@@ -14462,8 +14792,10 @@ import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_to.js"];
|
|
|
14462
14792
|
// stays a descendant of whatever it is anchored in (a callout next to a button,
|
|
14463
14793
|
// in the card that carries both), and the press bubbles through the surface as
|
|
14464
14794
|
// if it had landed on it.
|
|
14795
|
+
// An idle PopupGroup is a closed <dialog> that is part of the page, not a layer
|
|
14796
|
+
// over it (see @jsenv/navi's popup_group.jsx).
|
|
14465
14797
|
const DRAG_IGNORED_SELECTOR =
|
|
14466
|
-
'[data-drag-ignore],[data-self-interactions~="drag"],[data-self-interactions~="*"],[popover],dialog';
|
|
14798
|
+
'[data-drag-ignore],[data-self-interactions~="drag"],[data-self-interactions~="*"],[popover],dialog:not([navi-popup-group]:not(:modal))';
|
|
14467
14799
|
|
|
14468
14800
|
/**
|
|
14469
14801
|
* Creates a gesture controller that moves elements via drag.
|
|
@@ -16336,7 +16668,11 @@ const getResizeDirection = (element) => {
|
|
|
16336
16668
|
// directions: hide when a container closes, recheck when it reopens) — the
|
|
16337
16669
|
// selector/open-detection/timing primitives are identical for both, only
|
|
16338
16670
|
// what each does with a transition differs.
|
|
16339
|
-
|
|
16671
|
+
// Not a PopupGroup (@jsenv/navi's popup_group.jsx): a <dialog> that is a box
|
|
16672
|
+
// of the page, closed or shown modal where it stands. Its `open` says nothing
|
|
16673
|
+
// about whether what it holds is on screen.
|
|
16674
|
+
const OPENABLE_SELECTOR =
|
|
16675
|
+
"dialog:not([navi-popup-group]), details, [popover], [aria-expanded]";
|
|
16340
16676
|
|
|
16341
16677
|
// An element that IS openable is closed in exactly the same way an element
|
|
16342
16678
|
// inside one is — which matters for anything positioned against it, e.g. a
|