@octane-xplat/keyboard 0.0.1 → 0.17.1
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 +153 -1
- package/UPSTREAM.md +12 -0
- package/package.json +77 -9
- package/src/KeyboardAwareScrollView.mobile.tsrx +54 -0
- package/src/KeyboardAwareScrollView.tsrx +53 -0
- package/src/KeyboardStickyView.mobile.tsrx +66 -0
- package/src/KeyboardStickyView.tsrx +9 -0
- package/src/Provider.tsrx +11 -0
- package/src/binding-types.ts +16 -0
- package/src/controller.ts +243 -0
- package/src/focus-clock.macos.ts +13 -0
- package/src/focus-geometry.ts +34 -0
- package/src/focus-publish.ts +14 -0
- package/src/focus-runtime-types.ts +9 -0
- package/src/focus-runtime.macos.ts +62 -0
- package/src/focus-runtime.mobile.ts +57 -0
- package/src/focus-runtime.ts +14 -0
- package/src/focus-runtime.web.ts +139 -0
- package/src/focus-source.android.ts +173 -0
- package/src/focus-source.ios.ts +202 -0
- package/src/focus-source.macos.ts +113 -0
- package/src/focus-source.ts +12 -0
- package/src/focus-source.web.ts +157 -0
- package/src/focus-types.ts +19 -0
- package/src/focused-input.ts +43 -0
- package/src/index.ts +12 -0
- package/src/motion.ts +82 -0
- package/src/native-scope.ts +28 -0
- package/src/prediction.ts +32 -0
- package/src/props.ts +37 -0
- package/src/runtime-types.ts +10 -0
- package/src/runtime.mobile.ts +62 -0
- package/src/runtime.ts +16 -0
- package/src/scroll-controller.ts +136 -0
- package/src/scroll-geometry.ts +72 -0
- package/src/scroll-runtime-types.ts +6 -0
- package/src/scroll-runtime.macos.ts +41 -0
- package/src/scroll-runtime.mobile.ts +46 -0
- package/src/scroll-runtime.ts +7 -0
- package/src/scroll-runtime.web.ts +25 -0
- package/src/scroll-types.ts +24 -0
- package/src/source.android.ts +179 -0
- package/src/source.ios.ts +283 -0
- package/src/source.ts +5 -0
- package/src/tsconfig.json +1 -0
- package/src/types.ts +45 -0
- package/src/viewport-types.ts +7 -0
- package/src/viewport.android.ts +238 -0
- package/src/viewport.ios.ts +6 -0
- package/src/viewport.ts +6 -0
- package/types/focus-types.d.ts +17 -0
- package/types/index.d.ts +20 -0
- package/types/props.d.ts +35 -0
- package/types/scroll-types.d.ts +22 -0
- package/types/types.d.ts +40 -0
- package/index.js +0 -1
package/README.md
CHANGED
|
@@ -1,3 +1,155 @@
|
|
|
1
1
|
# @octane-xplat/keyboard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Keep a form above the phone keyboard while it moves. Add the optional keyboard
|
|
4
|
+
package around the existing UI wrapper:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pnpm add @octane-xplat/keyboard
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { KeyboardProvider } from '@octane-xplat/keyboard'
|
|
12
|
+
import { Field, KeyboardAvoiding, TextInput } from '@octane-xplat/ui'
|
|
13
|
+
|
|
14
|
+
export function MessageForm() {
|
|
15
|
+
return <KeyboardProvider>
|
|
16
|
+
<KeyboardAvoiding keyboardVerticalOffset={8}>
|
|
17
|
+
<Field label="Message">
|
|
18
|
+
<TextInput placeholder="Write a message" />
|
|
19
|
+
</Field>
|
|
20
|
+
</KeyboardAvoiding>
|
|
21
|
+
</KeyboardProvider>
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
On supported native hosts, the padding follows measured keyboard frames without
|
|
26
|
+
rendering the component on each animation frame. Native tracking uses the public
|
|
27
|
+
UIKit keyboard guide on iOS 17+ and AndroidX animation callbacks on Android 11+.
|
|
28
|
+
Runtime qualification currently covers iOS 27 and Android API 35 with NativeScript
|
|
29
|
+
9.1.3. Older or incompatible native contexts retain destination-only avoidance;
|
|
30
|
+
web and desktop retain neutral software-keyboard behavior.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// Pausing tracking retains the form's children and the UI wrapper's fallback.
|
|
34
|
+
<KeyboardProvider enabled={false}>
|
|
35
|
+
<KeyboardAvoiding>
|
|
36
|
+
<TextInput placeholder="Write a message" />
|
|
37
|
+
</KeyboardAvoiding>
|
|
38
|
+
</KeyboardProvider>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
On iOS, a fresh observer needs one native keyboard completion before it can
|
|
42
|
+
advertise measured movement. Until then, avoidance uses the destination, the
|
|
43
|
+
current frame is unavailable, and the native-frame capability is false. This
|
|
44
|
+
also applies when starting observation over an already-moving keyboard. Readers
|
|
45
|
+
that share a running window source reuse its available measurements.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { KeyboardController } from '@octane-xplat/keyboard'
|
|
49
|
+
|
|
50
|
+
export function canFollowMeasuredFrames(host: object) {
|
|
51
|
+
return KeyboardController.getSnapshot(host).capabilities.nativeFrames
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For native integrations, `KeyboardController` separates lifecycle subscriptions
|
|
56
|
+
from measured movement. Supply a mounted native host from your platform leaf,
|
|
57
|
+
and release subscriptions when that host detaches. These operations activate
|
|
58
|
+
tracking for their own lifetime; a Provider is optional for these readers.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { KeyboardController } from '@octane-xplat/keyboard'
|
|
62
|
+
|
|
63
|
+
export function observeHost(host: object) {
|
|
64
|
+
const stop = KeyboardController.subscribe(host, state => {
|
|
65
|
+
console.log(state.phase, state.capabilities.nativeFrames)
|
|
66
|
+
})
|
|
67
|
+
const stopMoving = KeyboardController.onMove(host, state => {
|
|
68
|
+
// Use measured geometry for native work; avoid rendering on every frame.
|
|
69
|
+
const top = state.currentFrame?.y
|
|
70
|
+
if (top !== undefined) updateNativeMeasurement(top)
|
|
71
|
+
})
|
|
72
|
+
return () => { stopMoving(); stop() }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function updateNativeMeasurement(_top: number) {}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Frames use device-independent coordinates in the owning window. The destination
|
|
79
|
+
can change before the measured current frame reaches it. Read capability flags
|
|
80
|
+
before depending on native tracking; floating-keyboard tracking and Android interactive dismissal are not yet
|
|
81
|
+
qualified. The supported iOS guide path tracks interactive dismissal when its
|
|
82
|
+
scroll host enables that gesture. No text contents are collected.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { KeyboardController } from '@octane-xplat/keyboard'
|
|
86
|
+
|
|
87
|
+
export function readHost(host: object) {
|
|
88
|
+
const state = KeyboardController.getSnapshot(host)
|
|
89
|
+
return {
|
|
90
|
+
current: state.currentFrame,
|
|
91
|
+
destination: state.targetFrame,
|
|
92
|
+
canTrackMovement: state.capabilities.nativeFrames,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The [maintained example](./examples/KeyboardMotionDemo.tsrx) uses the shared UI API.
|
|
98
|
+
Native layout and presentation limits are recorded in the framework's
|
|
99
|
+
[software keyboard notes](../../.agents/docs/keyboard.md).
|
|
100
|
+
|
|
101
|
+
`FocusedInputController` observes input identity, selection, input/caret bounds,
|
|
102
|
+
and scroll ancestors independently of keyboard motion. Coordinates use the
|
|
103
|
+
owning window. Caret precision is explicit; browser text fields expose box
|
|
104
|
+
bounds without a caret rectangle. Release subscriptions when the host is removed.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { FocusedInputController } from '@octane-xplat/keyboard'
|
|
108
|
+
|
|
109
|
+
export function observeEditing(host: object) {
|
|
110
|
+
return FocusedInputController.subscribe(host, state => {
|
|
111
|
+
console.log(state.inputRect, state.capabilities.caretBounds)
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
See the [focused-input guide](../../docs/app/software-keyboard.md#read-focused-input-geometry)
|
|
117
|
+
for capability limits, selection units, and immediate rechecks.
|
|
118
|
+
|
|
119
|
+
Use `KeyboardAwareScrollView` for long forms. It exposes the focused field or
|
|
120
|
+
caret with minimal scrolling, pauses after manual scrolling by default, and
|
|
121
|
+
provides a binding ref for explicit visibility rechecks.
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { KeyboardAwareScrollView } from '@octane-xplat/keyboard'
|
|
125
|
+
import { Field, TextInput } from '@octane-xplat/ui'
|
|
126
|
+
|
|
127
|
+
export function LongForm() {
|
|
128
|
+
return <KeyboardAwareScrollView clearance={12} extraScrollSpace={40}>
|
|
129
|
+
<Field label="Message"><TextInput placeholder="Write a message" /></Field>
|
|
130
|
+
</KeyboardAwareScrollView>
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The [form guide](../../docs/app/software-keyboard.md#keep-the-editing-field-visible)
|
|
135
|
+
explains scroll policies and the [maintained form example](./examples/KeyboardFormDemo.tsrx)
|
|
136
|
+
shows an explicit recheck.
|
|
137
|
+
|
|
138
|
+
`KeyboardStickyView` translates a bottom control independently of surrounding
|
|
139
|
+
layout on supported native hosts. Existing transforms compose with its owned
|
|
140
|
+
vertical channel; disabling releases that channel. Browser and desktop keep a
|
|
141
|
+
neutral View.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { KeyboardStickyView } from '@octane-xplat/keyboard'
|
|
145
|
+
import { TextInput } from '@octane-xplat/ui'
|
|
146
|
+
|
|
147
|
+
export function Composer() {
|
|
148
|
+
return <KeyboardStickyView openOffset={8}>
|
|
149
|
+
<TextInput placeholder="Write a message" />
|
|
150
|
+
</KeyboardStickyView>
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Place it at the screen's bottom. See the [sticky guide](../../docs/app/software-keyboard.md#keep-a-composer-beside-the-keyboard)
|
|
155
|
+
and [maintained composer](./examples/KeyboardComposerDemo.tsrx).
|
package/UPSTREAM.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Keyboard implementation references
|
|
2
|
+
|
|
3
|
+
The separation of measured keyboard frames, native movement bindings, focused
|
|
4
|
+
input geometry, and scroll policy is informed by
|
|
5
|
+
[react-native-keyboard-controller 1.22.0](https://github.com/kirillzyusko/react-native-keyboard-controller/tree/b80086f1207e36d634f93afabb65eb886fe1a2fe)
|
|
6
|
+
(MIT; copyright 2021 Kiryl Ziusko). This package does not copy its React
|
|
7
|
+
Native/Reanimated implementation or private keyboard-view discovery.
|
|
8
|
+
|
|
9
|
+
Native adapters use the public UIKit keyboard layout guide and CADisplayLink,
|
|
10
|
+
and AndroidX WindowInsetsAnimationCompat on an owned helper view. NativeScript
|
|
11
|
+
Page inset distribution is leased through its public androidOverflowInset event.
|
|
12
|
+
The package owns these helpers and releases them with its final window reader.
|
package/package.json
CHANGED
|
@@ -1,20 +1,88 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/keyboard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.17.1",
|
|
4
|
+
"description": "Window-scoped software keyboard tracking and native movement for Octane Xplat",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"android",
|
|
7
|
+
"cross-platform",
|
|
8
|
+
"ios",
|
|
9
|
+
"keyboard",
|
|
10
|
+
"nativescript",
|
|
11
|
+
"octane",
|
|
12
|
+
"xplat"
|
|
13
|
+
],
|
|
6
14
|
"license": "MIT",
|
|
15
|
+
"homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/keyboard",
|
|
7
16
|
"repository": {
|
|
8
17
|
"type": "git",
|
|
9
18
|
"url": "https://github.com/octane-xplat/octane-xplat",
|
|
10
19
|
"directory": "packages/keyboard"
|
|
11
20
|
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"types",
|
|
25
|
+
"UPSTREAM.md",
|
|
26
|
+
"!src/**/*.test.*",
|
|
27
|
+
"!src/**/*.spec.*"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./types/index.d.ts",
|
|
32
|
+
"default": "./src/index.ts"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
12
35
|
"publishConfig": {
|
|
13
36
|
"access": "public"
|
|
14
37
|
},
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@octane-xplat/platform": "0.17.1",
|
|
40
|
+
"@octane-xplat/ui": "0.17.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@nativescript-community/octane": "0.2.5",
|
|
44
|
+
"@nativescript/core": "9.1.3",
|
|
45
|
+
"@octanejs/vite-plugin": "0.1.61",
|
|
46
|
+
"@octane-xplat/jsx": "0.17.1",
|
|
47
|
+
"octane": "0.14.0",
|
|
48
|
+
"tsrx-typegen": "0.4.0",
|
|
49
|
+
"typescript": "5.9.3",
|
|
50
|
+
"vite": "8.3.0",
|
|
51
|
+
"vitest": "4.1.11"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
55
|
+
"@nativescript/core": ">=9.1.0 <10",
|
|
56
|
+
"octane": ">=0.11.0 <1"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@nativescript-community/octane": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@nativescript/core": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"typegen": "tsc -p tsconfig.types.json",
|
|
68
|
+
"typecheck": "node ../../scripts/typecheck.ts",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:motion:ios": "node ../../scripts/probe.ts run tests/fixtures/motion.ios.tsrx --target ios",
|
|
71
|
+
"test:motion:android": "node ../../scripts/probe.ts run tests/fixtures/motion.android.tsrx --target android",
|
|
72
|
+
"test:interactive:ios": "node ../../scripts/probe.ts run tests/fixtures/interactive.ios.tsrx --target ios",
|
|
73
|
+
"pack:check": "tsrx-typegen --pack-check",
|
|
74
|
+
"test:focus:web": "node ../../scripts/probe.ts run tests/fixtures/focus.web.tsrx --target web",
|
|
75
|
+
"test:focus:ios": "node ../../scripts/probe.ts run tests/fixtures/focus.ios.tsrx --target ios",
|
|
76
|
+
"test:focus:android": "node ../../scripts/probe.ts run tests/fixtures/focus.android.tsrx --target android",
|
|
77
|
+
"test:focus:macos": "node ../../scripts/probe.ts run tests/fixtures/focus.macos.tsrx --target macos",
|
|
78
|
+
"test:scroll:web": "node ../../scripts/probe.ts run tests/fixtures/scroll.web.tsrx --target web",
|
|
79
|
+
"test:scroll:ios": "node ../../scripts/probe.ts run tests/fixtures/scroll.ios.tsrx --target ios",
|
|
80
|
+
"test:scroll:android": "node ../../scripts/probe.ts run tests/fixtures/scroll.android.tsrx --target android",
|
|
81
|
+
"test:scroll:macos": "node ../../scripts/probe.ts run tests/fixtures/scroll.macos.tsrx --target macos",
|
|
82
|
+
"test:sticky:web": "node ../../scripts/probe.ts run tests/fixtures/sticky.web.tsrx --target web",
|
|
83
|
+
"test:sticky:ios": "node ../../scripts/probe.ts run tests/fixtures/sticky.ios.tsrx --target ios",
|
|
84
|
+
"test:sticky:android": "node ../../scripts/probe.ts run tests/fixtures/sticky.android.tsrx --target android",
|
|
85
|
+
"test:packed": "node tests/packed-consumer.mjs",
|
|
86
|
+
"test:os-input": "node tests/os-input.mjs"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** @jsxImportSource @octane-xplat/jsx */
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'octane'
|
|
3
|
+
import { KeyboardAvoiding } from '@octane-xplat/ui'
|
|
4
|
+
import { KeyboardScrollController } from './scroll-controller'
|
|
5
|
+
import type { KeyboardAwareScrollViewProps } from './props'
|
|
6
|
+
import type { KeyboardScrollBinding } from './scroll-types'
|
|
7
|
+
|
|
8
|
+
export function KeyboardAwareScrollView(props: KeyboardAwareScrollViewProps) @{
|
|
9
|
+
const [host, setHost] = useState<any>(null)
|
|
10
|
+
const attach = useCallback((next: any) => setHost(next), [])
|
|
11
|
+
const binding = useRef<KeyboardScrollBinding | null>(null)
|
|
12
|
+
const latest = useRef(props)
|
|
13
|
+
latest.current = props
|
|
14
|
+
const enabled = props.enabled ?? true
|
|
15
|
+
if (!Number.isFinite(props.extraScrollSpace ?? 0) || (props.extraScrollSpace ?? 0) < 0) {
|
|
16
|
+
throw new RangeError('Extra keyboard scroll space must be finite and nonnegative')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!host || !enabled) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const own = KeyboardScrollController.bind(host, latest.current)
|
|
25
|
+
binding.current = own
|
|
26
|
+
return () => {
|
|
27
|
+
own.release()
|
|
28
|
+
binding.current = null
|
|
29
|
+
}
|
|
30
|
+
}, [host, enabled])
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
props.ref?.(binding.current)
|
|
34
|
+
return () => props.ref?.(null)
|
|
35
|
+
}, [host, enabled, props.ref])
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
binding.current?.update(props)
|
|
39
|
+
}, [props.clearance, props.manualScroll, props.keyboardClose])
|
|
40
|
+
|
|
41
|
+
<KeyboardAvoiding
|
|
42
|
+
id={props.id}
|
|
43
|
+
className={props.className}
|
|
44
|
+
style={props.style}
|
|
45
|
+
ref={attach}
|
|
46
|
+
enabled={enabled}
|
|
47
|
+
keyboardVerticalOffset={(props.keyboardVerticalOffset ?? 0) + (props.extraScrollSpace ?? 0)}
|
|
48
|
+
ios={props.ios}
|
|
49
|
+
android={props.android}
|
|
50
|
+
web={props.web}
|
|
51
|
+
>
|
|
52
|
+
{props.children}
|
|
53
|
+
</KeyboardAvoiding>
|
|
54
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'octane'
|
|
2
|
+
import { ScrollableArea } from '@octane-xplat/ui'
|
|
3
|
+
import { KeyboardScrollController } from './scroll-controller'
|
|
4
|
+
import type { KeyboardAwareScrollViewProps } from './props'
|
|
5
|
+
import type { KeyboardScrollBinding } from './scroll-types'
|
|
6
|
+
|
|
7
|
+
/** Desktop/browser focus scrolling; software-keyboard movement remains neutral. */
|
|
8
|
+
export function KeyboardAwareScrollView(props: KeyboardAwareScrollViewProps) @{
|
|
9
|
+
const [host, setHost] = useState<any>(null)
|
|
10
|
+
const attach = useCallback((next: any) => setHost(next?.native ?? null), [])
|
|
11
|
+
const binding = useRef<KeyboardScrollBinding | null>(null)
|
|
12
|
+
const latest = useRef(props)
|
|
13
|
+
latest.current = props
|
|
14
|
+
const enabled = props.enabled ?? true
|
|
15
|
+
if (!Number.isFinite(props.extraScrollSpace ?? 0) || (props.extraScrollSpace ?? 0) < 0) {
|
|
16
|
+
throw new RangeError('Extra keyboard scroll space must be finite and nonnegative')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!host || !enabled) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const own = KeyboardScrollController.bind(host, latest.current)
|
|
25
|
+
binding.current = own
|
|
26
|
+
return () => {
|
|
27
|
+
own.release()
|
|
28
|
+
binding.current = null
|
|
29
|
+
}
|
|
30
|
+
}, [host, enabled])
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
props.ref?.(binding.current)
|
|
34
|
+
return () => props.ref?.(null)
|
|
35
|
+
}, [host, enabled, props.ref])
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
binding.current?.update(props)
|
|
39
|
+
}, [props.clearance, props.manualScroll, props.keyboardClose])
|
|
40
|
+
|
|
41
|
+
<ScrollableArea
|
|
42
|
+
id={props.id}
|
|
43
|
+
className={props.className}
|
|
44
|
+
style={props.style}
|
|
45
|
+
ref={attach}
|
|
46
|
+
axis="block"
|
|
47
|
+
ios={props.ios}
|
|
48
|
+
android={props.android}
|
|
49
|
+
web={props.web}
|
|
50
|
+
>
|
|
51
|
+
{props.children}
|
|
52
|
+
</ScrollableArea>
|
|
53
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** @jsxImportSource @octane-xplat/jsx */
|
|
2
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'octane'
|
|
3
|
+
import { View } from '@octane-xplat/ui'
|
|
4
|
+
import {
|
|
5
|
+
ConsumedInsetsContext,
|
|
6
|
+
KeyboardAvoidanceContext,
|
|
7
|
+
keyboardWindowForHost,
|
|
8
|
+
} from '@octane-xplat/ui/nativescript/keyboard'
|
|
9
|
+
|
|
10
|
+
import { bindKeyboardHost } from './controller'
|
|
11
|
+
import type { KeyboardHostBinding } from './binding-types'
|
|
12
|
+
import type { KeyboardStickyViewProps } from './props'
|
|
13
|
+
|
|
14
|
+
export function KeyboardStickyView(props: KeyboardStickyViewProps) @{
|
|
15
|
+
const [host, setHost] = useState<any>(null)
|
|
16
|
+
const attach = useCallback((next: any) => setHost(next), [])
|
|
17
|
+
const forwarded = useMemo(() => [attach, props.ref], [attach, props.ref])
|
|
18
|
+
const safe = useContext(ConsumedInsetsContext)
|
|
19
|
+
const ancestor = useContext(KeyboardAvoidanceContext)
|
|
20
|
+
const latest = useRef({ props, safe, ancestor })
|
|
21
|
+
latest.current = { props, safe, ancestor }
|
|
22
|
+
const binding = useRef<KeyboardHostBinding | null>(null)
|
|
23
|
+
const enabled = props.enabled ?? true
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!host || !enabled) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const own = bindKeyboardHost(host, () => {
|
|
30
|
+
const current = latest.current
|
|
31
|
+
return {
|
|
32
|
+
openOffset: current.props.openOffset,
|
|
33
|
+
closedOffset: current.props.closedOffset,
|
|
34
|
+
consumedBottom:
|
|
35
|
+
Math.max(0, current.safe.bottom) +
|
|
36
|
+
(current.ancestor.window === keyboardWindowForHost(host) ? current.ancestor.bottom : 0),
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
binding.current = own
|
|
41
|
+
return () => {
|
|
42
|
+
binding.current = null
|
|
43
|
+
own()
|
|
44
|
+
}
|
|
45
|
+
}, [host, enabled])
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
binding.current?.update()
|
|
49
|
+
}, [
|
|
50
|
+
props.openOffset,
|
|
51
|
+
props.closedOffset,
|
|
52
|
+
props.style,
|
|
53
|
+
props.className,
|
|
54
|
+
props.ios,
|
|
55
|
+
props.android,
|
|
56
|
+
safe.bottom,
|
|
57
|
+
ancestor.window,
|
|
58
|
+
ancestor.bottom,
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
const { enabled: _enabled, openOffset: _openOffset, closedOffset: _closedOffset, ...view } = props
|
|
62
|
+
|
|
63
|
+
<View {...view} ref={forwarded}>
|
|
64
|
+
{props.children}
|
|
65
|
+
</View>
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { View } from '@octane-xplat/ui'
|
|
2
|
+
import type { KeyboardStickyViewProps } from './props'
|
|
3
|
+
|
|
4
|
+
/** Neutral layout on hosts without measured software-keyboard movement. */
|
|
5
|
+
export function KeyboardStickyView(props: KeyboardStickyViewProps) @{
|
|
6
|
+
const { enabled: _enabled, openOffset: _openOffset, closedOffset: _closedOffset, ...view } = props
|
|
7
|
+
|
|
8
|
+
<View {...view}>{props.children}</View>
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect } from 'octane'
|
|
2
|
+
import { KeyboardController } from './controller'
|
|
3
|
+
import type { KeyboardProviderProps } from './props'
|
|
4
|
+
|
|
5
|
+
/** Activate shared tracking without adding a layout host. */
|
|
6
|
+
export function KeyboardProvider(props: KeyboardProviderProps) @{
|
|
7
|
+
const enabled = props.enabled ?? true
|
|
8
|
+
useEffect(() => (enabled ? KeyboardController.activate() : undefined), [enabled])
|
|
9
|
+
|
|
10
|
+
<>{props.children}</>
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** @internal Native host policy used by framework-owned movement bindings. */
|
|
2
|
+
/** Low-level native translation policy. Offsets are device-independent pixels. */
|
|
3
|
+
export interface KeyboardBindingOptions {
|
|
4
|
+
/** Do not observe or move the host while disabled. @default true */
|
|
5
|
+
enabled?: boolean
|
|
6
|
+
/** Additional upward clearance while the keyboard is open. @default 0 */
|
|
7
|
+
openOffset?: number
|
|
8
|
+
/** Upward displacement while closed. @default 0 */
|
|
9
|
+
closedOffset?: number
|
|
10
|
+
/** Space supplied by a safe-area/avoidance ancestor in the same window.
|
|
11
|
+
* Actual native resizing is measured separately. @default 0 */
|
|
12
|
+
consumedBottom?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @internal Options can change without replacing the shared window source. */
|
|
16
|
+
export type KeyboardHostBinding = (() => void) & { update(): void }
|