@jsenv/navi 0.27.76 → 0.27.78
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 +474 -270
- package/dist/jsenv_navi.js.map +26 -17
- package/package.json +3 -3
package/dist/jsenv_navi.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isValidElement, createContext, h, toChildArray, render, Fragment, clone
|
|
|
3
3
|
import { useErrorBoundary, useLayoutEffect, useEffect, useContext, useMemo, useRef, useState, useCallback, useId } from "preact/hooks";
|
|
4
4
|
import { jsxs, jsx, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
5
5
|
import { signal, effect, computed, batch, useSignal } from "@preact/signals";
|
|
6
|
-
import { createIterableWeakSet, createEventGroupLogger, normalizeStyle, mergeOneStyle, createPubSub, findEvent, dispatchInternalCustomEvent, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, dispatchCustomEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, getKeyboardEventDefaultAction, chainEvent, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, activeElementSignal, initFocusGroup, elementIsFocusable, parsePositionArea, getPositionedParent, snapToPixel, trapFocusInside, trapScrollInside, scrollIntoViewScoped, measureWidestChildRow, performTabNavigation, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
6
|
+
import { createIterableWeakSet, createEventGroupLogger, normalizeStyle, mergeOneStyle, createPubSub, findEvent, dispatchInternalCustomEvent, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, dispatchCustomEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, activeElementSignal, initFocusGroup, elementIsFocusable, parsePositionArea, getPositionedParent, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, scrollIntoViewScoped, measureWidestChildRow, performTabNavigation, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
7
7
|
export { contrastColor, startDragToReorder } from "@jsenv/dom";
|
|
8
8
|
import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
|
|
9
9
|
import { createValidity, parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, durationToISOString } from "@jsenv/validity";
|
|
@@ -15965,8 +15965,7 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
|
15965
15965
|
render(calloutJsx, calloutMessageElement);
|
|
15966
15966
|
};
|
|
15967
15967
|
|
|
15968
|
-
installImportMetaCssBuild(import.meta)
|
|
15969
|
-
/**
|
|
15968
|
+
installImportMetaCssBuild(import.meta);/**
|
|
15970
15969
|
* A callout component that mimics native browser validation messages.
|
|
15971
15970
|
* Features:
|
|
15972
15971
|
* - Positions above or below target element based on available space
|
|
@@ -15975,7 +15974,6 @@ installImportMetaCssBuild(import.meta);
|
|
|
15975
15974
|
* - Arrow automatically shows when pointing at a valid anchor element
|
|
15976
15975
|
* - Centers in viewport when no anchor element provided or anchor is too big
|
|
15977
15976
|
*/
|
|
15978
|
-
|
|
15979
15977
|
const css$Q = /* css */`
|
|
15980
15978
|
@layer navi {
|
|
15981
15979
|
.navi_callout {
|
|
@@ -15995,17 +15993,21 @@ const css$Q = /* css */`
|
|
|
15995
15993
|
--x-callout-border-color: var(--x-callout-status-color);
|
|
15996
15994
|
--x-callout-background-color: var(--callout-background-color);
|
|
15997
15995
|
--x-callout-icon-color: var(--x-callout-status-color);
|
|
15998
|
-
|
|
15996
|
+
/* Default: no anchor at all (docked/centered in the viewport) — fixed
|
|
15997
|
+
is the direct way to stay pinned there. Overridden to absolute below
|
|
15998
|
+
only when data-anchor-scrolls is set (see its own JS-side comment,
|
|
15999
|
+
right where it's set) — same reasoning as Popover's identical
|
|
16000
|
+
attribute (popover.jsx's own top comment has the full case). */
|
|
16001
|
+
position: fixed;
|
|
15999
16002
|
/* Popover resets */
|
|
16000
|
-
position: absolute;
|
|
16001
16003
|
inset: auto;
|
|
16002
16004
|
top: 0;
|
|
16003
16005
|
left: 0;
|
|
16004
16006
|
/* Callout styles */
|
|
16005
16007
|
display: block;
|
|
16006
|
-
height: auto;
|
|
16008
|
+
height: auto; /* User agent reset */
|
|
16007
16009
|
margin: 0;
|
|
16008
|
-
padding: 0;
|
|
16010
|
+
padding: 0; /* User agent reset */
|
|
16009
16011
|
color: revert; /* Do no inherit element color, callout is inside the element it should use document color though */
|
|
16010
16012
|
font-size: initial; /* Callout fells disconnected from the element, font size should be predictible and stable */
|
|
16011
16013
|
background: transparent;
|
|
@@ -16013,18 +16015,18 @@ const css$Q = /* css */`
|
|
|
16013
16015
|
outline: none; /* programmatic focus may land here briefly before being redirected to close button */
|
|
16014
16016
|
opacity: 0;
|
|
16015
16017
|
/* Positioned with plain left/top (applyNewPosition, visible_rect.js) —
|
|
16016
|
-
left/top
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
transition:
|
|
16021
|
-
opacity 0.2s ease-in-out,
|
|
16022
|
-
left var(--popup-position-transition-duration, 0s) ease-out,
|
|
16023
|
-
top var(--popup-position-transition-duration, 0s) ease-out;
|
|
16018
|
+
left/top are NOT transitioned here, applyNewPosition drives that
|
|
16019
|
+
itself via the Web Animations API instead of CSS, same mechanism
|
|
16020
|
+
Popover/Dialog use. */
|
|
16021
|
+
transition: opacity 0.2s ease-in-out;
|
|
16024
16022
|
cursor: initial; /* Do not inherit element cursor, inside the element but should use regular cursor */
|
|
16025
16023
|
pointer-events: auto; /* Must be interactive to be closabled (overrid list item pointer-events none for instance) */
|
|
16026
16024
|
overflow: visible;
|
|
16027
16025
|
|
|
16026
|
+
&[data-anchor-scrolls] {
|
|
16027
|
+
position: absolute;
|
|
16028
|
+
}
|
|
16029
|
+
|
|
16028
16030
|
&[data-status="success"] {
|
|
16029
16031
|
--x-callout-status-color: var(--callout-success-color);
|
|
16030
16032
|
}
|
|
@@ -16791,6 +16793,24 @@ const positionCallout = (calloutElement, anchorElement, {
|
|
|
16791
16793
|
calloutElement.setAttribute("data-anchor-box", alignToAnchorBox);
|
|
16792
16794
|
}
|
|
16793
16795
|
|
|
16796
|
+
// Drives the callout's own position: fixed/absolute switch (see
|
|
16797
|
+
// .navi_callout's own CSS) — same reasoning as Popover's identical
|
|
16798
|
+
// data-anchor-scrolls attribute (popover.jsx): true only for a real
|
|
16799
|
+
// anchor that itself scrolls with the document — that's the one case
|
|
16800
|
+
// absolute (scrolling in lockstep with it) is correct; not just "has an
|
|
16801
|
+
// anchor at all", since an anchor that's itself position: fixed, or
|
|
16802
|
+
// nested inside something that is (e.g. anchored to an element inside a
|
|
16803
|
+
// Popover/Dialog rendered in the top layer), stays pinned to the viewport
|
|
16804
|
+
// regardless of document scroll too, so the callout must stay fixed right
|
|
16805
|
+
// alongside it instead — switching to absolute there would make it drift
|
|
16806
|
+
// away from an anchor that never actually moves on scroll. No anchor at
|
|
16807
|
+
// all (docked/centered in the viewport) also stays fixed, the default.
|
|
16808
|
+
if (anchorElement && !findSelfOrAncestorFixedPosition(anchorElement)) {
|
|
16809
|
+
calloutElement.setAttribute("data-anchor-scrolls", "");
|
|
16810
|
+
} else {
|
|
16811
|
+
calloutElement.removeAttribute("data-anchor-scrolls");
|
|
16812
|
+
}
|
|
16813
|
+
|
|
16794
16814
|
// Set initial border styles
|
|
16795
16815
|
calloutBoxElement.style.borderWidth = `${BORDER_WIDTH}px`;
|
|
16796
16816
|
calloutFrameElement.style.left = `-${BORDER_WIDTH}px`;
|
|
@@ -17893,6 +17913,7 @@ const matchBestLang = (lang, languageMap) => {
|
|
|
17893
17913
|
* - `"time.less_than_minute"` — e.g. "in less than a minute"
|
|
17894
17914
|
* - `"time.ongoing"` — e.g. "Ongoing"
|
|
17895
17915
|
* - `"time.tomorrow_at"` — e.g. "[day] at [time]" ([day] and [time] are placeholders)
|
|
17916
|
+
* - `"time.midnight"` — e.g. "midnight"
|
|
17896
17917
|
*
|
|
17897
17918
|
* @example
|
|
17898
17919
|
* import { naviI18n } from "@jsenv/navi";
|
|
@@ -17986,6 +18007,24 @@ naviI18n.addAll({
|
|
|
17986
18007
|
pt: "em [duration]",
|
|
17987
18008
|
nl: "over [duration]",
|
|
17988
18009
|
},
|
|
18010
|
+
// Substituted in place of the "0 heure(s)" part of an Intl-generated
|
|
18011
|
+
// duration string when <Time type="time" format="long"> renders midnight
|
|
18012
|
+
// — see time.jsx's own TimeTime for why midnight can't just fall through
|
|
18013
|
+
// to formatMinuteDuration like every other hour does, and how this word
|
|
18014
|
+
// gets spliced in (formatToParts, not string concatenation) so the rest
|
|
18015
|
+
// of the sentence (conjunction, minutes) still comes out in whatever
|
|
18016
|
+
// grammar/word order this language's own Intl.DurationFormat produces.
|
|
18017
|
+
// Languages without an entry here fall back to that language's own
|
|
18018
|
+
// literal "0 heure(s)" wording instead (see TimeTime), never to this key.
|
|
18019
|
+
"time.midnight": {
|
|
18020
|
+
en: "midnight",
|
|
18021
|
+
fr: "minuit",
|
|
18022
|
+
de: "Mitternacht",
|
|
18023
|
+
es: "medianoche",
|
|
18024
|
+
it: "mezzanotte",
|
|
18025
|
+
pt: "meia-noite",
|
|
18026
|
+
nl: "middernacht",
|
|
18027
|
+
},
|
|
17989
18028
|
// Compact duration unit symbols used in "1h30", "45min", "2d", etc.
|
|
17990
18029
|
"time.duration.year_symbol": {
|
|
17991
18030
|
en: "y",
|
|
@@ -18368,24 +18407,24 @@ naviI18n.addAll({
|
|
|
18368
18407
|
en: "The date must be on or before <strong>[max]</strong>.",
|
|
18369
18408
|
},
|
|
18370
18409
|
"constraint.max.number.default": {
|
|
18371
|
-
fr: "
|
|
18372
|
-
en: "
|
|
18410
|
+
fr: "Max <strong>[max]</strong>.",
|
|
18411
|
+
en: "Max <strong>[max]</strong>.",
|
|
18373
18412
|
},
|
|
18374
18413
|
"constraint.max.hour.default": {
|
|
18375
|
-
fr: "
|
|
18376
|
-
en: "
|
|
18414
|
+
fr: "Max <strong>[max]</strong> heures.",
|
|
18415
|
+
en: "Max <strong>[max]</strong> hours.",
|
|
18377
18416
|
},
|
|
18378
18417
|
"constraint.max.minute.default": {
|
|
18379
|
-
fr: "
|
|
18380
|
-
en: "
|
|
18418
|
+
fr: "Max <strong>[max]</strong> minutes.",
|
|
18419
|
+
en: "Max <strong>[max]</strong> minutes.",
|
|
18381
18420
|
},
|
|
18382
18421
|
"constraint.max.second.default": {
|
|
18383
|
-
fr: "
|
|
18384
|
-
en: "
|
|
18422
|
+
fr: "Max <strong>[max]</strong> secondes.",
|
|
18423
|
+
en: "Max <strong>[max]</strong> secondes.",
|
|
18385
18424
|
},
|
|
18386
18425
|
"constraint.max.percentage.default": {
|
|
18387
|
-
fr: "
|
|
18388
|
-
en: "
|
|
18426
|
+
fr: "Max <strong>[max]</strong>%.",
|
|
18427
|
+
en: "Max <strong>[max]</strong>%.",
|
|
18389
18428
|
},
|
|
18390
18429
|
"constraint.step.number.default": {
|
|
18391
18430
|
fr: "Ce nombre doit être un multiple de <strong>[step]</strong> (par ex. <strong>[before]</strong> ou <strong>[after]</strong>).",
|
|
@@ -19165,7 +19204,15 @@ const formatTime = (date, lang) => {
|
|
|
19165
19204
|
* "compact" uses our own notation that omits the minute symbol when hours are present.
|
|
19166
19205
|
*
|
|
19167
19206
|
* @param {number} minutes
|
|
19168
|
-
* @param {{ lang?: string, format?: "long"|"short"|"narrow"|"compact" }} [options]
|
|
19207
|
+
* @param {{ lang?: string, format?: "long"|"short"|"narrow"|"compact", alwaysShowHours?: boolean }} [options]
|
|
19208
|
+
* @param {boolean} [options.alwaysShowHours=false] - Normally a zero-hours
|
|
19209
|
+
* component is dropped entirely (a real 5-minute duration should print as
|
|
19210
|
+
* "5 minutes", not "0 hours 5 minutes") — set this to keep it (e.g. "0 h
|
|
19211
|
+
* et 5 min"/"0h 5min"/"0h05") instead. Only meaningful when `minutes` is
|
|
19212
|
+
* itself less than 60, i.e. the hours component would otherwise be zero;
|
|
19213
|
+
* used by `<Time type="time">` for a time-of-day at midnight, where
|
|
19214
|
+
* dropping the hour would make it indistinguishable from an actual
|
|
19215
|
+
* duration (see time.jsx's own TimeTime).
|
|
19169
19216
|
*
|
|
19170
19217
|
* @example
|
|
19171
19218
|
* formatMinuteDuration(90, { lang: "fr" }) // "1 heure 30 minutes" (long, default)
|
|
@@ -19173,16 +19220,20 @@ const formatTime = (date, lang) => {
|
|
|
19173
19220
|
* formatMinuteDuration(90, { lang: "fr", format: "narrow" }) // "1h 30min" (Intl narrow)
|
|
19174
19221
|
* formatMinuteDuration(90, { lang: "fr", format: "compact" }) // "1h30" (custom, no minute symbol)
|
|
19175
19222
|
* formatMinuteDuration(45, { lang: "en", format: "compact" }) // "45min"
|
|
19223
|
+
* formatMinuteDuration(5, { lang: "fr", format: "narrow", alwaysShowHours: true }) // "0h 5min"
|
|
19176
19224
|
*/
|
|
19177
19225
|
const formatMinuteDuration = (
|
|
19178
19226
|
minutes,
|
|
19179
|
-
{ lang = languagesSignal.value, format = "long" } = {},
|
|
19227
|
+
{ lang = languagesSignal.value, format = "long", alwaysShowHours = false } = {},
|
|
19180
19228
|
) => {
|
|
19181
19229
|
const h = Math.floor(minutes / 60);
|
|
19182
19230
|
const m = minutes % 60;
|
|
19183
19231
|
if (format !== "compact" && typeof Intl.DurationFormat !== "undefined") {
|
|
19184
|
-
const fmt = new Intl.DurationFormat(lang, {
|
|
19185
|
-
|
|
19232
|
+
const fmt = new Intl.DurationFormat(lang, {
|
|
19233
|
+
style: format, // "long", "short", or "narrow"
|
|
19234
|
+
...(alwaysShowHours ? { hoursDisplay: "always" } : {}),
|
|
19235
|
+
});
|
|
19236
|
+
if (h === 0 && !alwaysShowHours) {
|
|
19186
19237
|
return fmt.format({ minutes: m });
|
|
19187
19238
|
}
|
|
19188
19239
|
if (m === 0) {
|
|
@@ -19193,7 +19244,7 @@ const formatMinuteDuration = (
|
|
|
19193
19244
|
// format="compact": "1h30", "45min", "2h" — no minute symbol when hours are present
|
|
19194
19245
|
const hSym = naviI18n("time.duration.hour_symbol", undefined, { lang });
|
|
19195
19246
|
const mSym = naviI18n("time.duration.minute_symbol", undefined, { lang });
|
|
19196
|
-
if (h === 0) {
|
|
19247
|
+
if (h === 0 && !alwaysShowHours) {
|
|
19197
19248
|
return `${m}${mSym}`;
|
|
19198
19249
|
}
|
|
19199
19250
|
if (m === 0) {
|
|
@@ -19340,40 +19391,50 @@ const formatDuration = (
|
|
|
19340
19391
|
// Fall through to compact notation when values are non-numeric or mixed-sign
|
|
19341
19392
|
}
|
|
19342
19393
|
|
|
19394
|
+
// A component explicitly present but numerically zero (e.g. the demo's own
|
|
19395
|
+
// { hours: 0, minutes: 5 }) conveys no information for a genuine duration
|
|
19396
|
+
// — same convention formatMinuteDuration/formatSecondDuration already
|
|
19397
|
+
// follow (checking h > 0/m > 0, not merely "was a value passed") — so
|
|
19398
|
+
// it's dropped here too, regardless of whether the caller included the
|
|
19399
|
+
// key at all. Non-numeric mid-edit values (e.g. "2a") still count as
|
|
19400
|
+
// present — Number("2a") is NaN, never === 0 — so those keep rendering
|
|
19401
|
+
// as-is with their own unit symbol.
|
|
19402
|
+
const hasNonZero = (key) => has(key) && Number(duration[key]) !== 0;
|
|
19403
|
+
|
|
19343
19404
|
const sym = (key) =>
|
|
19344
19405
|
naviI18n(`time.duration.${key}_symbol`, undefined, { lang });
|
|
19345
19406
|
const parts = [];
|
|
19346
19407
|
|
|
19347
|
-
if (
|
|
19408
|
+
if (hasNonZero("years")) {
|
|
19348
19409
|
parts.push(`${duration.years}${sym("year")}`);
|
|
19349
19410
|
}
|
|
19350
|
-
if (
|
|
19411
|
+
if (hasNonZero("months")) {
|
|
19351
19412
|
parts.push(`${duration.months}${sym("month")}`);
|
|
19352
19413
|
}
|
|
19353
|
-
if (
|
|
19414
|
+
if (hasNonZero("weeks")) {
|
|
19354
19415
|
parts.push(`${duration.weeks}${sym("week")}`);
|
|
19355
19416
|
}
|
|
19356
|
-
if (
|
|
19417
|
+
if (hasNonZero("days")) {
|
|
19357
19418
|
parts.push(`${duration.days}${sym("day")}`);
|
|
19358
19419
|
}
|
|
19359
19420
|
|
|
19360
19421
|
// Hours + minutes: when both present, pad minutes to 2 digits after the h symbol
|
|
19361
19422
|
const hSym = sym("hour");
|
|
19362
19423
|
const mSym = sym("minute");
|
|
19363
|
-
if (
|
|
19424
|
+
if (hasNonZero("hours") && hasNonZero("minutes")) {
|
|
19364
19425
|
parts.push(
|
|
19365
19426
|
`${duration.hours}${hSym}${String(duration.minutes).padStart(2, "0")}`,
|
|
19366
19427
|
);
|
|
19367
|
-
} else if (
|
|
19428
|
+
} else if (hasNonZero("hours")) {
|
|
19368
19429
|
parts.push(`${duration.hours}${hSym}`);
|
|
19369
|
-
} else if (
|
|
19430
|
+
} else if (hasNonZero("minutes")) {
|
|
19370
19431
|
parts.push(`${duration.minutes}${mSym}`);
|
|
19371
19432
|
}
|
|
19372
19433
|
|
|
19373
|
-
if (
|
|
19434
|
+
if (hasNonZero("seconds")) {
|
|
19374
19435
|
parts.push(`${duration.seconds}${sym("second")}`);
|
|
19375
19436
|
}
|
|
19376
|
-
if (
|
|
19437
|
+
if (hasNonZero("milliseconds")) {
|
|
19377
19438
|
parts.push(`${duration.milliseconds}${sym("millisecond")}`);
|
|
19378
19439
|
}
|
|
19379
19440
|
return parts.join("") || "0";
|
|
@@ -21420,9 +21481,9 @@ const tryActionAfterInteractionAllowed = (
|
|
|
21420
21481
|
* - No <dialog>/<details>/[popover] ancestor → runs like a normal
|
|
21421
21482
|
* useLayoutEffect with the provided deps.
|
|
21422
21483
|
* - Inside a closed/hidden ancestor → skips the initial run; instead runs
|
|
21423
|
-
* the callback once the ancestor opens — see
|
|
21424
|
-
*
|
|
21425
|
-
* happens before the browser paints.
|
|
21484
|
+
* the callback once the ancestor opens — see @jsenv/dom's own
|
|
21485
|
+
* observeAncestorOpenState for exactly how that's detected, and why it
|
|
21486
|
+
* matters that it happens before the browser paints.
|
|
21426
21487
|
* - Inside an open ancestor → runs on mount AND every subsequent open.
|
|
21427
21488
|
*
|
|
21428
21489
|
* The callback's second argument is always a `navi_displayed` CustomEvent,
|
|
@@ -21430,9 +21491,7 @@ const tryActionAfterInteractionAllowed = (
|
|
|
21430
21491
|
* - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
|
|
21431
21492
|
* `{ ancestor: document, ancestorType: "document" }`.
|
|
21432
21493
|
* - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
|
|
21433
|
-
* | "popover" | "details" | "aria-expanded" }`.
|
|
21434
|
-
* path (see addBeforePaintOpenCallback) `detail.event` is also set to
|
|
21435
|
-
* the native `toggle` event that triggered it.
|
|
21494
|
+
* | "popover" | "details" | "aria-expanded" }`.
|
|
21436
21495
|
* Consumers that only care about a genuine top-level, no-ancestor mount
|
|
21437
21496
|
* (e.g. use_auto_focus.js — an ancestor opening already has its own
|
|
21438
21497
|
* transferFocus/openEffect placing focus, so re-running a per-element
|
|
@@ -21460,20 +21519,17 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
21460
21519
|
if (!el) {
|
|
21461
21520
|
return;
|
|
21462
21521
|
}
|
|
21463
|
-
const ancestor = el
|
|
21522
|
+
const ancestor = closestOpenableAncestor(el);
|
|
21464
21523
|
if (!ancestor) {
|
|
21465
|
-
callbackRef.current(el, createDisplayedEvent(document
|
|
21524
|
+
callbackRef.current(el, createDisplayedEvent(document));
|
|
21466
21525
|
return;
|
|
21467
21526
|
}
|
|
21468
21527
|
if (!isAncestorOpen(ancestor)) {
|
|
21469
|
-
// Ancestor is closed — skip now;
|
|
21470
|
-
// will fire once it opens.
|
|
21528
|
+
// Ancestor is closed — skip now; the observeAncestorOpenState call
|
|
21529
|
+
// below will fire once it opens.
|
|
21471
21530
|
return;
|
|
21472
21531
|
}
|
|
21473
|
-
callbackRef.current(
|
|
21474
|
-
el,
|
|
21475
|
-
createDisplayedEvent(ancestor, getAncestorType(ancestor)),
|
|
21476
|
-
);
|
|
21532
|
+
callbackRef.current(el, createDisplayedEvent(ancestor));
|
|
21477
21533
|
}, deps);
|
|
21478
21534
|
|
|
21479
21535
|
// Re-run every time the ancestor opens.
|
|
@@ -21482,134 +21538,27 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
21482
21538
|
if (!el) {
|
|
21483
21539
|
return undefined;
|
|
21484
21540
|
}
|
|
21485
|
-
const ancestor = el
|
|
21541
|
+
const ancestor = closestOpenableAncestor(el);
|
|
21486
21542
|
if (!ancestor) {
|
|
21487
21543
|
return undefined;
|
|
21488
21544
|
}
|
|
21489
|
-
return
|
|
21545
|
+
return observeAncestorOpenState(ancestor, ({ isOpen }) => {
|
|
21546
|
+
if (!isOpen) {
|
|
21547
|
+
return;
|
|
21548
|
+
}
|
|
21490
21549
|
const lastEl = ref.current;
|
|
21491
|
-
callbackRef.current(lastEl,
|
|
21550
|
+
callbackRef.current(lastEl, createDisplayedEvent(ancestor));
|
|
21492
21551
|
});
|
|
21493
21552
|
}, []);
|
|
21494
21553
|
};
|
|
21495
21554
|
|
|
21496
|
-
const
|
|
21497
|
-
if (ancestor.tagName === "DIALOG" || ancestor.hasAttribute("popover")) {
|
|
21498
|
-
return ancestor.matches(":popover-open, [open]");
|
|
21499
|
-
}
|
|
21500
|
-
if (ancestor.tagName === "DETAILS") {
|
|
21501
|
-
return ancestor.open;
|
|
21502
|
-
}
|
|
21503
|
-
return ancestor.getAttribute("aria-expanded") === "true";
|
|
21504
|
-
};
|
|
21505
|
-
|
|
21506
|
-
const getAncestorType = (ancestor) => {
|
|
21507
|
-
if (ancestor.tagName === "DIALOG") {
|
|
21508
|
-
return "dialog";
|
|
21509
|
-
}
|
|
21510
|
-
if (ancestor.hasAttribute("popover")) {
|
|
21511
|
-
return "popover";
|
|
21512
|
-
}
|
|
21513
|
-
if (ancestor.tagName === "DETAILS") {
|
|
21514
|
-
return "details";
|
|
21515
|
-
}
|
|
21516
|
-
return `${ancestor.tagName}[aria-expanded]`;
|
|
21517
|
-
};
|
|
21518
|
-
|
|
21519
|
-
const createDisplayedEvent = (ancestor, ancestorType, eventInit) => {
|
|
21555
|
+
const createDisplayedEvent = (ancestor) => {
|
|
21520
21556
|
return new CustomEvent("navi_displayed", {
|
|
21521
|
-
detail: {
|
|
21522
|
-
|
|
21523
|
-
|
|
21524
|
-
|
|
21525
|
-
/**
|
|
21526
|
-
* Notifies `callback` the moment `ancestor` opens — timed to land strictly
|
|
21527
|
-
* before the browser's next paint, so a caller doing layout/measurement work
|
|
21528
|
-
* in response (e.g. text_anchor.jsx repositioning a badge) never flashes the
|
|
21529
|
-
* still-closed state first.
|
|
21530
|
-
*
|
|
21531
|
-
* We deliberately do NOT use the native `toggle` event as the primary
|
|
21532
|
-
* signal, even though every <dialog>/<details>/[popover] fires one: per the
|
|
21533
|
-
* WHATWG spec it's dispatched via a *queued task* ("queue a popover toggle
|
|
21534
|
-
* event task"), not synchronously and not as a microtask. The element's
|
|
21535
|
-
* shown state itself (showPopover()/showModal()) still flips synchronously,
|
|
21536
|
-
* so the browser can — and does — paint it in its default, uncorrected
|
|
21537
|
-
* position before that queued task ever runs. Relying on `toggle` alone
|
|
21538
|
-
* means the correction always arrives one paint late.
|
|
21539
|
-
*
|
|
21540
|
-
* Instead we watch `open`/`aria-expanded` via MutationObserver:
|
|
21541
|
-
* - <dialog>/<details> reflect `open` themselves, natively, synchronously.
|
|
21542
|
-
* - This codebase's own Popover.jsx sets `aria-expanded` synchronously in
|
|
21543
|
-
* the same call stack as showPopover() (see popover.jsx's own
|
|
21544
|
-
* aria-expanded comments) — not part of any web standard, just this
|
|
21545
|
-
* library's own convention, but reliable for anything built through it.
|
|
21546
|
-
* MutationObserver callbacks run as a microtask, strictly before paint —
|
|
21547
|
-
* exactly the timing we need, no ambiguity.
|
|
21548
|
-
*
|
|
21549
|
-
* The `toggle` listener is kept as a fallback, attached ONLY where the
|
|
21550
|
-
* MutationObserver above has no chance of ever firing: a bare [popover]
|
|
21551
|
-
* element with no `aria-expanded` of its own — i.e. one not built through
|
|
21552
|
-
* this codebase's own Popover.jsx (the only thing that reliably sets it).
|
|
21553
|
-
* That's the one case this file's author could think of with no other
|
|
21554
|
-
* synchronously-observable signal at all. It still arrives a paint late,
|
|
21555
|
-
* but a late correction beats none.
|
|
21556
|
-
*
|
|
21557
|
-
* This relies on `aria-expanded` genuinely being present on a navi Popover
|
|
21558
|
-
* by the time this code runs — see popover.jsx's own comments on why it
|
|
21559
|
-
* sets that attribute the way it does.
|
|
21560
|
-
*
|
|
21561
|
-
* @param {Element} ancestor
|
|
21562
|
-
* @param {(event: Event) => void} callback
|
|
21563
|
-
* @returns {() => void} cleanup — removes the observer/listener
|
|
21564
|
-
*/
|
|
21565
|
-
const addBeforePaintOpenCallback = (ancestor, callback) => {
|
|
21566
|
-
const ancestorType = getAncestorType(ancestor);
|
|
21567
|
-
const needsToggleFallback =
|
|
21568
|
-
ancestor.hasAttribute("popover") && !ancestor.hasAttribute("aria-expanded");
|
|
21569
|
-
if (needsToggleFallback) {
|
|
21570
|
-
const onToggle = (toggleEvent) => {
|
|
21571
|
-
if (!isAncestorOpen(ancestor)) {
|
|
21572
|
-
return;
|
|
21573
|
-
}
|
|
21574
|
-
callback(
|
|
21575
|
-
createDisplayedEvent(ancestor, ancestorType, { event: toggleEvent }),
|
|
21576
|
-
);
|
|
21577
|
-
};
|
|
21578
|
-
ancestor.addEventListener("toggle", onToggle);
|
|
21579
|
-
return () => {
|
|
21580
|
-
ancestor.removeEventListener("toggle", onToggle);
|
|
21581
|
-
};
|
|
21582
|
-
}
|
|
21583
|
-
|
|
21584
|
-
// Edge-triggered on purpose: some consumers (e.g. Popover.jsx) set
|
|
21585
|
-
// aria-expanded both imperatively (in their own openEffect, for precise
|
|
21586
|
-
// ordering relative to forced reflows/transitions) AND declaratively via a
|
|
21587
|
-
// JSX prop derived from the same open state — the latter is a deliberate
|
|
21588
|
-
// "always reflect current truth" prop, but Preact diffs against its own
|
|
21589
|
-
// previous *rendered* value, not the live DOM, so any later re-render that
|
|
21590
|
-
// happens to occur while already open re-applies the same "true" value as
|
|
21591
|
-
// a genuinely new attribute mutation. Tracking wasOpen here collapses that
|
|
21592
|
-
// redundant open→open mutation instead of re-running callback a second
|
|
21593
|
-
// time for the same open.
|
|
21594
|
-
let wasOpen = isAncestorOpen(ancestor);
|
|
21595
|
-
const observer = new MutationObserver(() => {
|
|
21596
|
-
const isOpen = isAncestorOpen(ancestor);
|
|
21597
|
-
if (isOpen === wasOpen) {
|
|
21598
|
-
return;
|
|
21599
|
-
}
|
|
21600
|
-
wasOpen = isOpen;
|
|
21601
|
-
if (!isOpen) {
|
|
21602
|
-
return;
|
|
21603
|
-
}
|
|
21604
|
-
callback(createDisplayedEvent(ancestor, ancestorType));
|
|
21605
|
-
});
|
|
21606
|
-
observer.observe(ancestor, {
|
|
21607
|
-
attributes: true,
|
|
21608
|
-
attributeFilter: ["open", "aria-expanded"],
|
|
21557
|
+
detail: {
|
|
21558
|
+
ancestor,
|
|
21559
|
+
ancestorType: getAncestorOpenType(ancestor),
|
|
21560
|
+
},
|
|
21609
21561
|
});
|
|
21610
|
-
return () => {
|
|
21611
|
-
observer.disconnect();
|
|
21612
|
-
};
|
|
21613
21562
|
};
|
|
21614
21563
|
|
|
21615
21564
|
// see also https://github.com/preactjs/preact/issues/1255
|
|
@@ -36232,6 +36181,42 @@ const useOpenController = (openHandler) => {
|
|
|
36232
36181
|
return controllerRef.current;
|
|
36233
36182
|
};
|
|
36234
36183
|
|
|
36184
|
+
// Nested popups that both mount already-open (`open`/`defaultOpen`) would
|
|
36185
|
+
// otherwise stack in the wrong order: Preact fires layout effects
|
|
36186
|
+
// child-first on mount, so a nested popup's own silent mount-open would call
|
|
36187
|
+
// showPopover() before its ancestor's — and the top layer stacks *later*
|
|
36188
|
+
// showPopover() calls above *earlier* ones (see popover.jsx's own openEffect
|
|
36189
|
+
// comment) — leaving the ancestor on top instead of the nested popup, the
|
|
36190
|
+
// opposite of what opening them one at a time (ancestor first, by real user
|
|
36191
|
+
// interaction) would produce. Batching every mount-time silent open queued
|
|
36192
|
+
// during the same commit's layout-effect phase into one microtask flush,
|
|
36193
|
+
// then simply running them in *reverse* of their registration order fixes
|
|
36194
|
+
// this — no need to compare DOM positions: since effects already fire
|
|
36195
|
+
// child-first, tree-wide, for *any* ancestor/descendant pair the descendant
|
|
36196
|
+
// is always queued before the ancestor, regardless of what else is in the
|
|
36197
|
+
// tree, so reversing the whole batch always puts every ancestor before its
|
|
36198
|
+
// own descendants. Works for any nesting depth for the same reason. Two
|
|
36199
|
+
// unrelated (sibling) popups both mounting open also get reordered
|
|
36200
|
+
// relative to each other, but there's no meaningful "correct" order between
|
|
36201
|
+
// those anyway.
|
|
36202
|
+
let pendingSilentOpens = [];
|
|
36203
|
+
let silentOpenFlushScheduled = false;
|
|
36204
|
+
const scheduleSilentOpen = (run) => {
|
|
36205
|
+
pendingSilentOpens.push(run);
|
|
36206
|
+
if (silentOpenFlushScheduled) {
|
|
36207
|
+
return;
|
|
36208
|
+
}
|
|
36209
|
+
silentOpenFlushScheduled = true;
|
|
36210
|
+
queueMicrotask(() => {
|
|
36211
|
+
const entries = pendingSilentOpens;
|
|
36212
|
+
pendingSilentOpens = [];
|
|
36213
|
+
silentOpenFlushScheduled = false;
|
|
36214
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
36215
|
+
entries[i]();
|
|
36216
|
+
}
|
|
36217
|
+
});
|
|
36218
|
+
};
|
|
36219
|
+
|
|
36235
36220
|
/**
|
|
36236
36221
|
* Keeps an open controller in sync with a plain `open`/`defaultOpen` pair —
|
|
36237
36222
|
* shared between `useOpenControllerByProps` below (Dialog/Popover driving
|
|
@@ -36262,9 +36247,15 @@ const useOpenPropsEffectOnOpenController = (openController, props) => {
|
|
|
36262
36247
|
// silent: true — nothing was ever shown as "closed" for the user to
|
|
36263
36248
|
// see transition away from, so this first open skips the entrance
|
|
36264
36249
|
// animation entirely (see popover.jsx's own openEffect for how).
|
|
36265
|
-
|
|
36266
|
-
|
|
36267
|
-
|
|
36250
|
+
// Deferred + batched (see scheduleSilentOpen above) rather than
|
|
36251
|
+
// called directly, so nested popups that both mount already-open
|
|
36252
|
+
// end up stacked ancestor-first instead of Preact's own child-first
|
|
36253
|
+
// effect order.
|
|
36254
|
+
scheduleSilentOpen(() =>
|
|
36255
|
+
openController.open(new CustomEvent("open_by_prop", { detail: {} }), {
|
|
36256
|
+
silent: true,
|
|
36257
|
+
}),
|
|
36258
|
+
);
|
|
36268
36259
|
}
|
|
36269
36260
|
return;
|
|
36270
36261
|
}
|
|
@@ -36394,25 +36385,17 @@ const popupCss = /* css */ `
|
|
|
36394
36385
|
|
|
36395
36386
|
.navi_popover,
|
|
36396
36387
|
.navi_dialog {
|
|
36397
|
-
/* left/top
|
|
36398
|
-
|
|
36399
|
-
|
|
36400
|
-
|
|
36401
|
-
|
|
36402
|
-
open/close transition itself, is the element's *entire* open
|
|
36403
|
-
duration — see openEffect in popover.jsx/dialog.jsx). Duration is
|
|
36404
|
-
driven by --popup-position-transition-duration either way — see
|
|
36405
|
-
applyNewPosition's own doc in visible_rect.js for what sets it and
|
|
36406
|
-
why. */
|
|
36388
|
+
/* left/top are deliberately absent from this list — applyNewPosition
|
|
36389
|
+
(visible_rect.js) drives that transition itself via the Web
|
|
36390
|
+
Animations API instead of CSS, so it stays independent of whatever
|
|
36391
|
+
this list contains (no shared transition-property to clobber, no
|
|
36392
|
+
propertyName to filter). */
|
|
36407
36393
|
&[navi-animation] {
|
|
36408
|
-
transition-property:
|
|
36409
|
-
display, overlay, opacity, translate, scale, box-shadow, left, top;
|
|
36394
|
+
transition-property: display, overlay, opacity, translate, scale, box-shadow;
|
|
36410
36395
|
transition-duration:
|
|
36411
36396
|
var(--popup-animation-duration), var(--popup-animation-duration),
|
|
36412
36397
|
var(--popup-opacity-duration), var(--popup-translate-duration),
|
|
36413
|
-
var(--popup-scale-duration), var(--popup-animation-duration)
|
|
36414
|
-
var(--popup-position-transition-duration, 0s),
|
|
36415
|
-
var(--popup-position-transition-duration, 0s);
|
|
36398
|
+
var(--popup-scale-duration), var(--popup-animation-duration);
|
|
36416
36399
|
transition-timing-function: ease;
|
|
36417
36400
|
transition-behavior: allow-discrete;
|
|
36418
36401
|
}
|
|
@@ -36858,13 +36841,11 @@ const css$s = /* css */`
|
|
|
36858
36841
|
outline-color: var(--dialog-outline-color);
|
|
36859
36842
|
outline-offset: 0;
|
|
36860
36843
|
box-shadow: var(--dialog-box-shadow);
|
|
36861
|
-
/*
|
|
36862
|
-
|
|
36863
|
-
|
|
36864
|
-
|
|
36865
|
-
|
|
36866
|
-
transition-duration: var(--popup-position-transition-duration, 0s);
|
|
36867
|
-
transition-timing-function: ease-out;
|
|
36844
|
+
/* left/top are NOT transitioned here — applyNewPosition (visible_rect.js)
|
|
36845
|
+
drives that itself via the Web Animations API instead of CSS, so it
|
|
36846
|
+
stays independent from navi-animation's own opacity/scale/display
|
|
36847
|
+
transition list below (no shared transition-property to clobber, no
|
|
36848
|
+
propertyName to filter). */
|
|
36868
36849
|
|
|
36869
36850
|
&::backdrop {
|
|
36870
36851
|
background: var(--navi-backdrop-close-background);
|
|
@@ -37028,10 +37009,12 @@ const css$s = /* css */`
|
|
|
37028
37009
|
* value is used as-is.
|
|
37029
37010
|
* @param {string} [props.animationDuration] - Maps to
|
|
37030
37011
|
* `--popup-animation-duration`.
|
|
37031
|
-
* @param {Element|{current: Element}} [props.anchor] - Only ever sizes
|
|
37032
|
-
* dialog via the `--anchor-width`/`--anchor-height` CSS vars — never
|
|
37033
|
-
* for positioning (see this file's top comment). Defaults to whatever
|
|
37034
|
-
* triggered the open (`e.detail.anchor`), if any.
|
|
37012
|
+
* @param {Element|{current: Element}|string} [props.anchor] - Only ever sizes
|
|
37013
|
+
* the dialog via the `--anchor-width`/`--anchor-height` CSS vars — never
|
|
37014
|
+
* used for positioning (see this file's top comment). Defaults to whatever
|
|
37015
|
+
* triggered the open (`e.detail.anchor`), if any. A string is resolved via
|
|
37016
|
+
* `document.getElementById` when the dialog opens — see popover.jsx's own
|
|
37017
|
+
* `anchor` doc for why (mainly `defaultOpen`).
|
|
37035
37018
|
* @param {string} [props.minWidth] - Maps to `--dialog-min-width`; clamped
|
|
37036
37019
|
* so it can never push the dialog past `--dialog-maxmax-width` (the
|
|
37037
37020
|
* viewport/container-spacing ceiling) regardless of how large a value is
|
|
@@ -37235,11 +37218,25 @@ const useDialogProps = props => {
|
|
|
37235
37218
|
// `open`/`defaultOpen` already being truthy at mount — see popover.jsx's
|
|
37236
37219
|
// own openEffect for the full reasoning, mirrored here identically.
|
|
37237
37220
|
const silent = Boolean(e.detail.silent);
|
|
37238
|
-
|
|
37221
|
+
|
|
37222
|
+
// document.documentElement — the shared "no real container, use the
|
|
37223
|
+
// viewport" sentinel (see offset_parent.js) — not just
|
|
37224
|
+
// getPositionedParent(dialogEl) unconditionally: that walks starting
|
|
37225
|
+
// from dialogEl.parentElement, which for DialogLocal is the
|
|
37226
|
+
// .navi_dialog_clip_wrapper (itself position: absolute) rather than the
|
|
37227
|
+
// real, meaningful ancestor beyond it.
|
|
37228
|
+
const positionedAncestor = isModal ? document.documentElement : getPositionedParent(dialogEl.parentElement /* dialogEl is inside the clip_wrapper */);
|
|
37239
37229
|
const [cleanup, addCleanup] = createPubSub(true);
|
|
37240
37230
|
let anchorElement;
|
|
37241
37231
|
if (typeof anchor === "string") {
|
|
37242
|
-
|
|
37232
|
+
// Resolved at open time (not render time) via getElementById — mainly
|
|
37233
|
+
// for defaultOpen, where there's no triggering event/ref yet to read a
|
|
37234
|
+
// real element from, only an id known up front. See popover.jsx's own
|
|
37235
|
+
// anchor handling for the full reasoning, mirrored here identically.
|
|
37236
|
+
anchorElement = document.getElementById(anchor);
|
|
37237
|
+
if (!anchorElement) {
|
|
37238
|
+
console.warn(`Dialog: anchor="${anchor}" did not match any element`);
|
|
37239
|
+
}
|
|
37243
37240
|
} else if (anchor) {
|
|
37244
37241
|
// anchor prop is a ref or a DOM element
|
|
37245
37242
|
anchorElement = anchor.current ?? anchor;
|
|
@@ -37324,31 +37321,47 @@ const useDialogProps = props => {
|
|
|
37324
37321
|
// custom renderer. applyNewPosition sets --container-position-remaining-height/-width
|
|
37325
37322
|
// from the result, same as popover.jsx.
|
|
37326
37323
|
const positionDialog = triggerEvent => {
|
|
37327
|
-
const
|
|
37324
|
+
const pickOptions = {
|
|
37328
37325
|
positionArea,
|
|
37329
|
-
container:
|
|
37326
|
+
container: positionedAncestor,
|
|
37330
37327
|
marginWithContainer: resolveSpacingSize(marginWithContainer),
|
|
37331
37328
|
event: triggerEvent
|
|
37332
|
-
}
|
|
37329
|
+
};
|
|
37330
|
+
let position = pickPositionRelativeTo(dialogEl, null, pickOptions);
|
|
37333
37331
|
applyNewPosition(dialogEl, position);
|
|
37334
|
-
//
|
|
37335
|
-
//
|
|
37336
|
-
//
|
|
37337
|
-
//
|
|
37338
|
-
//
|
|
37339
|
-
|
|
37332
|
+
// applyNewPosition above just set --container-position-remaining-
|
|
37333
|
+
// width/height to the real available space — narrower than whatever
|
|
37334
|
+
// dialogEl measured at just before (nothing, on a first open — see
|
|
37335
|
+
// popover.jsx's own identical comment on its own positionPopover for
|
|
37336
|
+
// the full reasoning, mirrored here). If that changes dialogEl's own
|
|
37337
|
+
// rendered box (its content rewraps once truly constrained), the
|
|
37338
|
+
// position picked above was computed against the wrong (wider,
|
|
37339
|
+
// shorter) box and needs a synchronous second pass, or it paints one
|
|
37340
|
+
// frame too high/low before the ResizeObserver watching this same
|
|
37341
|
+
// element (rectEffect.observeSize below) ever gets a chance to
|
|
37342
|
+
// correct it — that one only reacts on the *next* animation frame.
|
|
37343
|
+
if (dialogEl.offsetWidth !== position.width || dialogEl.offsetHeight !== position.height) {
|
|
37344
|
+
position = pickPositionRelativeTo(dialogEl, null, pickOptions);
|
|
37345
|
+
applyNewPosition(dialogEl, position);
|
|
37346
|
+
}
|
|
37347
|
+
// A descendant's own visibleRectEffect (visible_rect.js — e.g. a
|
|
37348
|
+
// Callout anchored to something inside this Dialog) knowing to
|
|
37349
|
+
// recheck its own position whenever this dialog itself moves is
|
|
37350
|
+
// handled generically by applyNewPosition itself (dispatches
|
|
37351
|
+
// navi_position_change on every call) — nothing to do here.
|
|
37340
37352
|
};
|
|
37341
37353
|
positionDialog();
|
|
37342
37354
|
|
|
37343
37355
|
// Reposition on the same triggers Popover's own visibleRectEffect
|
|
37344
37356
|
// already reacts to generically — window resize/scroll/visual-viewport
|
|
37345
|
-
// changes for layer="top"/isModal (
|
|
37357
|
+
// changes for layer="top"/isModal (positionedAncestor is already
|
|
37358
|
+
// document.documentElement there, see its own computation above;
|
|
37346
37359
|
// visibleRectEffect already debounces visualViewport resize by 100ms
|
|
37347
37360
|
// to avoid the mobile tap-to-tap-input keyboard flicker, so no
|
|
37348
37361
|
// separate mechanism is needed here for that), or the positioned
|
|
37349
|
-
// ancestor's own resize for layer="local"
|
|
37350
|
-
//
|
|
37351
|
-
const rectEffect = visibleRectEffect(
|
|
37362
|
+
// ancestor's own resize for layer="local" — see this file's top
|
|
37363
|
+
// comment.
|
|
37364
|
+
const rectEffect = visibleRectEffect(positionedAncestor, (visibleRect, {
|
|
37352
37365
|
event
|
|
37353
37366
|
}) => {
|
|
37354
37367
|
positionDialog(event);
|
|
@@ -37360,6 +37373,14 @@ const useDialogProps = props => {
|
|
|
37360
37373
|
addCleanup(() => {
|
|
37361
37374
|
rectEffect.disconnect();
|
|
37362
37375
|
});
|
|
37376
|
+
// A descendant anchored to something inside this dialog (a Callout, a
|
|
37377
|
+
// nested Popover) needing to know about this dialog's own left/top
|
|
37378
|
+
// repositioning transition — not just that the target changed
|
|
37379
|
+
// (navi_position_change above), but that a real, currently-playing
|
|
37380
|
+
// transition is moving it right now — is handled generically by
|
|
37381
|
+
// applyNewPosition itself (see its own notifyPositionTransition), since
|
|
37382
|
+
// positionDialog already goes through it above; nothing to wire up
|
|
37383
|
+
// here.
|
|
37363
37384
|
|
|
37364
37385
|
// Final commit — see popover.jsx's own openEffect for the full
|
|
37365
37386
|
// reasoning behind the `silent` ordering swap (forced reflow between
|
|
@@ -37395,6 +37416,20 @@ const useDialogProps = props => {
|
|
|
37395
37416
|
if (mouseDownEvent.button !== 0) {
|
|
37396
37417
|
return;
|
|
37397
37418
|
}
|
|
37419
|
+
// Real DOM containment wins over the coordinate check below — an
|
|
37420
|
+
// element genuinely inside the dialog (e.g. one with `overflow:
|
|
37421
|
+
// visible`, a negative margin, or an absolutely-positioned child)
|
|
37422
|
+
// can be visually painted outside dialogEl's own bounding rect, and
|
|
37423
|
+
// a click there must not count as "outside" just because its
|
|
37424
|
+
// coordinates fall outside that rect. Excludes dialogEl itself
|
|
37425
|
+
// though (contains() is true for the element itself, not just real
|
|
37426
|
+
// descendants) — a genuine backdrop click reports target === dialogEl
|
|
37427
|
+
// (there's no real ::backdrop node to be the target), so treating
|
|
37428
|
+
// that case as "contained" would make the coordinate check below
|
|
37429
|
+
// (the actual backdrop-vs-dialog-padding distinction) never run.
|
|
37430
|
+
if (mouseDownEvent.target !== dialogEl && dialogEl.contains(mouseDownEvent.target)) {
|
|
37431
|
+
return;
|
|
37432
|
+
}
|
|
37398
37433
|
const rect = dialogEl.getBoundingClientRect();
|
|
37399
37434
|
const isOutside = mouseDownEvent.clientX < rect.left || mouseDownEvent.clientX > rect.right || mouseDownEvent.clientY < rect.top || mouseDownEvent.clientY > rect.bottom;
|
|
37400
37435
|
if (!isOutside) {
|
|
@@ -37611,11 +37646,17 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
37611
37646
|
* popover's own content for attention.
|
|
37612
37647
|
*
|
|
37613
37648
|
* The via-attribute renderer defaults to `position: fixed`, overridden to
|
|
37614
|
-
* `absolute` only when there's a real anchor
|
|
37615
|
-
* popover to scroll in lockstep with
|
|
37616
|
-
* to
|
|
37617
|
-
* viewport when there's
|
|
37618
|
-
* scrollable area).
|
|
37649
|
+
* `absolute` only when there's a real anchor that itself scrolls with the
|
|
37650
|
+
* document: such an anchor needs the popover to scroll in lockstep with it
|
|
37651
|
+
* to stay visually attached, whereas `fixed` is the more direct way to stay
|
|
37652
|
+
* pinned to the viewport when there's no anchor (and avoids ever extending
|
|
37653
|
+
* the document's own scrollable area). When the anchor itself doesn't
|
|
37654
|
+
* scroll with the document — it's `position: fixed`, or nested inside
|
|
37655
|
+
* something that is, e.g. anchored to an element inside another top-layer
|
|
37656
|
+
* popover/modal dialog — staying `fixed` is correct *despite* having a real
|
|
37657
|
+
* anchor too, since the anchor never moves on scroll either; see
|
|
37658
|
+
* `data-anchor-scrolls` in openEffect/the CSS below for how that's detected
|
|
37659
|
+
* and applied.
|
|
37619
37660
|
*/
|
|
37620
37661
|
let openLocalPopoverCount = 0;
|
|
37621
37662
|
const css$r = /* css */`
|
|
@@ -37707,13 +37748,11 @@ const css$r = /* css */`
|
|
|
37707
37748
|
outline-color: var(--popover-outline-color);
|
|
37708
37749
|
outline-offset: 0px;
|
|
37709
37750
|
box-shadow: var(--popover-box-shadow);
|
|
37710
|
-
/*
|
|
37711
|
-
|
|
37712
|
-
|
|
37713
|
-
|
|
37714
|
-
|
|
37715
|
-
transition-duration: var(--popup-position-transition-duration, 0s);
|
|
37716
|
-
transition-timing-function: ease-out;
|
|
37751
|
+
/* left/top are NOT transitioned here — applyNewPosition (visible_rect.js)
|
|
37752
|
+
drives that itself via the Web Animations API instead of CSS, so it
|
|
37753
|
+
stays independent from navi-animation's own opacity/scale/display
|
|
37754
|
+
transition list below (no shared transition-property to clobber, no
|
|
37755
|
+
propertyName to filter). */
|
|
37717
37756
|
overflow: auto;
|
|
37718
37757
|
overscroll-behavior: none;
|
|
37719
37758
|
|
|
@@ -37750,11 +37789,12 @@ const css$r = /* css */`
|
|
|
37750
37789
|
/* The via-attribute renderer's own default: an element in the top layer
|
|
37751
37790
|
always uses the initial containing block regardless of "position",
|
|
37752
37791
|
so this is really "pinned to the viewport" either way — fixed is
|
|
37753
|
-
just the more direct way to say so. Overridden
|
|
37754
|
-
|
|
37755
|
-
|
|
37756
|
-
renderer (no [popover]
|
|
37757
|
-
rules, it's always the base
|
|
37792
|
+
just the more direct way to say so. Overridden to absolute below only
|
|
37793
|
+
when data-anchor-scrolls is set — see this file's top comment, and
|
|
37794
|
+
that attribute's own JS-side comment in openEffect, for the full
|
|
37795
|
+
fixed-vs-absolute reasoning. The custom renderer (no [popover]
|
|
37796
|
+
attribute) never matches either of these rules, it's always the base
|
|
37797
|
+
"position: absolute" above. */
|
|
37758
37798
|
&[popover] {
|
|
37759
37799
|
position: fixed;
|
|
37760
37800
|
/* The native top layer already gives "last shown wins" for free —
|
|
@@ -37763,9 +37803,10 @@ const css$r = /* css */`
|
|
|
37763
37803
|
var directly on an ancestor. */
|
|
37764
37804
|
z-index: unset;
|
|
37765
37805
|
padding: 0;
|
|
37766
|
-
|
|
37767
|
-
|
|
37768
|
-
|
|
37806
|
+
|
|
37807
|
+
&[data-anchor-scrolls] {
|
|
37808
|
+
position: absolute;
|
|
37809
|
+
}
|
|
37769
37810
|
}
|
|
37770
37811
|
|
|
37771
37812
|
&[data-anchor-out-of-view] {
|
|
@@ -37899,11 +37940,14 @@ const css$r = /* css */`
|
|
|
37899
37940
|
* based on `positionArea`. Any other explicit value is used as-is.
|
|
37900
37941
|
* @param {string} [props.animationDuration] - Maps to
|
|
37901
37942
|
* `--popup-animation-duration`.
|
|
37902
|
-
* @param {Element|{current: Element}} [props.anchor] - The element the
|
|
37943
|
+
* @param {Element|{current: Element}|string} [props.anchor] - The element the
|
|
37903
37944
|
* popover is positioned relative to. Defaults to whatever triggered the
|
|
37904
37945
|
* open (`e.detail.anchor`/`e.detail.source`), if any — with no anchor at
|
|
37905
37946
|
* all, the popover docks to its container instead (viewport for
|
|
37906
|
-
* `layer="top"`, positioned ancestor for `layer="local"`).
|
|
37947
|
+
* `layer="top"`, positioned ancestor for `layer="local"`). A string is
|
|
37948
|
+
* resolved via `document.getElementById` when the popover opens — mainly
|
|
37949
|
+
* useful for `defaultOpen` (there's no triggering event/ref to read a
|
|
37950
|
+
* real element from yet at that point, only an id known up front).
|
|
37907
37951
|
* @param {"override"|"ignore"} [props.anchorCustomEventDetail="override"] -
|
|
37908
37952
|
* Whether an explicit `anchor` prop takes precedence over
|
|
37909
37953
|
* (`"override"`, default) or is ignored in favor of
|
|
@@ -38144,11 +38188,14 @@ const usePopoverProps = props => {
|
|
|
38144
38188
|
// only has this one call site.
|
|
38145
38189
|
let anchorElement;
|
|
38146
38190
|
if (typeof anchor === "string") {
|
|
38147
|
-
//
|
|
38148
|
-
//
|
|
38149
|
-
//
|
|
38150
|
-
//
|
|
38151
|
-
|
|
38191
|
+
// Resolved at open time (not render time) via getElementById — mainly
|
|
38192
|
+
// for defaultOpen, where there's no triggering event/ref yet to read a
|
|
38193
|
+
// real element from, only an id known up front (e.g. a popover nested
|
|
38194
|
+
// inside another, anchored to an element rendered by that ancestor).
|
|
38195
|
+
anchorElement = document.getElementById(anchor);
|
|
38196
|
+
if (!anchorElement) {
|
|
38197
|
+
console.warn(`Popover: anchor="${anchor}" did not match any element`);
|
|
38198
|
+
}
|
|
38152
38199
|
} else if (anchor) {
|
|
38153
38200
|
// anchor prop is a ref or a DOM element — always a real anchor,
|
|
38154
38201
|
// regardless of anchorCustomEventDetail.
|
|
@@ -38157,16 +38204,31 @@ const usePopoverProps = props => {
|
|
|
38157
38204
|
anchorElement = e.detail.anchor;
|
|
38158
38205
|
}
|
|
38159
38206
|
const hasAnchorElement = Boolean(anchorElement);
|
|
38160
|
-
|
|
38207
|
+
// getPositionedParent already resolves to document.documentElement for
|
|
38208
|
+
// the via-attribute renderer on its own (popoverEl's own `popover`
|
|
38209
|
+
// attribute, present from render regardless of whether it's actually
|
|
38210
|
+
// open yet — see offset_parent.js's own doc) — no isTopLayer branch
|
|
38211
|
+
// needed here.
|
|
38212
|
+
const positionedAncestor = getPositionedParent(popoverEl);
|
|
38161
38213
|
// Drives the via-attribute renderer's own position: fixed/absolute
|
|
38162
38214
|
// switch (see this file's top comment) — set here, well before any
|
|
38163
38215
|
// positioning/measurement runs, so there's no ordering subtlety to get
|
|
38164
38216
|
// wrong (unlike the CSS this replaced, which keyed off the resolved
|
|
38165
38217
|
// animation instead, known too late relative to the first measurement).
|
|
38166
|
-
|
|
38167
|
-
|
|
38218
|
+
// True only for a real anchor that itself scrolls with the document —
|
|
38219
|
+
// that's the one case `absolute` (scrolling in lockstep with it) is
|
|
38220
|
+
// correct; not just "has an anchor at all", since an anchor that's
|
|
38221
|
+
// itself `position: fixed`, or nested inside something that is (e.g.
|
|
38222
|
+
// anchored to an element inside another top-layer popover/modal dialog,
|
|
38223
|
+
// which are always `position: fixed` — see popover.jsx's/dialog.jsx's
|
|
38224
|
+
// own `[popover]`/`[data-layer="top"]` rules), stays pinned to the
|
|
38225
|
+
// viewport regardless of document scroll too, so this popover must stay
|
|
38226
|
+
// `fixed` right alongside it instead — switching to `absolute` there
|
|
38227
|
+
// would make it drift away from an anchor that never actually moves.
|
|
38228
|
+
if (hasAnchorElement && !findSelfOrAncestorFixedPosition(anchorElement)) {
|
|
38229
|
+
popoverEl.setAttribute("data-anchor-scrolls", "");
|
|
38168
38230
|
} else {
|
|
38169
|
-
popoverEl.removeAttribute("data-anchor");
|
|
38231
|
+
popoverEl.removeAttribute("data-anchor-scrolls");
|
|
38170
38232
|
}
|
|
38171
38233
|
if (!isTopLayer) {
|
|
38172
38234
|
// Assigned fresh on every open (including reopen) — see
|
|
@@ -38266,10 +38328,10 @@ const usePopoverProps = props => {
|
|
|
38266
38328
|
|
|
38267
38329
|
// What we observe for repositioning on resize/scroll/visibility
|
|
38268
38330
|
// changes: the anchor itself whenever there's a real one (either
|
|
38269
|
-
// renderer), otherwise whatever we're docked against instead —
|
|
38270
|
-
//
|
|
38271
|
-
// via-attribute.
|
|
38272
|
-
const effectiveAnchor = hasAnchorElement ? anchorElement :
|
|
38331
|
+
// renderer), otherwise whatever we're docked against instead —
|
|
38332
|
+
// positionedAncestor already is document.documentElement for the
|
|
38333
|
+
// via-attribute renderer (see its own computation above).
|
|
38334
|
+
const effectiveAnchor = hasAnchorElement ? anchorElement : positionedAncestor;
|
|
38273
38335
|
const positionPopover = positionEvent => {
|
|
38274
38336
|
let position;
|
|
38275
38337
|
if (hasAnchorElement) {
|
|
@@ -38293,7 +38355,7 @@ const usePopoverProps = props => {
|
|
|
38293
38355
|
// real content size, not one already truncated by a stale value.
|
|
38294
38356
|
popoverEl.style.removeProperty("--container-position-remaining-height");
|
|
38295
38357
|
popoverEl.style.removeProperty("--container-position-remaining-width");
|
|
38296
|
-
|
|
38358
|
+
const pickOptions = {
|
|
38297
38359
|
positionArea,
|
|
38298
38360
|
positionAreaFixed,
|
|
38299
38361
|
positionAreaWhenAnchorIsInvalid,
|
|
@@ -38304,35 +38366,78 @@ const usePopoverProps = props => {
|
|
|
38304
38366
|
// real anchor or not — this tells pickPositionRelativeTo to
|
|
38305
38367
|
// convert the computed coordinates into that ancestor's own
|
|
38306
38368
|
// local space instead of assuming document-relative absolute
|
|
38307
|
-
// (see its own doc in visible_rect.js).
|
|
38308
|
-
|
|
38369
|
+
// (see its own doc in visible_rect.js). positionedAncestor is
|
|
38370
|
+
// already document.documentElement for the via-attribute
|
|
38371
|
+
// renderer, same as omitting `container` entirely would resolve
|
|
38372
|
+
// to on its own.
|
|
38373
|
+
container: positionedAncestor,
|
|
38309
38374
|
minLeft,
|
|
38310
38375
|
event: positionEvent
|
|
38311
|
-
}
|
|
38376
|
+
};
|
|
38377
|
+
position = pickPositionRelativeTo(popoverEl, anchorElement, pickOptions);
|
|
38312
38378
|
position = {
|
|
38313
38379
|
...position,
|
|
38314
38380
|
left: Math.max(position.left, minLeft)
|
|
38315
38381
|
};
|
|
38382
|
+
applyNewPosition(popoverEl, position);
|
|
38383
|
+
// applyNewPosition above just set --container-position-remaining-
|
|
38384
|
+
// width/height to the real available space near the anchor —
|
|
38385
|
+
// narrower than the "natural" size just measured, whenever there
|
|
38386
|
+
// isn't enough room. If that actually changes the popover's own
|
|
38387
|
+
// rendered box (e.g. its text rewraps once truly constrained), the
|
|
38388
|
+
// position picked above was computed against the wrong (wider,
|
|
38389
|
+
// shorter) box — left uncorrected, it paints one frame too high/low
|
|
38390
|
+
// before ever getting a chance to fix itself, since the
|
|
38391
|
+
// ResizeObserver watching this same element for exactly that
|
|
38392
|
+
// (rectEffect.observeSize below) only reacts on the *next*
|
|
38393
|
+
// animation frame (see visible_rect.js's own observeSize comment
|
|
38394
|
+
// for why: reacting synchronously there would re-trigger the very
|
|
38395
|
+
// observer that just fired, the "ResizeObserver loop" it's built to
|
|
38396
|
+
// avoid). Re-measuring here instead, still without clearing the
|
|
38397
|
+
// constraint this time (unlike above — it's now the real, final
|
|
38398
|
+
// value, not a stale one), converges before this function ever
|
|
38399
|
+
// returns, so nothing gets painted in between.
|
|
38400
|
+
if (popoverEl.offsetWidth !== position.width || popoverEl.offsetHeight !== position.height) {
|
|
38401
|
+
position = pickPositionRelativeTo(popoverEl, anchorElement, pickOptions);
|
|
38402
|
+
position = {
|
|
38403
|
+
...position,
|
|
38404
|
+
left: Math.max(position.left, minLeft)
|
|
38405
|
+
};
|
|
38406
|
+
applyNewPosition(popoverEl, position);
|
|
38407
|
+
}
|
|
38316
38408
|
} else {
|
|
38317
38409
|
// No real anchor: dock against a container instead — omitting
|
|
38318
38410
|
// pickPositionRelativeTo's own `anchor` argument entirely puts it
|
|
38319
38411
|
// in its own container-docked mode (see its own doc for what that
|
|
38320
|
-
// changes).
|
|
38321
|
-
//
|
|
38322
|
-
//
|
|
38323
|
-
//
|
|
38324
|
-
//
|
|
38325
|
-
|
|
38326
|
-
// visibleRectEffect's own observation target.
|
|
38327
|
-
position = pickPositionRelativeTo(popoverEl, null, {
|
|
38412
|
+
// changes). positionedAncestor is passed either way — already
|
|
38413
|
+
// document.documentElement for the via-attribute renderer (see its
|
|
38414
|
+
// own computation above), the real positioned ancestor for the
|
|
38415
|
+
// custom renderer, already computed above for visibleRectEffect's
|
|
38416
|
+
// own observation target regardless.
|
|
38417
|
+
const pickOptions = {
|
|
38328
38418
|
positionArea,
|
|
38329
|
-
container:
|
|
38419
|
+
container: positionedAncestor,
|
|
38330
38420
|
marginWithContainer: resolveSpacingSize(marginWithContainer),
|
|
38331
38421
|
event: positionEvent
|
|
38332
|
-
}
|
|
38422
|
+
};
|
|
38423
|
+
position = pickPositionRelativeTo(popoverEl, null, pickOptions);
|
|
38424
|
+
applyNewPosition(popoverEl, position);
|
|
38425
|
+
// Same reasoning as the real-anchor branch above — a first open
|
|
38426
|
+
// starts from the same unconstrained state (no
|
|
38427
|
+
// --container-position-remaining-width/height set yet either),
|
|
38428
|
+
// so the same rewrap-after-constraint mismatch can happen here too.
|
|
38429
|
+
if (popoverEl.offsetWidth !== position.width || popoverEl.offsetHeight !== position.height) {
|
|
38430
|
+
position = pickPositionRelativeTo(popoverEl, null, pickOptions);
|
|
38431
|
+
applyNewPosition(popoverEl, position);
|
|
38432
|
+
}
|
|
38333
38433
|
}
|
|
38334
38434
|
debugPopup(positionEvent, `positionPopover() -> left: ${position.left}, top: ${position.top}`);
|
|
38335
|
-
|
|
38435
|
+
// A descendant's own visibleRectEffect (visible_rect.js — e.g. a
|
|
38436
|
+
// Callout, or another nested Popover, anchored to something inside
|
|
38437
|
+
// this one) knowing to recheck its own position whenever this popover
|
|
38438
|
+
// itself moves is handled generically by applyNewPosition itself
|
|
38439
|
+
// (dispatches navi_position_change on every call) — nothing to do
|
|
38440
|
+
// here.
|
|
38336
38441
|
};
|
|
38337
38442
|
if (scrollCapture) {
|
|
38338
38443
|
addCleanup(trapScrollInside(popoverEl));
|
|
@@ -38342,6 +38447,32 @@ const usePopoverProps = props => {
|
|
|
38342
38447
|
debug: debugFocus
|
|
38343
38448
|
}));
|
|
38344
38449
|
}
|
|
38450
|
+
|
|
38451
|
+
// Real top-layer stacking is call-order-based (see this file's own
|
|
38452
|
+
// "the top layer stacks later showPopover() calls above earlier ones"
|
|
38453
|
+
// comment) — so when a popover nests inside another openable ancestor
|
|
38454
|
+
// (a <dialog>, another popover, a <details>, or an [aria-expanded]
|
|
38455
|
+
// container) and that ancestor *closes and reopens* while we ourselves
|
|
38456
|
+
// stay open the whole time, the ancestor's own fresh showModal()/
|
|
38457
|
+
// showPopover() call ends up later than our own stale one, leaving us
|
|
38458
|
+
// stacked underneath it — even though we never actually closed (an
|
|
38459
|
+
// ancestor closing hides us for free via the normal "display: none
|
|
38460
|
+
// cascades through the top layer too" rule, no explicit hidePopover()
|
|
38461
|
+
// needed for that half). Deliberately does NOT go through
|
|
38462
|
+
// openController/requestClose — we're not being asked to close, only to
|
|
38463
|
+
// re-assert our own stacking position once the ancestor comes back.
|
|
38464
|
+
if (isTopLayer) {
|
|
38465
|
+
onAncestorReopen(popoverEl, () => {
|
|
38466
|
+
if (backdropEl?.matches(":popover-open")) {
|
|
38467
|
+
backdropEl.hidePopover();
|
|
38468
|
+
}
|
|
38469
|
+
backdropEl?.showPopover();
|
|
38470
|
+
if (popoverEl.matches(":popover-open")) {
|
|
38471
|
+
popoverEl.hidePopover();
|
|
38472
|
+
}
|
|
38473
|
+
popoverEl.showPopover();
|
|
38474
|
+
});
|
|
38475
|
+
}
|
|
38345
38476
|
const rectEffect = visibleRectEffect(effectiveAnchor, ({
|
|
38346
38477
|
visibilityRatio
|
|
38347
38478
|
}, {
|
|
@@ -38369,6 +38500,14 @@ const usePopoverProps = props => {
|
|
|
38369
38500
|
// while open (e.g. an expand/collapse toggle inside it) — not just when
|
|
38370
38501
|
// the anchor itself moves/resizes/re-anchors.
|
|
38371
38502
|
rectEffect.observeSize(popoverEl);
|
|
38503
|
+
// A descendant anchored to something inside this popover (a Callout, a
|
|
38504
|
+
// further-nested Popover) needing to know about this popover's own
|
|
38505
|
+
// left/top repositioning transition — not just that the target changed
|
|
38506
|
+
// (navi_position_change above), but that a real, currently-playing
|
|
38507
|
+
// transition is moving it right now — is handled generically by
|
|
38508
|
+
// applyNewPosition itself (see its own notifyPositionTransition in
|
|
38509
|
+
// visible_rect.js), since positionPopover already goes through it
|
|
38510
|
+
// above; nothing to wire up here.
|
|
38372
38511
|
addCleanup(() => {
|
|
38373
38512
|
rectEffect.disconnect();
|
|
38374
38513
|
});
|
|
@@ -39810,10 +39949,75 @@ const TimeTime = ({
|
|
|
39810
39949
|
});
|
|
39811
39950
|
}
|
|
39812
39951
|
const totalMinutes = date.getHours() * 60 + date.getMinutes();
|
|
39813
|
-
|
|
39814
|
-
|
|
39815
|
-
|
|
39816
|
-
|
|
39952
|
+
// Midnight (hour 0) can't go through formatMinuteDuration's own default
|
|
39953
|
+
// zero-hour handling: it drops a zero-valued unit entirely (by design — a
|
|
39954
|
+
// real 5-minute duration should print as "5 minutes", not "0 hours 5
|
|
39955
|
+
// minutes"), so "00:05" would otherwise render identically to an actual
|
|
39956
|
+
// 5-minute duration, silently losing the fact that it's midnight. Every
|
|
39957
|
+
// other hour keeps at least its own "N hour(s)" wording as a hint that
|
|
39958
|
+
// this is a time-of-day, not a duration — only hour 0 loses that hint
|
|
39959
|
+
// entirely.
|
|
39960
|
+
let text;
|
|
39961
|
+
if (date.getHours() !== 0) {
|
|
39962
|
+
text = formatMinuteDuration(totalMinutes, {
|
|
39963
|
+
lang,
|
|
39964
|
+
format
|
|
39965
|
+
});
|
|
39966
|
+
} else if (format !== "long") {
|
|
39967
|
+
// short/narrow/compact: keep the "0 h"/"0h" hour part instead of
|
|
39968
|
+
// dropping it (formatMinuteDuration's own alwaysShowHours) — e.g.
|
|
39969
|
+
// "0 h et 5 min"/"0h 5min"/"0h05" — rather than substituting a
|
|
39970
|
+
// translated "midnight" word, which would look out of place squeezed
|
|
39971
|
+
// into these otherwise terse, symbol-based formats.
|
|
39972
|
+
text = formatMinuteDuration(totalMinutes, {
|
|
39973
|
+
lang,
|
|
39974
|
+
format,
|
|
39975
|
+
alwaysShowHours: true
|
|
39976
|
+
});
|
|
39977
|
+
} else {
|
|
39978
|
+
const midnightWord = naviI18n("time.midnight", undefined, {
|
|
39979
|
+
lang
|
|
39980
|
+
});
|
|
39981
|
+
if (midnightWord === "time.midnight") {
|
|
39982
|
+
// No "midnight" translation registered for this language — fall back
|
|
39983
|
+
// to this language's own literal "0 heure(s)" wording instead (still
|
|
39984
|
+
// better than leaking the untranslated key, or substituting an
|
|
39985
|
+
// English word that wouldn't grammatically match the rest of the
|
|
39986
|
+
// sentence in whatever language this actually is).
|
|
39987
|
+
text = formatMinuteDuration(totalMinutes, {
|
|
39988
|
+
lang,
|
|
39989
|
+
format,
|
|
39990
|
+
alwaysShowHours: true
|
|
39991
|
+
});
|
|
39992
|
+
} else {
|
|
39993
|
+
// Swap just the "0 heure(s)" part of the Intl-generated duration
|
|
39994
|
+
// string for the translated "midnight" word, keeping everything else
|
|
39995
|
+
// (the conjunction, the minutes part) exactly as Intl would produce
|
|
39996
|
+
// for this locale — formatToParts tags each token with the unit it
|
|
39997
|
+
// belongs to, so the swap doesn't need to know the locale's own
|
|
39998
|
+
// grammar/word order. Only ever one hour-tagged group per call
|
|
39999
|
+
// (hours is always 0 or absent here), but guarded anyway in case a
|
|
40000
|
+
// future Intl implementation ever splits it into more parts.
|
|
40001
|
+
const parts = new Intl.DurationFormat(lang, {
|
|
40002
|
+
style: "long",
|
|
40003
|
+
hoursDisplay: "always"
|
|
40004
|
+
}).formatToParts({
|
|
40005
|
+
hours: 0,
|
|
40006
|
+
minutes: date.getMinutes()
|
|
40007
|
+
});
|
|
40008
|
+
let hourGroupReplaced = false;
|
|
40009
|
+
text = parts.map(part => {
|
|
40010
|
+
if (part.unit !== "hour") {
|
|
40011
|
+
return part.value;
|
|
40012
|
+
}
|
|
40013
|
+
if (hourGroupReplaced) {
|
|
40014
|
+
return "";
|
|
40015
|
+
}
|
|
40016
|
+
hourGroupReplaced = true;
|
|
40017
|
+
return midnightWord;
|
|
40018
|
+
}).join("");
|
|
40019
|
+
}
|
|
40020
|
+
}
|
|
39817
40021
|
return jsx(TimeText, {
|
|
39818
40022
|
dateTime: dateTime,
|
|
39819
40023
|
...props,
|