@jsenv/dom 0.17.22 → 0.17.24

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.
Files changed (2) hide show
  1. package/dist/jsenv_dom.js +149 -22
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -290,6 +290,42 @@ const chainEvent = (customEvent, parentEvent) => {
290
290
  return customEvent;
291
291
  };
292
292
 
293
+ /**
294
+ * Whether `event` was caused by a finger — a predicate for findEvent, so a
295
+ * question about a whole interaction reads as
296
+ * `findEvent(openEvent, isTouchDrivenEvent)`.
297
+ *
298
+ * Asked of the interaction and not of the device (a media query, a pointer:
299
+ * coarse signal) on purpose: a hybrid tablet has both a touchscreen and a
300
+ * trackpad, and answers "coarse" whichever one was just used. What matters is
301
+ * which one WAS used — a tap brings the on-screen keyboard up, the trackpad
302
+ * next to it does not.
303
+ *
304
+ * Three readings, because no single one covers every path from a finger to an
305
+ * event:
306
+ * - a touch* event says it outright;
307
+ * - `pointerType` says it on a PointerEvent, which "click" also is in some
308
+ * engines and not in others — hence not the only reading;
309
+ * - `sourceCapabilities.firesTouchEvents` is what is left for the compatibility
310
+ * mouse events a tap synthesizes, where nothing else remembers the finger.
311
+ * Absent outside Chromium, where it costs nothing: the readings above have
312
+ * already answered by then, or there was no pointer event to answer about.
313
+ */
314
+ const isTouchDrivenEvent = (event) => {
315
+ if (!event) {
316
+ return false;
317
+ }
318
+ if (typeof event.type === "string" && event.type.startsWith("touch")) {
319
+ return true;
320
+ }
321
+ // "" on a pointer event the engine could not attribute — not an answer, so
322
+ // it falls through to the last reading rather than being read as "not touch".
323
+ if (event.pointerType) {
324
+ return event.pointerType === "touch";
325
+ }
326
+ return event.sourceCapabilities?.firesTouchEvents === true;
327
+ };
328
+
293
329
  /**
294
330
  * Returns true if the event itself or any event in its chain matches the predicate.
295
331
  *
@@ -4295,41 +4331,77 @@ const createPreviousNodeIterator = (fromNode, rootNode, skipRoot = null) => {
4295
4331
  * link or a button that click means "follow me", which is not what the hand
4296
4332
  * asked for: the press was already answered, by the gesture.
4297
4333
  *
4298
- * So it is swallowed, once, in capture on the document before any handler an
4299
- * element may have, and without anyone having to know which element that is.
4334
+ * So it is swallowed, once, before any other listener sees it. Being first is
4335
+ * earned twice, because both orderings matter:
4336
+ *
4337
+ * - on `window`, the first target of the capture phase — a listener anywhere
4338
+ * lower (document included) comes after, no matter when it was registered.
4339
+ * - registered at module load — among listeners on the same target and phase,
4340
+ * registration order decides. A listener added when the gesture ends would
4341
+ * lose to any window-capture listener registered at startup (@jsenv/navi's
4342
+ * link interception is one), so the listener is permanent and merely armed
4343
+ * by each gesture.
4344
+ *
4345
+ * A window-capture listener that this module's evaluation cannot be proven to
4346
+ * precede must not bet on that order: it checks `clickIsSuppressed()` and
4347
+ * stands aside on its own.
4300
4348
  */
4301
4349
 
4350
+ let suppressing = false;
4351
+ let disarmAtNextPress = false;
4352
+
4353
+ const suppressClick = (clickEvent) => {
4354
+ if (!suppressing) {
4355
+ return;
4356
+ }
4357
+ suppressing = false;
4358
+ disarmAtNextPress = false;
4359
+ clickEvent.preventDefault();
4360
+ clickEvent.stopImmediatePropagation();
4361
+ };
4362
+ const onPointerDown = () => {
4363
+ if (disarmAtNextPress) {
4364
+ suppressing = false;
4365
+ disarmAtNextPress = false;
4366
+ }
4367
+ };
4368
+ window.addEventListener("click", suppressClick, { capture: true });
4369
+ window.addEventListener("pointerdown", onPointerDown, { capture: true });
4370
+
4302
4371
  /**
4303
4372
  * Swallows the next click, for a gesture that has just answered the press.
4304
4373
  *
4305
- * @returns {() => void} the gesture is over. The suppressor cannot be taken
4306
- * down with it — the click is dispatched AFTER the pointerup that ends the
4374
+ * @returns {() => void} the gesture is over. The suppression cannot be lifted
4375
+ * with it — the click is dispatched AFTER the pointerup that ends the
4307
4376
  * gesture, so it would be gone one event too early, and the drag would end on
4308
- * the link it started from being followed. It goes once it has swallowed a
4377
+ * the link it started from being followed. It lifts once it has swallowed a
4309
4378
  * click, or at the next press if the gesture produced none: a click is always
4310
- * preceded by a press, so a suppressor that outlives one press can never
4379
+ * preceded by a press, so a suppression that outlives one press can never
4311
4380
  * reach the click of another.
4312
4381
  */
4313
4382
  const suppressClickAfterGesture = () => {
4314
- const suppressClick = (clickEvent) => {
4315
- clickEvent.stopPropagation();
4316
- clickEvent.preventDefault();
4317
- stopSuppressing();
4318
- };
4319
- const stopSuppressing = () => {
4320
- document.removeEventListener("click", suppressClick, { capture: true });
4321
- document.removeEventListener("pointerdown", stopSuppressing, {
4322
- capture: true,
4323
- });
4324
- };
4325
- document.addEventListener("click", suppressClick, { capture: true });
4383
+ suppressing = true;
4384
+ disarmAtNextPress = false;
4326
4385
  return () => {
4327
- document.addEventListener("pointerdown", stopSuppressing, {
4328
- capture: true,
4329
- });
4386
+ disarmAtNextPress = true;
4330
4387
  };
4331
4388
  };
4332
4389
 
4390
+ /**
4391
+ * Whether the click being dispatched is one a gesture left behind — armed by
4392
+ * `suppressClickAfterGesture`, waiting to be swallowed by this module.
4393
+ *
4394
+ * A last resort, not a convenience. The suppressor already swallows the click
4395
+ * before anyone else sees it; the one listener that legitimately needs to ask
4396
+ * is a `click` listener in capture on `window` whose registration cannot be
4397
+ * proven to come after this module's evaluation — that one may run before the
4398
+ * suppressor and must stand aside on its own. Everywhere else (an element,
4399
+ * `document`, the bubble phase) the click never arrives and checking this is
4400
+ * dead code. Reach for it only when you are sure that is your situation and
4401
+ * no other ordering is available.
4402
+ */
4403
+ const clickIsSuppressed = () => suppressing;
4404
+
4333
4405
  /**
4334
4406
  * A press that says something by NOT moving.
4335
4407
  *
@@ -14585,6 +14657,40 @@ if (window.visualViewport) {
14585
14657
  // to the next hides and re-shows the keyboard.
14586
14658
  subscribeVirtualKeyboardGeometryChange(scheduleVisualViewportResize);
14587
14659
 
14660
+ // A focus change is not a resize, and yet: on a phone, giving focus to a field
14661
+ // is the moment the browser decides what to put over the page — the on-screen
14662
+ // keyboard, and above it the suggestion/autofill strip whose height NOTHING
14663
+ // reports. No event describes that strip: visualViewport stays silent about
14664
+ // it, and so does the keyboard's own geometrychange. So the focus itself is
14665
+ // taken as the only hint there is, and whoever sizes against the viewport
14666
+ // re-measures while the furniture settles.
14667
+ //
14668
+ // Several delays rather than one because there is nothing to wait for: the
14669
+ // strip comes up on its own schedule, after the keyboard, sometimes after a
14670
+ // round-trip to the IME. Polling is what is left when the platform describes
14671
+ // nothing — bounded, and free whenever it finds nothing: a re-measure that
14672
+ // reads the same numbers does nothing at all (visible_rect.js's own check()
14673
+ // dedupes on exactly that).
14674
+ const FOCUS_SETTLE_DELAYS = [250, 350, 700];
14675
+ const [publishFocusSettled, subscribeFocusSettled] = createPubSub();
14676
+ let focusSettleTimeoutIds = [];
14677
+ document.addEventListener(
14678
+ "focusin",
14679
+ (event) => {
14680
+ for (const timeoutId of focusSettleTimeoutIds) {
14681
+ clearTimeout(timeoutId);
14682
+ }
14683
+ focusSettleTimeoutIds = FOCUS_SETTLE_DELAYS.map((delay) =>
14684
+ setTimeout(() => {
14685
+ publishFocusSettled(event);
14686
+ }, delay),
14687
+ );
14688
+ },
14689
+ // Capture: a focus moving into something that stops the event on its way up
14690
+ // still moved the furniture.
14691
+ { capture: true },
14692
+ );
14693
+
14588
14694
  let windowResizeTimeoutId;
14589
14695
  window.addEventListener("resize", (event) => {
14590
14696
  clearTimeout(windowResizeTimeoutId);
@@ -15056,6 +15162,27 @@ const visibleRectEffect = (
15056
15162
  );
15057
15163
  addTeardown(subscribeWindowResizeSettled(onWindowOrViewportResize));
15058
15164
  }
15165
+ {
15166
+ // The room left on screen can change with nothing announcing it — see
15167
+ // subscribeFocusSettled in window_size.js for what does that and why a
15168
+ // focus change is the only hint available. Same guard as the resize
15169
+ // reaction just above, for the same reason.
15170
+ //
15171
+ // Not routed through onWindowOrViewportResize despite doing the same
15172
+ // thing: the event reaching autoCheck is what decides whether the move
15173
+ // is animated (pickPositionRelativeTo's own shouldTransition reads
15174
+ // event.type === "resize"), and a focus change must not animate. It is
15175
+ // a correction of a measurement that went stale unannounced, not a
15176
+ // viewport the user watched change — and it fires on every focus, where
15177
+ // an animated slide of a popup nobody touched would be the bug.
15178
+ const onFocusSettled = (event) => {
15179
+ if (ancestorRepositioningCount > 0) {
15180
+ return;
15181
+ }
15182
+ autoCheck(event);
15183
+ };
15184
+ addTeardown(subscribeFocusSettled(onFocusSettled));
15185
+ }
15059
15186
  on_element_resize: {
15060
15187
  if (skipElementResize) {
15061
15188
  break on_element_resize;
@@ -19623,4 +19750,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
19623
19750
  };
19624
19751
  };
19625
19752
 
19626
- export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, claimWheelGesture, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterIntent, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVirtualKeyboardOverlayHeight, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isPrimaryButtonEvent, isSameColor, isScrollable, markDragSource, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, observeAncestorOpenState, onAncestorReopen, parsePositionArea, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, releaseWheelGesture, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, scrollRoomTowards, setAttribute, setAttributes, setStyles, setVirtualKeyboardOverlaysContent, snapToPixel, startDragTo, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVirtualKeyboardGeometryChange, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, suppressClickAfterGesture, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, waitForPressHeld, watchWheelTravel, wheelGestureIsTakenFrom };
19753
+ export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, claimWheelGesture, clickIsSuppressed, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterIntent, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVirtualKeyboardOverlayHeight, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isPrimaryButtonEvent, isSameColor, isScrollable, isTouchDrivenEvent, markDragSource, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, observeAncestorOpenState, onAncestorReopen, parsePositionArea, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, releaseWheelGesture, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, scrollRoomTowards, setAttribute, setAttributes, setStyles, setVirtualKeyboardOverlaysContent, snapToPixel, startDragTo, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVirtualKeyboardGeometryChange, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, suppressClickAfterGesture, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, waitForPressHeld, watchWheelTravel, wheelGestureIsTakenFrom };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.22",
3
+ "version": "0.17.24",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {