@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,238 @@
1
+ import { leaseKeyboardProperty } from '@octane-xplat/platform/keyboard-property'
2
+ import type { KeyboardPropertyLease } from '@octane-xplat/platform/keyboard-property'
3
+ import type { KeyboardViewportLease } from './viewport-types'
4
+
5
+ type Entry = {
6
+ owner: any
7
+ native: any
8
+ previous: string
9
+ count: number
10
+ padding: KeyboardPropertyLease[]
11
+ listener: (event: any) => void
12
+ }
13
+
14
+ const entries = new WeakMap<object, Entry>()
15
+ const ownedPolicy = 'dont-apply'
16
+ const noLease = (supported: boolean): KeyboardViewportLease => ({
17
+ supported,
18
+ update() {},
19
+ release() {},
20
+ })
21
+
22
+ const nativeFor = (view: any) => view?.nativeViewProtected ?? view?.android
23
+ const supportsPadding = (native: any) =>
24
+ native && typeof native.getEdgeInsets === 'function' && typeof native.getImeInsets === 'function'
25
+
26
+ const paddingOf = (native: any): number[] => {
27
+ const edges = native.getEdgeInsets(),
28
+ ime = native.getImeInsets()
29
+ // Own base padding, not the automatically applied strip. This remains valid
30
+ // if another owner changes the overflow policy while the lease is active.
31
+ return [
32
+ native.getPaddingLeft() - edges.left,
33
+ native.getPaddingTop() - edges.top,
34
+ native.getPaddingRight() - edges.right,
35
+ native.getPaddingBottom() - Math.max(edges.bottom, ime.bottom),
36
+ ]
37
+ }
38
+
39
+ const paddingLeases = (native: any) =>
40
+ [0, 1, 2, 3].map((index) =>
41
+ leaseKeyboardProperty(
42
+ () => paddingOf(native)[index],
43
+ (value) => {
44
+ const values = paddingOf(native)
45
+ values[index] = value
46
+ native.setPadding(...values)
47
+ },
48
+ ),
49
+ )
50
+
51
+ const applyBars = (entry: Entry, inset: any) => {
52
+ const cutout = entry.previous.includes('cutout')
53
+ const values = [inset.left, inset.top, inset.right, inset.bottom]
54
+ const cuts = [
55
+ inset.cutoutLeft ?? 0,
56
+ inset.cutoutTop ?? 0,
57
+ inset.cutoutRight ?? 0,
58
+ inset.cutoutBottom ?? 0,
59
+ ]
60
+ entry.padding.forEach((lease, index) =>
61
+ lease.set(Math.max(0, values[index] ?? 0, cutout ? cuts[index] : 0)),
62
+ )
63
+ }
64
+
65
+ const requestInsets = (native: any) =>
66
+ (globalThis as any).androidx?.core?.view?.ViewCompat?.requestApplyInsets(native)
67
+
68
+ const seedBars = (entry: Entry) => {
69
+ const core = (globalThis as any).androidx?.core?.view
70
+ const insets = core?.ViewCompat?.getRootWindowInsets(entry.native)
71
+ const type = core?.WindowInsetsCompat?.Type
72
+ if (!insets || !type) {
73
+ return
74
+ }
75
+ const bars = insets.getInsets(type.systemBars())
76
+ const cutout = type.displayCutout ? insets.getInsets(type.displayCutout()) : undefined
77
+ applyBars(entry, {
78
+ left: bars.left,
79
+ top: bars.top,
80
+ right: bars.right,
81
+ bottom: bars.bottom,
82
+ cutoutLeft: cutout?.left,
83
+ cutoutTop: cutout?.top,
84
+ cutoutRight: cutout?.right,
85
+ cutoutBottom: cutout?.bottom,
86
+ })
87
+ }
88
+
89
+ const update = (entry: Entry, request = true) => {
90
+ if (entry.owner.androidOverflowEdge !== ownedPolicy) {
91
+ return
92
+ }
93
+ const native = nativeFor(entry.owner)
94
+ if (!supportsPadding(native) || entry.native === native) {
95
+ return
96
+ }
97
+ // A detached native view no longer owns the replacement's properties.
98
+ entry.padding.forEach((lease) => lease.release())
99
+ entry.native = native
100
+ entry.padding = paddingLeases(native)
101
+ if (request) {
102
+ seedBars(entry)
103
+ requestInsets(native)
104
+ }
105
+ }
106
+
107
+ /** Lease NativeScript's public inset-distribution event. Its default Page
108
+ * pads by max(navigation, IME) independently of softInputMode. We retain bars
109
+ * and consume IME here so consumers can follow measured frames themselves.
110
+ * No listener on the Activity/decor is replaced. @internal */
111
+ export function leaseKeyboardViewport(host: any): KeyboardViewportLease {
112
+ const chain: any[] = []
113
+ for (let view = host; view; view = view.parent) {
114
+ chain.push(view)
115
+ }
116
+ chain.reverse()
117
+ let owner: any
118
+ for (const view of chain) {
119
+ if (entries.has(view)) {
120
+ owner = view
121
+ break
122
+ }
123
+
124
+ const policy = view.androidOverflowEdge
125
+ if (policy === 'none' || policy === 'none,cutout') {
126
+ owner = view
127
+ break
128
+ }
129
+
130
+ if (policy && policy !== 'ignore') {
131
+ return noLease(false)
132
+ }
133
+ }
134
+ if (!owner) {
135
+ return noLease(!!host?.page)
136
+ }
137
+ let entry = entries.get(owner)
138
+ if (!entry) {
139
+ const native = nativeFor(owner)
140
+ if (
141
+ !supportsPadding(native) ||
142
+ typeof native.setPadding !== 'function' ||
143
+ owner.hasListeners?.('androidOverflowInset')
144
+ ) {
145
+ return noLease(false)
146
+ }
147
+
148
+ const previous = owner.androidOverflowEdge
149
+ // Clear the old automatically applied strip before capturing base padding.
150
+ owner.androidOverflowEdge = 'ignore'
151
+ entry = {
152
+ owner,
153
+ native,
154
+ previous,
155
+ count: 0,
156
+ padding: paddingLeases(native),
157
+ listener: () => {},
158
+ }
159
+
160
+ const own = entry
161
+ own.listener = (event) => {
162
+ if (own.owner.androidOverflowEdge !== ownedPolicy || !event.inset) {
163
+ return
164
+ }
165
+ update(own, false)
166
+ if (own.native !== nativeFor(own.owner)) {
167
+ return
168
+ }
169
+ const inset = event.inset
170
+ applyBars(own, inset)
171
+ inset.leftConsumed = inset.topConsumed = inset.rightConsumed = inset.bottomConsumed = true
172
+ inset.imeBottom = 0
173
+ inset.imeBottomConsumed = true
174
+ if (own.previous.includes('cutout')) {
175
+ inset.cutoutLeftConsumed =
176
+ inset.cutoutTopConsumed =
177
+ inset.cutoutRightConsumed =
178
+ inset.cutoutBottomConsumed =
179
+ true
180
+ }
181
+ }
182
+
183
+ entries.set(owner, own)
184
+ try {
185
+ owner.on('androidOverflowInset', own.listener)
186
+ owner.androidOverflowEdge = ownedPolicy
187
+ seedBars(own)
188
+ requestInsets(native)
189
+ } catch (error) {
190
+ entries.delete(owner)
191
+ owner.off('androidOverflowInset', own.listener)
192
+ if (owner.androidOverflowEdge === ownedPolicy || owner.androidOverflowEdge === 'ignore') {
193
+ own.padding.forEach((lease) => lease.release())
194
+ owner.androidOverflowEdge = previous
195
+ }
196
+
197
+ throw error
198
+ }
199
+ }
200
+ const own = entry
201
+ own.count++
202
+ let active = true
203
+ return {
204
+ get supported() {
205
+ return (
206
+ active &&
207
+ own.owner.androidOverflowEdge === ownedPolicy &&
208
+ own.native === nativeFor(own.owner)
209
+ )
210
+ },
211
+ update() {
212
+ if (active) {
213
+ update(own)
214
+ }
215
+ },
216
+ release() {
217
+ if (!active) {
218
+ return
219
+ }
220
+ active = false
221
+ if (--own.count) {
222
+ return
223
+ }
224
+ entries.delete(own.owner)
225
+ own.owner.off('androidOverflowInset', own.listener)
226
+ if (own.native === nativeFor(own.owner)) {
227
+ own.padding.forEach((lease) => lease.release())
228
+ }
229
+ if (own.owner.androidOverflowEdge === ownedPolicy) {
230
+ own.owner.androidOverflowEdge = own.previous
231
+ const native = nativeFor(own.owner)
232
+ if (native) {
233
+ requestInsets(native)
234
+ }
235
+ }
236
+ },
237
+ }
238
+ }
@@ -0,0 +1,6 @@
1
+ import type { KeyboardViewportLease } from './viewport-types'
2
+
3
+ /** @internal UIKit's owning-window keyboard guide needs no ancestor padding lease. */
4
+ export function leaseKeyboardViewport(_host: object): KeyboardViewportLease {
5
+ return { supported: true, update() {}, release() {} }
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { KeyboardViewportLease } from './viewport-types'
2
+
3
+ /** @internal Unsupported software-keyboard targets retain their native viewport. */
4
+ export function leaseKeyboardViewport(_host: object): KeyboardViewportLease {
5
+ return { supported: false, update() {}, release() {} }
6
+ }
@@ -0,0 +1,17 @@
1
+ import type { FocusedInputState } from '@octane-xplat/platform/focused-input-state';
2
+ /** Focused input geometry in device-independent window coordinates. Identity
3
+ * and scroll owners are opaque native/DOM handles. No text contents are read.
4
+ * Unsupported targets return null geometry and false capabilities. */
5
+ export type FocusedInputSnapshot = FocusedInputState;
6
+ /** A vertical scroll viewport containing the input, nearest owner first. */
7
+ export type { FocusedInputScrollOwner } from '@octane-xplat/platform/focused-input-state';
8
+ /** Separate focus/selection geometry from software-keyboard movement. */
9
+ export interface FocusedInputService {
10
+ /** Cached read; does not start observation. */
11
+ getSnapshot(host: object): FocusedInputSnapshot;
12
+ /** Observe identity, selection and layout changes; release on host removal.
13
+ * Geometry callbacks can occur each frame during native layout movement. */
14
+ subscribe(host: object, listener: (snapshot: FocusedInputSnapshot) => void): () => void;
15
+ /** Refresh an actively observed host. No-op without a reader. */
16
+ recheck(host: object): void;
17
+ }
@@ -0,0 +1,20 @@
1
+ import type { UniversalComponent } from 'octane/universal'
2
+ import type { KeyboardProviderProps } from './props.js'
3
+ import type { SoftwareKeyboardController } from './types.js'
4
+ import type { FocusedInputService } from './focus-types.js'
5
+
6
+ export type * from './types.js'
7
+ export type * from './props.js'
8
+ export type * from './focus-types.js'
9
+
10
+ /** Share native tracking across roots and existing UI keyboard consumers. */
11
+ export declare const KeyboardProvider: UniversalComponent<KeyboardProviderProps>
12
+ export declare const KeyboardController: SoftwareKeyboardController
13
+ /** Focus/selection geometry without collecting text contents. */
14
+ export declare const FocusedInputController: FocusedInputService
15
+
16
+ export type * from './scroll-types.js'
17
+ export declare const KeyboardScrollController: import('./scroll-types.js').KeyboardScrollService
18
+ export declare const KeyboardAwareScrollView: UniversalComponent<import('./props.js').KeyboardAwareScrollViewProps>
19
+
20
+ export declare const KeyboardStickyView: UniversalComponent<import('./props.js').KeyboardStickyViewProps>
@@ -0,0 +1,35 @@
1
+ import type { KeyboardAvoidingProps, ViewProps } from '@octane-xplat/ui';
2
+ /** Shared tracking for existing keyboard consumers. Native observers start with
3
+ * the first window reader and stop with the last. Web/desktop are neutral.
4
+ * @example
5
+ * ```tsx
6
+ * import { KeyboardProvider } from '@octane-xplat/keyboard'
7
+ * import { KeyboardAvoiding, TextInput } from '@octane-xplat/ui'
8
+ * <KeyboardProvider><KeyboardAvoiding><TextInput /></KeyboardAvoiding></KeyboardProvider>
9
+ * ```
10
+ */
11
+ export interface KeyboardProviderProps {
12
+ children?: unknown;
13
+ /** Activate native tracking; false retains children and fallback behavior. @default true */
14
+ enabled?: boolean;
15
+ }
16
+ /** A vertical form that exposes the focused field/caret while the keyboard moves. */
17
+ export interface KeyboardAwareScrollViewProps extends Omit<KeyboardAvoidingProps, 'ref'> {
18
+ /** Minimum visible gap; does not create scroll range. @default 8 */
19
+ clearance?: number;
20
+ /** Additional bottom scroll range while the keyboard is open. @default 0 */
21
+ extraScrollSpace?: number;
22
+ manualScroll?: 'pause' | 'follow';
23
+ keyboardClose?: 'preserve' | 'restore';
24
+ /** Mounted visibility binding; null on cleanup. */
25
+ ref?: (binding: import('./scroll-types').KeyboardScrollBinding | null) => void;
26
+ }
27
+ /** Translate a control independently of surrounding layout as the native keyboard moves. */
28
+ export interface KeyboardStickyViewProps extends ViewProps {
29
+ /** Disabled keeps its ordinary layout and releases native movement. @default true */
30
+ enabled?: boolean;
31
+ /** Additional upward offset while open, in dips. @default 0 */
32
+ openOffset?: number;
33
+ /** Upward offset while closed, in dips. @default 0 */
34
+ closedOffset?: number;
35
+ }
@@ -0,0 +1,22 @@
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
+ export interface KeyboardScrollBinding {
11
+ /** Explicitly resume visibility enforcement after validation or manual scrolling. */
12
+ recheck(): void;
13
+ /** Apply changed options without restarting window observation. */
14
+ update(options: KeyboardScrollOptions): void;
15
+ /** Release observation; preserves the current scroll position. */
16
+ release(): void;
17
+ }
18
+ export interface KeyboardScrollService {
19
+ /** Attach to a vertical scroll viewport. Only scroll ancestors through this
20
+ * viewport are eligible; unrelated fields in the same window are ignored. */
21
+ bind(host: object, options?: KeyboardScrollOptions): KeyboardScrollBinding;
22
+ }
@@ -0,0 +1,40 @@
1
+ import type { KeyboardRect } from '@octane-xplat/platform/keyboard-geometry';
2
+ export type { KeyboardRect } from '@octane-xplat/platform/keyboard-geometry';
3
+ /** Lifecycle of one keyboard transition. An interrupted transition cancels
4
+ * before the replacement starts; current geometry never jumps to its target. */
5
+ export type KeyboardPhase = 'idle' | 'start' | 'move' | 'end' | 'cancel';
6
+ /** Capabilities belong to the actual host window and active adapter. */
7
+ export interface KeyboardCapabilities {
8
+ readonly geometry: boolean;
9
+ readonly nativeFrames: boolean;
10
+ readonly interactive: boolean;
11
+ readonly floating: boolean;
12
+ }
13
+ /** Geometry is in device-independent coordinates in the owning window.
14
+ * Null frames are unavailable; source distinguishes unknown from measured hidden.
15
+ * A destination frame is never presented as measured intermediate movement. */
16
+ export interface KeyboardSnapshot {
17
+ readonly inset: number;
18
+ readonly windowRect: KeyboardRect | null;
19
+ readonly currentFrame: KeyboardRect | null;
20
+ readonly targetFrame: KeyboardRect | null;
21
+ readonly source: 'unknown' | 'destination' | 'native-frames';
22
+ readonly capabilities: KeyboardCapabilities;
23
+ readonly phase: KeyboardPhase;
24
+ readonly transitionId: number;
25
+ /** Unix milliseconds at delivery. */
26
+ readonly timestampMs: number;
27
+ /** Native destination transition duration, or null when unknown. */
28
+ readonly durationMs: number | null;
29
+ }
30
+ export interface SoftwareKeyboardController {
31
+ /** Activate shared native tracking until release. Observation starts only
32
+ * when a window has readers. Repeated activations share ownership. */
33
+ activate(): () => void;
34
+ /** Read available cached state without activating observation. */
35
+ getSnapshot(host: object): KeyboardSnapshot;
36
+ /** Observe destination/lifecycle changes, excluding per-frame movement. */
37
+ subscribe(host: object, listener: (snapshot: KeyboardSnapshot) => void): () => void;
38
+ /** Observe measured movement directly; callbacks must not render per frame. */
39
+ onMove(host: object, listener: (snapshot: KeyboardSnapshot) => void): () => void;
40
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- throw new Error('This is the keyboard bootstrap stub. Install a supported release instead.');