@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.
Files changed (56) hide show
  1. package/README.md +153 -1
  2. package/UPSTREAM.md +12 -0
  3. package/package.json +77 -9
  4. package/src/KeyboardAwareScrollView.mobile.tsrx +54 -0
  5. package/src/KeyboardAwareScrollView.tsrx +53 -0
  6. package/src/KeyboardStickyView.mobile.tsrx +66 -0
  7. package/src/KeyboardStickyView.tsrx +9 -0
  8. package/src/Provider.tsrx +11 -0
  9. package/src/binding-types.ts +16 -0
  10. package/src/controller.ts +243 -0
  11. package/src/focus-clock.macos.ts +13 -0
  12. package/src/focus-geometry.ts +34 -0
  13. package/src/focus-publish.ts +14 -0
  14. package/src/focus-runtime-types.ts +9 -0
  15. package/src/focus-runtime.macos.ts +62 -0
  16. package/src/focus-runtime.mobile.ts +57 -0
  17. package/src/focus-runtime.ts +14 -0
  18. package/src/focus-runtime.web.ts +139 -0
  19. package/src/focus-source.android.ts +173 -0
  20. package/src/focus-source.ios.ts +202 -0
  21. package/src/focus-source.macos.ts +113 -0
  22. package/src/focus-source.ts +12 -0
  23. package/src/focus-source.web.ts +157 -0
  24. package/src/focus-types.ts +19 -0
  25. package/src/focused-input.ts +43 -0
  26. package/src/index.ts +12 -0
  27. package/src/motion.ts +82 -0
  28. package/src/native-scope.ts +28 -0
  29. package/src/prediction.ts +32 -0
  30. package/src/props.ts +37 -0
  31. package/src/runtime-types.ts +10 -0
  32. package/src/runtime.mobile.ts +62 -0
  33. package/src/runtime.ts +16 -0
  34. package/src/scroll-controller.ts +136 -0
  35. package/src/scroll-geometry.ts +72 -0
  36. package/src/scroll-runtime-types.ts +6 -0
  37. package/src/scroll-runtime.macos.ts +41 -0
  38. package/src/scroll-runtime.mobile.ts +46 -0
  39. package/src/scroll-runtime.ts +7 -0
  40. package/src/scroll-runtime.web.ts +25 -0
  41. package/src/scroll-types.ts +24 -0
  42. package/src/source.android.ts +179 -0
  43. package/src/source.ios.ts +283 -0
  44. package/src/source.ts +5 -0
  45. package/src/tsconfig.json +1 -0
  46. package/src/types.ts +45 -0
  47. package/src/viewport-types.ts +7 -0
  48. package/src/viewport.android.ts +238 -0
  49. package/src/viewport.ios.ts +6 -0
  50. package/src/viewport.ts +6 -0
  51. package/types/focus-types.d.ts +17 -0
  52. package/types/index.d.ts +20 -0
  53. package/types/props.d.ts +35 -0
  54. package/types/scroll-types.d.ts +22 -0
  55. package/types/types.d.ts +40 -0
  56. package/index.js +0 -1
@@ -0,0 +1,24 @@
1
+ /** Geometry policy for vertical focused-input scrolling. */
2
+ export interface KeyboardScrollOptions {
3
+ /** Minimum gap around the input/caret, in window layout units. @default 8 */
4
+ clearance?: number
5
+ /** Pause after a user scroll until focus/selection changes or recheck. @default 'pause' */
6
+ manualScroll?: 'pause' | 'follow'
7
+ /** Keep the resulting offset, or restore untouched offsets on close. @default 'preserve' */
8
+ keyboardClose?: 'preserve' | 'restore'
9
+ }
10
+
11
+ export interface KeyboardScrollBinding {
12
+ /** Explicitly resume visibility enforcement after validation or manual scrolling. */
13
+ recheck(): void
14
+ /** Apply changed options without restarting window observation. */
15
+ update(options: KeyboardScrollOptions): void
16
+ /** Release observation; preserves the current scroll position. */
17
+ release(): void
18
+ }
19
+
20
+ export interface KeyboardScrollService {
21
+ /** Attach to a vertical scroll viewport. Only scroll ancestors through this
22
+ * viewport are eligible; unrelated fields in the same window are ignored. */
23
+ bind(host: object, options?: KeyboardScrollOptions): KeyboardScrollBinding
24
+ }
@@ -0,0 +1,179 @@
1
+ import { Utils } from '@nativescript/core'
2
+ import { android } from '@octane-xplat/platform/android'
3
+ import type { KeyboardWindowSource } from '@octane-xplat/platform/keyboard-registry'
4
+ import type { KeyboardWindowState } from '@octane-xplat/platform/keyboard-state'
5
+ import { observeKeyboardDestination } from '@octane-xplat/ui/nativescript/keyboard'
6
+ import { createKeyboardMotion } from './motion'
7
+ import { keyboardNativeScope } from './native-scope'
8
+
9
+ let AnimationCallback: any
10
+ const callbackClass = (core: any) => {
11
+ if (!AnimationCallback) {
12
+ AnimationCallback = core.WindowInsetsAnimationCompat.Callback.extend({
13
+ onPrepare(this: any, animation: any) {
14
+ this.handlers?.prepare(animation)
15
+ },
16
+ onStart(this: any, animation: any, bounds: any) {
17
+ this.handlers?.start(animation)
18
+ return bounds
19
+ },
20
+ onProgress(this: any, insets: any) {
21
+ this.handlers?.progress(insets)
22
+ return insets
23
+ },
24
+ onEnd(this: any, animation: any) {
25
+ this.handlers?.end(animation)
26
+ },
27
+ })
28
+ }
29
+ return AnimationCallback
30
+ }
31
+
32
+ /** Callback lives only on our helper; decor/foreign callbacks remain untouched. */
33
+ export const observeKeyboardMotion: KeyboardWindowSource<KeyboardWindowState> = (key, publish) => {
34
+ const decor = key as any
35
+ const core = (globalThis as any).androidx?.core?.view
36
+ if (
37
+ !core?.WindowInsetsAnimationCompat ||
38
+ android.os.Build.VERSION.SDK_INT < 30 ||
39
+ !decor.addView
40
+ ) {
41
+ return observeKeyboardDestination(key, publish)
42
+ }
43
+ const scope = keyboardNativeScope()
44
+ try {
45
+ const helper = new android.view.View(decor.getContext())
46
+ helper.setImportantForAccessibility(2)
47
+ helper.setFocusable(false)
48
+ helper.setClickable(false)
49
+ decor.addView(helper, new android.view.ViewGroup.LayoutParams(0, 0))
50
+ scope.defer(() => {
51
+ const remove = () => {
52
+ if (helper.getParent() === decor) {
53
+ decor.removeView(helper)
54
+ }
55
+ }
56
+ // A reader may release from a child's native detach callback. Android
57
+ // is still iterating decor children then; removing one corrupts that walk.
58
+ setTimeout(remove, 0)
59
+ })
60
+
61
+ const type = core.WindowInsetsCompat.Type
62
+ const dip = Utils.layout.toDeviceIndependentPixels
63
+ let targetInset = 0
64
+ const motion = createKeyboardMotion((state) => publish({ ...state, inset: targetInset }), {
65
+ interactive: false,
66
+ floating: false,
67
+ })
68
+
69
+ let active = true
70
+ scope.defer(() => {
71
+ active = false
72
+ })
73
+
74
+ let animation: any
75
+ let target: any
76
+ const geometry = (insets: any) => {
77
+ const width = dip(decor.getWidth()),
78
+ height = dip(decor.getHeight())
79
+ const ime = dip(insets?.getInsets(type.ime()).bottom ?? 0)
80
+ return {
81
+ window: { x: 0, y: 0, width, height },
82
+ frame: { x: 0, y: height - ime, width, height: ime },
83
+ }
84
+ }
85
+
86
+ const read = () => core.ViewCompat.getRootWindowInsets(decor)
87
+ const destination = () => {
88
+ const insets = read()
89
+ if (!insets) {
90
+ return
91
+ }
92
+ target = geometry(insets).frame
93
+ targetInset = dip(
94
+ Math.max(
95
+ 0,
96
+ insets.getInsets(type.ime()).bottom - insets.getInsets(type.systemBars()).bottom,
97
+ ),
98
+ )
99
+ }
100
+
101
+ const measure = (insets: any) => {
102
+ if (!active || !insets) {
103
+ return
104
+ }
105
+ const value = geometry(insets)
106
+ if (value.window.width > 0 && value.window.height > 0) {
107
+ motion.measure(value.window, value.frame, target ?? value.frame)
108
+ }
109
+ }
110
+
111
+ const callback = new (callbackClass(core))(1)
112
+ callback.handlers = {
113
+ prepare(next: any) {
114
+ if (!active || !(next.getTypeMask() & type.ime())) {
115
+ return
116
+ }
117
+ // Replacing a running animation cancels it before the new start.
118
+ if (animation) {
119
+ motion.cancel()
120
+ }
121
+ animation = next
122
+ },
123
+ start(next: any) {
124
+ if (!active || next !== animation) {
125
+ return
126
+ }
127
+ destination()
128
+ motion.begin(target, Number(next.getDurationMillis()))
129
+ },
130
+ progress(insets: any) {
131
+ measure(insets)
132
+ },
133
+ end(next: any) {
134
+ if (!active || next !== animation) {
135
+ return
136
+ }
137
+ measure(read())
138
+ motion.end()
139
+ animation = undefined
140
+ },
141
+ }
142
+
143
+ scope.defer(() => {
144
+ callback.handlers = undefined
145
+ core.ViewCompat.setWindowInsetsAnimationCallback(helper, null)
146
+ })
147
+
148
+ core.ViewCompat.setWindowInsetsAnimationCallback(helper, callback)
149
+ const layoutListener = new android.view.ViewTreeObserver.OnGlobalLayoutListener({
150
+ onGlobalLayout() {
151
+ if (!active || animation) {
152
+ return
153
+ }
154
+ destination()
155
+ measure(read())
156
+ },
157
+ })
158
+
159
+ scope.defer(() => {
160
+ const tree = decor.getViewTreeObserver()
161
+ if (tree.isAlive()) {
162
+ tree.removeOnGlobalLayoutListener(layoutListener)
163
+ }
164
+ })
165
+
166
+ decor.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener)
167
+ destination()
168
+ measure(read())
169
+ return () => scope.release()
170
+ } catch (error) {
171
+ try {
172
+ scope.release()
173
+ } catch (cleanup) {
174
+ throw new AggregateError([error, cleanup], 'Keyboard native attachment failed')
175
+ }
176
+
177
+ throw error
178
+ }
179
+ }
@@ -0,0 +1,283 @@
1
+ import {
2
+ NSObject,
3
+ CADisplayLink,
4
+ NSRunLoop,
5
+ UIView,
6
+ NSLayoutConstraint,
7
+ NSNotificationCenter,
8
+ NSOperationQueue,
9
+ } from '@octane-xplat/platform/ios'
10
+
11
+ import { isKeyboardRect } from '@octane-xplat/platform/keyboard-geometry'
12
+ import type { KeyboardRect } from '@octane-xplat/platform/keyboard-geometry'
13
+ import type { KeyboardWindowSource } from '@octane-xplat/platform/keyboard-registry'
14
+ import type { KeyboardWindowState } from '@octane-xplat/platform/keyboard-state'
15
+ import { observeKeyboardDestination } from '@octane-xplat/ui/nativescript/keyboard'
16
+ import { createKeyboardMotion } from './motion'
17
+ import { nextKeyboardPosition } from './prediction'
18
+ import { keyboardNativeScope } from './native-scope'
19
+
20
+ let DisplayTarget: any
21
+ const displayTarget = () => {
22
+ if (!DisplayTarget) {
23
+ const interop = (globalThis as any).interop
24
+ DisplayTarget = (NSObject as any).extend(
25
+ {
26
+ ['tick:'](this: any, link: any) {
27
+ this.onTick?.(link)
28
+ },
29
+ },
30
+ { exposedMethods: { 'tick:': { returns: interop.types.void, params: [CADisplayLink] } } },
31
+ )
32
+ }
33
+ return DisplayTarget.new()
34
+ }
35
+
36
+ const rect = (value: any): KeyboardRect => ({
37
+ x: value.origin.x,
38
+ y: value.origin.y,
39
+ width: value.size.width,
40
+ height: value.size.height,
41
+ })
42
+
43
+ /** Own every guide, constraint and display-link target; never mutate a host guide. */
44
+ export const observeKeyboardMotion: KeyboardWindowSource<KeyboardWindowState> = (key, publish) => {
45
+ const window = key as any
46
+ const root = window.rootViewController?.view
47
+ if (!root?.keyboardLayoutGuide?.respondsToSelector('setUsesBottomSafeArea:')) {
48
+ return observeKeyboardDestination(key, publish)
49
+ }
50
+ const scope = keyboardNativeScope()
51
+ try {
52
+ const container = UIView.new()
53
+ container.translatesAutoresizingMaskIntoConstraints = false
54
+ container.userInteractionEnabled = false
55
+ container.accessibilityElementsHidden = true
56
+ root.addSubview(container)
57
+ scope.defer(() => container.removeFromSuperview())
58
+ const guide = container.keyboardLayoutGuide
59
+ guide.usesBottomSafeArea = false
60
+ const tracker = UIView.new()
61
+ tracker.translatesAutoresizingMaskIntoConstraints = false
62
+ tracker.userInteractionEnabled = false
63
+ container.addSubview(tracker)
64
+ const constraints = [
65
+ container.leadingAnchor.constraintEqualToAnchor(root.leadingAnchor),
66
+ container.trailingAnchor.constraintEqualToAnchor(root.trailingAnchor),
67
+ container.topAnchor.constraintEqualToAnchor(root.topAnchor),
68
+ container.bottomAnchor.constraintEqualToAnchor(root.bottomAnchor),
69
+ tracker.leadingAnchor.constraintEqualToAnchor(container.leadingAnchor),
70
+ tracker.trailingAnchor.constraintEqualToAnchor(container.trailingAnchor),
71
+ tracker.bottomAnchor.constraintEqualToAnchor(guide.topAnchor),
72
+ tracker.heightAnchor.constraintEqualToConstant(0),
73
+ ]
74
+
75
+ scope.defer(() => NSLayoutConstraint.deactivateConstraints(constraints))
76
+ NSLayoutConstraint.activateConstraints(constraints)
77
+ root.layoutIfNeeded()
78
+ // A newly constrained view starts at the guide's model destination. It
79
+ // cannot measure a transition that began before it existed. Wait for a
80
+ // native completion and matching presentation before advertising frames.
81
+ let calibrated = false
82
+ let confirmedFrame: KeyboardRect | null = null
83
+ let bootstrapPhase: 'idle' | 'start' = 'idle'
84
+ let coarseKey = ''
85
+ const motion = createKeyboardMotion(
86
+ (state) => {
87
+ if (calibrated) {
88
+ publish(state)
89
+ return
90
+ }
91
+ const key = JSON.stringify([
92
+ state.windowRect,
93
+ state.targetFrame,
94
+ bootstrapPhase,
95
+ state.motion?.transitionId,
96
+ ])
97
+ if (key === coarseKey) {
98
+ return
99
+ }
100
+ coarseKey = key
101
+ publish({
102
+ ...state,
103
+ source: 'destination',
104
+ currentFrame: null,
105
+ bindingFrame: undefined,
106
+ motion: state.motion && {
107
+ ...state.motion,
108
+ phase: bootstrapPhase,
109
+ nativeFrames: false,
110
+ interactive: false,
111
+ },
112
+ })
113
+ },
114
+ { interactive: true, floating: false },
115
+ )
116
+
117
+ let active = true
118
+ scope.defer(() => {
119
+ active = false
120
+ })
121
+
122
+ let moving = false
123
+ let settled = 0
124
+ let lastY = NaN
125
+ let lastTimestamp = NaN
126
+ let lastWidth = NaN
127
+ let lastHeight = NaN
128
+ let targetFrame: KeyboardRect | null = null
129
+ const read = (link?: any) => {
130
+ if (!active || container.window !== window) {
131
+ return
132
+ }
133
+ const bounds = rect(window.bounds)
134
+ const current = tracker.layer.presentationLayer()?.frame ?? tracker.frame
135
+ const local = container.convertRectToView(current, window)
136
+ const y = local.origin.y
137
+ const frame = {
138
+ x: bounds.x,
139
+ y,
140
+ width: bounds.width,
141
+ height: Math.max(0, bounds.y + bounds.height - y),
142
+ }
143
+ if (!isKeyboardRect(bounds) || !isKeyboardRect(frame)) {
144
+ return
145
+ }
146
+ const destination = rect(container.convertRectToView(guide.layoutFrame, window))
147
+ if (!targetFrame) {
148
+ targetFrame = destination
149
+ }
150
+ const justCalibrated =
151
+ !calibrated &&
152
+ confirmedFrame !== null &&
153
+ Math.abs(y - confirmedFrame.y) < 0.5 &&
154
+ Math.abs(destination.y - confirmedFrame.y) < 0.5
155
+ if (justCalibrated) {
156
+ calibrated = true
157
+ }
158
+ if (!moving && Number.isFinite(lastY) && Math.abs(lastY - y) > 0.1) {
159
+ motion.begin(targetFrame)
160
+ moving = true
161
+ }
162
+ // Stable reads do not wake consumers at display refresh rate.
163
+ if (
164
+ justCalibrated ||
165
+ moving ||
166
+ !Number.isFinite(lastY) ||
167
+ Math.abs(lastY - y) > 0.01 ||
168
+ lastWidth !== bounds.width ||
169
+ lastHeight !== bounds.height
170
+ ) {
171
+ const nextY =
172
+ moving && link
173
+ ? nextKeyboardPosition({
174
+ current: y,
175
+ previous: lastY,
176
+ elapsedMs: (link.timestamp - lastTimestamp) * 1000,
177
+ nextMs: (link.targetTimestamp - link.timestamp) * 1000,
178
+ target: targetFrame.y,
179
+ top: bounds.y,
180
+ bottom: bounds.y + bounds.height,
181
+ })
182
+ : y
183
+
184
+ motion.measure(bounds, frame, targetFrame, {
185
+ ...frame,
186
+ y: nextY,
187
+ height: Math.max(0, bounds.y + bounds.height - nextY),
188
+ })
189
+
190
+ if (justCalibrated) {
191
+ motion.end(true)
192
+ }
193
+ }
194
+ if (moving) {
195
+ settled = Math.abs(y - destination.y) < 0.5 && Math.abs(y - lastY) < 0.1 ? settled + 1 : 0
196
+ if (settled >= 2) {
197
+ moving = false
198
+ motion.end()
199
+ }
200
+ }
201
+ lastY = y
202
+ lastTimestamp = link?.timestamp ?? NaN
203
+ lastWidth = bounds.width
204
+ lastHeight = bounds.height
205
+ }
206
+
207
+ read()
208
+ const target = displayTarget()
209
+ target.onTick = read
210
+ const link = CADisplayLink.displayLinkWithTargetSelector(target, 'tick:')
211
+ scope.defer(() => {
212
+ target.onTick = undefined
213
+ link.invalidate()
214
+ })
215
+
216
+ link.addToRunLoopForMode(NSRunLoop.mainRunLoop, 'kCFRunLoopCommonModes')
217
+ const center = NSNotificationCenter.defaultCenter
218
+ const token = center.addObserverForNameObjectQueueUsingBlock(
219
+ UIKeyboardWillChangeFrameNotification,
220
+ null,
221
+ NSOperationQueue.mainQueue,
222
+ (note: any) => {
223
+ if (!active || (note.object?.coordinateSpace && note.object !== window.screen)) {
224
+ return
225
+ }
226
+ const raw = note.userInfo.objectForKey(UIKeyboardFrameEndUserInfoKey)
227
+ const value = typeof raw?.CGRectValue === 'function' ? raw.CGRectValue() : raw?.CGRectValue
228
+ if (!value) {
229
+ return
230
+ }
231
+ const local = window.screen?.coordinateSpace
232
+ ? window.convertRectFromCoordinateSpace(value, window.screen.coordinateSpace)
233
+ : window.convertRectFromWindow(value, null)
234
+ targetFrame = rect(local)
235
+ bootstrapPhase = 'start'
236
+ confirmedFrame = null
237
+ const duration =
238
+ Number(note.userInfo.objectForKey(UIKeyboardAnimationDurationUserInfoKey)) * 1000
239
+ motion.begin(targetFrame, Number.isFinite(duration) ? duration : null)
240
+ moving = true
241
+ settled = 0
242
+ },
243
+ )
244
+
245
+ scope.defer(() => center.removeObserver(token))
246
+ const completion = center.addObserverForNameObjectQueueUsingBlock(
247
+ UIKeyboardDidChangeFrameNotification,
248
+ null,
249
+ NSOperationQueue.mainQueue,
250
+ (note: any) => {
251
+ if (
252
+ !active ||
253
+ calibrated ||
254
+ (note.object?.coordinateSpace && note.object !== window.screen)
255
+ ) {
256
+ return
257
+ }
258
+ const raw = note.userInfo.objectForKey(UIKeyboardFrameEndUserInfoKey)
259
+ const value = typeof raw?.CGRectValue === 'function' ? raw.CGRectValue() : raw?.CGRectValue
260
+ if (!value) {
261
+ return
262
+ }
263
+ confirmedFrame = rect(
264
+ window.screen?.coordinateSpace
265
+ ? window.convertRectFromCoordinateSpace(value, window.screen.coordinateSpace)
266
+ : window.convertRectFromWindow(value, null),
267
+ )
268
+ targetFrame = confirmedFrame
269
+ },
270
+ )
271
+
272
+ scope.defer(() => center.removeObserver(completion))
273
+ return () => scope.release()
274
+ } catch (error) {
275
+ try {
276
+ scope.release()
277
+ } catch (cleanup) {
278
+ throw new AggregateError([error, cleanup], 'Keyboard native attachment failed')
279
+ }
280
+
281
+ throw error
282
+ }
283
+ }
package/src/source.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { KeyboardWindowSource } from '@octane-xplat/platform/keyboard-registry'
2
+ import type { KeyboardWindowState } from '@octane-xplat/platform/keyboard-state'
3
+
4
+ /** Unsupported native contexts retain destination observation. */
5
+ export const observeKeyboardMotion: KeyboardWindowSource<KeyboardWindowState> | null = null
@@ -0,0 +1 @@
1
+ {"extends":"../../../tsconfig.base.json"}
package/src/types.ts ADDED
@@ -0,0 +1,45 @@
1
+ import type { KeyboardRect } from '@octane-xplat/platform/keyboard-geometry'
2
+
3
+ export type { KeyboardRect } from '@octane-xplat/platform/keyboard-geometry'
4
+
5
+ /** Lifecycle of one keyboard transition. An interrupted transition cancels
6
+ * before the replacement starts; current geometry never jumps to its target. */
7
+ export type KeyboardPhase = 'idle' | 'start' | 'move' | 'end' | 'cancel'
8
+
9
+ /** Capabilities belong to the actual host window and active adapter. */
10
+ export interface KeyboardCapabilities {
11
+ readonly geometry: boolean
12
+ readonly nativeFrames: boolean
13
+ readonly interactive: boolean
14
+ readonly floating: boolean
15
+ }
16
+
17
+ /** Geometry is in device-independent coordinates in the owning window.
18
+ * Null frames are unavailable; source distinguishes unknown from measured hidden.
19
+ * A destination frame is never presented as measured intermediate movement. */
20
+ export interface KeyboardSnapshot {
21
+ readonly inset: number
22
+ readonly windowRect: KeyboardRect | null
23
+ readonly currentFrame: KeyboardRect | null
24
+ readonly targetFrame: KeyboardRect | null
25
+ readonly source: 'unknown' | 'destination' | 'native-frames'
26
+ readonly capabilities: KeyboardCapabilities
27
+ readonly phase: KeyboardPhase
28
+ readonly transitionId: number
29
+ /** Unix milliseconds at delivery. */
30
+ readonly timestampMs: number
31
+ /** Native destination transition duration, or null when unknown. */
32
+ readonly durationMs: number | null
33
+ }
34
+
35
+ export interface SoftwareKeyboardController {
36
+ /** Activate shared native tracking until release. Observation starts only
37
+ * when a window has readers. Repeated activations share ownership. */
38
+ activate(): () => void
39
+ /** Read available cached state without activating observation. */
40
+ getSnapshot(host: object): KeyboardSnapshot
41
+ /** Observe destination/lifecycle changes, excluding per-frame movement. */
42
+ subscribe(host: object, listener: (snapshot: KeyboardSnapshot) => void): () => void
43
+ /** Observe measured movement directly; callbacks must not render per frame. */
44
+ onMove(host: object, listener: (snapshot: KeyboardSnapshot) => void): () => void
45
+ }
@@ -0,0 +1,7 @@
1
+ /** @internal A consumer-owned native viewport policy, separate from observation. */
2
+ export interface KeyboardViewportLease {
3
+ readonly supported: boolean
4
+ /** Follow replacement native views belonging to the same framework host. */
5
+ update(): void
6
+ release(): void
7
+ }