@jsenv/navi 0.29.359 → 0.29.361
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/dev/jsenv_navi.js +98 -14
- package/dist/dev/jsenv_navi.js.map +4 -4
- package/dist/jsenv_navi.js +97 -13
- package/dist/jsenv_navi.js.map +4 -4
- package/docs/interactions.md +8 -2
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, trapScrollInside, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
7
|
+
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, trapScrollInside, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
10
10
|
import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
@@ -4474,9 +4474,12 @@ const findControlHost = (el) => {
|
|
|
4474
4474
|
* `findControlHost` above answers for a control's own DOM (itself, or the native
|
|
4475
4475
|
* element it wraps). This one is for something that is not part of a control but
|
|
4476
4476
|
* has to reach one — an interaction declared on a box (see
|
|
4477
|
-
* interaction/interactions.js), which may be the control, may
|
|
4478
|
-
*
|
|
4479
|
-
* control
|
|
4477
|
+
* interaction/interactions.js), which may be the control, may sit inside it, or
|
|
4478
|
+
* may be a wrapper around exactly one. Nearest wins in that order, and the answer
|
|
4479
|
+
* is null when there is no control the box belongs to: not every box lives in
|
|
4480
|
+
* one, and a box laying out several controls (a row of badges, a toolbar) is
|
|
4481
|
+
* held by none of them — its interactions are its own, and a press on its empty
|
|
4482
|
+
* part is not a press on the first control it happens to contain.
|
|
4480
4483
|
*/
|
|
4481
4484
|
const findNearestControlHost = (el) => {
|
|
4482
4485
|
// Itself, then upwards — the box is inside a button, or is one.
|
|
@@ -4484,8 +4487,22 @@ const findNearestControlHost = (el) => {
|
|
|
4484
4487
|
if (selfOrAncestor) {
|
|
4485
4488
|
return selfOrAncestor;
|
|
4486
4489
|
}
|
|
4487
|
-
// …then downwards
|
|
4488
|
-
|
|
4490
|
+
// …then downwards, only when the box wraps a single control. Counted by
|
|
4491
|
+
// control, not by host: a picker holds the hosts of its popup content, and a
|
|
4492
|
+
// box around that one picker is still around one control.
|
|
4493
|
+
let single = null;
|
|
4494
|
+
for (const host of el.querySelectorAll("[navi-control-host]")) {
|
|
4495
|
+
const controlRoot = host.closest("[navi-control]") || host;
|
|
4496
|
+
const controlAbove = controlRoot.parentElement.closest("[navi-control]");
|
|
4497
|
+
if (controlAbove && el.contains(controlAbove)) {
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
if (single && single !== controlRoot) {
|
|
4501
|
+
return null;
|
|
4502
|
+
}
|
|
4503
|
+
single = controlRoot;
|
|
4504
|
+
}
|
|
4505
|
+
return single ? findControlHost(single) : null;
|
|
4489
4506
|
};
|
|
4490
4507
|
const isControlRoot = (el) => {
|
|
4491
4508
|
return el.hasAttribute("navi-control");
|
|
@@ -10600,9 +10617,10 @@ const interactionsDisputeThePress = (interactions) => {
|
|
|
10600
10617
|
*
|
|
10601
10618
|
* The control is not passed in: it is found from the element, which is what lets
|
|
10602
10619
|
* `interactions` live on a Box rather than on the control itself. A Box that IS a
|
|
10603
|
-
* control (a Button) is its own; a Box
|
|
10604
|
-
* with no control anywhere near it
|
|
10605
|
-
*
|
|
10620
|
+
* control (a Button) is its own; a Box inside one, or wrapping exactly one,
|
|
10621
|
+
* reaches it; a Box with no control anywhere near it — or laying out several,
|
|
10622
|
+
* which belongs to none of them — still answers with a callback of the caller's,
|
|
10623
|
+
* and only "request_action" has nothing to ask.
|
|
10606
10624
|
*
|
|
10607
10625
|
* Set up once per element rather than on every render, which is what lets a
|
|
10608
10626
|
* detector be a plain `setup`/teardown pair. So the interactions themselves are
|
|
@@ -48198,7 +48216,8 @@ const css$R = /* css */`.navi_expandable {
|
|
|
48198
48216
|
}
|
|
48199
48217
|
|
|
48200
48218
|
&:not([aria-expanded="true"])[data-settled] > .navi_expandable_content_container {
|
|
48201
|
-
clip-path:
|
|
48219
|
+
clip-path: none;
|
|
48220
|
+
overflow: clip;
|
|
48202
48221
|
}
|
|
48203
48222
|
|
|
48204
48223
|
&[data-content-scrolls] > .navi_expandable_content_container > .navi_expandable_content_sizer > .navi_expandable_content {
|
|
@@ -48232,6 +48251,7 @@ const useExpandableContext = partName => {
|
|
|
48232
48251
|
* layout?: "row" | "column",
|
|
48233
48252
|
* openDirection?: "down" | "up" | "right" | "left",
|
|
48234
48253
|
* autoFocus?: boolean,
|
|
48254
|
+
* keepInView?: boolean,
|
|
48235
48255
|
* maxContentHeight?: string | number,
|
|
48236
48256
|
* mount?: "always" | "idle" | "from-first-open" | "while-opened",
|
|
48237
48257
|
* arrowKeyShortcuts?: boolean,
|
|
@@ -48289,6 +48309,15 @@ const useExpandableContext = partName => {
|
|
|
48289
48309
|
* came from. An expandable that mounts already open never takes it. Whatever
|
|
48290
48310
|
* the setting, closing while the focus sits inside the content hands it back
|
|
48291
48311
|
* to the UI part (it would otherwise be lost to the closed, inert content).
|
|
48312
|
+
* @param keepInView - Off by default. `true` scrolls the nearest scroller
|
|
48313
|
+
* that can scroll so the content revealed past its edge is on screen: an
|
|
48314
|
+
* expandable opening at the bottom of a list otherwise grows below the
|
|
48315
|
+
* fold, and the user sees the row grow but not what it grew with. Measured
|
|
48316
|
+
* on the open layout and done once per opening, alongside the reveal
|
|
48317
|
+
* (smoothly when `animation` plays, at once otherwise). An expandable
|
|
48318
|
+
* taller than the scroller keeps its UI part on screen and the content
|
|
48319
|
+
* follows as far as it fits. Only the nearest scroller moves — a popup or
|
|
48320
|
+
* a page around it stays where it is.
|
|
48292
48321
|
* @param maxContentHeight - Caps the content height; taller content scrolls
|
|
48293
48322
|
* inside the expandable instead of growing it.
|
|
48294
48323
|
* @param mount - When the content is built and thrown away, a popup's own
|
|
@@ -48324,6 +48353,7 @@ const Expandable = props => {
|
|
|
48324
48353
|
layout,
|
|
48325
48354
|
openDirection,
|
|
48326
48355
|
autoFocus,
|
|
48356
|
+
keepInView = false,
|
|
48327
48357
|
maxContentHeight,
|
|
48328
48358
|
mount = MOUNT_DEFAULT,
|
|
48329
48359
|
arrowKeyShortcuts = true,
|
|
@@ -48341,6 +48371,9 @@ const Expandable = props => {
|
|
|
48341
48371
|
const contentId = useId();
|
|
48342
48372
|
const isColumn = layout === "column";
|
|
48343
48373
|
const closedContentSized = isColumn && mount === "always";
|
|
48374
|
+
// Only the two directions the layout has room for: a column layout reveals
|
|
48375
|
+
// sideways, a stacked one vertically.
|
|
48376
|
+
const revealDirection = isColumn ? openDirection === "left" ? "left" : "right" : openDirection === "up" ? "up" : "down";
|
|
48344
48377
|
const hasAction = Boolean(action);
|
|
48345
48378
|
const effectiveAction = useAction(action);
|
|
48346
48379
|
const {
|
|
@@ -48433,6 +48466,18 @@ const Expandable = props => {
|
|
|
48433
48466
|
} else {
|
|
48434
48467
|
contentElement.style.height = `${finalRect.height}px`;
|
|
48435
48468
|
}
|
|
48469
|
+
if (keepInView) {
|
|
48470
|
+
// Here and not once the reveal settles: the layout is the open one at
|
|
48471
|
+
// this point, so where the content ends is known before the movement
|
|
48472
|
+
// starts, and the scroll can travel along with it. The frozen content
|
|
48473
|
+
// overflowing the collapsed track is still scrollable overflow (the
|
|
48474
|
+
// one-sided clip-path, see the CSS), so the scroller can reach that
|
|
48475
|
+
// position from the first frame.
|
|
48476
|
+
scrollExpandableIntoView(rootRef.current, {
|
|
48477
|
+
revealDirection,
|
|
48478
|
+
behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth"
|
|
48479
|
+
});
|
|
48480
|
+
}
|
|
48436
48481
|
// Put the tracks back where the last paint left them and let the
|
|
48437
48482
|
// transition play from there. In fr — px does not interpolate with fr.
|
|
48438
48483
|
const startFrOf = (startSize, finalSize) => finalSize > 0 ? startSize / finalSize : 0;
|
|
@@ -48487,6 +48532,10 @@ const Expandable = props => {
|
|
|
48487
48532
|
if (revealing) {
|
|
48488
48533
|
armReveal(startRect);
|
|
48489
48534
|
watchSettle();
|
|
48535
|
+
} else if (keepInView && !silent && contentContainer) {
|
|
48536
|
+
scrollExpandableIntoView(rootRef.current, {
|
|
48537
|
+
revealDirection
|
|
48538
|
+
});
|
|
48490
48539
|
}
|
|
48491
48540
|
if (!silent) {
|
|
48492
48541
|
rootRef.current.dispatchEvent(createToggleEvent(true));
|
|
@@ -48667,9 +48716,6 @@ const Expandable = props => {
|
|
|
48667
48716
|
}
|
|
48668
48717
|
};
|
|
48669
48718
|
|
|
48670
|
-
// Only the two directions the layout has room for: a column layout reveals
|
|
48671
|
-
// sideways, a stacked one vertically.
|
|
48672
|
-
const revealDirection = isColumn ? openDirection === "left" ? "left" : "right" : openDirection === "up" ? "up" : "down";
|
|
48673
48719
|
// While open the marker points at the content, while closed always right.
|
|
48674
48720
|
// In a column layout it points back toward the UI part whichever side the
|
|
48675
48721
|
// content took, "right" being the closed direction already.
|
|
@@ -48870,6 +48916,44 @@ Expandable.Content = ExpandableContent;
|
|
|
48870
48916
|
// control, not at the row. The UI part itself matches [role='button'] and is
|
|
48871
48917
|
// the one exception, excluded at the call site.
|
|
48872
48918
|
const UI_INTERACTIVE_SELECTOR = ["a[href]", "button", "input", "select", "textarea", "label", "[role='button']", "[contenteditable='']", "[contenteditable='true']", "audio[controls]", "video[controls]"].join(", ");
|
|
48919
|
+
|
|
48920
|
+
// Scrolls the nearest scroller that can scroll so the whole expandable — UI
|
|
48921
|
+
// part and revealed content — is in view. Measured on the layout as it stands,
|
|
48922
|
+
// so the caller calls it while the open layout is the one laid out (see
|
|
48923
|
+
// openEffect). A root taller than the view is aligned on its UI side instead:
|
|
48924
|
+
// what the user pressed stays on screen, and the content follows as far as it
|
|
48925
|
+
// fits. One scroller only — a popup or a page around it is left where it is.
|
|
48926
|
+
const scrollExpandableIntoView = (rootEl, {
|
|
48927
|
+
revealDirection,
|
|
48928
|
+
behavior
|
|
48929
|
+
}) => {
|
|
48930
|
+
let scrollerEl = null;
|
|
48931
|
+
for (const scrollContainer of getScrollContainerSet(rootEl)) {
|
|
48932
|
+
if (isScrollable(scrollContainer)) {
|
|
48933
|
+
scrollerEl = scrollContainer;
|
|
48934
|
+
break;
|
|
48935
|
+
}
|
|
48936
|
+
}
|
|
48937
|
+
if (!scrollerEl) {
|
|
48938
|
+
return;
|
|
48939
|
+
}
|
|
48940
|
+
const rootRect = rootEl.getBoundingClientRect();
|
|
48941
|
+
let block = "nearest";
|
|
48942
|
+
let inline = "nearest";
|
|
48943
|
+
if (revealDirection === "down" || revealDirection === "up") {
|
|
48944
|
+
if (rootRect.height > scrollerEl.clientHeight) {
|
|
48945
|
+
block = revealDirection === "down" ? "start" : "end";
|
|
48946
|
+
}
|
|
48947
|
+
} else if (rootRect.width > scrollerEl.clientWidth) {
|
|
48948
|
+
inline = revealDirection === "right" ? "start" : "end";
|
|
48949
|
+
}
|
|
48950
|
+
scrollIntoViewScoped(rootEl, {
|
|
48951
|
+
container: scrollerEl,
|
|
48952
|
+
block,
|
|
48953
|
+
inline,
|
|
48954
|
+
behavior
|
|
48955
|
+
});
|
|
48956
|
+
};
|
|
48873
48957
|
const createToggleEvent = open => {
|
|
48874
48958
|
const newState = open ? "open" : "closed";
|
|
48875
48959
|
const oldState = open ? "closed" : "open";
|