@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,106 @@
|
|
|
1
|
+
/** Tap-outside-to-blur: iOS keeps a focused UITextField/UITextView as first
|
|
2
|
+
* responder until something else takes it, so a tap on blank chrome never
|
|
3
|
+
* dismisses the keyboard on its own.
|
|
4
|
+
*
|
|
5
|
+
* iOS: a single window-level UITapGestureRecognizer (ref-counted across
|
|
6
|
+
* attach calls) sees every touch in the app window — popups/sheets included —
|
|
7
|
+
* where a recognizer on an intermediate container proved unreliable. On each
|
|
8
|
+
* tap we hit-test and resign first responder unless the hit view is an
|
|
9
|
+
* editable text view (otherwise the field would focus-then-blur in the same
|
|
10
|
+
* tap — recognizers fire on touch-up, after the field's touch-down focus).
|
|
11
|
+
* Keyboard taps live in a separate UIWindow, so they never reach it.
|
|
12
|
+
* `cancelsTouchesInView` stays false so taps still reach buttons. */
|
|
13
|
+
export function attachTapToBlur(view: any): () => void {
|
|
14
|
+
const marker = '__xplatTapToBlur'
|
|
15
|
+
if (!view || view[marker]) {
|
|
16
|
+
return () => {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
view[marker] = true
|
|
20
|
+
|
|
21
|
+
iosAttach()
|
|
22
|
+
return () => {
|
|
23
|
+
view[marker] = false
|
|
24
|
+
iosDetach()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let iosCount = 0
|
|
29
|
+
let iosTarget: any = null
|
|
30
|
+
let iosRecognizer: any = null
|
|
31
|
+
let iosWindow: any = null
|
|
32
|
+
|
|
33
|
+
function iosAttach() {
|
|
34
|
+
iosCount++
|
|
35
|
+
if (iosRecognizer) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const appWin = appWindow()
|
|
40
|
+
if (!appWin) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
iosWindow = appWin
|
|
45
|
+
// Resolve Objective-C bridge globals only when the iOS view is attached.
|
|
46
|
+
@NativeClass
|
|
47
|
+
class TapToBlurRecognizerTarget extends NSObject implements UIGestureRecognizerDelegate {
|
|
48
|
+
static ObjCProtocols = [UIGestureRecognizerDelegate]
|
|
49
|
+
static ObjCExposedMethods = {
|
|
50
|
+
tap: { returns: interop.types.void, params: [UITapGestureRecognizer] },
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
gestureRecognizerShouldReceiveTouch(_recognizer: any, touch: any) {
|
|
54
|
+
let hit = touch.view
|
|
55
|
+
while (hit) {
|
|
56
|
+
const textField = hit.isKindOfClass(UITextField.class())
|
|
57
|
+
const textView = hit.isKindOfClass(UITextView.class())
|
|
58
|
+
if (textField || textView) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
hit = hit.superview
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
tap(_recognizer: any) {
|
|
69
|
+
iosWindow.endEditing(true)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
iosTarget = TapToBlurRecognizerTarget.new()
|
|
74
|
+
iosRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(iosTarget, 'tap')
|
|
75
|
+
iosRecognizer.delegate = iosTarget
|
|
76
|
+
iosRecognizer.cancelsTouchesInView = false
|
|
77
|
+
appWin.addGestureRecognizer(iosRecognizer)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function iosDetach() {
|
|
81
|
+
iosCount--
|
|
82
|
+
if (iosCount > 0 || !iosRecognizer) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
iosWindow?.removeGestureRecognizer(iosRecognizer)
|
|
87
|
+
iosRecognizer = null
|
|
88
|
+
iosTarget = null
|
|
89
|
+
iosWindow = null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function appWindow() {
|
|
93
|
+
const scenes = UIApplication.sharedApplication.connectedScenes
|
|
94
|
+
// connectedScenes is an NSSet — `allObjects` marshals to a JS array.
|
|
95
|
+
for (const scene of (scenes as any).allObjects ?? []) {
|
|
96
|
+
if (scene instanceof UIWindowScene) {
|
|
97
|
+
for (const w of (scene.windows as any)?.allObjects ?? scene.windows ?? []) {
|
|
98
|
+
if (w.isKeyWindow) {
|
|
99
|
+
return w
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return UIApplication.sharedApplication.keyWindow
|
|
106
|
+
}
|
|
@@ -1,108 +1,34 @@
|
|
|
1
|
-
import { Application } from '@nativescript/core'
|
|
1
|
+
import { Application } from '@nativescript/core'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* responder until something else takes it, so a tap on blank chrome never
|
|
5
|
-
* dismisses the keyboard on its own.
|
|
6
|
-
*
|
|
7
|
-
* iOS: a single window-level UITapGestureRecognizer (ref-counted across
|
|
8
|
-
* attach calls) sees every touch in the app window — popups/sheets included —
|
|
9
|
-
* where a recognizer on an intermediate container proved unreliable. On each
|
|
10
|
-
* tap we hit-test and resign first responder unless the hit view is an
|
|
11
|
-
* editable text view (otherwise the field would focus-then-blur in the same
|
|
12
|
-
* tap — recognizers fire on touch-up, after the field's touch-down focus).
|
|
13
|
-
* Keyboard taps live in a separate UIWindow, so they never reach it.
|
|
14
|
-
* `cancelsTouchesInView` stays false so taps still reach buttons.
|
|
15
|
-
*
|
|
16
|
-
* Android: hides the soft keyboard from the currently focused view when a
|
|
17
|
-
* tap lands on the attached container. */
|
|
3
|
+
/** Hide the Android soft keyboard when the user taps the attached view. */
|
|
18
4
|
export function attachTapToBlur(view: any): () => void {
|
|
19
|
-
const marker = '__xplatTapToBlur'
|
|
20
|
-
if (!view || view[marker]) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (Application.ios) {
|
|
24
|
-
iosAttach();
|
|
25
|
-
return () => {
|
|
26
|
-
view[marker] = false;
|
|
27
|
-
iosDetach();
|
|
28
|
-
};
|
|
5
|
+
const marker = '__xplatTapToBlur'
|
|
6
|
+
if (!view || view[marker]) {
|
|
7
|
+
return () => {}
|
|
29
8
|
}
|
|
30
9
|
|
|
31
|
-
|
|
32
|
-
const handler = (_args: any) => {
|
|
33
|
-
const activity = Application.android.foregroundActivity;
|
|
34
|
-
const focused = activity?.currentFocus;
|
|
35
|
-
if (!focused) {return;}
|
|
36
|
-
const imm = activity.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
|
|
37
|
-
imm?.hideSoftInputFromWindow(focused.getWindowToken(), 0);
|
|
38
|
-
};
|
|
10
|
+
view[marker] = true
|
|
39
11
|
|
|
40
|
-
|
|
12
|
+
if (!Application.android) {
|
|
41
13
|
return () => {
|
|
42
|
-
view[marker] = false
|
|
43
|
-
|
|
44
|
-
};
|
|
14
|
+
view[marker] = false
|
|
15
|
+
}
|
|
45
16
|
}
|
|
46
17
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
let iosCount = 0;
|
|
53
|
-
let iosTarget: any = null;
|
|
54
|
-
let iosRecognizer: any = null;
|
|
55
|
-
let iosWindow: any = null;
|
|
56
|
-
|
|
57
|
-
function iosAttach() {
|
|
58
|
-
iosCount++;
|
|
59
|
-
if (iosRecognizer) {return;}
|
|
60
|
-
const appWin = appWindow();
|
|
61
|
-
if (!appWin) {return;}
|
|
62
|
-
iosWindow = appWin;
|
|
63
|
-
|
|
64
|
-
const Owner = class extends NSObject {
|
|
65
|
-
static ObjCExposedMethods = {
|
|
66
|
-
tap: { returns: interop.types.void, params: [UITapGestureRecognizer] },
|
|
67
|
-
};
|
|
68
|
-
tap(recognizer: any) {
|
|
69
|
-
const point = recognizer.locationInView(iosWindow);
|
|
70
|
-
// hitTest can land on a text field's internal subview — walk up.
|
|
71
|
-
let hit = iosWindow.hitTest(point, null);
|
|
72
|
-
while (hit) {
|
|
73
|
-
if (hit instanceof UITextField || hit instanceof UITextView) {return;}
|
|
74
|
-
hit = hit.superview;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
iosWindow.endEditing(true);
|
|
18
|
+
const handler = (_args: any) => {
|
|
19
|
+
const activity = Application.android.foregroundActivity
|
|
20
|
+
const focused = activity?.getCurrentFocus()
|
|
21
|
+
if (!focused) {
|
|
22
|
+
return
|
|
78
23
|
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
iosTarget = Owner.new();
|
|
82
|
-
iosRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(iosTarget, 'tap');
|
|
83
|
-
iosRecognizer.cancelsTouchesInView = false;
|
|
84
|
-
appWin.addGestureRecognizer(iosRecognizer);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function iosDetach() {
|
|
88
|
-
iosCount--;
|
|
89
|
-
if (iosCount > 0 || !iosRecognizer) {return;}
|
|
90
|
-
iosWindow?.removeGestureRecognizer(iosRecognizer);
|
|
91
|
-
iosRecognizer = null;
|
|
92
|
-
iosTarget = null;
|
|
93
|
-
iosWindow = null;
|
|
94
|
-
}
|
|
95
24
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// connectedScenes is an NSSet — `allObjects` marshals to a JS array.
|
|
99
|
-
for (const scene of (scenes as any).allObjects ?? []) {
|
|
100
|
-
if (scene instanceof UIWindowScene) {
|
|
101
|
-
for (const w of (scene.windows as any)?.allObjects ?? scene.windows ?? []) {
|
|
102
|
-
if (w.isKeyWindow) {return w;}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
25
|
+
const imm = activity.getSystemService(android.content.Context.INPUT_METHOD_SERVICE)
|
|
26
|
+
imm?.hideSoftInputFromWindow(focused.getWindowToken(), 0)
|
|
105
27
|
}
|
|
106
28
|
|
|
107
|
-
|
|
29
|
+
view.on('tap', handler)
|
|
30
|
+
return () => {
|
|
31
|
+
view[marker] = false
|
|
32
|
+
view.off('tap', handler)
|
|
33
|
+
}
|
|
108
34
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Dismiss the keyboard and release native editing focus. NativeScript's
|
|
2
|
+
* dismissSoftInput returns void; its return value is not a fallback signal. */
|
|
3
|
+
export function blurText(view: any): void {
|
|
4
|
+
if (typeof view.dismissSoftInput === 'function') {
|
|
5
|
+
view.dismissSoftInput()
|
|
6
|
+
} else {
|
|
7
|
+
view.blur?.()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Android dismissSoftInput hides the IME but retains EditText focus.
|
|
11
|
+
view.android?.clearFocus?.()
|
|
12
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createObjectContainer,
|
|
4
|
+
createObjectDriver,
|
|
5
|
+
createUniversalRoot,
|
|
6
|
+
flushUniversalSync,
|
|
7
|
+
} from 'octane/universal/native'
|
|
8
|
+
|
|
9
|
+
vi.mock('@nativescript/core', () => ({
|
|
10
|
+
isIOS: false,
|
|
11
|
+
isAndroid: true,
|
|
12
|
+
Utils: { layout: { toDeviceIndependentPixels: (value: number) => value } },
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
vi.mock('./escape-props', () => ({ applyEscapeProps: () => {} }))
|
|
16
|
+
vi.mock('./Icon.tsrx', async () => {
|
|
17
|
+
const { defineUniversalComponent } = await import('octane/universal/native')
|
|
18
|
+
return { Icon: defineUniversalComponent('nativescript', () => null) }
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
import { TextInput } from './TextInput.tsrx'
|
|
22
|
+
import { TextArea } from './TextArea.tsrx'
|
|
23
|
+
import { SearchInput } from './SearchInput.tsrx'
|
|
24
|
+
|
|
25
|
+
// NativeScript Property raises a synchronous textChange on script writes.
|
|
26
|
+
// Model that event here, through the real compiled leaves and event dispatch.
|
|
27
|
+
// This is regression coverage, not keyboard/IME/device evidence.
|
|
28
|
+
describe('native controlled input writes', () => {
|
|
29
|
+
for (const [name, Component] of [
|
|
30
|
+
['TextInput', TextInput],
|
|
31
|
+
['TextArea', TextArea],
|
|
32
|
+
['SearchInput', SearchInput],
|
|
33
|
+
] as const) {
|
|
34
|
+
it(`${name} does not report controlled writes as user edits`, async () => {
|
|
35
|
+
const container = createObjectContainer('nativescript')
|
|
36
|
+
const root = createUniversalRoot(container, createObjectDriver('nativescript'))
|
|
37
|
+
const change = vi.fn()
|
|
38
|
+
let node: any
|
|
39
|
+
let inputHandle: any
|
|
40
|
+
let text = ''
|
|
41
|
+
let selection = [2, 4]
|
|
42
|
+
const bind = (handle: any) => {
|
|
43
|
+
inputHandle = handle
|
|
44
|
+
node = handle.native
|
|
45
|
+
node.style = {}
|
|
46
|
+
node.android = {
|
|
47
|
+
getSelectionStart: () => selection[0],
|
|
48
|
+
getSelectionEnd: () => selection[1],
|
|
49
|
+
setSelection: (start: number, end: number) => {
|
|
50
|
+
selection = [start, end]
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Object.defineProperty(node, 'text', {
|
|
55
|
+
configurable: true,
|
|
56
|
+
get: () => text,
|
|
57
|
+
set: (value) => {
|
|
58
|
+
text = value
|
|
59
|
+
container.dispatchEvent(node, 'textchange', { object: node, value })
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
root.render(Component as any, { value: 'hello', onChange: change, bind, icon: false })
|
|
65
|
+
await vi.waitFor(() => expect(text).toBe('hello'))
|
|
66
|
+
expect(change).not.toHaveBeenCalled()
|
|
67
|
+
expect(selection).toEqual([2, 4])
|
|
68
|
+
root.render(Component as any, { value: 'x', onChange: change, bind, icon: false })
|
|
69
|
+
await vi.waitFor(() => expect(text).toBe('x'))
|
|
70
|
+
expect(change).not.toHaveBeenCalled()
|
|
71
|
+
expect(selection).toEqual([1, 1])
|
|
72
|
+
// A native edit still reaches the callback exactly once.
|
|
73
|
+
text = 'xy'
|
|
74
|
+
flushUniversalSync(() =>
|
|
75
|
+
container.dispatchEvent(node, 'textchange', { object: node, value: text }),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(change).toHaveBeenCalledExactlyOnceWith('xy')
|
|
79
|
+
const dismiss = vi.fn()
|
|
80
|
+
const clearFocus = vi.fn()
|
|
81
|
+
node.android.clearFocus = clearFocus
|
|
82
|
+
node.dismissSoftInput = dismiss // NativeScript returns void and has no blur method.
|
|
83
|
+
expect(() => inputHandle.blur()).not.toThrow()
|
|
84
|
+
expect(dismiss).toHaveBeenCalledTimes(1)
|
|
85
|
+
expect(clearFocus).toHaveBeenCalledTimes(1)
|
|
86
|
+
root.unmount()
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
})
|
package/src/text-write.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
const writing = new WeakSet<object>()
|
|
2
|
+
|
|
3
|
+
/** Whether a textChange is the synchronous echo of our controlled write. */
|
|
4
|
+
export function isWritingText(view: object): boolean {
|
|
5
|
+
return writing.has(view)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Controlled `text` write for TextInput/TextArea/SearchInput leaves. The driver's
|
|
2
9
|
* generic prop path writes view.text verbatim; on Android the EditText
|
|
3
10
|
* setText resets the cursor to 0 on every controlled write. Park and
|
|
4
11
|
* restore the selection around the write (clamped to the new length so an
|
|
@@ -13,16 +20,19 @@ export function writeText(view: any, value: string | undefined | null): void {
|
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
const et = view.android
|
|
16
|
-
|
|
23
|
+
const start = et?.getSelectionStart?.() ?? -1
|
|
24
|
+
const end = et?.getSelectionEnd?.() ?? -1
|
|
25
|
+
const nested = writing.has(view)
|
|
26
|
+
writing.add(view)
|
|
27
|
+
try {
|
|
17
28
|
view.text = next
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
et.setSelection(Math.min(start, len), Math.min(end >= 0 ? end : start, len))
|
|
29
|
+
if (start >= 0) {
|
|
30
|
+
const len = next.length
|
|
31
|
+
et.setSelection(Math.min(start, len), Math.min(end >= 0 ? end : start, len))
|
|
32
|
+
}
|
|
33
|
+
} finally {
|
|
34
|
+
if (!nested) {
|
|
35
|
+
writing.delete(view)
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
}
|
|
@@ -13,15 +13,13 @@ export type { ColorScheme } from '../props'
|
|
|
13
13
|
import type { ColorScheme } from '../props'
|
|
14
14
|
|
|
15
15
|
const bridged = () =>
|
|
16
|
-
typeof window !== 'undefined' &&
|
|
17
|
-
(window as any).webkit?.messageHandlers?.xplat !== undefined
|
|
16
|
+
typeof window !== 'undefined' && (window as any).webkit?.messageHandlers?.xplat !== undefined
|
|
18
17
|
|
|
19
18
|
let lastScheme: ColorScheme =
|
|
20
19
|
((window as any).__xplatColorScheme as ColorScheme | undefined) ?? 'light'
|
|
21
20
|
|
|
22
21
|
function getSystemScheme(): ColorScheme {
|
|
23
|
-
return typeof matchMedia === 'function' &&
|
|
24
|
-
matchMedia('(prefers-color-scheme: dark)').matches
|
|
22
|
+
return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches
|
|
25
23
|
? 'dark'
|
|
26
24
|
: 'light'
|
|
27
25
|
}
|
|
@@ -3,16 +3,63 @@ import { useSyncExternalStore } from 'octane'
|
|
|
3
3
|
export type { ColorScheme } from '../props'
|
|
4
4
|
import type { ColorScheme } from '../props'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
type HostTransport = {
|
|
7
|
+
listen(listener: (message: string) => void): () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const hostTransport = () =>
|
|
11
|
+
typeof window === 'undefined'
|
|
12
|
+
? null
|
|
13
|
+
: (((window as any).__xplatHostTransport as HostTransport | undefined) ?? null)
|
|
14
|
+
|
|
15
|
+
let hostedScheme: ColorScheme =
|
|
16
|
+
typeof window === 'undefined'
|
|
17
|
+
? 'light'
|
|
18
|
+
: ((window as any).__xplatHostSnapshot?.colorScheme ??
|
|
19
|
+
(window as any).__xplatColorScheme ??
|
|
20
|
+
'light')
|
|
21
|
+
|
|
22
|
+
function getSystemScheme(): ColorScheme {
|
|
7
23
|
return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches
|
|
8
24
|
? 'dark'
|
|
9
25
|
: 'light'
|
|
10
26
|
}
|
|
11
27
|
|
|
28
|
+
export function getColorScheme(): ColorScheme {
|
|
29
|
+
return hostTransport() ? hostedScheme : getSystemScheme()
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
export function subscribeSystemScheme(cb: () => void): () => void {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
33
|
+
const transport = hostTransport()
|
|
34
|
+
if (!transport) {
|
|
35
|
+
const mq = matchMedia('(prefers-color-scheme: dark)')
|
|
36
|
+
mq.addEventListener('change', cb)
|
|
37
|
+
return () => mq.removeEventListener('change', cb)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const onTransportMessage = (message: string) => {
|
|
41
|
+
try {
|
|
42
|
+
const packet = JSON.parse(message) as { type?: string; name?: string; payload?: unknown }
|
|
43
|
+
if (packet.type === 'event' && packet.name === 'appearance.change') {
|
|
44
|
+
hostedScheme = packet.payload === 'dark' ? 'dark' : 'light'
|
|
45
|
+
cb()
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Host messages are strings; unrelated malformed traffic is ignored.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const onCompatEvent = (event: Event) => {
|
|
53
|
+
hostedScheme = (event as CustomEvent).detail === 'dark' ? 'dark' : 'light'
|
|
54
|
+
cb()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const offTransport = transport.listen(onTransportMessage)
|
|
58
|
+
window.addEventListener('xplat:appearance.change', onCompatEvent)
|
|
59
|
+
return () => {
|
|
60
|
+
offTransport()
|
|
61
|
+
window.removeEventListener('xplat:appearance.change', onCompatEvent)
|
|
62
|
+
}
|
|
16
63
|
}
|
|
17
64
|
|
|
18
65
|
export function useColorScheme(): ColorScheme {
|
|
@@ -9,7 +9,10 @@ const listeners = new Set<() => void>()
|
|
|
9
9
|
const emit = () => listeners.forEach((listener) => listener())
|
|
10
10
|
|
|
11
11
|
export function setThemePreference(value: ThemePreference): void {
|
|
12
|
-
if (preference === value) {
|
|
12
|
+
if (preference === value) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
13
16
|
preference = value
|
|
14
17
|
emit()
|
|
15
18
|
}
|
|
@@ -44,7 +47,10 @@ export function onThemeSchemeChange(callback: () => void): () => void {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export function applyThemeClasses(view: any, base = ''): () => void {
|
|
47
|
-
const apply = () => {
|
|
50
|
+
const apply = () => {
|
|
51
|
+
view.className = cx(base, themeSchemeClasses())
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
apply()
|
|
49
55
|
return onThemeSchemeChange(apply)
|
|
50
56
|
}
|