@octane-xplat/ui 0.7.2 → 0.9.0
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 +4 -3
- package/dist/native/Button.tsrx.js +1 -1
- package/dist/native/CommandPalette.tsrx.js +7 -1
- package/dist/native/Grid.tsrx.js +31 -2
- package/dist/native/KeyboardAvoiding.mobile.tsrx.js +2 -2
- package/dist/native/Markdown.tsrx.js +233 -0
- package/dist/native/Overlay.tsrx.js +15 -26
- package/dist/native/Pagination.tsrx.js +15 -12
- package/dist/native/PinInput.tsrx.js +25 -7
- package/dist/native/Popover.tsrx.js +33 -23
- package/dist/native/Pressable.tsrx.js +4 -1
- package/dist/native/Screen.tsrx.js +1 -1
- package/dist/native/SearchInput.tsrx.js +6 -3
- package/dist/native/Sheet.tsrx.js +23 -38
- package/dist/native/Slider.tsrx.js +23 -14
- package/dist/native/Text.tsrx.js +16 -0
- package/dist/native/TextArea.tsrx.js +6 -3
- package/dist/native/TextInput.tsrx.js +6 -3
- package/dist/native/VirtualList.tsrx.js +305 -121
- package/dist/native/WebView.tsrx.js +17 -2
- package/dist/native/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
- package/dist/native/escape-props.js +2 -0
- package/dist/native/grid-auto-placement.js +83 -0
- package/dist/native/grid-placement.js +20 -0
- package/dist/native/index.shared.js +7 -5
- package/dist/native/keyboard-inset.mobile.js +5 -5
- package/dist/native/navigation-request.js +26 -0
- package/dist/native/octane-client-build.json +1 -1
- package/dist/native/overlay-lifetime.mobile.js +65 -0
- package/dist/native/pan.tsrx.js +16 -2
- package/dist/native/route-table.js +90 -10
- package/dist/native/route.js +26 -16
- package/dist/native/shared.js +7 -5
- package/dist/native/sheet-service.js +2 -2
- package/dist/native/svg.mobile.js +1 -1
- package/dist/native/{tap-to-blur.mobile.js → tap-to-blur.ios.js} +20 -32
- package/dist/native/text-focus.js +10 -0
- package/dist/native/text-write.js +18 -11
- package/dist/native/toast-anchor.tsrx.js +1 -1
- package/dist/native/ui.js +7 -6
- package/dist/native/useMeasure.tsrx.js +17 -18
- package/dist/native/vendor/ui-svg/index.android.js +2 -2
- package/dist/native/vendor/ui-svg/index.ios.js +2 -2
- package/dist/native/vendor/ui-svg/{index.common.js → src/index.common.js} +1 -1
- package/dist/native/virtual-list-cell-pool.js +63 -0
- package/dist/web/Button.web.tsrx.js +1 -1
- package/dist/web/KeyboardAvoiding.web.tsrx.js +78 -0
- package/dist/web/Markdown.tsrx.js +98 -0
- package/dist/web/Overlay.web.tsrx.js +81 -32
- package/dist/web/PinInput.web.tsrx.js +24 -10
- package/dist/web/Pressable.web.tsrx.js +169 -53
- package/dist/web/SearchInput.web.tsrx.js +2 -2
- package/dist/web/Sheet.web.tsrx.js +20 -7
- package/dist/web/TextArea.web.tsrx.js +22 -17
- package/dist/web/TextInput.web.tsrx.js +2 -2
- package/dist/web/VirtualList.web.tsrx.js +220 -109
- package/dist/web/WebView.web.tsrx.js +3 -3
- package/dist/web/{index.shared.js → index.shared.web.js} +7 -5
- package/dist/web/modal-focus.web.js +95 -0
- package/dist/web/navigation-request.js +26 -0
- package/dist/web/octane-client-build.json +1 -1
- package/dist/web/pan.web.tsrx.js +27 -10
- package/dist/web/route-table.js +90 -10
- package/dist/web/route.web.js +88 -22
- package/dist/web/sheet-service.web.js +8 -0
- package/dist/web/theme/colorScheme.web.js +31 -4
- package/dist/web/ui.js +8 -6
- package/dist/web/virtual-list-cell-pool.js +63 -0
- package/package.json +21 -19
- package/platforms/android/native-api-usage.json +7 -7
- package/src/CommandPalette.tsrx +8 -1
- package/src/Grid.tsrx +32 -1
- package/src/Icon.macos.tsrx +1 -0
- package/src/KeyboardAvoiding.macos.tsrx +7 -1
- package/src/KeyboardAvoiding.mobile.tsrx +2 -2
- package/src/KeyboardAvoiding.tsrx +15 -0
- package/src/KeyboardAvoiding.web.tsrx +15 -0
- package/src/Markdown.tsrx +121 -0
- package/src/Meter.tsrx +1 -0
- package/src/Overlay.tsrx +15 -30
- package/src/Overlay.web.tsrx +12 -3
- package/src/Pagination.tsrx +6 -4
- package/src/PinInput.macos.tsrx +20 -6
- package/src/PinInput.mobile.test.ts +107 -0
- package/src/PinInput.tsrx +20 -6
- package/src/PinInput.web.test.tsrx +42 -0
- package/src/PinInput.web.tsrx +20 -6
- package/src/Popover.tsrx +36 -22
- package/src/Pressable.tsrx +1 -1
- package/src/Pressable.web.test.tsrx +36 -0
- package/src/Pressable.web.tsrx +37 -6
- package/src/RouteHost.macos.tsrx +1 -0
- package/src/Screen.tsrx +1 -1
- package/src/SearchInput.tsrx +4 -3
- package/src/SearchInput.web.tsrx +1 -1
- package/src/Sheet.tsrx +21 -42
- package/src/Sheet.web.tsrx +9 -1
- package/src/Slider.tsrx +13 -11
- package/src/Text.tsrx +36 -0
- package/src/TextArea.tsrx +6 -5
- package/src/TextArea.web.test.tsrx +39 -0
- package/src/TextArea.web.tsrx +7 -5
- package/src/TextInput.tsrx +6 -6
- package/src/TextInput.web.tsrx +1 -1
- package/src/VirtualList.macos.tsrx +93 -6
- package/src/VirtualList.mobile.test.tsrx +232 -0
- package/src/VirtualList.tsrx +285 -81
- package/src/VirtualList.web.tsrx +131 -25
- package/src/WebView.tsrx +20 -10
- package/src/WebView.web.tsrx +3 -0
- package/src/android/BottomNavigationView.android.tsrx +1 -0
- package/src/android/MaterialDialog.android.tsrx +1 -0
- package/src/anim.macos.tsrx +1 -0
- package/src/apply-native-modifiers.native.ts +4 -1
- package/src/child-array.ts +12 -7
- package/src/colorScheme.macos.ts +1 -0
- package/src/controls.macos.tsrx +4 -1
- package/src/data-driver.mobile.test.ts +120 -0
- package/src/data-lifecycle.mobile.test.ts +251 -0
- package/src/escape-props.ts +13 -1
- package/src/grid-auto-placement.ts +163 -0
- package/src/grid-placement.ts +29 -0
- package/src/hosted-unsupported.macos.tsrx +1 -0
- package/src/index.macos.ts +23 -3
- package/src/index.mobile.ts +0 -1
- package/src/index.shared.ts +22 -1
- package/src/index.shared.web.ts +291 -0
- package/src/index.web.ts +1 -1
- package/src/input-readiness.web.test.tsrx +82 -0
- package/src/ios/UITabBar.ios.tsrx +1 -0
- package/src/keyboard-inset.mobile.test.ts +62 -0
- package/src/keyboard-inset.mobile.ts +63 -43
- package/src/layout-shells.macos.tsrx +1 -5
- package/src/macos-extras.macos.tsrx +2 -0
- package/src/modal-focus.web.test.tsrx +63 -0
- package/src/modal-focus.web.ts +162 -0
- package/src/navigation-request.ts +37 -0
- package/src/overlay-lifecycle.mobile.test.ts +316 -0
- package/src/overlay-lifetime.mobile.ts +107 -0
- package/src/pan.mobile.test.tsrx +116 -0
- package/src/pan.tsrx +20 -2
- package/src/pan.web.tsrx +35 -7
- package/src/platformProps.ts +1 -0
- package/src/popover.mobile.test.ts +9 -1
- package/src/pressable-accessibility.mobile.test.ts +30 -0
- package/src/props.ts +163 -3
- package/src/refresh.ts +6 -1
- package/src/refresh.web.test.ts +77 -77
- package/src/route-android.test.ts +162 -0
- package/src/route-history.web.test.ts +80 -0
- package/src/route-navigation.web.test.ts +173 -0
- package/src/route-table.test.ts +188 -9
- package/src/route-table.ts +138 -10
- package/src/route.macos.ts +56 -13
- package/src/route.ts +54 -24
- package/src/route.web.ts +141 -24
- package/src/sheet-detents.ts +5 -3
- package/src/sheet-service.macos.ts +5 -1
- package/src/sheet-service.ts +1 -1
- package/src/sheet-service.web.ts +8 -0
- package/src/sheet-snap.test.ts +23 -23
- package/src/sheet-snap.ts +6 -1
- package/src/svg.mobile.ts +9 -3
- package/src/tap-to-blur.ios.ts +106 -0
- package/src/tap-to-blur.mobile.ts +21 -95
- package/src/text-focus.ts +12 -0
- package/src/text-input.mobile.test.ts +89 -0
- package/src/text-write.ts +21 -11
- package/src/theme/colorScheme.linux.ts +2 -4
- package/src/theme/colorScheme.web.ts +51 -4
- package/src/theme/theme-scheme.macos.ts +8 -2
- package/src/theme/tokens.css +1740 -1718
- package/src/translate.macos.ts +4 -1
- package/src/useMeasure.tsrx +20 -21
- package/src/virtual-list-cell-pool.test.ts +53 -0
- package/src/virtual-list-cell-pool.ts +81 -0
- package/src/virtual-list-layout.test.ts +11 -8
- package/src/virtual-list-layout.ts +4 -8
- package/src/windows.linux.ts +7 -2
- package/src/windows.macos.ts +1 -0
- package/types/generated/.tsrx-typegen-manifest.json +16 -3
- package/types/generated/Grid.d.ts +0 -1
- package/types/generated/KeyboardAvoiding.d.ts +3 -0
- package/types/generated/KeyboardAvoiding.web.d.ts +2 -0
- package/types/generated/Markdown.d.ts +59 -0
- package/types/generated/TextArea.d.ts +2 -2
- package/types/generated/TextArea.web.d.ts +1 -1
- package/types/generated/TextInput.d.ts +2 -3
- package/types/generated/VirtualList.d.ts +3 -3
- package/types/generated/escape-props.d.ts +4 -0
- package/types/generated/grid-auto-placement.d.ts +13 -0
- package/types/generated/grid-placement.d.ts +5 -0
- package/types/generated/index.mobile.d.ts +0 -1
- package/types/generated/index.shared.d.ts +6 -3
- package/types/generated/index.shared.web.d.ts +114 -0
- package/types/generated/index.web.d.ts +1 -1
- package/types/generated/modal-focus.web.d.ts +3 -0
- package/types/generated/navigation-request.d.ts +10 -0
- package/types/generated/overlay-lifetime.mobile.d.ts +10 -0
- package/types/generated/props.d.ts +111 -3
- package/types/generated/route-table.d.ts +14 -3
- package/types/generated/svg.mobile.d.ts +2 -2
- package/types/generated/tap-to-blur.ios.d.ts +13 -0
- package/types/generated/tap-to-blur.mobile.d.ts +1 -15
- package/types/generated/text-focus.d.ts +3 -0
- package/types/generated/text-write.d.ts +3 -1
- package/types/generated/theme/colorScheme.web.d.ts +5 -0
- package/types/generated/virtual-list-cell-pool.d.ts +18 -0
- package/types/index.macos.d.ts +15 -3
- /package/src/vendor/ui-svg/{index.android.ts → src/index.android.ts} +0 -0
- /package/src/vendor/ui-svg/{index.common.ts → src/index.common.ts} +0 -0
- /package/src/vendor/ui-svg/{index.ios.ts → src/index.ios.ts} +0 -0
- /package/src/vendor/ui-svg/{index.ts → src/index.ts} +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/SVGKit.d.ts +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/android.d.ts +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/ios.d.ts +0 -0
- /package/types/generated/vendor/ui-svg/{index.common.d.ts → src/index.common.d.ts} +0 -0
- /package/types/generated/vendor/ui-svg/{index.d.ts → src/index.d.ts} +0 -0
- /package/types/generated/vendor/ui-svg/{index.ios.d.ts → src/index.ios.d.ts} +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/modal-focus.web.ts
|
|
2
|
+
var scopes = [];
|
|
3
|
+
var originalInert = /* @__PURE__ */ new Map();
|
|
4
|
+
var observer;
|
|
5
|
+
function topScope() {
|
|
6
|
+
return scopes[scopes.length - 1];
|
|
7
|
+
}
|
|
8
|
+
function updateIsolation() {
|
|
9
|
+
const top = topScope();
|
|
10
|
+
if (!top) {
|
|
11
|
+
for (const [element, inert] of originalInert) if (!inert) element.removeAttribute("inert");
|
|
12
|
+
originalInert.clear();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
for (const element of Array.from(top.layer.ownerDocument.body.children)) {
|
|
16
|
+
if (!originalInert.has(element)) originalInert.set(element, element.hasAttribute("inert"));
|
|
17
|
+
const inert = originalInert.get(element) || !element.contains(top.layer);
|
|
18
|
+
element.toggleAttribute("inert", inert);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function tabStops(panel) {
|
|
22
|
+
return Array.from(panel.querySelectorAll("a[href],button,input,textarea,select,[tabindex],[contenteditable=\"true\"]")).filter((element) => {
|
|
23
|
+
if (element.tabIndex < 0 || element.matches(":disabled") || element.closest("[inert],[hidden]")) return false;
|
|
24
|
+
for (let parent = element; parent; parent = parent.parentElement) {
|
|
25
|
+
const style = getComputedStyle(parent);
|
|
26
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Own keyboard focus only for a modal body portal. Inert preserves the
|
|
32
|
+
* background's state while hiding it from both focus and the browser AX tree. */
|
|
33
|
+
function isolateModalFocus(layer, panel, dismiss) {
|
|
34
|
+
const document = layer.ownerDocument;
|
|
35
|
+
const scope = {
|
|
36
|
+
layer,
|
|
37
|
+
panel,
|
|
38
|
+
trigger: document.activeElement
|
|
39
|
+
};
|
|
40
|
+
const tabIndex = panel.getAttribute("tabindex");
|
|
41
|
+
if (tabIndex === null) panel.tabIndex = -1;
|
|
42
|
+
scopes.push(scope);
|
|
43
|
+
updateIsolation();
|
|
44
|
+
(tabStops(panel)[0] ?? panel).focus({ preventScroll: true });
|
|
45
|
+
if (!observer) {
|
|
46
|
+
observer = new MutationObserver(updateIsolation);
|
|
47
|
+
observer.observe(document.body, { childList: true });
|
|
48
|
+
}
|
|
49
|
+
const focusInside = () => (tabStops(panel)[0] ?? panel).focus({ preventScroll: true });
|
|
50
|
+
const onFocus = (event) => {
|
|
51
|
+
if (topScope() === scope && !panel.contains(event.target)) focusInside();
|
|
52
|
+
};
|
|
53
|
+
const onKey = (event) => {
|
|
54
|
+
if (topScope() !== scope || event.isComposing || event.keyCode === 229) return;
|
|
55
|
+
if (event.key === "Escape") {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
dismiss();
|
|
59
|
+
} else if (event.key === "Tab") {
|
|
60
|
+
const stops = tabStops(panel);
|
|
61
|
+
const index = stops.indexOf(document.activeElement);
|
|
62
|
+
if (!stops.length || index < 0 || (event.shiftKey ? index === 0 : index === stops.length - 1)) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
(stops.length ? stops[event.shiftKey ? stops.length - 1 : 0] : panel).focus({ preventScroll: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
document.addEventListener("focusin", onFocus);
|
|
69
|
+
document.addEventListener("keydown", onKey, true);
|
|
70
|
+
return () => {
|
|
71
|
+
const wasTop = topScope() === scope;
|
|
72
|
+
const index = scopes.indexOf(scope);
|
|
73
|
+
if (index < 0) return;
|
|
74
|
+
for (const other of scopes) if (other !== scope && other.trigger && scope.layer.contains(other.trigger)) other.trigger = scope.trigger;
|
|
75
|
+
scopes.splice(index, 1);
|
|
76
|
+
document.removeEventListener("focusin", onFocus);
|
|
77
|
+
document.removeEventListener("keydown", onKey, true);
|
|
78
|
+
if (tabIndex === null) panel.removeAttribute("tabindex");
|
|
79
|
+
updateIsolation();
|
|
80
|
+
if (!scopes.length) {
|
|
81
|
+
observer?.disconnect();
|
|
82
|
+
observer = void 0;
|
|
83
|
+
}
|
|
84
|
+
if (wasTop) {
|
|
85
|
+
const trigger = scope.trigger;
|
|
86
|
+
if (trigger?.isConnected && !trigger.closest("[inert]")) trigger.focus({ preventScroll: true });
|
|
87
|
+
else {
|
|
88
|
+
const remaining = topScope();
|
|
89
|
+
if (remaining) (tabStops(remaining.panel)[0] ?? remaining.panel).focus({ preventScroll: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { isolateModalFocus };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/navigation-request.ts
|
|
2
|
+
function createNavigationRequests() {
|
|
3
|
+
const versions = /* @__PURE__ */ new Map();
|
|
4
|
+
const invalidate = (stack) => {
|
|
5
|
+
const version = (versions.get(stack) ?? 0) + 1;
|
|
6
|
+
versions.set(stack, version);
|
|
7
|
+
return version;
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
invalidate,
|
|
11
|
+
begin(stack) {
|
|
12
|
+
const owned = /* @__PURE__ */ new Map([[stack, invalidate(stack)]]);
|
|
13
|
+
const isCurrent = () => [...owned].every(([name, version]) => versions.get(name) === version);
|
|
14
|
+
return {
|
|
15
|
+
isCurrent,
|
|
16
|
+
claim(name) {
|
|
17
|
+
if (!isCurrent()) return false;
|
|
18
|
+
if (!owned.has(name)) owned.set(name, invalidate(name));
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { createNavigationRequests };
|
package/dist/web/pan.web.tsrx.js
CHANGED
|
@@ -1,26 +1,43 @@
|
|
|
1
|
-
import { hookSlots, useEffect } from "octane";
|
|
1
|
+
import { hookSlots, useEffect, useRef } from "octane";
|
|
2
2
|
//#region src/pan.web.tsrx
|
|
3
|
-
var
|
|
3
|
+
var _hs$ = /* @__PURE__ */ hookSlots(2);
|
|
4
|
+
var _h$0 = /* @__PURE__ */ Symbol(_hs$);
|
|
5
|
+
var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
|
|
6
|
+
var VELOCITY_MAX_AGE_MS = 100;
|
|
4
7
|
function usePan(el, cb) {
|
|
8
|
+
const callback = useRef(cb, _h$0);
|
|
9
|
+
callback.current = cb;
|
|
10
|
+
const enabled = Boolean(cb);
|
|
5
11
|
useEffect(() => {
|
|
6
12
|
const node = el.current;
|
|
7
|
-
if (!node || !
|
|
8
|
-
let sx = 0, sy = 0, active = false
|
|
13
|
+
if (!node || !enabled) return;
|
|
14
|
+
let sx = 0, sy = 0, active = false;
|
|
15
|
+
let last = 0, lx = 0, t0 = 0;
|
|
16
|
+
let vx = 0, vy = 0, velocityAt = 0;
|
|
9
17
|
const emit = (e, state) => {
|
|
10
18
|
const now = performance.now();
|
|
11
19
|
const dt = Math.max(now - t0, 1) / 1e3;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
20
|
+
const dx = e.clientX - lx;
|
|
21
|
+
const dy = e.clientY - last;
|
|
22
|
+
if (state === "began") {
|
|
23
|
+
vx = vy = 0;
|
|
24
|
+
velocityAt = now;
|
|
25
|
+
} else if (dx !== 0 || dy !== 0) {
|
|
26
|
+
vx = dx / dt;
|
|
27
|
+
vy = dy / dt;
|
|
28
|
+
velocityAt = now;
|
|
29
|
+
}
|
|
14
30
|
lx = e.clientX;
|
|
15
31
|
last = e.clientY;
|
|
16
32
|
t0 = now;
|
|
17
|
-
|
|
33
|
+
const fresh = now - velocityAt <= VELOCITY_MAX_AGE_MS;
|
|
34
|
+
callback.current?.({
|
|
18
35
|
x: e.clientX - sx,
|
|
19
36
|
y: e.clientY - sy,
|
|
20
37
|
dx: e.clientX - sx,
|
|
21
38
|
dy: e.clientY - sy,
|
|
22
|
-
vx,
|
|
23
|
-
vy,
|
|
39
|
+
vx: fresh ? vx : 0,
|
|
40
|
+
vy: fresh ? vy : 0,
|
|
24
41
|
state,
|
|
25
42
|
target: node
|
|
26
43
|
});
|
|
@@ -60,7 +77,7 @@ function usePan(el, cb) {
|
|
|
60
77
|
node.removeEventListener("pointerup", up);
|
|
61
78
|
node.removeEventListener("pointercancel", cancel);
|
|
62
79
|
};
|
|
63
|
-
}, [el,
|
|
80
|
+
}, [el, enabled], _h$1);
|
|
64
81
|
}
|
|
65
82
|
//#endregion
|
|
66
83
|
export { usePan };
|
package/dist/web/route-table.js
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
var EXT = /\.(tsrx|tsx|ts|mts|cts|js|mjs|cjs|jsx)$/;
|
|
3
3
|
var SUFFIX = /\.(web|mobile|ios|android|macos|windows|linux)$/;
|
|
4
4
|
var PARAM = /^\[(.+)\]$/;
|
|
5
|
+
/** Build-time loader modules — `docs.loader.ts` pairs with route `docs`
|
|
6
|
+
* for `dataMode: 'baked'`. Never a route itself, never in a runtime glob. */
|
|
7
|
+
var LOADER_FILE = /\.loader\.(ts|mts|cts|js|mjs|cjs|jsx)$/;
|
|
5
8
|
var PRESENT = /\+(modal|fade|push)$/;
|
|
9
|
+
/** Loader for a `dataMode: 'baked'` route — resolves the name out of its
|
|
10
|
+
* owning manifest's `baked` map at push time. The map is assigned after
|
|
11
|
+
* the manifest is built (generated glue) or passed in (RouteSpecSet), so
|
|
12
|
+
* the lookup reads it lazily. A missing entry throws, landing as the
|
|
13
|
+
* screen's `error` prop like a live loader rejection. */
|
|
14
|
+
function bakedLookup(manifest, name) {
|
|
15
|
+
return () => {
|
|
16
|
+
const baked = manifest()?.baked;
|
|
17
|
+
if (baked && Object.prototype.hasOwnProperty.call(baked, name)) return baked[name];
|
|
18
|
+
throw new Error(`[octane-xplat] baked route '${name}' has no baked data — run \`xplat routes\` to regenerate`);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
6
21
|
/** Internal error carrier used by the public `redirect()` helper. Keeping
|
|
7
22
|
* this in the shared route table gives both platform leaves the same guard
|
|
8
23
|
* behavior without importing a platform runtime. */
|
|
@@ -35,7 +50,9 @@ function deriveRouteManifest(files, prefer, dir = "app") {
|
|
|
35
50
|
const layouts = {};
|
|
36
51
|
const loaders = {};
|
|
37
52
|
const warned = /* @__PURE__ */ new Set();
|
|
53
|
+
const self = {};
|
|
38
54
|
for (const key of Object.keys(files).sort()) {
|
|
55
|
+
if (LOADER_FILE.test(key)) continue;
|
|
39
56
|
let rel = key.replace(/^\.?\//, "");
|
|
40
57
|
if (rel.startsWith(prefix)) rel = rel.slice(prefix.length);
|
|
41
58
|
rel = rel.replace(EXT, "");
|
|
@@ -74,8 +91,13 @@ function deriveRouteManifest(files, prefer, dir = "app") {
|
|
|
74
91
|
file: key,
|
|
75
92
|
presentation
|
|
76
93
|
};
|
|
94
|
+
const dataMode = files[key]?.dataMode;
|
|
95
|
+
if (dataMode === "baked" || dataMode === "live") meta.dataMode = dataMode;
|
|
77
96
|
const loader = files[key]?.loader;
|
|
78
|
-
if (
|
|
97
|
+
if (meta.dataMode === "baked") {
|
|
98
|
+
meta.loader = bakedLookup(() => self.m, name);
|
|
99
|
+
if (typeof loader === "function") console.warn(`[octane-xplat] baked route '${name}' also exports a loader — move it to a .loader.ts sibling so its imports stay out of the bundle`);
|
|
100
|
+
} else if (typeof loader === "function") meta.loader = loader;
|
|
79
101
|
const beforeLoad = files[key]?.beforeLoad;
|
|
80
102
|
if (typeof beforeLoad === "function") meta.beforeLoad = beforeLoad;
|
|
81
103
|
if (files[key]?.head && (typeof files[key].head === "object" || typeof files[key].head === "function")) meta.head = files[key].head;
|
|
@@ -102,12 +124,14 @@ function deriveRouteManifest(files, prefer, dir = "app") {
|
|
|
102
124
|
if (meta.loader) loaders[meta.name] = meta.loader;
|
|
103
125
|
}
|
|
104
126
|
routes.sort(bySpecificity);
|
|
105
|
-
|
|
127
|
+
const manifest = {
|
|
106
128
|
screens,
|
|
107
129
|
routes,
|
|
108
130
|
layouts,
|
|
109
131
|
loaders
|
|
110
132
|
};
|
|
133
|
+
self.m = manifest;
|
|
134
|
+
return manifest;
|
|
111
135
|
}
|
|
112
136
|
/** Match-order comparator: static segments (2) outrank params (1), ties
|
|
113
137
|
* break alphabetically so ordering is total and stable across merges. */
|
|
@@ -131,7 +155,12 @@ var mergeWarned = /* @__PURE__ */ new Set();
|
|
|
131
155
|
* from their own sources, e.g. `readdirSync` over a content dir). Each
|
|
132
156
|
* spec's `path` uses the route-dir vocabulary; `layouts` keys are path
|
|
133
157
|
* prefixes playing the `_layout` role. Same-name duplicates inside the
|
|
134
|
-
* set keep the later entry with a warn, matching merge precedence.
|
|
158
|
+
* set keep the later entry with a warn, matching merge precedence.
|
|
159
|
+
*
|
|
160
|
+
* The return is branded with the spec-derived route types — read them
|
|
161
|
+
* with `ManifestRouteParams`/`ManifestRouteNames`/
|
|
162
|
+
* `ManifestRoutePresentations` to merge programmatic names into the
|
|
163
|
+
* app's typed navigation surface (`RouteName | …`, `RouteParams & …`). */
|
|
135
164
|
function defineRoutes(input) {
|
|
136
165
|
const set = Array.isArray(input) ? { routes: input } : input;
|
|
137
166
|
const specs = set.routes;
|
|
@@ -140,6 +169,7 @@ function defineRoutes(input) {
|
|
|
140
169
|
const layouts = {};
|
|
141
170
|
const metas = /* @__PURE__ */ new Map();
|
|
142
171
|
const warned = /* @__PURE__ */ new Set();
|
|
172
|
+
const self = {};
|
|
143
173
|
for (const [key, layout] of Object.entries(extra ?? {})) {
|
|
144
174
|
const dir = key.replace(/^\/+|\/+$/g, "");
|
|
145
175
|
if (dir !== key) console.warn(`[octane-xplat] layout key '${key}' normalized to '${dir}'`);
|
|
@@ -155,7 +185,11 @@ function defineRoutes(input) {
|
|
|
155
185
|
file: spec.source ?? `programmatic:${spec.path}`,
|
|
156
186
|
presentation: spec.presentation
|
|
157
187
|
};
|
|
158
|
-
if (spec.
|
|
188
|
+
if (spec.dataMode) meta.dataMode = spec.dataMode;
|
|
189
|
+
if (spec.dataMode === "baked") {
|
|
190
|
+
meta.loader = bakedLookup(() => self.m, name);
|
|
191
|
+
if (spec.loader) console.warn(`[octane-xplat] baked route '${name}' also carries a loader — bake it in the host and pass the result via 'baked' so its code stays out of the bundle`);
|
|
192
|
+
} else if (spec.loader) meta.loader = spec.loader;
|
|
159
193
|
if (spec.beforeLoad) meta.beforeLoad = spec.beforeLoad;
|
|
160
194
|
if (spec.head) meta.head = spec.head;
|
|
161
195
|
if (metas.has(name) && !warned.has(name)) {
|
|
@@ -172,12 +206,15 @@ function defineRoutes(input) {
|
|
|
172
206
|
const routes = [...metas.values()].sort(bySpecificity);
|
|
173
207
|
const loaders = {};
|
|
174
208
|
for (const meta of routes) if (meta.loader) loaders[meta.name] = meta.loader;
|
|
175
|
-
|
|
209
|
+
const manifest = {
|
|
176
210
|
screens,
|
|
177
211
|
routes,
|
|
178
212
|
layouts,
|
|
179
|
-
loaders
|
|
213
|
+
loaders,
|
|
214
|
+
baked: set.baked
|
|
180
215
|
};
|
|
216
|
+
self.m = manifest;
|
|
217
|
+
return manifest;
|
|
181
218
|
}
|
|
182
219
|
/** Compose manifests into one — file-derived base plus any number of
|
|
183
220
|
* programmatic sets (`registerRoutes(mergeRouteManifests(fromDir, dynamic))`).
|
|
@@ -188,9 +225,11 @@ function defineRoutes(input) {
|
|
|
188
225
|
function mergeRouteManifests(...manifests) {
|
|
189
226
|
const screens = {};
|
|
190
227
|
const layouts = {};
|
|
228
|
+
const baked = {};
|
|
191
229
|
const winners = /* @__PURE__ */ new Map();
|
|
192
230
|
for (const m of manifests) {
|
|
193
231
|
Object.assign(layouts, m.layouts);
|
|
232
|
+
Object.assign(baked, m.baked);
|
|
194
233
|
Object.assign(screens, m.screens);
|
|
195
234
|
for (const meta of m.routes) {
|
|
196
235
|
const prev = winners.get(meta.name);
|
|
@@ -208,7 +247,38 @@ function mergeRouteManifests(...manifests) {
|
|
|
208
247
|
screens,
|
|
209
248
|
routes,
|
|
210
249
|
layouts,
|
|
211
|
-
loaders
|
|
250
|
+
loaders,
|
|
251
|
+
baked
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** Serialize a manifest to the normalized `RouteManifestJson` shape — the
|
|
255
|
+
* same list `xplat routes` emits for file routes, produced here for
|
|
256
|
+
* programmatic/merged manifests so hosts consume one schema. */
|
|
257
|
+
function manifestToJson(manifest) {
|
|
258
|
+
return {
|
|
259
|
+
version: 1,
|
|
260
|
+
layouts: Object.keys(manifest.layouts).sort(),
|
|
261
|
+
screens: Object.keys(manifest.screens).sort(),
|
|
262
|
+
routes: manifest.routes.map((meta) => {
|
|
263
|
+
const segs = meta.name.split("/");
|
|
264
|
+
const layoutDirs = manifest.layouts[""] ? [""] : [];
|
|
265
|
+
for (let i = 1; i < segs.length; i++) {
|
|
266
|
+
const d = segs.slice(0, i).join("/");
|
|
267
|
+
if (manifest.layouts[d]) layoutDirs.push(d);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
name: meta.name,
|
|
271
|
+
path: meta.name === "index" ? "" : meta.segments.join("/"),
|
|
272
|
+
params: [...meta.params],
|
|
273
|
+
...meta.presentation ? { presentation: meta.presentation } : {},
|
|
274
|
+
...meta.dataMode ? { dataMode: meta.dataMode } : {},
|
|
275
|
+
layouts: layoutDirs,
|
|
276
|
+
source: meta.file,
|
|
277
|
+
loader: !!meta.loader,
|
|
278
|
+
guard: !!meta.beforeLoad,
|
|
279
|
+
head: !!meta.head
|
|
280
|
+
};
|
|
281
|
+
})
|
|
212
282
|
};
|
|
213
283
|
}
|
|
214
284
|
/** Match URL path segments against a manifest — returns the winning meta
|
|
@@ -309,9 +379,19 @@ function linkPath(url) {
|
|
|
309
379
|
* is the stack prefix ('/demos/demo/x' → stack 'demos'). Unmatched names
|
|
310
380
|
* fall through as literal routes for pre-manifest callers. */
|
|
311
381
|
function matchUrl(routes, url) {
|
|
312
|
-
|
|
382
|
+
url = url.split("#", 1)[0];
|
|
383
|
+
const at = url.indexOf("?");
|
|
384
|
+
const p = at === -1 ? url : url.slice(0, at);
|
|
385
|
+
const qs = at === -1 ? void 0 : url.slice(at + 1);
|
|
313
386
|
const segs = p.split("/").filter(Boolean);
|
|
314
|
-
|
|
387
|
+
let query;
|
|
388
|
+
try {
|
|
389
|
+
if (!decodeURIComponent(p)) return null;
|
|
390
|
+
query = parseQueryString(qs);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
if (error instanceof URIError) return null;
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
315
395
|
if (!segs.length) return null;
|
|
316
396
|
const finish = (stack, m, fallbackName) => ({
|
|
317
397
|
stack,
|
|
@@ -347,4 +427,4 @@ function layoutChain(layouts, name) {
|
|
|
347
427
|
return chain;
|
|
348
428
|
}
|
|
349
429
|
//#endregion
|
|
350
|
-
export { RouteRedirect, buildRoutePath, defineRoutes, deriveRouteManifest, layoutChain, linkPath, matchRoute, matchUrl, mergeRouteManifests, parseQueryString };
|
|
430
|
+
export { RouteRedirect, buildRoutePath, defineRoutes, deriveRouteManifest, layoutChain, linkPath, manifestToJson, matchRoute, matchUrl, mergeRouteManifests, parseQueryString };
|
package/dist/web/route.web.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createNavigationRequests } from "./navigation-request.js";
|
|
1
2
|
import { RouteRedirect, buildRoutePath, layoutChain, linkPath, matchUrl, mergeRouteManifests } from "./route-table.js";
|
|
2
3
|
import { hookSlots, useSyncExternalStore } from "octane";
|
|
3
4
|
//#region src/route.web.ts
|
|
5
|
+
var navigationRequests = createNavigationRequests();
|
|
4
6
|
var baseManifest = {
|
|
5
7
|
screens: {},
|
|
6
8
|
routes: [],
|
|
@@ -18,7 +20,7 @@ function rebuildRegistry() {
|
|
|
18
20
|
routeLayouts = merged.layouts;
|
|
19
21
|
routeLoaders = merged.loaders ?? {};
|
|
20
22
|
if (current !== void 0) {
|
|
21
|
-
|
|
23
|
+
restoreRouteState();
|
|
22
24
|
emit();
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -40,7 +42,8 @@ function registerScreens(table, manifest) {
|
|
|
40
42
|
function registerRoutes(manifest) {
|
|
41
43
|
baseManifest = manifest;
|
|
42
44
|
rebuildRegistry();
|
|
43
|
-
|
|
45
|
+
read();
|
|
46
|
+
applyHead(modalRoute ?? current ?? null);
|
|
44
47
|
}
|
|
45
48
|
/** Layer a programmatic manifest (from `defineRoutes`) over the registered
|
|
46
49
|
* routes — same-name entries win over the base with a warn, so a host
|
|
@@ -49,7 +52,8 @@ function registerRoutes(manifest) {
|
|
|
49
52
|
function addRoutes(manifest) {
|
|
50
53
|
dynamicManifests.push(manifest);
|
|
51
54
|
rebuildRegistry();
|
|
52
|
-
|
|
55
|
+
read();
|
|
56
|
+
applyHead(modalRoute ?? current ?? null);
|
|
53
57
|
}
|
|
54
58
|
function screenFor(name) {
|
|
55
59
|
return screens[name];
|
|
@@ -63,6 +67,53 @@ var listeners = /* @__PURE__ */ new Set();
|
|
|
63
67
|
var current;
|
|
64
68
|
var modalRoute = null;
|
|
65
69
|
var historyDepth = history.state?.__octaneXplatDepth ?? 0;
|
|
70
|
+
var historyRoutes = /* @__PURE__ */ new Map();
|
|
71
|
+
var historyEntryPrefix = Math.random().toString(36).slice(2);
|
|
72
|
+
var nextHistoryEntry = 0;
|
|
73
|
+
function rememberRouteState(entry = historyEntryPrefix + ":" + ++nextHistoryEntry) {
|
|
74
|
+
historyRoutes.set(entry, {
|
|
75
|
+
current: current ?? null,
|
|
76
|
+
modal: modalRoute
|
|
77
|
+
});
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
function restoreRouteState() {
|
|
81
|
+
const saved = historyRoutes.get(history.state?.__octaneXplatEntry);
|
|
82
|
+
if (saved) {
|
|
83
|
+
current = saved.current;
|
|
84
|
+
modalRoute = saved.modal;
|
|
85
|
+
} else {
|
|
86
|
+
const route = parse();
|
|
87
|
+
modalRoute = route?.presentation === "modal" ? route : null;
|
|
88
|
+
current = modalRoute ? null : route;
|
|
89
|
+
if (route) hydrateRoute(route);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function hydrateRoute(route) {
|
|
93
|
+
const loader = routeLoaders[route.name] ?? metaFor(route.name)?.loader;
|
|
94
|
+
if (!loader || Object.prototype.hasOwnProperty.call(route, "loaderData") || Object.prototype.hasOwnProperty.call(route, "loaderError")) return;
|
|
95
|
+
const finish = (result) => {
|
|
96
|
+
const prepared = {
|
|
97
|
+
...route,
|
|
98
|
+
...result
|
|
99
|
+
};
|
|
100
|
+
let changed = false;
|
|
101
|
+
if (current === route) {
|
|
102
|
+
current = prepared;
|
|
103
|
+
changed = true;
|
|
104
|
+
}
|
|
105
|
+
if (modalRoute === route) {
|
|
106
|
+
modalRoute = prepared;
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
for (const saved of historyRoutes.values()) {
|
|
110
|
+
if (saved.current === route) saved.current = prepared;
|
|
111
|
+
if (saved.modal === route) saved.modal = prepared;
|
|
112
|
+
}
|
|
113
|
+
if (changed) emit();
|
|
114
|
+
};
|
|
115
|
+
Promise.resolve().then(() => loader(route.params)).then((loaderData) => finish({ loaderData }), (loaderError) => finish({ loaderError }));
|
|
116
|
+
}
|
|
66
117
|
var ROUTE_HEAD_ATTR = "data-octane-xplat-route-head";
|
|
67
118
|
var scrollPositions = /* @__PURE__ */ new Map();
|
|
68
119
|
var lastKey = location.pathname + location.search;
|
|
@@ -76,8 +127,8 @@ function parse() {
|
|
|
76
127
|
return matchUrl(routes, location.pathname + location.search);
|
|
77
128
|
}
|
|
78
129
|
function read() {
|
|
79
|
-
if (current === void 0)
|
|
80
|
-
return current;
|
|
130
|
+
if (current === void 0) restoreRouteState();
|
|
131
|
+
return current ?? null;
|
|
81
132
|
}
|
|
82
133
|
function emit() {
|
|
83
134
|
listeners.forEach((l) => l());
|
|
@@ -109,10 +160,11 @@ function applyHead(r) {
|
|
|
109
160
|
function contextFor(r) {
|
|
110
161
|
return r.context ?? {};
|
|
111
162
|
}
|
|
112
|
-
async function prepareRoute(r, redirects = 0) {
|
|
163
|
+
async function prepareRoute(r, request, redirects = 0) {
|
|
164
|
+
if (!request.isCurrent()) return;
|
|
113
165
|
const beforeLoad = metaFor(r.name)?.beforeLoad;
|
|
114
166
|
if (!beforeLoad) {
|
|
115
|
-
commitRoute(r);
|
|
167
|
+
commitRoute(r, request);
|
|
116
168
|
return;
|
|
117
169
|
}
|
|
118
170
|
if (redirects > 16) {
|
|
@@ -131,10 +183,11 @@ async function prepareRoute(r, redirects = 0) {
|
|
|
131
183
|
commitRoute({
|
|
132
184
|
...r,
|
|
133
185
|
context
|
|
134
|
-
});
|
|
186
|
+
}, request);
|
|
135
187
|
} catch (e) {
|
|
188
|
+
if (!request.isCurrent()) return;
|
|
136
189
|
if (e instanceof RouteRedirect) {
|
|
137
|
-
await prepareRoute(e.route, redirects + 1);
|
|
190
|
+
await prepareRoute(e.route, request, redirects + 1);
|
|
138
191
|
return;
|
|
139
192
|
}
|
|
140
193
|
console.warn(`[octane-xplat] beforeLoad('${r.name}') rejected: ${e?.message ?? e}`);
|
|
@@ -144,13 +197,15 @@ function redirect(r) {
|
|
|
144
197
|
throw new RouteRedirect(r);
|
|
145
198
|
}
|
|
146
199
|
function pushRoute(r) {
|
|
200
|
+
const request = navigationRequests.begin("history");
|
|
147
201
|
if (metaFor(r.name)?.beforeLoad) {
|
|
148
|
-
prepareRoute(r);
|
|
202
|
+
prepareRoute(r, request);
|
|
149
203
|
return;
|
|
150
204
|
}
|
|
151
|
-
commitRoute(r);
|
|
205
|
+
commitRoute(r, request);
|
|
152
206
|
}
|
|
153
|
-
function commitRoute(r) {
|
|
207
|
+
function commitRoute(r, request) {
|
|
208
|
+
if (!request.isCurrent()) return;
|
|
154
209
|
const route = {
|
|
155
210
|
...r,
|
|
156
211
|
presentation: r.presentation ?? presentationFor(r.name)
|
|
@@ -160,21 +215,30 @@ function commitRoute(r) {
|
|
|
160
215
|
Promise.resolve().then(() => loader(route.params)).then((loaderData) => commitRoute({
|
|
161
216
|
...route,
|
|
162
217
|
loaderData
|
|
163
|
-
}), (loaderError) => commitRoute({
|
|
218
|
+
}, request), (loaderError) => commitRoute({
|
|
164
219
|
...route,
|
|
165
220
|
loaderError
|
|
166
|
-
}));
|
|
221
|
+
}, request));
|
|
167
222
|
return;
|
|
168
223
|
}
|
|
169
224
|
saveScroll();
|
|
225
|
+
read();
|
|
226
|
+
history.replaceState({
|
|
227
|
+
...history.state,
|
|
228
|
+
__octaneXplatDepth: historyDepth,
|
|
229
|
+
__octaneXplatEntry: rememberRouteState(history.state?.__octaneXplatEntry)
|
|
230
|
+
}, "");
|
|
170
231
|
historyDepth += 1;
|
|
171
|
-
history.pushState({ __octaneXplatDepth: historyDepth }, "", buildRoutePath(routes, route));
|
|
172
|
-
lastKey = scrollKey();
|
|
173
232
|
if (route.presentation === "modal") modalRoute = route;
|
|
174
233
|
else {
|
|
175
234
|
current = route;
|
|
176
235
|
modalRoute = null;
|
|
177
236
|
}
|
|
237
|
+
history.pushState({
|
|
238
|
+
__octaneXplatDepth: historyDepth,
|
|
239
|
+
__octaneXplatEntry: rememberRouteState()
|
|
240
|
+
}, "", buildRoutePath(routes, route));
|
|
241
|
+
lastKey = scrollKey();
|
|
178
242
|
applyHead(modalRoute ?? current ?? null);
|
|
179
243
|
emit();
|
|
180
244
|
}
|
|
@@ -185,6 +249,7 @@ function presentationFor(name) {
|
|
|
185
249
|
* per-stack pops aren't expressible, so `stack` is accepted for parity
|
|
186
250
|
* and ignored. A pushed modal is the top entry, so back dismisses it. */
|
|
187
251
|
function popRoute(_stack = "root") {
|
|
252
|
+
navigationRequests.invalidate("history");
|
|
188
253
|
history.back();
|
|
189
254
|
}
|
|
190
255
|
var backInterceptorWarned = false;
|
|
@@ -200,12 +265,12 @@ function addBackInterceptor(_fn) {
|
|
|
200
265
|
return () => {};
|
|
201
266
|
}
|
|
202
267
|
window.addEventListener("popstate", () => {
|
|
268
|
+
navigationRequests.invalidate("history");
|
|
203
269
|
saveScroll();
|
|
204
270
|
historyDepth = history.state?.__octaneXplatDepth ?? 0;
|
|
205
271
|
lastKey = scrollKey();
|
|
206
|
-
|
|
207
|
-
current
|
|
208
|
-
applyHead(current);
|
|
272
|
+
restoreRouteState();
|
|
273
|
+
applyHead(modalRoute ?? current ?? null);
|
|
209
274
|
restoreScroll();
|
|
210
275
|
emit();
|
|
211
276
|
});
|
|
@@ -225,6 +290,7 @@ function currentRoute() {
|
|
|
225
290
|
}
|
|
226
291
|
/** The modal route overlaying the current one, if any. */
|
|
227
292
|
function currentModalRoute() {
|
|
293
|
+
read();
|
|
228
294
|
return modalRoute;
|
|
229
295
|
}
|
|
230
296
|
/** Deep-link entry, web half — the URL *is* the route state, so this is a
|
|
@@ -232,7 +298,7 @@ function currentModalRoute() {
|
|
|
232
298
|
* apps wire `onDeepLink(pushDeepLink)`). */
|
|
233
299
|
function pushDeepLink(url) {
|
|
234
300
|
const r = matchUrl(routes, linkPath(url));
|
|
235
|
-
if (!r) {
|
|
301
|
+
if (!r || !screenFor(r.name)) {
|
|
236
302
|
console.warn(`[octane-xplat] pushDeepLink('${url}') dropped — no route matches.`);
|
|
237
303
|
return false;
|
|
238
304
|
}
|
|
@@ -252,8 +318,8 @@ function useRoute(stack) {
|
|
|
252
318
|
/** Whether the selected conceptual stack has an in-app route to pop. A
|
|
253
319
|
* browser's pre-app history is intentionally not counted. */
|
|
254
320
|
function canGoBack(stack = "root") {
|
|
255
|
-
|
|
256
|
-
return historyDepth > 0 &&
|
|
321
|
+
read();
|
|
322
|
+
return historyDepth > 0 && (modalRoute ?? current)?.stack === stack;
|
|
257
323
|
}
|
|
258
324
|
function useCanGoBack(stack = "root") {
|
|
259
325
|
return useSyncExternalStore((cb) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyThemeClasses } from "./theme/theme-scheme.js";
|
|
2
|
+
import { isolateModalFocus } from "./modal-focus.web.js";
|
|
2
3
|
import { attachSheetDetents } from "./sheet-detents.web.js";
|
|
3
4
|
import { createRoot } from "octane";
|
|
4
5
|
//#region src/sheet-service.web.ts
|
|
@@ -16,10 +17,12 @@ var openSheet = (Component, params, options = {}) => new Promise((resolve) => {
|
|
|
16
17
|
panel.className = "vx-sheet";
|
|
17
18
|
if (options.detents?.length) panel.classList.add("vx-sheet-detents");
|
|
18
19
|
let finished = false;
|
|
20
|
+
let releaseFocus;
|
|
19
21
|
const finish = (result) => {
|
|
20
22
|
if (finished) return;
|
|
21
23
|
finished = true;
|
|
22
24
|
active.delete(entry);
|
|
25
|
+
releaseFocus?.();
|
|
23
26
|
unbindTheme();
|
|
24
27
|
detents?.detach();
|
|
25
28
|
root.unmount();
|
|
@@ -45,6 +48,11 @@ var openSheet = (Component, params, options = {}) => new Promise((resolve) => {
|
|
|
45
48
|
close: finish
|
|
46
49
|
});
|
|
47
50
|
active.add(entry);
|
|
51
|
+
if (options.shadeCover ?? true) {
|
|
52
|
+
panel.setAttribute("role", "dialog");
|
|
53
|
+
panel.setAttribute("aria-modal", "true");
|
|
54
|
+
releaseFocus = isolateModalFocus(layer, panel, () => finish());
|
|
55
|
+
}
|
|
48
56
|
});
|
|
49
57
|
//#endregion
|
|
50
58
|
export { closeSheet, openSheet };
|