@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,116 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
vi.mock('@nativescript/core', () => ({
|
|
5
|
+
Utils: { layout: { getDisplayDensity: () => 2 } },
|
|
6
|
+
}))
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createObjectContainer,
|
|
10
|
+
createObjectDriver,
|
|
11
|
+
createUniversalRoot,
|
|
12
|
+
} from 'octane/universal/native'
|
|
13
|
+
|
|
14
|
+
import { usePan } from './pan.tsrx'
|
|
15
|
+
import type { PanEvent } from './props'
|
|
16
|
+
|
|
17
|
+
const events: PanEvent[] = []
|
|
18
|
+
|
|
19
|
+
function PanHost() @{
|
|
20
|
+
const onPan = usePan((event) => events.push(event))
|
|
21
|
+
|
|
22
|
+
<flexboxlayout id="pan-host" onPan={onPan} />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mountPanHost() {
|
|
26
|
+
const container = createObjectContainer('nativescript')
|
|
27
|
+
const root = createUniversalRoot(container, createObjectDriver('nativescript'))
|
|
28
|
+
root.render(PanHost as any, {})
|
|
29
|
+
return { container, root, host: container.children[0] }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nativeEvent(action: number) {
|
|
33
|
+
return { getActionMasked: () => action }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tracker() {
|
|
37
|
+
return {
|
|
38
|
+
addMovement: vi.fn(),
|
|
39
|
+
computeCurrentVelocity: vi.fn(),
|
|
40
|
+
getXVelocity: () => 800,
|
|
41
|
+
getYVelocity: () => -240,
|
|
42
|
+
recycle: vi.fn(),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function installAndroid(trackerInstance: ReturnType<typeof tracker>) {
|
|
47
|
+
vi.stubGlobal('android', {
|
|
48
|
+
view: {
|
|
49
|
+
MotionEvent: { ACTION_CANCEL: 3 },
|
|
50
|
+
VelocityTracker: { obtain: () => trackerInstance },
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
events.length = 0
|
|
57
|
+
vi.unstubAllGlobals()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('native pan normalization', () => {
|
|
61
|
+
it('preserves Android ACTION_CANCEL as cancelled and clears release velocity', () => {
|
|
62
|
+
const velocityTracker = tracker()
|
|
63
|
+
installAndroid(velocityTracker)
|
|
64
|
+
const { container, root, host } = mountPanHost()
|
|
65
|
+
const target = { id: 'native-pan-target' }
|
|
66
|
+
|
|
67
|
+
container.dispatchEvent(host, 'pan', {
|
|
68
|
+
state: 1,
|
|
69
|
+
deltaX: 0,
|
|
70
|
+
deltaY: 0,
|
|
71
|
+
android: { current: nativeEvent(2) },
|
|
72
|
+
view: target,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
container.dispatchEvent(host, 'pan', {
|
|
76
|
+
// NativeScript's Android pan recognizer reports ended for both
|
|
77
|
+
// ACTION_UP and ACTION_CANCEL; the MotionEvent disambiguates them.
|
|
78
|
+
state: 3,
|
|
79
|
+
deltaX: 40,
|
|
80
|
+
deltaY: 0,
|
|
81
|
+
android: { current: nativeEvent(3) },
|
|
82
|
+
view: target,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(events.map((event) => event.state)).toEqual(['began', 'cancelled'])
|
|
86
|
+
expect(events[1]).toMatchObject({ state: 'cancelled', vx: 0, vy: 0, target })
|
|
87
|
+
expect(velocityTracker.recycle).toHaveBeenCalledOnce()
|
|
88
|
+
expect(velocityTracker.addMovement).toHaveBeenCalledOnce()
|
|
89
|
+
root.unmount()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('keeps ACTION_UP ended and converts Android velocity from px/s to dip/s', () => {
|
|
93
|
+
const velocityTracker = tracker()
|
|
94
|
+
installAndroid(velocityTracker)
|
|
95
|
+
const { container, root, host } = mountPanHost()
|
|
96
|
+
|
|
97
|
+
container.dispatchEvent(host, 'pan', {
|
|
98
|
+
state: 1,
|
|
99
|
+
deltaX: 0,
|
|
100
|
+
deltaY: 0,
|
|
101
|
+
android: { current: nativeEvent(0) },
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
container.dispatchEvent(host, 'pan', {
|
|
105
|
+
state: 3,
|
|
106
|
+
deltaX: 40,
|
|
107
|
+
deltaY: 0,
|
|
108
|
+
android: { current: nativeEvent(1) },
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
expect(events.map((event) => event.state)).toEqual(['began', 'ended'])
|
|
112
|
+
expect(events[1]).toMatchObject({ state: 'ended', vx: 400, vy: -120 })
|
|
113
|
+
expect(velocityTracker.recycle).toHaveBeenCalledOnce()
|
|
114
|
+
root.unmount()
|
|
115
|
+
})
|
|
116
|
+
})
|
package/src/pan.tsrx
CHANGED
|
@@ -11,6 +11,12 @@ import type { PanEvent } from './props';
|
|
|
11
11
|
const PAN_STATE = ['cancelled', 'began', 'moved', 'ended'] as const;
|
|
12
12
|
|
|
13
13
|
function panVelocity(e: any, state: number, trackerRef: { current: any }): { vx: number; vy: number } {
|
|
14
|
+
if (state === 0) {
|
|
15
|
+
trackerRef.current?.recycle?.()
|
|
16
|
+
trackerRef.current = null
|
|
17
|
+
return { vx: 0, vy: 0 }
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
const recognizer = e.ios;
|
|
15
21
|
const nativeView = e.view?.nativeViewProtected;
|
|
16
22
|
if (recognizer?.velocityInView && nativeView) {
|
|
@@ -45,13 +51,25 @@ function panVelocity(e: any, state: number, trackerRef: { current: any }): { vx:
|
|
|
45
51
|
return velocity;
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
function nativePanState(e: any): number {
|
|
55
|
+
const motionEvent = e.android?.current;
|
|
56
|
+
if (!motionEvent) return e.state;
|
|
57
|
+
|
|
58
|
+
const action = motionEvent.getActionMasked?.();
|
|
59
|
+
const actionCancel = (globalThis as any).android?.view?.MotionEvent?.ACTION_CANCEL ?? 3;
|
|
60
|
+
// NativeScript Android reports ACTION_CANCEL as `ended`; the MotionEvent
|
|
61
|
+
// preserves the distinction needed by the shared pan contract.
|
|
62
|
+
return action === actionCancel ? 0 : e.state;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
export function usePan(cb?: (e: PanEvent) => void): ((e: any) => void) | undefined {
|
|
49
66
|
const velocityTracker = useRef<any>(null);
|
|
50
67
|
// Memoized so the driver's observer doesn't churn per render — each
|
|
51
68
|
// re-subscribe would re-attach a UIPanGestureRecognizer.
|
|
52
69
|
const handler = useCallback((e: any) => {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
70
|
+
const nativeState = nativePanState(e);
|
|
71
|
+
const state = PAN_STATE[nativeState] ?? 'moved';
|
|
72
|
+
const velocity = panVelocity(e, nativeState, velocityTracker);
|
|
55
73
|
cb?.({
|
|
56
74
|
x: e.deltaX, y: e.deltaY, dx: e.deltaX, dy: e.deltaY,
|
|
57
75
|
vx: velocity.vx, vy: velocity.vy,
|
package/src/pan.web.tsrx
CHANGED
|
@@ -1,23 +1,51 @@
|
|
|
1
|
-
import { useEffect } from 'octane';
|
|
1
|
+
import { useEffect, useRef } from 'octane';
|
|
2
2
|
import type { PanEvent } from './props';
|
|
3
3
|
|
|
4
|
+
const VELOCITY_MAX_AGE_MS = 100;
|
|
5
|
+
|
|
4
6
|
/** Shared pan plumbing for web leaves — pointerdown/move/up/cancel with
|
|
5
7
|
* pointer capture, normalized to the shared gesture shape
|
|
6
8
|
* {x,y,dx,dy,vx,vy,state,target}. `pointermove` isn't in octane's
|
|
7
9
|
* delegated-event set, so listeners attach to the element directly —
|
|
8
10
|
* that's why callers pass the bound-element ref. */
|
|
9
11
|
export function usePan(el: { current: HTMLElement | null }, cb?: (e: PanEvent) => void): void {
|
|
12
|
+
const callback = useRef(cb);
|
|
13
|
+
callback.current = cb;
|
|
14
|
+
const enabled = Boolean(cb);
|
|
10
15
|
useEffect(() => {
|
|
11
16
|
const node = el.current;
|
|
12
|
-
if (!node || !
|
|
13
|
-
let sx = 0, sy = 0, active = false
|
|
17
|
+
if (!node || !enabled) return;
|
|
18
|
+
let sx = 0, sy = 0, active = false;
|
|
19
|
+
let last = 0, lx = 0, t0 = 0;
|
|
20
|
+
let vx = 0, vy = 0, velocityAt = 0;
|
|
14
21
|
const emit = (e: PointerEvent, state: string) => {
|
|
15
22
|
const now = performance.now();
|
|
16
23
|
const dt = Math.max(now - t0, 1) / 1000;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
24
|
+
const dx = e.clientX - lx;
|
|
25
|
+
const dy = e.clientY - last;
|
|
26
|
+
if (state === 'began') {
|
|
27
|
+
vx = vy = 0;
|
|
28
|
+
velocityAt = now;
|
|
29
|
+
} else if (dx !== 0 || dy !== 0) {
|
|
30
|
+
// Preserve this sample for pointer-up when it repeats the final coordinates.
|
|
31
|
+
vx = dx / dt;
|
|
32
|
+
vy = dy / dt;
|
|
33
|
+
velocityAt = now;
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
lx = e.clientX; last = e.clientY; t0 = now;
|
|
20
|
-
|
|
37
|
+
const fresh = now - velocityAt <= VELOCITY_MAX_AGE_MS;
|
|
38
|
+
callback.current?.({
|
|
39
|
+
x: e.clientX - sx,
|
|
40
|
+
y: e.clientY - sy,
|
|
41
|
+
dx: e.clientX - sx,
|
|
42
|
+
dy: e.clientY - sy,
|
|
43
|
+
vx: fresh ? vx : 0,
|
|
44
|
+
vy: fresh ? vy : 0,
|
|
45
|
+
state,
|
|
46
|
+
target: node,
|
|
47
|
+
});
|
|
48
|
+
|
|
21
49
|
};
|
|
22
50
|
|
|
23
51
|
const down = (e: PointerEvent) => { active = true; sx = e.clientX; sy = e.clientY; t0 = performance.now(); lx = sx; last = sy; node.setPointerCapture?.(e.pointerId); emit(e, 'began'); };
|
|
@@ -34,5 +62,5 @@ export function usePan(el: { current: HTMLElement | null }, cb?: (e: PanEvent) =
|
|
|
34
62
|
node.removeEventListener('pointerup', up);
|
|
35
63
|
node.removeEventListener('pointercancel', cancel);
|
|
36
64
|
};
|
|
37
|
-
}, [el,
|
|
65
|
+
}, [el, enabled]);
|
|
38
66
|
}
|
package/src/platformProps.ts
CHANGED
|
@@ -13,6 +13,8 @@ vi.mock('@nativescript/core', () => ({
|
|
|
13
13
|
horizontalAlignment = ''
|
|
14
14
|
verticalAlignment = ''
|
|
15
15
|
style: Record<string, unknown> = {}
|
|
16
|
+
on() {}
|
|
17
|
+
off() {}
|
|
16
18
|
constructor() {
|
|
17
19
|
hosts.push(this)
|
|
18
20
|
}
|
|
@@ -32,7 +34,13 @@ vi.mock('./root-layout.mobile', () => ({
|
|
|
32
34
|
}),
|
|
33
35
|
}))
|
|
34
36
|
|
|
35
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
createObjectContainer,
|
|
39
|
+
createObjectDriver,
|
|
40
|
+
createUniversalRoot,
|
|
41
|
+
flushUniversalSync,
|
|
42
|
+
} from 'octane/universal/native'
|
|
43
|
+
|
|
36
44
|
import { Popover } from './Popover.tsrx'
|
|
37
45
|
|
|
38
46
|
// The full-screen host must not wear the caller's className — a backgrounded
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createObjectContainer,
|
|
4
|
+
createObjectDriver,
|
|
5
|
+
createUniversalRoot,
|
|
6
|
+
} from 'octane/universal/native'
|
|
7
|
+
|
|
8
|
+
vi.mock('@nativescript/core', () => ({ isIOS: false, isAndroid: true }))
|
|
9
|
+
vi.mock('./pan.tsrx', () => ({ usePan: () => undefined }))
|
|
10
|
+
import { Pressable } from './Pressable.tsrx'
|
|
11
|
+
|
|
12
|
+
describe('native disabled Pressable accessibility state', () => {
|
|
13
|
+
it('reports disabled ahead of selected/checked and restores state when enabled', () => {
|
|
14
|
+
const container = createObjectContainer('nativescript')
|
|
15
|
+
const root = createUniversalRoot(container, createObjectDriver('nativescript'))
|
|
16
|
+
root.render(Pressable as any, {
|
|
17
|
+
disabled: true,
|
|
18
|
+
accessibilityState: { selected: true, checked: true },
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
expect(container.children[0].props.accessibilityState).toBe('disabled')
|
|
22
|
+
root.render(Pressable as any, {
|
|
23
|
+
disabled: false,
|
|
24
|
+
accessibilityState: { selected: true, checked: true },
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
expect(container.children[0].props.accessibilityState).toBe('selected')
|
|
28
|
+
root.unmount()
|
|
29
|
+
})
|
|
30
|
+
})
|
package/src/props.ts
CHANGED
|
@@ -364,7 +364,8 @@ export interface TextAreaProps extends TextInputProps {
|
|
|
364
364
|
* minHeight at the widget's measured line height. With `autoGrow` it
|
|
365
365
|
* becomes the starting height instead of a fixed one. */
|
|
366
366
|
rows?: number
|
|
367
|
-
/** Grow
|
|
367
|
+
/** Grow on input and value updates, capped by `maxRows`, including when
|
|
368
|
+
* `value` is omitted. Native TextView grows by
|
|
368
369
|
* default — this prop exists so web (<textarea> is fixed-rows) matches;
|
|
369
370
|
* without it, native gets a fixed `rows`-high box like web. */
|
|
370
371
|
autoGrow?: boolean
|
|
@@ -1041,8 +1042,14 @@ export interface RouteMeta {
|
|
|
1041
1042
|
/** Optional `loader` named export — a prefetch hook, not a data layer.
|
|
1042
1043
|
* pushRoute fires it (fire-and-forget) before navigating so the
|
|
1043
1044
|
* screen's `query$` reads hit a warm cache; boot/deep-link routes
|
|
1044
|
-
* skip it (the screen mounts in the same tick anyway).
|
|
1045
|
+
* skip it (the screen mounts in the same tick anyway). Ignored when
|
|
1046
|
+
* `dataMode: 'baked'` — baked routes resolve from `manifest.baked`. */
|
|
1045
1047
|
loader?: (params: Record<string, unknown>) => unknown
|
|
1048
|
+
/** Data provenance: 'live' (default) runs `loader` at navigation;
|
|
1049
|
+
* 'baked' reads build-time output from the manifest's `baked` map —
|
|
1050
|
+
* for file routes the loader lives in a `<route>.loader.ts` sibling
|
|
1051
|
+
* that `xplat routes`/`xplat build` executes and never bundles. */
|
|
1052
|
+
dataMode?: RouteDataMode
|
|
1046
1053
|
/** Optional guard awaited before a push commits. A thrown `redirect(...)`
|
|
1047
1054
|
* short-circuits the attempted route. */
|
|
1048
1055
|
beforeLoad?: BeforeLoad
|
|
@@ -1050,6 +1057,10 @@ export interface RouteMeta {
|
|
|
1050
1057
|
head?: RouteHeadExport
|
|
1051
1058
|
}
|
|
1052
1059
|
|
|
1060
|
+
/** How a route's data is produced — `'live'` runs the loader at navigation,
|
|
1061
|
+
* `'baked'` freezes loader output into the bundle at build time. */
|
|
1062
|
+
export type RouteDataMode = 'live' | 'baked'
|
|
1063
|
+
|
|
1053
1064
|
/** Output of `deriveRouteManifest` — `screens` feeds `registerScreens`
|
|
1054
1065
|
* (native pushRoute + web outlet fallback), `routes` feeds web URL
|
|
1055
1066
|
* matching, `layouts` catalogs `_layout` files by directory ('' = root). */
|
|
@@ -1058,6 +1069,10 @@ export interface RouteManifest {
|
|
|
1058
1069
|
routes: RouteMeta[]
|
|
1059
1070
|
layouts: Record<string, any>
|
|
1060
1071
|
loaders?: Record<string, (params: Record<string, unknown>) => unknown | Promise<unknown>>
|
|
1072
|
+
/** Build-time loader output for `dataMode: 'baked'` routes — the
|
|
1073
|
+
* generated `routes.gen.*` glue assigns it from `routes.gen.data`;
|
|
1074
|
+
* programmatic hosts pass it on `RouteSpecSet`/`addRoutes` directly. */
|
|
1075
|
+
baked?: Record<string, unknown>
|
|
1061
1076
|
}
|
|
1062
1077
|
|
|
1063
1078
|
/** One programmatic route entry for `defineRoutes` — the manifest fields a
|
|
@@ -1070,6 +1085,7 @@ export interface RouteSpec {
|
|
|
1070
1085
|
/** Default presentation, like a `+modal`/`+fade` filename suffix. */
|
|
1071
1086
|
presentation?: 'push' | 'modal' | 'fade'
|
|
1072
1087
|
loader?: RouteMeta['loader']
|
|
1088
|
+
dataMode?: RouteDataMode
|
|
1073
1089
|
beforeLoad?: BeforeLoad
|
|
1074
1090
|
head?: RouteHeadExport
|
|
1075
1091
|
/** Diagnostics label recorded on `RouteMeta.file` (warn strings). */
|
|
@@ -1078,11 +1094,155 @@ export interface RouteSpec {
|
|
|
1078
1094
|
|
|
1079
1095
|
/** Input to `defineRoutes`: the route list plus optional path-keyed layouts
|
|
1080
1096
|
* — `layouts: { docs: Shell }` plays the `_layout.tsrx` role for every
|
|
1081
|
-
* route under `docs/*`.
|
|
1097
|
+
* route under `docs/*`. `baked` carries build-time data for
|
|
1098
|
+
* `dataMode: 'baked'` specs — the host computes it (a content dir read, a
|
|
1099
|
+
* build step) and ships it in the manifest. */
|
|
1082
1100
|
export interface RouteSpecSet {
|
|
1083
1101
|
routes: readonly RouteSpec[]
|
|
1084
1102
|
layouts?: Record<string, any>
|
|
1103
|
+
baked?: Record<string, unknown>
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/** The host-consumable route schema — what `xplat routes` writes to
|
|
1107
|
+
* `routes.gen.manifest.json` and `manifestToJson` produces from any
|
|
1108
|
+
* RouteManifest. Same shape whether the routes came from the file dir
|
|
1109
|
+
* or `defineRoutes`, so an external host (e.g. a web SSR tier) consumes
|
|
1110
|
+
* one normalized list. `loader`/`guard`/`head` are presence flags — the
|
|
1111
|
+
* functions don't cross the JSON boundary. */
|
|
1112
|
+
export interface RouteManifestJson {
|
|
1113
|
+
version: 1
|
|
1114
|
+
/** Layout dirs present, '' = the root `_layout` — sorted. */
|
|
1115
|
+
layouts: string[]
|
|
1116
|
+
/** Registered screen names, including any without a URL pattern. */
|
|
1117
|
+
screens: string[]
|
|
1118
|
+
routes: RouteJson[]
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export interface RouteJson {
|
|
1122
|
+
name: string
|
|
1123
|
+
/** Colon-syntax path — 'docs/:slug', '' for the root index. */
|
|
1124
|
+
path: string
|
|
1125
|
+
params: string[]
|
|
1126
|
+
presentation?: 'push' | 'modal' | 'fade'
|
|
1127
|
+
dataMode?: RouteDataMode
|
|
1128
|
+
/** Layout dirs applying to this route, root '' first, innermost last. */
|
|
1129
|
+
layouts: string[]
|
|
1130
|
+
/** Source label — file path or `programmatic:<path>`. */
|
|
1131
|
+
source: string
|
|
1132
|
+
loader: boolean
|
|
1133
|
+
guard: boolean
|
|
1134
|
+
head: boolean
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// ---------- programmatic route typing ----------
|
|
1138
|
+
|
|
1139
|
+
/** Phantom key carrying a `defineRoutes` manifest's spec-derived route
|
|
1140
|
+
* types — type-level only, never present at runtime. Module-private so the
|
|
1141
|
+
* published declarations carry no value binding for it. */
|
|
1142
|
+
declare const specRouteTypes: unique symbol
|
|
1143
|
+
|
|
1144
|
+
/** ':id' or '[id]' segment → 'id'; a static segment contributes nothing. */
|
|
1145
|
+
type RouteParamOf<S extends string> = S extends `:${infer P}`
|
|
1146
|
+
? P
|
|
1147
|
+
: S extends `[${infer P}]`
|
|
1148
|
+
? P
|
|
1149
|
+
: never
|
|
1150
|
+
|
|
1151
|
+
type TrimSlashes<P extends string> = P extends `/${infer R}`
|
|
1152
|
+
? TrimSlashes<R>
|
|
1153
|
+
: P extends `${infer R}/`
|
|
1154
|
+
? TrimSlashes<R>
|
|
1155
|
+
: P
|
|
1156
|
+
|
|
1157
|
+
/** Route-dir vocabulary at the type level: '[id]' normalizes to ':id'. */
|
|
1158
|
+
type NormSegment<S extends string> = S extends `[${infer P}]` ? `:${P}` : S
|
|
1159
|
+
|
|
1160
|
+
type NormPath<P extends string> = P extends `${infer Head}/${infer Tail}`
|
|
1161
|
+
? `${NormSegment<Head>}/${NormPath<Tail>}`
|
|
1162
|
+
: NormSegment<P>
|
|
1163
|
+
|
|
1164
|
+
/** The route name a spec `path` produces — `''`/`'index'` and a trailing
|
|
1165
|
+
* 'index' segment normalize like `specSegments` does at runtime. */
|
|
1166
|
+
export type RouteNameOfPath<P extends string> =
|
|
1167
|
+
NormPath<TrimSlashes<P>> extends infer N extends string
|
|
1168
|
+
? N extends '' | 'index'
|
|
1169
|
+
? 'index'
|
|
1170
|
+
: N extends `${infer R}/index`
|
|
1171
|
+
? R
|
|
1172
|
+
: N
|
|
1173
|
+
: never
|
|
1174
|
+
|
|
1175
|
+
/** Param names in a spec path — `'docs/[slug]'` and `'docs/:slug'` both
|
|
1176
|
+
* give `'slug'`; static-only paths give `never`. */
|
|
1177
|
+
export type RoutePathParams<Path extends string> =
|
|
1178
|
+
TrimSlashes<Path> extends infer P extends string
|
|
1179
|
+
? P extends `${infer Head}/${infer Tail}`
|
|
1180
|
+
? RouteParamOf<Head> | RoutePathParams<Tail>
|
|
1181
|
+
: RouteParamOf<P>
|
|
1182
|
+
: never
|
|
1183
|
+
|
|
1184
|
+
type RouteParamRecord<Path extends string> = { [K in RoutePathParams<Path>]: string }
|
|
1185
|
+
|
|
1186
|
+
type SpecPaths<Specs extends readonly RouteSpec[]> = Specs[number] extends infer S
|
|
1187
|
+
? S extends { path: infer P extends string }
|
|
1188
|
+
? P
|
|
1189
|
+
: never
|
|
1190
|
+
: never
|
|
1191
|
+
|
|
1192
|
+
type SpecsToParamMap<P extends string> = P extends string
|
|
1193
|
+
? { [N in RouteNameOfPath<P>]: RouteParamRecord<P> }
|
|
1194
|
+
: never
|
|
1195
|
+
|
|
1196
|
+
type UnionToIntersection<U> = (U extends unknown ? (value: U) => void : never) extends (
|
|
1197
|
+
value: infer I,
|
|
1198
|
+
) => void
|
|
1199
|
+
? I
|
|
1200
|
+
: never
|
|
1201
|
+
|
|
1202
|
+
/** name → params map for a spec list — same shape `routes.gen.types.ts`
|
|
1203
|
+
* emits for file routes, so the two merge with `&`/`|`. */
|
|
1204
|
+
export type RouteParamsFromSpecs<Specs extends readonly RouteSpec[]> = UnionToIntersection<
|
|
1205
|
+
SpecsToParamMap<SpecPaths<Specs>>
|
|
1206
|
+
>
|
|
1207
|
+
|
|
1208
|
+
type SpecPresentations<S> = S extends {
|
|
1209
|
+
path: infer P extends string
|
|
1210
|
+
presentation: infer Pr
|
|
1211
|
+
}
|
|
1212
|
+
? { [N in RouteNameOfPath<P>]: Pr }
|
|
1213
|
+
: {}
|
|
1214
|
+
|
|
1215
|
+
export type RoutePresentationsFromSpecs<Specs extends readonly RouteSpec[]> = UnionToIntersection<
|
|
1216
|
+
SpecPresentations<Specs[number]>
|
|
1217
|
+
>
|
|
1218
|
+
|
|
1219
|
+
/** The spec-derived typing record `defineRoutes` brands onto its manifest
|
|
1220
|
+
* return — read it with the `ManifestRoute*` helpers, never directly. */
|
|
1221
|
+
export interface SpecRouteInfo<Specs extends readonly RouteSpec[]> {
|
|
1222
|
+
readonly [specRouteTypes]: {
|
|
1223
|
+
params: RouteParamsFromSpecs<Specs>
|
|
1224
|
+
presentations: RoutePresentationsFromSpecs<Specs>
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/** Pull the `{name: params}` map out of a `defineRoutes` manifest's type —
|
|
1229
|
+
* `RouteParams & ManifestRouteParams<typeof manifest>` merges file and
|
|
1230
|
+
* programmatic routes into one typed navigation surface. A plain
|
|
1231
|
+
* `RouteManifest` (untagged) extracts to `{}`, so `ManifestRouteNames`
|
|
1232
|
+
* is `never` and unions degrade to the file-derived names. */
|
|
1233
|
+
export type ManifestRouteParams<M> = M extends {
|
|
1234
|
+
readonly [specRouteTypes]: { params: infer P }
|
|
1235
|
+
}
|
|
1236
|
+
? P
|
|
1237
|
+
: {}
|
|
1238
|
+
|
|
1239
|
+
export type ManifestRouteNames<M> = keyof ManifestRouteParams<M> & string
|
|
1240
|
+
|
|
1241
|
+
export type ManifestRoutePresentations<M> = M extends {
|
|
1242
|
+
readonly [specRouteTypes]: { presentations: infer P }
|
|
1085
1243
|
}
|
|
1244
|
+
? P
|
|
1245
|
+
: {}
|
|
1086
1246
|
|
|
1087
1247
|
export interface OpenWindowOptions {
|
|
1088
1248
|
/** Data made available to the native window content resolver. */
|
package/src/refresh.ts
CHANGED
|
@@ -125,7 +125,12 @@ export function createPullToRefresh(host: RefreshHost): RefreshController {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
const apply = () => {
|
|
128
|
-
sv.contentInset = makeInsets(
|
|
128
|
+
sv.contentInset = makeInsets(
|
|
129
|
+
next,
|
|
130
|
+
sv.contentInset.left,
|
|
131
|
+
sv.contentInset.bottom,
|
|
132
|
+
sv.contentInset.right,
|
|
133
|
+
)
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
const UIView = (globalThis as any).UIView
|