@jsenv/navi 0.29.49 → 0.29.51
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_navi.js +281 -146
- package/dist/jsenv_navi.js.map +17 -9
- package/dist/jsenv_navi_side_effects.js +36 -16
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/drag_to_travel.md +97 -8
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel,
|
|
7
|
+
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
8
8
|
export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, batch, useSignal } from "@preact/signals";
|
|
10
10
|
import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
|
|
@@ -19034,6 +19034,43 @@ import.meta.css = [/* css */`
|
|
|
19034
19034
|
--loading-outline-min-inset: 0px;
|
|
19035
19035
|
}
|
|
19036
19036
|
|
|
19037
|
+
/* A scroller inside a box that TRAVELS keeps its leftovers to itself: what a
|
|
19038
|
+
list has left of a gesture once it has reached its end must not reach the
|
|
19039
|
+
page, or the page moves behind a travel that is already answering the same
|
|
19040
|
+
finger. The rule belongs to @jsenv/dom's drag_to_travel — this is the part
|
|
19041
|
+
of it only navi can write.
|
|
19042
|
+
|
|
19043
|
+
What a browser is asked, and where, is not the same everywhere: Blink asks
|
|
19044
|
+
every scroll container between the pointer and the page, so containing the
|
|
19045
|
+
travelling box answers for everything inside it; Gecko and WebKit ask only
|
|
19046
|
+
the ones that actually scroll, so the box — which travels, it does not
|
|
19047
|
+
scroll — is walked past and the SCROLLER itself has to be told. drag_to_
|
|
19048
|
+
travel says it to everything inside the box for those two, and to the box
|
|
19049
|
+
alone on Blink, where saying it to everything turns anything that clips
|
|
19050
|
+
(an ellipsis, a rounded card, the invisible checkbox covering a selectable
|
|
19051
|
+
row) into a dead zone under the wheel — a scroll container with nothing to
|
|
19052
|
+
scroll, and Blink stops at it.
|
|
19053
|
+
|
|
19054
|
+
Which leaves, on Blink, a travelling box that does NOT clip: nothing is
|
|
19055
|
+
asked, and the leftovers reach the page. RouteTravel is that box, and
|
|
19056
|
+
cannot be made to clip — a scroll container there would become the nearest
|
|
19057
|
+
one for every "position: sticky" inside the pages it holds.
|
|
19058
|
+
|
|
19059
|
+
So navi contains what it KNOWS scrolls. [data-scrollable] is worn by a Box
|
|
19060
|
+
that ASKED for overflow auto/scroll (see below), never by one that merely
|
|
19061
|
+
clips: containing it costs nothing on the engines where the rule above
|
|
19062
|
+
already covers it, and on Blink it is what keeps a list inside a travelling
|
|
19063
|
+
page from taking the page with it. What is left over is a scroller nobody
|
|
19064
|
+
declared — a bare div with an overflow of its own, a textarea, a widget
|
|
19065
|
+
from elsewhere: those still hand their leftovers up, on Blink, under a box
|
|
19066
|
+
that does not clip. */
|
|
19067
|
+
[data-drag-travel*="x"] [data-scrollable] {
|
|
19068
|
+
overscroll-behavior-x: contain !important;
|
|
19069
|
+
}
|
|
19070
|
+
[data-drag-travel*="y"] [data-scrollable] {
|
|
19071
|
+
overscroll-behavior-y: contain !important;
|
|
19072
|
+
}
|
|
19073
|
+
|
|
19037
19074
|
[data-scrollable] {
|
|
19038
19075
|
overflow: var(--x-scrollable-overflow, auto);
|
|
19039
19076
|
--box-header-z-index: var(--navi-z-index-sticky);
|
|
@@ -28818,6 +28855,164 @@ const unfreezeSize = (el) => {
|
|
|
28818
28855
|
el.style.height = "";
|
|
28819
28856
|
};
|
|
28820
28857
|
|
|
28858
|
+
/**
|
|
28859
|
+
* Pushing a popup docked to an edge (a side panel, a bottom sheet) back the way
|
|
28860
|
+
* it came in closes it: it follows the finger while it travels, and letting go
|
|
28861
|
+
* either finishes the departure or brings it back to rest — whichever the
|
|
28862
|
+
* travel was already heading to.
|
|
28863
|
+
*
|
|
28864
|
+
* Reading the pointer is not done here: when a press becomes a gesture, which
|
|
28865
|
+
* axis it leans on, how fast it was going when it was let go, who else has a
|
|
28866
|
+
* claim on it (a field, a scroller with room left, a box inside the panel that
|
|
28867
|
+
* travels the same way) is one gesture system for the whole codebase
|
|
28868
|
+
* (@jsenv/dom's startDragToTravel). A panel closing is a travel of exactly one
|
|
28869
|
+
* box, towards the edge it is docked to and nowhere else — which is all this
|
|
28870
|
+
* file has to say.
|
|
28871
|
+
*
|
|
28872
|
+
* The release travel is driven here (Web Animations) rather than handed to the
|
|
28873
|
+
* exit transition the `animation` prop may install: its pace is what is left to
|
|
28874
|
+
* cover, which a CSS transition written for a full-size travel cannot know, and
|
|
28875
|
+
* two movements on the same property would contradict each other. So CSS
|
|
28876
|
+
* transitions are suppressed on the panel for the whole gesture, release
|
|
28877
|
+
* included, and restored once the panel has either left or come back.
|
|
28878
|
+
*/
|
|
28879
|
+
|
|
28880
|
+
|
|
28881
|
+
// What a full-size release travel takes; a shorter one takes proportionally
|
|
28882
|
+
// less, so the panel keeps the same speed whenever the finger let go.
|
|
28883
|
+
const TRAVEL_DURATION = 220;
|
|
28884
|
+
|
|
28885
|
+
// Which way a popup docked to each side travels — read by the panel itself to
|
|
28886
|
+
// say so in the DOM, which is how a box travelling INSIDE it (a row of slides,
|
|
28887
|
+
// a route travel) knows that axis is already walked.
|
|
28888
|
+
const SWIPE_AXIS_BY_SIDE = {
|
|
28889
|
+
left: "x",
|
|
28890
|
+
right: "x",
|
|
28891
|
+
top: "y",
|
|
28892
|
+
bottom: "y",
|
|
28893
|
+
};
|
|
28894
|
+
// Which way the finger pushes to send the panel back where it came from, in
|
|
28895
|
+
// screen coordinates: positive is right/down, the same sign the gesture reports.
|
|
28896
|
+
const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
28897
|
+
|
|
28898
|
+
/**
|
|
28899
|
+
* Builds the `pointerdown` handler a popup docked to `side` answers with.
|
|
28900
|
+
*
|
|
28901
|
+
* `grip` narrows where the gesture may start to one part of the popup, given as
|
|
28902
|
+
* a selector matched inside it: a popup whose whole surface is a place to push
|
|
28903
|
+
* from has nothing else to do with the press, while one made of content the
|
|
28904
|
+
* finger operates (a bottom sheet full of controls) only offers the strip that
|
|
28905
|
+
* is there to be held. A popup that has no such part is pushed from anywhere,
|
|
28906
|
+
* so naming a grip never leaves it undismissable.
|
|
28907
|
+
*/
|
|
28908
|
+
const createSwipeToClose = (side, { grip } = {}) => {
|
|
28909
|
+
const axis = SWIPE_AXIS_BY_SIDE[side];
|
|
28910
|
+
const closeDirection = CLOSE_DIRECTION_BY_SIDE[side];
|
|
28911
|
+
|
|
28912
|
+
return (pointerDownEvent) => {
|
|
28913
|
+
const panelEl = pointerDownEvent.currentTarget;
|
|
28914
|
+
if (panelEl.getAttribute("aria-expanded") !== "true") {
|
|
28915
|
+
return;
|
|
28916
|
+
}
|
|
28917
|
+
if (grip) {
|
|
28918
|
+
const gripEl = panelEl.querySelector(grip);
|
|
28919
|
+
if (gripEl && !gripEl.contains(pointerDownEvent.target)) {
|
|
28920
|
+
return;
|
|
28921
|
+
}
|
|
28922
|
+
}
|
|
28923
|
+
|
|
28924
|
+
// Where the panel stands, written on it directly: the gesture reports a
|
|
28925
|
+
// distance in screen coordinates, which is exactly what a translate takes.
|
|
28926
|
+
const paint = (distance) => {
|
|
28927
|
+
panelEl.style.translate =
|
|
28928
|
+
axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
|
|
28929
|
+
};
|
|
28930
|
+
const restore = () => {
|
|
28931
|
+
panelEl.style.translate = "";
|
|
28932
|
+
panelEl.style.transitionProperty = "";
|
|
28933
|
+
panelEl.style.userSelect = "";
|
|
28934
|
+
};
|
|
28935
|
+
// The resting value is written first and the animation only covers the way
|
|
28936
|
+
// to it: both end on the same number, so nothing is seen changing when the
|
|
28937
|
+
// animation hands the panel back to its own style.
|
|
28938
|
+
const travelTo = (from, to, onArrival) => {
|
|
28939
|
+
const covered = from > to ? from - to : to - from;
|
|
28940
|
+
paint(to);
|
|
28941
|
+
const animation = panelEl.animate(
|
|
28942
|
+
[
|
|
28943
|
+
{ translate: translateOf(axis, from) },
|
|
28944
|
+
{ translate: translateOf(axis, to) },
|
|
28945
|
+
],
|
|
28946
|
+
{
|
|
28947
|
+
duration: (covered / sizeOf(panelEl, axis)) * TRAVEL_DURATION,
|
|
28948
|
+
easing: "ease-out",
|
|
28949
|
+
},
|
|
28950
|
+
);
|
|
28951
|
+
animation.finished.then(onArrival, () => {});
|
|
28952
|
+
};
|
|
28953
|
+
const close = (event) => {
|
|
28954
|
+
triggerNaviCommand(panelEl, "--navi-close", event);
|
|
28955
|
+
if (panelEl.getAttribute("aria-expanded") === "true") {
|
|
28956
|
+
// Refused: the panel is staying, so it goes back where it was.
|
|
28957
|
+
travelTo(sizeOf(panelEl, axis) * closeDirection, 0, restore);
|
|
28958
|
+
return;
|
|
28959
|
+
}
|
|
28960
|
+
restore();
|
|
28961
|
+
};
|
|
28962
|
+
|
|
28963
|
+
startDragToTravel(pointerDownEvent, {
|
|
28964
|
+
element: panelEl,
|
|
28965
|
+
axes: axis,
|
|
28966
|
+
onStart: ({ sign, target }) => {
|
|
28967
|
+
if (sign !== closeDirection) {
|
|
28968
|
+
// Pushing the panel further in is not a travel: there is nowhere that
|
|
28969
|
+
// way, and reading it as one would make the panel lean at every
|
|
28970
|
+
// gesture that starts by going the wrong way.
|
|
28971
|
+
return false;
|
|
28972
|
+
}
|
|
28973
|
+
const size = sizeOf(panelEl, axis);
|
|
28974
|
+
if (!size) {
|
|
28975
|
+
return false;
|
|
28976
|
+
}
|
|
28977
|
+
// Something else with a better claim on the gesture: a scroller between
|
|
28978
|
+
// the finger and the panel, with room left that way.
|
|
28979
|
+
if (scrollRoomTowards(target, panelEl, axis, sign)) {
|
|
28980
|
+
return false;
|
|
28981
|
+
}
|
|
28982
|
+
panelEl.style.transitionProperty = "none";
|
|
28983
|
+
panelEl.style.userSelect = "none";
|
|
28984
|
+
// What the first pixels of the gesture may have started selecting is
|
|
28985
|
+
// not a selection, it is the beginning of this travel.
|
|
28986
|
+
window.getSelection().removeAllRanges();
|
|
28987
|
+
// The way out is the only way there is anything: pulling the other way
|
|
28988
|
+
// leans against a wall and comes back.
|
|
28989
|
+
return {
|
|
28990
|
+
size,
|
|
28991
|
+
travelBack: closeDirection > 0,
|
|
28992
|
+
travelOn: closeDirection < 0,
|
|
28993
|
+
};
|
|
28994
|
+
},
|
|
28995
|
+
onPull: ({ pulled }) => {
|
|
28996
|
+
paint(pulled);
|
|
28997
|
+
},
|
|
28998
|
+
onEnd: ({ pulled, size, travels, event }) => {
|
|
28999
|
+
if (travels) {
|
|
29000
|
+
travelTo(pulled, size * closeDirection, () => close(event));
|
|
29001
|
+
return;
|
|
29002
|
+
}
|
|
29003
|
+
travelTo(pulled, 0, restore);
|
|
29004
|
+
},
|
|
29005
|
+
});
|
|
29006
|
+
};
|
|
29007
|
+
};
|
|
29008
|
+
|
|
29009
|
+
const sizeOf = (element, axis) => {
|
|
29010
|
+
const rect = element.getBoundingClientRect();
|
|
29011
|
+
return axis === "x" ? rect.width : rect.height;
|
|
29012
|
+
};
|
|
29013
|
+
const translateOf = (axis, distance) =>
|
|
29014
|
+
axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
|
|
29015
|
+
|
|
28821
29016
|
installImportMetaCssBuild(import.meta);/**
|
|
28822
29017
|
* A dialog is a surface, not a control: it holds no value, has no action and
|
|
28823
29018
|
* aggregates nothing. Fields and a submit go in a `<Form>` inside it, exactly
|
|
@@ -29109,6 +29304,16 @@ const css$V = /* css */`
|
|
|
29109
29304
|
&:has(> [data-focus-outline-delegate][data-focus-visible]) {
|
|
29110
29305
|
outline-style: solid;
|
|
29111
29306
|
}
|
|
29307
|
+
/* The header of a sheet that closes by being pushed back down is a handle,
|
|
29308
|
+
not a piece of the scroll: a finger on it moves the sheet, and letting
|
|
29309
|
+
the browser scroll the sheet under the same gesture would show two
|
|
29310
|
+
movements answering one drag. Direct children only — the sheet's own
|
|
29311
|
+
header, not one belonging to something it contains (the same part
|
|
29312
|
+
swipe_to_close.js takes hold of). */
|
|
29313
|
+
&[data-swipe-to-close] > [data-header] {
|
|
29314
|
+
touch-action: none;
|
|
29315
|
+
}
|
|
29316
|
+
|
|
29112
29317
|
/* …and the delegate stands down. */
|
|
29113
29318
|
> [data-focus-outline-delegate] {
|
|
29114
29319
|
--navi-focus-outline-style: none;
|
|
@@ -29250,7 +29455,9 @@ const css$V = /* css */`
|
|
|
29250
29455
|
* desktop window, which is still a mouse. It supplies defaults for
|
|
29251
29456
|
* `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`, so
|
|
29252
29457
|
* any of them can still be pinned explicitly. Re-resolves live as the pointer
|
|
29253
|
-
* type or the window size changes.
|
|
29458
|
+
* type or the window size changes. A sheet resting on the bottom edge is also
|
|
29459
|
+
* pushed back down to close it, held by its header (a direct child `Box` with
|
|
29460
|
+
* the `header` prop) — or from anywhere when it has none. See `swipe_to_close.js`.
|
|
29254
29461
|
* @param {string} [props.positionArea="center"] - Where to dock the dialog
|
|
29255
29462
|
* within its container (the viewport for `layer="top"`, the positioned
|
|
29256
29463
|
* ancestor for `layer="local"`) — Dialog is never anchored to a real
|
|
@@ -29504,6 +29711,13 @@ const DOCKED = {
|
|
|
29504
29711
|
scrollCapture: true
|
|
29505
29712
|
};
|
|
29506
29713
|
|
|
29714
|
+
// Where a bottom sheet is held to push it back down: the strip a direct-child
|
|
29715
|
+
// Box declares with `header` — the rest of the sheet is content the finger
|
|
29716
|
+
// operates, and a drag started there would fight whatever it landed on. A sheet
|
|
29717
|
+
// with no header of its own is pushed from anywhere (see createSwipeToClose's
|
|
29718
|
+
// own `grip`).
|
|
29719
|
+
const DOCKED_SWIPE_GRIP = ":scope > [data-header]";
|
|
29720
|
+
|
|
29507
29721
|
// The first control inside `dialogEl` that is mid-action, if any. Walks the
|
|
29508
29722
|
// controls rather than reading an attribute off the dialog: a dialog carries no
|
|
29509
29723
|
// state of its own (see this file's top comment), and `aria-busy` on the
|
|
@@ -29654,6 +29868,14 @@ const useDialogProps = props => {
|
|
|
29654
29868
|
y: "center",
|
|
29655
29869
|
x: "center"
|
|
29656
29870
|
};
|
|
29871
|
+
// Pushing the sheet back down closes it — a bottom sheet is reached with a
|
|
29872
|
+
// thumb, and the thumb is already on the edge it would push. Only a sheet
|
|
29873
|
+
// actually resting on the bottom edge: anywhere else the gesture would send
|
|
29874
|
+
// the dialog somewhere it never came from.
|
|
29875
|
+
const swipeToCloseDown = isDocked && parsedPositionArea.y === "bottom";
|
|
29876
|
+
const onSwipePointerDown = swipeToCloseDown ? createSwipeToClose("bottom", {
|
|
29877
|
+
grip: DOCKED_SWIPE_GRIP
|
|
29878
|
+
}) : null;
|
|
29657
29879
|
// A corner sitting exactly on the container's own corner must not be
|
|
29658
29880
|
// rounded: the gap a radius carves out would show the container through it,
|
|
29659
29881
|
// reading as a rendering glitch rather than as a rounded box. A corner is on
|
|
@@ -30151,6 +30373,16 @@ const useDialogProps = props => {
|
|
|
30151
30373
|
"data-flush-right": flushEdges.right ? "" : undefined,
|
|
30152
30374
|
"data-flush-bottom": flushEdges.bottom ? "" : undefined,
|
|
30153
30375
|
"data-flush-left": flushEdges.left ? "" : undefined,
|
|
30376
|
+
"data-swipe-to-close": swipeToCloseDown ? "" : undefined,
|
|
30377
|
+
// The axis the sheet travels on when it is pushed back, said to the shared
|
|
30378
|
+
// gesture layer: it is what a box travelling inside the sheet reads to know
|
|
30379
|
+
// this axis is already walked (see @jsenv/dom's drag_to_travel).
|
|
30380
|
+
"data-drag-travel": swipeToCloseDown ? SWIPE_AXIS_BY_SIDE.bottom : undefined,
|
|
30381
|
+
"data-travel-by-drag": swipeToCloseDown ? SWIPE_AXIS_BY_SIDE.bottom : undefined,
|
|
30382
|
+
"onPointerDown": e => {
|
|
30383
|
+
rest.onPointerDown?.(e);
|
|
30384
|
+
onSwipePointerDown?.(e);
|
|
30385
|
+
},
|
|
30154
30386
|
"onKeyDown": e => {
|
|
30155
30387
|
onKeyDown?.(e);
|
|
30156
30388
|
onKeyDownShortcuts(e);
|
|
@@ -48954,6 +49186,21 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
48954
49186
|
* a component of your own wrapping one. Nothing here assumes the children ARE
|
|
48955
49187
|
* the slides, so nothing breaks when they are not.
|
|
48956
49188
|
*
|
|
49189
|
+
* Only slides go in it, so everything drawn AROUND the travel is written around
|
|
49190
|
+
* the box and reaches it by id: a chevron pinned to the edge of a full-screen
|
|
49191
|
+
* viewer, a "3 / 8" counter under it, a tab bar. Two things are said to such an
|
|
49192
|
+
* element, and both are needed for it to feel part of the same thing:
|
|
49193
|
+
* `commandFor={id}` on a button, which is how it asks for a travel
|
|
49194
|
+
* (--navi-left/--navi-right/--navi-first/…), and
|
|
49195
|
+
* `data-slide-container-follows={id}` on whatever holds them, which makes it a
|
|
49196
|
+
* follower: the travel's progress is painted onto it to draw with, and the
|
|
49197
|
+
* arrows walk the slides wherever the focus is inside it — otherwise they stop
|
|
49198
|
+
* working the moment one Tabs onto the chevron that walks them. Say it on the
|
|
49199
|
+
* outermost element of the surface — the <Dialog> itself rather than a box
|
|
49200
|
+
* inside it — since that is what holds the keyboard when nothing in it does.
|
|
49201
|
+
* <Nav slideContainer={id}> is a tab bar built out of exactly that. See the
|
|
49202
|
+
* full-screen section of the demo.
|
|
49203
|
+
*
|
|
48957
49204
|
* @param {object} props
|
|
48958
49205
|
* @param {"row"|"column"|string[]} [props.layout="row"] - where the slides are.
|
|
48959
49206
|
* A word for a line — "row" to the right, "column" downwards, both in DOM
|
|
@@ -50467,6 +50714,38 @@ const SlideContainer = ({
|
|
|
50467
50714
|
handler: e => travelled(goToEnd(true, e))
|
|
50468
50715
|
}
|
|
50469
50716
|
});
|
|
50717
|
+
// …and they are the same shortcuts wherever they are pressed in what follows
|
|
50718
|
+
// this box: a shortcut only ever reaches what has the focus, and a way out
|
|
50719
|
+
// drawn BESIDE the container — a chevron in the column next to it, a counter
|
|
50720
|
+
// one can Tab to — is not inside it. So the arrows that walk the slides stop
|
|
50721
|
+
// walking the moment one Tabs onto the chevron that walks them. A follower
|
|
50722
|
+
// already says which box it is about (data-slide-container-follows), which is
|
|
50723
|
+
// the same thing said for the same reason.
|
|
50724
|
+
// Read on every render: a follower appearing is not a render of this box, and
|
|
50725
|
+
// the list is refreshed by the layout effect above, which runs first.
|
|
50726
|
+
useLayoutEffect(() => {
|
|
50727
|
+
const followerElements = followerElementsRef.current;
|
|
50728
|
+
if (followerElements.length === 0) {
|
|
50729
|
+
return undefined;
|
|
50730
|
+
}
|
|
50731
|
+
const onFollowerKeyDown = keyDownEvent => {
|
|
50732
|
+
// Already answered: a follower AROUND this box — the frame of a
|
|
50733
|
+
// full-screen surface, holding the slides and the ways out — hears the
|
|
50734
|
+
// same press bubbling out of it.
|
|
50735
|
+
if (containerRef.current?.contains(keyDownEvent.target)) {
|
|
50736
|
+
return;
|
|
50737
|
+
}
|
|
50738
|
+
onKeyDownShortcuts(keyDownEvent);
|
|
50739
|
+
};
|
|
50740
|
+
for (const followerElement of followerElements) {
|
|
50741
|
+
followerElement.addEventListener("keydown", onFollowerKeyDown);
|
|
50742
|
+
}
|
|
50743
|
+
return () => {
|
|
50744
|
+
for (const followerElement of followerElements) {
|
|
50745
|
+
followerElement.removeEventListener("keydown", onFollowerKeyDown);
|
|
50746
|
+
}
|
|
50747
|
+
};
|
|
50748
|
+
});
|
|
50470
50749
|
return (
|
|
50471
50750
|
// Box rather than a plain div: it is how every navi component takes the
|
|
50472
50751
|
// onnavi_* handlers below — they are navi's own event names, and Box is
|
|
@@ -70769,150 +71048,6 @@ const ViewportLayout = props => {
|
|
|
70769
71048
|
});
|
|
70770
71049
|
};
|
|
70771
71050
|
|
|
70772
|
-
/**
|
|
70773
|
-
* Pushing a side panel back the way it came in closes it: the panel follows
|
|
70774
|
-
* the finger while it travels, and letting go either finishes the departure or
|
|
70775
|
-
* brings it back to rest — whichever the travel was already heading to.
|
|
70776
|
-
*
|
|
70777
|
-
* Reading the pointer is not done here: when a press becomes a gesture, which
|
|
70778
|
-
* axis it leans on, how fast it was going when it was let go, who else has a
|
|
70779
|
-
* claim on it (a field, a scroller with room left, a box inside the panel that
|
|
70780
|
-
* travels the same way) is one gesture system for the whole codebase
|
|
70781
|
-
* (@jsenv/dom's startDragToTravel). A panel closing is a travel of exactly one
|
|
70782
|
-
* box, towards the edge it is docked to and nowhere else — which is all this
|
|
70783
|
-
* file has to say.
|
|
70784
|
-
*
|
|
70785
|
-
* The release travel is driven here (Web Animations) rather than handed to the
|
|
70786
|
-
* exit transition the `animation` prop may install: its pace is what is left to
|
|
70787
|
-
* cover, which a CSS transition written for a full-size travel cannot know, and
|
|
70788
|
-
* two movements on the same property would contradict each other. So CSS
|
|
70789
|
-
* transitions are suppressed on the panel for the whole gesture, release
|
|
70790
|
-
* included, and restored once the panel has either left or come back.
|
|
70791
|
-
*/
|
|
70792
|
-
|
|
70793
|
-
|
|
70794
|
-
// What a full-size release travel takes; a shorter one takes proportionally
|
|
70795
|
-
// less, so the panel keeps the same speed whenever the finger let go.
|
|
70796
|
-
const TRAVEL_DURATION = 220;
|
|
70797
|
-
|
|
70798
|
-
// Which way a panel docked to each side travels — read by the panel itself to
|
|
70799
|
-
// say so in the DOM, which is how a box travelling INSIDE it (a row of slides,
|
|
70800
|
-
// a route travel) knows that axis is already walked.
|
|
70801
|
-
const SWIPE_AXIS_BY_SIDE = {
|
|
70802
|
-
left: "x",
|
|
70803
|
-
right: "x",
|
|
70804
|
-
top: "y",
|
|
70805
|
-
bottom: "y",
|
|
70806
|
-
};
|
|
70807
|
-
// Which way the finger pushes to send the panel back where it came from, in
|
|
70808
|
-
// screen coordinates: positive is right/down, the same sign the gesture reports.
|
|
70809
|
-
const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
70810
|
-
|
|
70811
|
-
/**
|
|
70812
|
-
* Builds the `pointerdown` handler a side panel docked to `side` answers with.
|
|
70813
|
-
*/
|
|
70814
|
-
const createSwipeToClose = (side) => {
|
|
70815
|
-
const axis = SWIPE_AXIS_BY_SIDE[side];
|
|
70816
|
-
const closeDirection = CLOSE_DIRECTION_BY_SIDE[side];
|
|
70817
|
-
|
|
70818
|
-
return (pointerDownEvent) => {
|
|
70819
|
-
const panelEl = pointerDownEvent.currentTarget;
|
|
70820
|
-
if (panelEl.getAttribute("aria-expanded") !== "true") {
|
|
70821
|
-
return;
|
|
70822
|
-
}
|
|
70823
|
-
|
|
70824
|
-
// Where the panel stands, written on it directly: the gesture reports a
|
|
70825
|
-
// distance in screen coordinates, which is exactly what a translate takes.
|
|
70826
|
-
const paint = (distance) => {
|
|
70827
|
-
panelEl.style.translate =
|
|
70828
|
-
axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
|
|
70829
|
-
};
|
|
70830
|
-
const restore = () => {
|
|
70831
|
-
panelEl.style.translate = "";
|
|
70832
|
-
panelEl.style.transitionProperty = "";
|
|
70833
|
-
panelEl.style.userSelect = "";
|
|
70834
|
-
};
|
|
70835
|
-
// The resting value is written first and the animation only covers the way
|
|
70836
|
-
// to it: both end on the same number, so nothing is seen changing when the
|
|
70837
|
-
// animation hands the panel back to its own style.
|
|
70838
|
-
const travelTo = (from, to, onArrival) => {
|
|
70839
|
-
const covered = from > to ? from - to : to - from;
|
|
70840
|
-
paint(to);
|
|
70841
|
-
const animation = panelEl.animate(
|
|
70842
|
-
[
|
|
70843
|
-
{ translate: translateOf(axis, from) },
|
|
70844
|
-
{ translate: translateOf(axis, to) },
|
|
70845
|
-
],
|
|
70846
|
-
{
|
|
70847
|
-
duration: (covered / sizeOf(panelEl, axis)) * TRAVEL_DURATION,
|
|
70848
|
-
easing: "ease-out",
|
|
70849
|
-
},
|
|
70850
|
-
);
|
|
70851
|
-
animation.finished.then(onArrival, () => {});
|
|
70852
|
-
};
|
|
70853
|
-
const close = (event) => {
|
|
70854
|
-
triggerNaviCommand(panelEl, "--navi-close", event);
|
|
70855
|
-
if (panelEl.getAttribute("aria-expanded") === "true") {
|
|
70856
|
-
// Refused: the panel is staying, so it goes back where it was.
|
|
70857
|
-
travelTo(sizeOf(panelEl, axis) * closeDirection, 0, restore);
|
|
70858
|
-
return;
|
|
70859
|
-
}
|
|
70860
|
-
restore();
|
|
70861
|
-
};
|
|
70862
|
-
|
|
70863
|
-
startDragToTravel(pointerDownEvent, {
|
|
70864
|
-
element: panelEl,
|
|
70865
|
-
axes: axis,
|
|
70866
|
-
onStart: ({ sign, target }) => {
|
|
70867
|
-
if (sign !== closeDirection) {
|
|
70868
|
-
// Pushing the panel further in is not a travel: there is nowhere that
|
|
70869
|
-
// way, and reading it as one would make the panel lean at every
|
|
70870
|
-
// gesture that starts by going the wrong way.
|
|
70871
|
-
return false;
|
|
70872
|
-
}
|
|
70873
|
-
const size = sizeOf(panelEl, axis);
|
|
70874
|
-
if (!size) {
|
|
70875
|
-
return false;
|
|
70876
|
-
}
|
|
70877
|
-
// Something else with a better claim on the gesture: a scroller between
|
|
70878
|
-
// the finger and the panel, with room left that way.
|
|
70879
|
-
if (scrollRoomTowards(target, panelEl, axis, sign)) {
|
|
70880
|
-
return false;
|
|
70881
|
-
}
|
|
70882
|
-
panelEl.style.transitionProperty = "none";
|
|
70883
|
-
panelEl.style.userSelect = "none";
|
|
70884
|
-
// What the first pixels of the gesture may have started selecting is
|
|
70885
|
-
// not a selection, it is the beginning of this travel.
|
|
70886
|
-
window.getSelection().removeAllRanges();
|
|
70887
|
-
// The way out is the only way there is anything: pulling the other way
|
|
70888
|
-
// leans against a wall and comes back.
|
|
70889
|
-
return {
|
|
70890
|
-
size,
|
|
70891
|
-
travelBack: closeDirection > 0,
|
|
70892
|
-
travelOn: closeDirection < 0,
|
|
70893
|
-
};
|
|
70894
|
-
},
|
|
70895
|
-
onPull: ({ pulled }) => {
|
|
70896
|
-
paint(pulled);
|
|
70897
|
-
},
|
|
70898
|
-
onEnd: ({ pulled, size, travels, event }) => {
|
|
70899
|
-
if (travels) {
|
|
70900
|
-
travelTo(pulled, size * closeDirection, () => close(event));
|
|
70901
|
-
return;
|
|
70902
|
-
}
|
|
70903
|
-
travelTo(pulled, 0, restore);
|
|
70904
|
-
},
|
|
70905
|
-
});
|
|
70906
|
-
};
|
|
70907
|
-
};
|
|
70908
|
-
|
|
70909
|
-
const sizeOf = (element, axis) => {
|
|
70910
|
-
const rect = element.getBoundingClientRect();
|
|
70911
|
-
return axis === "x" ? rect.width : rect.height;
|
|
70912
|
-
};
|
|
70913
|
-
const translateOf = (axis, distance) =>
|
|
70914
|
-
axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
|
|
70915
|
-
|
|
70916
71051
|
installImportMetaCssBuild(import.meta);/**
|
|
70917
71052
|
* A drawer docked flush to a viewport (or container) edge, built on top of
|
|
70918
71053
|
* `Popup`. Sizing, the perpendicular-axis fill, and the flush-edge
|