@jsenv/navi 0.29.322 → 0.29.324
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/README.md +2 -0
- package/dist/dev/jsenv_navi.js +366 -229
- package/dist/dev/jsenv_navi.js.map +21 -21
- package/dist/jsenv_navi.js +362 -229
- package/dist/jsenv_navi.js.map +21 -21
- package/docs/AI_INSTRUCTIONS.md +5 -3
- package/docs/popup_backdrop.md +31 -0
- package/docs/popup_open.md +45 -0
- package/docs/scroll.md +6 -4
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -7,7 +7,7 @@ export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
|
7
7
|
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, getVirtualKeyboardOverlayHeight, 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
|
-
import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, Fragment as Fragment$1 } from "preact";
|
|
10
|
+
import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
11
11
|
import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
|
|
12
12
|
import { humanizeI18n, prefixFirstAndIndentRemainingLines, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
|
|
13
13
|
export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
|
|
@@ -32476,8 +32476,26 @@ const resolveExplicitTarget = (element) => {
|
|
|
32476
32476
|
}
|
|
32477
32477
|
return undefined;
|
|
32478
32478
|
};
|
|
32479
|
+
// The control around the source that a command can be about: one holding a
|
|
32480
|
+
// value of its own. A popup is a control root too (it answers interactions,
|
|
32481
|
+
// carries a callout), but it has no host of its own — asked for one, it
|
|
32482
|
+
// answers with the first control INSIDE it, which for a field sending
|
|
32483
|
+
// --navi-update from a picker's popup is that very field: the command then
|
|
32484
|
+
// sets the field to what it already holds, which fires the command again,
|
|
32485
|
+
// without end. Such a surface is walked past, up to the picker or the form.
|
|
32479
32486
|
const resolveFirstParentControl = (el) => {
|
|
32480
|
-
|
|
32487
|
+
let parentControl = getParentControl(el);
|
|
32488
|
+
while (parentControl && !ownsControlHost(parentControl)) {
|
|
32489
|
+
parentControl = getParentControl(parentControl);
|
|
32490
|
+
}
|
|
32491
|
+
return parentControl;
|
|
32492
|
+
};
|
|
32493
|
+
const ownsControlHost = (controlRoot) => {
|
|
32494
|
+
const controlHost = findControlHost(controlRoot);
|
|
32495
|
+
return (
|
|
32496
|
+
Boolean(controlHost) &&
|
|
32497
|
+
controlHost.closest("[navi-control]") === controlRoot
|
|
32498
|
+
);
|
|
32481
32499
|
};
|
|
32482
32500
|
const resolveFirstChildControl = (el) => {
|
|
32483
32501
|
let startEl;
|
|
@@ -34689,19 +34707,27 @@ const writeInSignal = (signal, value, { history }) => {
|
|
|
34689
34707
|
// is open (`?seat=<gameId>` over a list of cards), and closed is the signal
|
|
34690
34708
|
// holding none of their values — `undefined`, which a state signal reads as
|
|
34691
34709
|
// its default.
|
|
34710
|
+
//
|
|
34711
|
+
// A popup without a `value` reads anything the signal holds — save `false`,
|
|
34712
|
+
// `null` and `undefined` — as open: a single sheet bound to `?open=<id>` is
|
|
34713
|
+
// open on whichever id the address names, and the id is the popup's content
|
|
34714
|
+
// to read, not its to write. Opening writes `true` only into a signal that
|
|
34715
|
+
// reads closed; closing writes `false` where the signal held `true`, so a
|
|
34716
|
+
// boolean signal keeps its boolean, and `undefined` for anything else.
|
|
34692
34717
|
const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
34693
|
-
|
|
34718
|
+
const signalValue = signal.peek();
|
|
34719
|
+
if (readOpened(signalValue, popupValue) === opened) {
|
|
34694
34720
|
// The signal already says so, meaning this open/close IS what it asked
|
|
34695
34721
|
// for: a back press that took the popup out of the url, the application
|
|
34696
34722
|
// writing it. Nothing to write back — and nothing to go back to either,
|
|
34697
34723
|
// since the navigation navBack would undo is the one that asked for this.
|
|
34698
34724
|
return;
|
|
34699
34725
|
}
|
|
34700
|
-
const closedValue = popupValue === undefined ? false : undefined;
|
|
34701
34726
|
if (opened) {
|
|
34702
34727
|
signal.value = popupValue === undefined ? true : popupValue;
|
|
34703
34728
|
return;
|
|
34704
34729
|
}
|
|
34730
|
+
const closedValue = readClosedValue(signalValue);
|
|
34705
34731
|
if (
|
|
34706
34732
|
signal.options?.getHistory?.() === "push" &&
|
|
34707
34733
|
// Nothing of this document behind: the popup was opened by the url itself
|
|
@@ -34722,10 +34748,15 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
|
34722
34748
|
};
|
|
34723
34749
|
const readOpened = (signalValue, popupValue) => {
|
|
34724
34750
|
if (popupValue === undefined) {
|
|
34725
|
-
return
|
|
34751
|
+
return (
|
|
34752
|
+
signalValue !== undefined && signalValue !== null && signalValue !== false
|
|
34753
|
+
);
|
|
34726
34754
|
}
|
|
34727
34755
|
return signalValue === popupValue;
|
|
34728
34756
|
};
|
|
34757
|
+
const readClosedValue = (openSignalValue) => {
|
|
34758
|
+
return openSignalValue === true ? false : undefined;
|
|
34759
|
+
};
|
|
34729
34760
|
|
|
34730
34761
|
/**
|
|
34731
34762
|
* Keeps an open controller in sync with where the caller says the popup should
|
|
@@ -34780,6 +34811,13 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
34780
34811
|
: signal
|
|
34781
34812
|
? readOpened(signal.value, value)
|
|
34782
34813
|
: props.open;
|
|
34814
|
+
// What the signal holds while it reads open — `true`, the popup's `value`,
|
|
34815
|
+
// or the id a single sheet is open on. Put back when a close is refused,
|
|
34816
|
+
// since by then the signal has been written closed.
|
|
34817
|
+
const openSignalValueRef = useRef(undefined);
|
|
34818
|
+
if (signal && open) {
|
|
34819
|
+
openSignalValueRef.current = signal.value;
|
|
34820
|
+
}
|
|
34783
34821
|
// Assigned on every render, like openEffect, so it always closes over the
|
|
34784
34822
|
// latest prop: a popup that opens or closes on its own (Escape, backdrop, a
|
|
34785
34823
|
// --navi-close command) writes what happened where the caller keeps it, so
|
|
@@ -34860,10 +34898,11 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
34860
34898
|
// request already reads in the signal, and with a `value` what the
|
|
34861
34899
|
// signal holds may be another popup's, which the write would erase.
|
|
34862
34900
|
if (signal && openController.opened !== open) {
|
|
34863
|
-
const opened = openController.opened;
|
|
34864
34901
|
writeInSignal(
|
|
34865
34902
|
signal,
|
|
34866
|
-
|
|
34903
|
+
openController.opened
|
|
34904
|
+
? openSignalValueRef.current
|
|
34905
|
+
: readClosedValue(signal.peek()),
|
|
34867
34906
|
{ history: "replace" },
|
|
34868
34907
|
);
|
|
34869
34908
|
}
|
|
@@ -35257,6 +35296,21 @@ const warnPopupHasNoElementToOpen = (popupKind) => {
|
|
|
35257
35296
|
*/
|
|
35258
35297
|
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
35259
35298
|
|
|
35299
|
+
/**
|
|
35300
|
+
* `data-navi-popup-inside` is the other direction: a box of the PAGE the
|
|
35301
|
+
* caller says belongs to a popup, so a press on it is not outside that popup
|
|
35302
|
+
* even though it lies beyond the border box — a card whose press is what
|
|
35303
|
+
* fills the one panel of a board. Its value names the popup by `id`, the way
|
|
35304
|
+
* `commandfor` does (several ids, space separated), because the exemption is
|
|
35305
|
+
* one popup's: a menu open beside the board still closes on the same press.
|
|
35306
|
+
*
|
|
35307
|
+
* Unlike the outside marker it answers for its whole subtree: the outside
|
|
35308
|
+
* marker carves free space out of a surface whose controls stay surface, this
|
|
35309
|
+
* one adds a whole thing — the card, its count, its buttons — to the popup's
|
|
35310
|
+
* own ground, and nothing in it is a dismissal.
|
|
35311
|
+
*/
|
|
35312
|
+
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
35313
|
+
|
|
35260
35314
|
/**
|
|
35261
35315
|
* What a press landing on a region the caller declared as not-its-surface
|
|
35262
35316
|
* does: exactly what the same press on the backdrop would do.
|
|
@@ -35403,6 +35457,14 @@ const armOutsidePressClose = (
|
|
|
35403
35457
|
) {
|
|
35404
35458
|
return;
|
|
35405
35459
|
}
|
|
35460
|
+
if (
|
|
35461
|
+
popupEl.id &&
|
|
35462
|
+
pointerDownEvent.target.closest?.(
|
|
35463
|
+
`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
|
|
35464
|
+
)
|
|
35465
|
+
) {
|
|
35466
|
+
return;
|
|
35467
|
+
}
|
|
35406
35468
|
// Real DOM containment wins over the rectangle below — an element
|
|
35407
35469
|
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
35408
35470
|
// absolutely-positioned child) can be painted outside its border box, and
|
|
@@ -37410,6 +37472,46 @@ const acknowledgeOwnAction = (controller) => {
|
|
|
37410
37472
|
controller.acknowledgeUIState();
|
|
37411
37473
|
};
|
|
37412
37474
|
|
|
37475
|
+
// What a group of checkboxes or radios is worth is a claim about values, and
|
|
37476
|
+
// a value whose box is not there cannot be contradicted by that box: a row
|
|
37477
|
+
// scrolled out of a virtualized list, a filtered-out option, a whole page being
|
|
37478
|
+
// parked while a route action reruns — its children leave one at a time, and
|
|
37479
|
+
// each partial reading would otherwise be taken for the answer and written
|
|
37480
|
+
// into whatever the group is bound to (an array signal emptied down to `[]`
|
|
37481
|
+
// by its own unmount). `kept` is what the group holds as it is asked; a value
|
|
37482
|
+
// in it stays until a DRAWN child carrying that value says otherwise.
|
|
37483
|
+
//
|
|
37484
|
+
// Read off the group rather than remembered here on purpose: a value put ON
|
|
37485
|
+
// the group (a `value` prop, a signal, a reopened popup) replaces the whole
|
|
37486
|
+
// selection, and a private memory would go on holding the rows it could not
|
|
37487
|
+
// see being unselected.
|
|
37488
|
+
const keptAndCheckedChildUIStates = (children, fallbackState, kept) => {
|
|
37489
|
+
const drawnValues = new Set(children.map((child) => child.props.value));
|
|
37490
|
+
const values = Array.isArray(kept)
|
|
37491
|
+
? kept.filter((value) => !drawnValues.has(value))
|
|
37492
|
+
: [];
|
|
37493
|
+
for (const child of children) {
|
|
37494
|
+
if (child.uiState !== undefined) {
|
|
37495
|
+
values.push(child.uiState);
|
|
37496
|
+
}
|
|
37497
|
+
}
|
|
37498
|
+
return values.length === 0 ? undefined : values;
|
|
37499
|
+
};
|
|
37500
|
+
const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
|
|
37501
|
+
for (const child of children) {
|
|
37502
|
+
if (child.uiState !== undefined) {
|
|
37503
|
+
return child.uiState;
|
|
37504
|
+
}
|
|
37505
|
+
}
|
|
37506
|
+
// No drawn child claims it. If the child that held it IS drawn, it was really
|
|
37507
|
+
// deselected; if it is not, the group keeps what it holds.
|
|
37508
|
+
const keptIsDrawn = children.some((child) => child.props.value === kept);
|
|
37509
|
+
if (keptIsDrawn) {
|
|
37510
|
+
return undefined;
|
|
37511
|
+
}
|
|
37512
|
+
return kept;
|
|
37513
|
+
};
|
|
37514
|
+
|
|
37413
37515
|
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
37414
37516
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
37415
37517
|
// distributeChildUIState. If neither a default nor an explicit impl is found for a
|
|
@@ -37418,7 +37520,7 @@ const GROUP_DEFAULTS = {
|
|
|
37418
37520
|
radio_group: {
|
|
37419
37521
|
childControlFilter: (child) =>
|
|
37420
37522
|
child.controlType === "input" && child.controlHostProps?.type === "radio",
|
|
37421
|
-
aggregateChildStates:
|
|
37523
|
+
aggregateChildStates: keptOrFirstDefinedChildUIState,
|
|
37422
37524
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
37423
37525
|
const childSelected = childUIStateController.props.value === newUIState;
|
|
37424
37526
|
if (childSelected) {
|
|
@@ -37431,16 +37533,7 @@ const GROUP_DEFAULTS = {
|
|
|
37431
37533
|
childControlFilter: (child) =>
|
|
37432
37534
|
child.controlType === "input" &&
|
|
37433
37535
|
child.controlHostProps?.type === "checkbox",
|
|
37434
|
-
aggregateChildStates:
|
|
37435
|
-
const values = [];
|
|
37436
|
-
for (const child of children) {
|
|
37437
|
-
const childUIState = child.uiState;
|
|
37438
|
-
if (childUIState !== undefined) {
|
|
37439
|
-
values.push(childUIState);
|
|
37440
|
-
}
|
|
37441
|
-
}
|
|
37442
|
-
return values.length === 0 ? undefined : values;
|
|
37443
|
-
},
|
|
37536
|
+
aggregateChildStates: keptAndCheckedChildUIStates,
|
|
37444
37537
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
37445
37538
|
const childSelected =
|
|
37446
37539
|
Array.isArray(newUIState) &&
|
|
@@ -48792,33 +48885,6 @@ const createToggleEvent = open => {
|
|
|
48792
48885
|
return toggleEvent;
|
|
48793
48886
|
};
|
|
48794
48887
|
|
|
48795
|
-
const NextResolverContext = createContext(null);
|
|
48796
|
-
const useNextResolver = () => useContext(NextResolverContext);
|
|
48797
|
-
|
|
48798
|
-
/**
|
|
48799
|
-
* Creates a renderComponent function that passes props through a chain of resolvers.
|
|
48800
|
-
* Each resolver is a Preact component rendered in sequence (hooks are allowed).
|
|
48801
|
-
* To pass through to the next resolver, call useNextResolver() and render the
|
|
48802
|
-
* returned Next component with the desired props.
|
|
48803
|
-
* To terminate the chain early (e.g. render a specialized component), render
|
|
48804
|
-
* directly without calling Next.
|
|
48805
|
-
*
|
|
48806
|
-
* The last entry in the array is the final/target component — it receives null
|
|
48807
|
-
* from useNextResolver() indicating it is terminal.
|
|
48808
|
-
*
|
|
48809
|
-
* Usage:
|
|
48810
|
-
* const renderButton = createComponentResolver([ResolverA, ResolverB, ButtonTarget]);
|
|
48811
|
-
* // Then inside a component render:
|
|
48812
|
-
* renderButton(props)
|
|
48813
|
-
*
|
|
48814
|
-
* Each position of the chain has a runner of its own, defined once: it renders
|
|
48815
|
-
* its resolver under a NextResolverContext holding the runner after it. A
|
|
48816
|
-
* resolver that re-renders on its own and renders <Next> therefore resumes at
|
|
48817
|
-
* its own position without any bookkeeping — the Next it was given is the one
|
|
48818
|
-
* made for it. A chain rendered by the hundred (a list's rows) pays two
|
|
48819
|
-
* components per position, the runner and the resolver, so nothing here is a
|
|
48820
|
-
* component that merely forwards.
|
|
48821
|
-
*/
|
|
48822
48888
|
const createComponentResolver = (resolvers, {
|
|
48823
48889
|
pure
|
|
48824
48890
|
} = {}) => {
|
|
@@ -48827,13 +48893,10 @@ const createComponentResolver = (resolvers, {
|
|
|
48827
48893
|
for (let index = 0; index < resolvers.length; index++) {
|
|
48828
48894
|
const Resolver = resolvers[index];
|
|
48829
48895
|
const isLast = index === lastIndex;
|
|
48830
|
-
|
|
48831
|
-
|
|
48832
|
-
|
|
48833
|
-
|
|
48834
|
-
})
|
|
48835
|
-
});
|
|
48836
|
-
Runner.displayName = `${Resolver.displayName || Resolver.name}Runner`;
|
|
48896
|
+
function Runner(props) {
|
|
48897
|
+
return runResolverBody(this, Resolver, props, isLast ? null : runners[index + 1]);
|
|
48898
|
+
}
|
|
48899
|
+
Runner.displayName = Resolver.displayName || Resolver.name;
|
|
48837
48900
|
runners.push(Runner);
|
|
48838
48901
|
}
|
|
48839
48902
|
const FirstRunner = runners[0];
|
|
@@ -48864,6 +48927,54 @@ const createComponentResolver = (resolvers, {
|
|
|
48864
48927
|
}
|
|
48865
48928
|
return PureRenderComponent;
|
|
48866
48929
|
};
|
|
48930
|
+
const useNextResolver = () => {
|
|
48931
|
+
if (currentNext === OUTSIDE_RESOLVER_BODY) {
|
|
48932
|
+
throw new Error("useNextResolver() called outside a resolver body: a resolver rendered by another resolver is rendered with renderResolver(Resolver, props), not as a JSX child.");
|
|
48933
|
+
}
|
|
48934
|
+
return currentNext;
|
|
48935
|
+
};
|
|
48936
|
+
const renderResolver = (Resolver, props) => {
|
|
48937
|
+
if (currentNext === OUTSIDE_RESOLVER_BODY) {
|
|
48938
|
+
throw new Error(`renderResolver(${Resolver.displayName || Resolver.name}) called outside a resolver body: only a resolver can render another one.`);
|
|
48939
|
+
}
|
|
48940
|
+
let SubRunner = subRunnerByResolver.get(Resolver);
|
|
48941
|
+
if (!SubRunner) {
|
|
48942
|
+
SubRunner = function (props) {
|
|
48943
|
+
const {
|
|
48944
|
+
[NEXT_PROP]: next,
|
|
48945
|
+
...ownProps
|
|
48946
|
+
} = props;
|
|
48947
|
+
return runResolverBody(this, Resolver, ownProps, next);
|
|
48948
|
+
};
|
|
48949
|
+
SubRunner.displayName = Resolver.displayName || Resolver.name;
|
|
48950
|
+
subRunnerByResolver.set(Resolver, SubRunner);
|
|
48951
|
+
}
|
|
48952
|
+
return createElement(SubRunner, {
|
|
48953
|
+
...props,
|
|
48954
|
+
[NEXT_PROP]: currentNext
|
|
48955
|
+
});
|
|
48956
|
+
};
|
|
48957
|
+
|
|
48958
|
+
// What useNextResolver() answers: the runner after the one whose body is
|
|
48959
|
+
// running, null for the last — and, outside any body, a value that is neither.
|
|
48960
|
+
const OUTSIDE_RESOLVER_BODY = Symbol("outside_resolver_body");
|
|
48961
|
+
let currentNext = OUTSIDE_RESOLVER_BODY;
|
|
48962
|
+
const runResolverBody = (instance, Resolver, props, next) => {
|
|
48963
|
+
const nextBefore = currentNext;
|
|
48964
|
+
currentNext = next;
|
|
48965
|
+
try {
|
|
48966
|
+
return Resolver.call(instance, props);
|
|
48967
|
+
} finally {
|
|
48968
|
+
currentNext = nextBefore;
|
|
48969
|
+
}
|
|
48970
|
+
};
|
|
48971
|
+
|
|
48972
|
+
// One runner per resolver rendered through renderResolver, made on first use:
|
|
48973
|
+
// the same component type on every render, so a resolver picked again is
|
|
48974
|
+
// updated in place and another one picked is a remount, like any child.
|
|
48975
|
+
const subRunnerByResolver = new WeakMap();
|
|
48976
|
+
// Rides on the props of a sub-runner, stripped before the body sees them.
|
|
48977
|
+
const NEXT_PROP = "navi-resolver-next";
|
|
48867
48978
|
function pureShouldComponentUpdate(nextProps) {
|
|
48868
48979
|
return shallowDiffers(this.props, nextProps);
|
|
48869
48980
|
}
|
|
@@ -49689,9 +49800,7 @@ const ButtonConfirm = ({
|
|
|
49689
49800
|
const ButtonRouteResolver = props => {
|
|
49690
49801
|
const Next = useNextResolver();
|
|
49691
49802
|
if (props.route) {
|
|
49692
|
-
return
|
|
49693
|
-
...props
|
|
49694
|
-
});
|
|
49803
|
+
return renderResolver(ButtonWithRoute, props);
|
|
49695
49804
|
}
|
|
49696
49805
|
return jsx(Next, {
|
|
49697
49806
|
...props
|
|
@@ -52295,9 +52404,7 @@ const installInputCss = () => {
|
|
|
52295
52404
|
const InputModeResolver = props => {
|
|
52296
52405
|
const Next = useNextResolver();
|
|
52297
52406
|
if (props.inputMode === "numeric" || props.inputMode === "decimal") {
|
|
52298
|
-
return
|
|
52299
|
-
...props
|
|
52300
|
-
});
|
|
52407
|
+
return renderResolver(InputModeNumericOrDecimal, props);
|
|
52301
52408
|
}
|
|
52302
52409
|
return jsx(Next, {
|
|
52303
52410
|
...props
|
|
@@ -52525,39 +52632,25 @@ const InputTypeResolver = props => {
|
|
|
52525
52632
|
// very same "icon while empty, clear button once filled" affordance without
|
|
52526
52633
|
// pretending to be a search box.
|
|
52527
52634
|
if (props.clearable && props.type !== "search") {
|
|
52528
|
-
return
|
|
52529
|
-
...props
|
|
52530
|
-
});
|
|
52635
|
+
return renderResolver(InputClearable, props);
|
|
52531
52636
|
}
|
|
52532
52637
|
if (props.type === "search") {
|
|
52533
|
-
return
|
|
52534
|
-
...props
|
|
52535
|
-
});
|
|
52638
|
+
return renderResolver(InputSearch, props);
|
|
52536
52639
|
}
|
|
52537
52640
|
if (props.type === "email") {
|
|
52538
|
-
return
|
|
52539
|
-
...props
|
|
52540
|
-
});
|
|
52641
|
+
return renderResolver(InputEmail, props);
|
|
52541
52642
|
}
|
|
52542
52643
|
if (props.type === "tel") {
|
|
52543
|
-
return
|
|
52544
|
-
...props
|
|
52545
|
-
});
|
|
52644
|
+
return renderResolver(InputTel, props);
|
|
52546
52645
|
}
|
|
52547
52646
|
if (props.type === "number") {
|
|
52548
|
-
return
|
|
52549
|
-
...props
|
|
52550
|
-
});
|
|
52647
|
+
return renderResolver(InputNumber, props);
|
|
52551
52648
|
}
|
|
52552
52649
|
if (props.type === "color") {
|
|
52553
|
-
return
|
|
52554
|
-
...props
|
|
52555
|
-
});
|
|
52650
|
+
return renderResolver(InputColor, props);
|
|
52556
52651
|
}
|
|
52557
52652
|
if (props.type === "datetime-local") {
|
|
52558
|
-
return
|
|
52559
|
-
...props
|
|
52560
|
-
});
|
|
52653
|
+
return renderResolver(InputDatetimeLocal, props);
|
|
52561
52654
|
}
|
|
52562
52655
|
return jsx(Next, {
|
|
52563
52656
|
...props
|
|
@@ -52670,9 +52763,7 @@ const InputDatetimeLocal = props => {
|
|
|
52670
52763
|
const InputWithListResolver = props => {
|
|
52671
52764
|
const Next = useNextResolver();
|
|
52672
52765
|
if (props["navi-list"]) {
|
|
52673
|
-
return
|
|
52674
|
-
...props
|
|
52675
|
-
});
|
|
52766
|
+
return renderResolver(InputWithList, props);
|
|
52676
52767
|
}
|
|
52677
52768
|
return jsx(Next, {
|
|
52678
52769
|
...props
|
|
@@ -52779,9 +52870,7 @@ const ChevronDownSvg$1 = () => {
|
|
|
52779
52870
|
const InputWithSuggestionsResolver = props => {
|
|
52780
52871
|
const Next = useNextResolver();
|
|
52781
52872
|
if (props["navi-suggestions"]) {
|
|
52782
|
-
return
|
|
52783
|
-
...props
|
|
52784
|
-
});
|
|
52873
|
+
return renderResolver(InputTextualWithSuggestions, props);
|
|
52785
52874
|
}
|
|
52786
52875
|
return jsx(Next, {
|
|
52787
52876
|
...props
|
|
@@ -60553,14 +60642,15 @@ const css$E = /* css */`
|
|
|
60553
60642
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
60554
60643
|
* that element, which outranks whatever the dialog says).
|
|
60555
60644
|
* @param {boolean} [props.open] - Controlled open state.
|
|
60556
|
-
* @param {import("@preact/signals").Signal<
|
|
60645
|
+
* @param {import("@preact/signals").Signal<any>} [props.signal] - The open
|
|
60557
60646
|
* state said the way every navi control says it: the dialog opens and closes
|
|
60558
60647
|
* to match the signal, and writes into it whenever it opens or closes on its
|
|
60559
60648
|
* own (Escape, backdrop, a --navi-close command) — one binding to both drive
|
|
60560
60649
|
* the dialog and know where it is, and the state stays where the app put it.
|
|
60561
60650
|
* Excludes `open`; `onOpen`/`onClose` still fire. A signal holding `true` at
|
|
60562
60651
|
* mount behaves like `defaultOpen`: the dialog was already open, no entrance
|
|
60563
|
-
* plays.
|
|
60652
|
+
* plays. A signal holding an id (one dialog showing whichever card
|
|
60653
|
+
* the address names) reads as open, and the dialog leaves the id alone.
|
|
60564
60654
|
* @param {any} [props.value] - What `signal` holds while THIS dialog is
|
|
60565
60655
|
* open, for several of them sharing one signal that says which is open
|
|
60566
60656
|
* (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
|
|
@@ -62214,14 +62304,15 @@ const css$D = /* css */`
|
|
|
62214
62304
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
62215
62305
|
* that element, which outranks whatever the popover says).
|
|
62216
62306
|
* @param {boolean} [props.open] - Controlled open state.
|
|
62217
|
-
* @param {import("@preact/signals").Signal<
|
|
62307
|
+
* @param {import("@preact/signals").Signal<any>} [props.signal] - The open
|
|
62218
62308
|
* state said the way every navi control says it: the popover opens and closes
|
|
62219
62309
|
* to match the signal, and writes into it whenever it opens or closes on its
|
|
62220
62310
|
* own (Escape, light dismiss, a --navi-close command) — one binding to both
|
|
62221
62311
|
* drive the popover and know where it is, and the state stays where the app
|
|
62222
62312
|
* put it. Excludes `open`; `onOpen`/`onClose` still fire. A signal holding
|
|
62223
62313
|
* `true` at mount behaves like `defaultOpen`: the popover was already open,
|
|
62224
|
-
* no entrance plays.
|
|
62314
|
+
* no entrance plays. A signal holding an id (one popover showing whichever card
|
|
62315
|
+
* the address names) reads as open, and the popover leaves the id alone.
|
|
62225
62316
|
* @param {any} [props.value] - What `signal` holds while THIS popover is
|
|
62226
62317
|
* open, for several of them sharing one signal that says which is open
|
|
62227
62318
|
* (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
|
|
@@ -63663,9 +63754,7 @@ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
|
|
|
63663
63754
|
const PickerCustomResolver = props => {
|
|
63664
63755
|
import.meta.css = [css$B, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
|
|
63665
63756
|
if (props.children === undefined) {
|
|
63666
|
-
return
|
|
63667
|
-
...props
|
|
63668
|
-
});
|
|
63757
|
+
return renderResolver(PickerNative, props);
|
|
63669
63758
|
}
|
|
63670
63759
|
if (props.mode === "callout") {
|
|
63671
63760
|
// A tooltip is an icon one presses, unless told otherwise. Own-property
|
|
@@ -63727,14 +63816,12 @@ const PickerCustomResolver = props => {
|
|
|
63727
63816
|
// how a field says its value is a JS one, kept beside the DOM (see
|
|
63728
63817
|
// controller_registry.js) — the same thing type="array"/"object" already
|
|
63729
63818
|
// say for their shapes.
|
|
63730
|
-
return
|
|
63819
|
+
return renderResolver(PickerCustom, {
|
|
63731
63820
|
...props,
|
|
63732
63821
|
type: "navi_js"
|
|
63733
63822
|
});
|
|
63734
63823
|
}
|
|
63735
|
-
return
|
|
63736
|
-
...props
|
|
63737
|
-
});
|
|
63824
|
+
return renderResolver(PickerCustom, props);
|
|
63738
63825
|
};
|
|
63739
63826
|
const PickerNative = props => {
|
|
63740
63827
|
const Next = useNextResolver();
|
|
@@ -64360,9 +64447,9 @@ const PickerCustom = props => {
|
|
|
64360
64447
|
});
|
|
64361
64448
|
}
|
|
64362
64449
|
}
|
|
64363
|
-
return
|
|
64450
|
+
return renderResolver(PickerContentInsidePopup, {
|
|
64364
64451
|
...pickerProps,
|
|
64365
|
-
mode
|
|
64452
|
+
mode
|
|
64366
64453
|
});
|
|
64367
64454
|
};
|
|
64368
64455
|
|
|
@@ -65629,14 +65716,10 @@ const createItemTracker = (onChange) => {
|
|
|
65629
65716
|
const ListItemHeaderOrFooterResolver = props => {
|
|
65630
65717
|
const Next = useNextResolver();
|
|
65631
65718
|
if (props.header) {
|
|
65632
|
-
return
|
|
65633
|
-
...props
|
|
65634
|
-
});
|
|
65719
|
+
return renderResolver(ListItemHeader, props);
|
|
65635
65720
|
}
|
|
65636
65721
|
if (props.footer) {
|
|
65637
|
-
return
|
|
65638
|
-
...props
|
|
65639
|
-
});
|
|
65722
|
+
return renderResolver(ListItemFooter, props);
|
|
65640
65723
|
}
|
|
65641
65724
|
return jsx(Next, {
|
|
65642
65725
|
...props
|
|
@@ -65868,9 +65951,7 @@ const ListSelectableContext = createContext(false);
|
|
|
65868
65951
|
const ListSelectableResolver = props => {
|
|
65869
65952
|
const Next = useNextResolver();
|
|
65870
65953
|
if (props.selectable) {
|
|
65871
|
-
return
|
|
65872
|
-
...props
|
|
65873
|
-
});
|
|
65954
|
+
return renderResolver(ListSelectable, props);
|
|
65874
65955
|
}
|
|
65875
65956
|
return jsx(ListSelectableContext.Provider, {
|
|
65876
65957
|
value: false,
|
|
@@ -65892,45 +65973,6 @@ const ListSelectable = props => {
|
|
|
65892
65973
|
focusGroupDirection,
|
|
65893
65974
|
focusGroupWrap
|
|
65894
65975
|
} = props;
|
|
65895
|
-
// `kept` is what the list holds as it is asked, which is not the same as what
|
|
65896
|
-
// its rows say: a list draws the rows it needs and no more, so the selected
|
|
65897
|
-
// one may be scrolled out of the window or filtered out of the view, and a
|
|
65898
|
-
// row that is not there cannot say it is not selected. Reading it off the
|
|
65899
|
-
// group rather than remembering it here is what makes a value put ON the list
|
|
65900
|
-
// (a `value` prop, a signal, a reopened popup) replace the whole selection —
|
|
65901
|
-
// a private memory of its own would go on holding the rows it could not see
|
|
65902
|
-
// being unselected.
|
|
65903
|
-
//
|
|
65904
|
-
// `fallbackState` is the empty of the shape the list declared below
|
|
65905
|
-
// (`stateType`): `[]` for a multiple list, nothing for a single one. Taking
|
|
65906
|
-
// it is what lets an emptied list say "empty" — `undefined` is the word for
|
|
65907
|
-
// "unset", and a bound stateSignal reads that as "nothing decided here, go
|
|
65908
|
-
// back to the default" (see docs/control_value.md), which is how a list
|
|
65909
|
-
// emptied down to its last row puts that row back on reload.
|
|
65910
|
-
const aggregateChildStates = (children, fallbackState, kept) => {
|
|
65911
|
-
if (multiple) {
|
|
65912
|
-
const drawnValues = new Set(children.map(child => child.props.value));
|
|
65913
|
-
const stillSelected = Array.isArray(kept) ? kept.filter(value => !drawnValues.has(value)) : [];
|
|
65914
|
-
for (const child of children) {
|
|
65915
|
-
if (child.uiState !== undefined) {
|
|
65916
|
-
stillSelected.push(child.uiState);
|
|
65917
|
-
}
|
|
65918
|
-
}
|
|
65919
|
-
return stillSelected.length === 0 ? fallbackState : stillSelected;
|
|
65920
|
-
}
|
|
65921
|
-
for (const child of children) {
|
|
65922
|
-
if (child.uiState !== undefined) {
|
|
65923
|
-
return child.uiState;
|
|
65924
|
-
}
|
|
65925
|
-
}
|
|
65926
|
-
// No drawn row claims it. If the row that held it IS drawn, it was really
|
|
65927
|
-
// deselected; if it is not, the list keeps what it holds.
|
|
65928
|
-
const keptIsDrawn = children.some(child => child.props.value === kept);
|
|
65929
|
-
if (keptIsDrawn) {
|
|
65930
|
-
return undefined;
|
|
65931
|
-
}
|
|
65932
|
-
return kept;
|
|
65933
|
-
};
|
|
65934
65976
|
const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps({
|
|
65935
65977
|
// The defaults of the group this IS (see CheckboxGroup / RadioGroup):
|
|
65936
65978
|
// what a selectable list shows is a claim about what was accepted, so
|
|
@@ -65942,9 +65984,15 @@ const ListSelectable = props => {
|
|
|
65942
65984
|
resetOnError: true,
|
|
65943
65985
|
...props
|
|
65944
65986
|
}, {
|
|
65987
|
+
// A list draws the rows it needs and no more, so a selected row may be
|
|
65988
|
+
// scrolled out of the window or filtered out of the view: the default
|
|
65989
|
+
// aggregate of these two groups keeps a value whose row is not drawn
|
|
65990
|
+
// (see GROUP_DEFAULTS in ui_state_controller.js). `stateType` is what
|
|
65991
|
+
// an emptied multiple list says — `[]`, not `undefined`, which a bound
|
|
65992
|
+
// stateSignal reads as "nothing decided here, go back to the default"
|
|
65993
|
+
// (see docs/control_value.md).
|
|
65945
65994
|
stateType: multiple ? "array" : "",
|
|
65946
|
-
controlType: multiple ? "checkbox_group" : "radio_group"
|
|
65947
|
-
aggregateChildStates
|
|
65995
|
+
controlType: multiple ? "checkbox_group" : "radio_group"
|
|
65948
65996
|
});
|
|
65949
65997
|
const uiGroupStateController = getUIStateControllerById(listControlProps.id);
|
|
65950
65998
|
useFocusGroup(ref, {
|
|
@@ -66203,7 +66251,7 @@ const ListItemSelectableResolver = props => {
|
|
|
66203
66251
|
const isHeaderOrFooter = Boolean(props.header || props.footer);
|
|
66204
66252
|
const selectable = props.selectable ?? (isHeaderOrFooter || props.role === "presentation" ? false : listSelectable);
|
|
66205
66253
|
if (selectable) {
|
|
66206
|
-
return
|
|
66254
|
+
return renderResolver(ListItemSelectable, {
|
|
66207
66255
|
...props,
|
|
66208
66256
|
selectable: true
|
|
66209
66257
|
});
|
|
@@ -67146,6 +67194,7 @@ const ListUI = props => {
|
|
|
67146
67194
|
ref,
|
|
67147
67195
|
tracker,
|
|
67148
67196
|
renderBudget,
|
|
67197
|
+
renderBudgetSteady: renderBudgetAfterPaint,
|
|
67149
67198
|
virtualItemSize,
|
|
67150
67199
|
virtual,
|
|
67151
67200
|
scrolled,
|
|
@@ -67473,6 +67522,7 @@ const useListScrollSync = ({
|
|
|
67473
67522
|
ref,
|
|
67474
67523
|
tracker,
|
|
67475
67524
|
renderBudget,
|
|
67525
|
+
renderBudgetSteady,
|
|
67476
67526
|
virtualItemSize,
|
|
67477
67527
|
virtual,
|
|
67478
67528
|
scrolled,
|
|
@@ -68226,31 +68276,36 @@ const useListScrollSync = ({
|
|
|
68226
68276
|
}
|
|
68227
68277
|
});
|
|
68228
68278
|
|
|
68229
|
-
//
|
|
68230
|
-
|
|
68231
|
-
|
|
68232
|
-
|
|
68233
|
-
|
|
68279
|
+
// The window the visible band asks for, decided from geometry: the band's
|
|
68280
|
+
// two edges in row units, from where the scroller's viewport cuts the list
|
|
68281
|
+
// and the row size. What has to be drawn is everything between the edges,
|
|
68282
|
+
// plus what the scroll is about to bring — so the window is judged on the
|
|
68283
|
+
// rows it keeps AHEAD of the band, in the direction the user goes, and moves
|
|
68284
|
+
// once those fall under half a screen. Re-framed, it puts three quarters of
|
|
68285
|
+
// its spare rows ahead and one quarter behind: a window centred on the band
|
|
68286
|
+
// is due to move again as soon as the band has crossed the few rows it kept
|
|
68287
|
+
// ahead, and each move is rows drawn while the user waits for them.
|
|
68288
|
+
// Without a row size there is no geometry to reason on; the row under a
|
|
68289
|
+
// probe stands in for the band, framed the old way.
|
|
68290
|
+
const scrollDirectionRef = useRef(1);
|
|
68291
|
+
const windowSlidRef = useRef(false);
|
|
68292
|
+
const budgetWarnedRef = useRef(false);
|
|
68293
|
+
const evaluateWindow = reason => {
|
|
68294
|
+
const total = virtual.totalSignal.peek();
|
|
68295
|
+
if (total <= renderBudget) {
|
|
68296
|
+
return;
|
|
68234
68297
|
}
|
|
68235
68298
|
const scrollerEl = getScroller();
|
|
68236
68299
|
const listEl = getListEl();
|
|
68237
|
-
|
|
68238
|
-
|
|
68239
|
-
|
|
68240
|
-
|
|
68241
|
-
|
|
68242
|
-
|
|
68243
|
-
|
|
68244
|
-
|
|
68245
|
-
|
|
68246
|
-
reportPosition();
|
|
68247
|
-
return;
|
|
68248
|
-
}
|
|
68249
|
-
reportPosition();
|
|
68250
|
-
const total = virtual.totalSignal.peek();
|
|
68251
|
-
if (total <= renderBudget) {
|
|
68252
|
-
return;
|
|
68253
|
-
}
|
|
68300
|
+
if (!scrollerEl || !listEl) {
|
|
68301
|
+
return;
|
|
68302
|
+
}
|
|
68303
|
+
const {
|
|
68304
|
+
start,
|
|
68305
|
+
end
|
|
68306
|
+
} = renderWindowRef.current;
|
|
68307
|
+
const virtualItemSize = virtualItemSizeSignal.peek();
|
|
68308
|
+
if (virtualItemSize === 0) {
|
|
68254
68309
|
const scrollInfo = getScrollInfo({
|
|
68255
68310
|
scrollValues: {
|
|
68256
68311
|
left: scrollerEl.scrollLeft,
|
|
@@ -68267,17 +68322,8 @@ const useListScrollSync = ({
|
|
|
68267
68322
|
return;
|
|
68268
68323
|
}
|
|
68269
68324
|
const {
|
|
68270
|
-
index
|
|
68271
|
-
reason
|
|
68325
|
+
index
|
|
68272
68326
|
} = scrollInfo;
|
|
68273
|
-
// Recentering on every row crossed would rebuild the whole window a few
|
|
68274
|
-
// times a second, and a window rebuilt is every row of it rendered
|
|
68275
|
-
// again. It only moves once what is on screen comes near one of its
|
|
68276
|
-
// edges — until then, it already holds what has to be drawn.
|
|
68277
|
-
const {
|
|
68278
|
-
start,
|
|
68279
|
-
end
|
|
68280
|
-
} = renderWindowRef.current;
|
|
68281
68327
|
const margin = Math.floor(renderBudget / 4);
|
|
68282
68328
|
const farFromStart = index - start >= margin || start === 0;
|
|
68283
68329
|
const farFromEnd = end - index > margin || end === total;
|
|
@@ -68285,12 +68331,117 @@ const useListScrollSync = ({
|
|
|
68285
68331
|
return;
|
|
68286
68332
|
}
|
|
68287
68333
|
const half = Math.floor(renderBudget / 2);
|
|
68288
|
-
let newStart =
|
|
68289
|
-
let newEnd =
|
|
68290
|
-
if (newEnd
|
|
68291
|
-
|
|
68334
|
+
let newStart = index - half < 0 ? 0 : index - half;
|
|
68335
|
+
let newEnd = newStart + renderBudget;
|
|
68336
|
+
if (newEnd > total) {
|
|
68337
|
+
newEnd = total;
|
|
68338
|
+
newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
|
|
68339
|
+
}
|
|
68340
|
+
windowSlidRef.current = true;
|
|
68341
|
+
updateRenderWindow(newStart, newEnd, `${reason}: ${scrollInfo.reason}`);
|
|
68342
|
+
return;
|
|
68343
|
+
}
|
|
68344
|
+
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
68345
|
+
const listRect = listEl.getBoundingClientRect();
|
|
68346
|
+
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
68347
|
+
const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
|
|
68348
|
+
const listFrom = horizontal ? listRect.left : listRect.top;
|
|
68349
|
+
let bandStart = Math.floor((viewportFrom - listFrom) / virtualItemSize);
|
|
68350
|
+
// Exclusive, like the window's end.
|
|
68351
|
+
let bandEnd = Math.ceil((viewportTo - listFrom) / virtualItemSize);
|
|
68352
|
+
if (bandStart < 0) {
|
|
68353
|
+
bandStart = 0;
|
|
68354
|
+
}
|
|
68355
|
+
if (bandEnd > total) {
|
|
68356
|
+
bandEnd = total;
|
|
68357
|
+
}
|
|
68358
|
+
if (bandEnd <= bandStart) {
|
|
68359
|
+
return;
|
|
68360
|
+
}
|
|
68361
|
+
const visibleCount = bandEnd - bandStart;
|
|
68362
|
+
const spare = renderBudget - visibleCount;
|
|
68363
|
+
if (!budgetWarnedRef.current && renderBudget === renderBudgetSteady && spare < 2) {
|
|
68364
|
+
budgetWarnedRef.current = true;
|
|
68365
|
+
console.warn(`List: renderBudget=${renderBudget} draws ${renderBudget} rows and this scroller shows ${visibleCount} at once: rows will go blank as it scrolls. Give it room for a screen ahead — ${visibleCount * 2} or more.`);
|
|
68366
|
+
}
|
|
68367
|
+
const behind = spare > 0 ? Math.floor(spare / 4) : 0;
|
|
68368
|
+
const ahead = spare > 0 ? spare - behind : 0;
|
|
68369
|
+
const halfScreen = Math.ceil(visibleCount / 2);
|
|
68370
|
+
const aheadNeeded = halfScreen < ahead / 2 ? halfScreen : Math.floor(ahead / 2);
|
|
68371
|
+
const behindNeeded = halfScreen < behind / 2 ? halfScreen : Math.floor(behind / 2);
|
|
68372
|
+
const forward = scrollDirectionRef.current > 0;
|
|
68373
|
+
const rowsAheadOfBand = forward ? end - bandEnd : bandStart - start;
|
|
68374
|
+
const rowsBehindBand = forward ? bandStart - start : end - bandEnd;
|
|
68375
|
+
const atEdgeAhead = forward ? end === total : start === 0;
|
|
68376
|
+
const atEdgeBehind = forward ? start === 0 : end === total;
|
|
68377
|
+
const coversBand = start <= bandStart && end >= bandEnd;
|
|
68378
|
+
const coversAhead = atEdgeAhead || rowsAheadOfBand >= aheadNeeded;
|
|
68379
|
+
const coversBehind = atEdgeBehind || rowsBehindBand >= behindNeeded;
|
|
68380
|
+
if (coversBand && coversAhead && coversBehind) {
|
|
68381
|
+
return;
|
|
68382
|
+
}
|
|
68383
|
+
let newStart = forward ? bandStart - behind : bandEnd + behind - renderBudget;
|
|
68384
|
+
if (newStart < 0) {
|
|
68385
|
+
newStart = 0;
|
|
68386
|
+
}
|
|
68387
|
+
let newEnd = newStart + renderBudget;
|
|
68388
|
+
if (newEnd > total) {
|
|
68389
|
+
newEnd = total;
|
|
68390
|
+
newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
|
|
68391
|
+
}
|
|
68392
|
+
windowSlidRef.current = true;
|
|
68393
|
+
updateRenderWindow(newStart, newEnd, `${reason}: rows ${bandStart}-${bandEnd} on screen, going ${forward ? "forward" : "backward"}`);
|
|
68394
|
+
};
|
|
68395
|
+
// A slide is judged again once its rows are laid out: the scroll had moved
|
|
68396
|
+
// on while the rows were being drawn, or the anchoring of this very commit
|
|
68397
|
+
// moved it, and a window that stops short of the screen's edge with no
|
|
68398
|
+
// scroll event to come would stay short for good. On the next frame, after
|
|
68399
|
+
// the layout this commit's anchoring settles on.
|
|
68400
|
+
useLayoutEffect(() => {
|
|
68401
|
+
if (!windowSlidRef.current) {
|
|
68402
|
+
return undefined;
|
|
68403
|
+
}
|
|
68404
|
+
windowSlidRef.current = false;
|
|
68405
|
+
const frameId = requestAnimationFrame(() => {
|
|
68406
|
+
evaluateWindow("after slide");
|
|
68407
|
+
});
|
|
68408
|
+
return () => {
|
|
68409
|
+
cancelAnimationFrame(frameId);
|
|
68410
|
+
};
|
|
68411
|
+
}, [renderWindow]);
|
|
68412
|
+
|
|
68413
|
+
// Scroll listener — slides the window as the user scrolls.
|
|
68414
|
+
useLayoutEffect(() => {
|
|
68415
|
+
const listContainerEl = ref.current;
|
|
68416
|
+
if (!listContainerEl) {
|
|
68417
|
+
return undefined;
|
|
68418
|
+
}
|
|
68419
|
+
const scrollerEl = getScroller();
|
|
68420
|
+
const onScroll = () => {
|
|
68421
|
+
// Which way the user is going, read before the position is stored: what
|
|
68422
|
+
// the window keeps ahead is ahead of this.
|
|
68423
|
+
const previousScroll = currentScrollRef.current;
|
|
68424
|
+
if (previousScroll) {
|
|
68425
|
+
const delta = horizontal ? scrollerEl.scrollLeft - previousScroll.left : scrollerEl.scrollTop - previousScroll.top;
|
|
68426
|
+
if (delta > 0) {
|
|
68427
|
+
scrollDirectionRef.current = 1;
|
|
68428
|
+
} else if (delta < 0) {
|
|
68429
|
+
scrollDirectionRef.current = -1;
|
|
68430
|
+
}
|
|
68431
|
+
}
|
|
68432
|
+
updateCurrentScroll();
|
|
68433
|
+
// Where the user is now is where things must be held from now on.
|
|
68434
|
+
anchorRef.current = null;
|
|
68435
|
+
if (scrolledByListRef.current) {
|
|
68436
|
+
// The window stays where it is — the position it would be re-derived
|
|
68437
|
+
// from was chosen to keep the rows still — but where the list is has
|
|
68438
|
+
// genuinely changed, and whoever keeps that position must hear it.
|
|
68439
|
+
scrolledByListRef.current = false;
|
|
68440
|
+
reportPosition();
|
|
68441
|
+
return;
|
|
68292
68442
|
}
|
|
68293
|
-
|
|
68443
|
+
reportPosition();
|
|
68444
|
+
evaluateWindow("scroll");
|
|
68294
68445
|
};
|
|
68295
68446
|
// A page-level scroller does not emit "scroll" on the element itself
|
|
68296
68447
|
// (document.scrollingElement); the document does.
|
|
@@ -71282,8 +71433,11 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
71282
71433
|
* window, which slides as the user scrolls while fillers hold the room of
|
|
71283
71434
|
* the rows outside it. Rows declared one by one as `<List.Item>` children
|
|
71284
71435
|
* are all drawn, whatever this says — a list with more than a few dozen rows
|
|
71285
|
-
* gives them to a run (see docs/scroll.md, "Many rows").
|
|
71286
|
-
*
|
|
71436
|
+
* gives them to a run (see docs/scroll.md, "Many rows"). The window keeps
|
|
71437
|
+
* most of its spare rows ahead of the scroll, and moves once those fall
|
|
71438
|
+
* under half a screen; it has to be larger than what the scroller shows at
|
|
71439
|
+
* once, with room for that lookahead — the list warns below 30, and when a
|
|
71440
|
+
* budget leaves fewer than two rows beyond the screen.
|
|
71287
71441
|
*
|
|
71288
71442
|
* `{ initial, after }` for a list drawn in the click that opens it (a popup):
|
|
71289
71443
|
* `initial` rows in the commit the browser paints first — about what a phone
|
|
@@ -71405,14 +71559,10 @@ const generateTimeSlots = (min, max, stepSeconds) => {
|
|
|
71405
71559
|
const PickerPresetResolver = props => {
|
|
71406
71560
|
const Next = useNextResolver();
|
|
71407
71561
|
if (props.type === "navi_time") {
|
|
71408
|
-
return
|
|
71409
|
-
...props
|
|
71410
|
-
});
|
|
71562
|
+
return renderResolver(PickerNaviTime, props);
|
|
71411
71563
|
}
|
|
71412
71564
|
if (props.type === "navi_minute") {
|
|
71413
|
-
return
|
|
71414
|
-
...props
|
|
71415
|
-
});
|
|
71565
|
+
return renderResolver(PickerNaviMinute, props);
|
|
71416
71566
|
}
|
|
71417
71567
|
return jsx(Next, {
|
|
71418
71568
|
...props
|
|
@@ -72210,59 +72360,37 @@ const PencilSvg = () => {
|
|
|
72210
72360
|
const PickerTypeResolver = props => {
|
|
72211
72361
|
const Next = useNextResolver();
|
|
72212
72362
|
if (props.type === "color") {
|
|
72213
|
-
return
|
|
72214
|
-
...props
|
|
72215
|
-
});
|
|
72363
|
+
return renderResolver(PickerColor, props);
|
|
72216
72364
|
}
|
|
72217
72365
|
if (props.type === "datetime") {
|
|
72218
|
-
return
|
|
72219
|
-
...props
|
|
72220
|
-
});
|
|
72366
|
+
return renderResolver(PickerDatetime, props);
|
|
72221
72367
|
}
|
|
72222
72368
|
if (props.type === "date") {
|
|
72223
|
-
return
|
|
72224
|
-
...props
|
|
72225
|
-
});
|
|
72369
|
+
return renderResolver(PickerDate, props);
|
|
72226
72370
|
}
|
|
72227
72371
|
if (props.type === "month") {
|
|
72228
|
-
return
|
|
72229
|
-
...props
|
|
72230
|
-
});
|
|
72372
|
+
return renderResolver(PickerMonth, props);
|
|
72231
72373
|
}
|
|
72232
72374
|
if (props.type === "week") {
|
|
72233
|
-
return
|
|
72234
|
-
...props
|
|
72235
|
-
});
|
|
72375
|
+
return renderResolver(PickerWeek, props);
|
|
72236
72376
|
}
|
|
72237
72377
|
if (props.type === "time") {
|
|
72238
|
-
return
|
|
72239
|
-
...props
|
|
72240
|
-
});
|
|
72378
|
+
return renderResolver(PickerTime, props);
|
|
72241
72379
|
}
|
|
72242
72380
|
if (props.type === "duration") {
|
|
72243
|
-
return
|
|
72244
|
-
...props
|
|
72245
|
-
});
|
|
72381
|
+
return renderResolver(PickerDuration, props);
|
|
72246
72382
|
}
|
|
72247
72383
|
if (props.type === "file") {
|
|
72248
|
-
return
|
|
72249
|
-
...props
|
|
72250
|
-
});
|
|
72384
|
+
return renderResolver(PickerFile, props);
|
|
72251
72385
|
}
|
|
72252
72386
|
if (props.type === "text") {
|
|
72253
|
-
return
|
|
72254
|
-
...props
|
|
72255
|
-
});
|
|
72387
|
+
return renderResolver(PickerText, props);
|
|
72256
72388
|
}
|
|
72257
72389
|
if (props.type === "array") {
|
|
72258
|
-
return
|
|
72259
|
-
...props
|
|
72260
|
-
});
|
|
72390
|
+
return renderResolver(PickerArray, props);
|
|
72261
72391
|
}
|
|
72262
72392
|
if (props.type === "object") {
|
|
72263
|
-
return
|
|
72264
|
-
...props
|
|
72265
|
-
});
|
|
72393
|
+
return renderResolver(PickerObject, props);
|
|
72266
72394
|
}
|
|
72267
72395
|
return jsx(Next, {
|
|
72268
72396
|
...props
|
|
@@ -85896,7 +86024,7 @@ const css = /* css */`.navi_side_panel {
|
|
|
85896
86024
|
* @param {object} props
|
|
85897
86025
|
* @param {boolean} [props.open] - Controlled open state, forwarded as-is to
|
|
85898
86026
|
* `Popup`'s own `open`.
|
|
85899
|
-
* @param {import("@preact/signals").Signal<
|
|
86027
|
+
* @param {import("@preact/signals").Signal<any>} [props.signal] - The open
|
|
85900
86028
|
* state said the way every navi control says it: the panel opens and closes
|
|
85901
86029
|
* to match the signal, and writes into it whenever it opens or closes on its
|
|
85902
86030
|
* own (Escape, swipe, a --navi-close command) — one binding to both drive
|
|
@@ -85970,7 +86098,9 @@ const css = /* css */`.navi_side_panel {
|
|
|
85970
86098
|
* closes the panel on an outside click instead, and also enables trapping
|
|
85971
86099
|
* Tab navigation inside the panel (`focusCapture`) — closing on outside
|
|
85972
86100
|
* interaction only makes sense paired with not letting focus silently
|
|
85973
|
-
* leave the panel first.
|
|
86101
|
+
* leave the panel first. A box of the page whose press must not close the
|
|
86102
|
+
* panel (a card that fills it) names the panel:
|
|
86103
|
+
* `data-navi-popup-inside={id}` — see docs/popup_backdrop.md.
|
|
85974
86104
|
* @param {boolean} [props.swipeToClose=true] - Pushing the panel back
|
|
85975
86105
|
* towards the edge it is docked to closes it: the panel follows the
|
|
85976
86106
|
* pointer and finishes leaving (or comes back to rest) when it is
|
|
@@ -86105,6 +86235,7 @@ SidePanel.Foot = SidePanelFoot;
|
|
|
86105
86235
|
|
|
86106
86236
|
const createSlot = (SlotRenderer = Box) => {
|
|
86107
86237
|
const slotPropsSignal = signal();
|
|
86238
|
+
let filler = null;
|
|
86108
86239
|
const Slot = () => {
|
|
86109
86240
|
const props = slotPropsSignal.value;
|
|
86110
86241
|
return jsx(SlotRenderer, {
|
|
@@ -86113,9 +86244,15 @@ const createSlot = (SlotRenderer = Box) => {
|
|
|
86113
86244
|
});
|
|
86114
86245
|
};
|
|
86115
86246
|
const SlotFill = props => {
|
|
86247
|
+
const fillerRef = useRef();
|
|
86248
|
+
filler = fillerRef;
|
|
86116
86249
|
slotPropsSignal.value = props;
|
|
86117
86250
|
useLayoutEffect(() => {
|
|
86118
86251
|
return () => {
|
|
86252
|
+
if (filler !== fillerRef) {
|
|
86253
|
+
return;
|
|
86254
|
+
}
|
|
86255
|
+
filler = null;
|
|
86119
86256
|
slotPropsSignal.value = null;
|
|
86120
86257
|
};
|
|
86121
86258
|
}, []);
|