@flighthq/keyboard 0.1.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 ADDED
@@ -0,0 +1,105 @@
1
+ # @flighthq/keyboard
2
+
3
+ On-screen (soft) keyboard visibility, height, and frame over a swappable web/native backend.
4
+
5
+ `keyboard` is an **event** cell in the platform-integration suite. A `SoftKeyboard` is a plain entity of signals; you allocate it with `createSoftKeyboard()`, start delivery with `attachSoftKeyboard()`, and release it with `disposeSoftKeyboard()`. Snapshot reads (`getSoftKeyboardInfo`, `getSoftKeyboardHeight`) and the control functions (`showSoftKeyboard`, `hideSoftKeyboard`, resize-mode / accessory-bar / scroll-assist / style) are free functions that delegate to the active `SoftKeyboardBackend`. A web/DOM backend is lazily available, so every function works on the web; a native host (Electron, Tauri, Capacitor, a C/C++ shell) replaces it via `setSoftKeyboardBackend`.
6
+
7
+ Per-field input traits (input type, return-key label, auto-capitalize/correct, spell-check) are **not** here — they associate with a focused field and belong to `@flighthq/textinput`; this package owns the _global_ keyboard. Safe-area insets are owned by `@flighthq/device`.
8
+
9
+ ## Functions
10
+
11
+ | Function | Purpose |
12
+ | --- | --- |
13
+ | `attachSoftKeyboard(keyboard)` | Start delivering backend changes to the entity's signals. Idempotent — a prior subscription is torn down first. |
14
+ | `createSoftKeyboard()` | Allocate a `SoftKeyboard` with inert signals. |
15
+ | `createSoftKeyboardInfo()` | Allocate a zeroed `SoftKeyboardInfo` to pass as `out`. |
16
+ | `createSoftKeyboardTransition()` | Allocate a zeroed `SoftKeyboardTransition` (`durationSeconds: 0`, `height: 0`). |
17
+ | `createWebSoftKeyboardBackend()` | Build the default web backend. |
18
+ | `detachSoftKeyboard(keyboard)` | Stop delivery and forget the subscription. Safe when not attached. |
19
+ | `disposeSoftKeyboard(keyboard)` | Detach the subscription so the entity is GC-eligible. |
20
+ | `getSoftKeyboardBackend()` | Return the active backend, lazily creating the web default. There is always a backend. |
21
+ | `getSoftKeyboardHeight()` | Return the current keyboard height in CSS pixels (`0` when hidden), no allocation. |
22
+ | `getSoftKeyboardInfo(out)` | Fill `out` with the current keyboard snapshot and return it. |
23
+ | `getSoftKeyboardResizeMode()` | Return the backend resize mode, or `SoftKeyboardResizeNoneKind` when unsupported. |
24
+ | `hideSoftKeyboard()` | Request dismissal. No-op on web unless the Chromium VirtualKeyboard API is available. |
25
+ | `isSoftKeyboardAccessoryBarVisible()` | Whether the iOS accessory bar is visible. `false` when unsupported. |
26
+ | `isSoftKeyboardScrollAssistEnabled()` | Whether scroll-assist is enabled. `false` when unsupported. |
27
+ | `setSoftKeyboardAccessoryBarVisible(visible)` | Show/hide the iOS accessory bar. No-op when unsupported. |
28
+ | `setSoftKeyboardBackend(backend)` | Install a native host backend; pass `null` to fall back to the web default. |
29
+ | `setSoftKeyboardResizeMode(mode)` | Set how the viewport reacts to the keyboard. No-op when unsupported. |
30
+ | `setSoftKeyboardScrollAssistEnabled(enabled)` | Enable/disable scroll-assist. No-op when unsupported. |
31
+ | `setSoftKeyboardStyle(style)` | Set the keyboard's light/dark appearance. No-op when unsupported. |
32
+ | `showSoftKeyboard()` | Request presentation. No-op on web unless the Chromium VirtualKeyboard API is available. |
33
+
34
+ ## Signals
35
+
36
+ `attachSoftKeyboard` drives nine signals on a `SoftKeyboard`. The **will** signals fire before the animation begins and carry a `SoftKeyboardTransition` (timing + target height); the **did** signals fire after it settles and carry the settled height. The simple-path aliases (`onShow` / `onHide` / `onResize`) fire alongside their did-phase counterparts.
37
+
38
+ | Signal | Phase | Payload | Fires on |
39
+ | -------------- | ----- | ------------------------ | ---------------------------------------------------------- |
40
+ | `onWillShow` | will | `SoftKeyboardTransition` | keyboard about to appear (hidden → visible) |
41
+ | `onWillHide` | will | `SoftKeyboardTransition` | keyboard about to disappear (visible → hidden) |
42
+ | `onWillResize` | will | `SoftKeyboardTransition` | height about to change while staying visible |
43
+ | `onDidShow` | did | `height: number` | keyboard finished appearing |
44
+ | `onDidHide` | did | _(none)_ | keyboard finished disappearing |
45
+ | `onDidResize` | did | `height: number` | keyboard settled at a new height (fires on every did edge) |
46
+ | `onShow` | did | `height: number` | alias firing with `onDidShow` |
47
+ | `onHide` | did | _(none)_ | alias firing with `onDidHide` |
48
+ | `onResize` | did | `height: number` | alias firing with `onDidResize` |
49
+
50
+ The web default reports only the settled **did** phase (`durationSeconds: 0`); a native host that knows its animation timing emits the **will** phase first, so an app can animate in lockstep with the slide.
51
+
52
+ ## `SoftKeyboardInfo` fields
53
+
54
+ | Field | Type | Unit | Hidden value |
55
+ | --------- | --------- | ------------------------- | ------------ |
56
+ | `visible` | `boolean` | — | `false` |
57
+ | `height` | `number` | CSS pixels | `0` |
58
+ | `x` | `number` | CSS pixels (frame origin) | `0` |
59
+ | `y` | `number` | CSS pixels (frame origin) | `0` |
60
+ | `width` | `number` | CSS pixels (frame width) | `0` |
61
+
62
+ The web backend prefers the Chromium VirtualKeyboard API's `boundingRect` for a precise frame; it otherwise infers `height` from the `window.visualViewport` shrink relative to `window.innerHeight`, reporting `y` as the viewport height and `width` as `window.innerWidth`. Where neither is present (no `window`, no `visualViewport`, no `navigator.virtualKeyboard`) it degrades to a hidden snapshot.
63
+
64
+ ## Resize-mode / style matrix
65
+
66
+ These are optional backend capabilities. Each kind is an **open** string contract — a native host may register its own vendor-prefixed value (`'acme.custom'`).
67
+
68
+ | Concern | Kind constants | Web default |
69
+ | --- | --- | --- |
70
+ | Resize mode | `SoftKeyboardResizeNoneKind` (`'None'`), `SoftKeyboardResizeBodyKind` (`'Body'`) | unsupported — `getSoftKeyboardResizeMode()` returns `None`, `setSoftKeyboardResizeMode` is a no-op |
71
+ | Style | `SoftKeyboardStyleDefaultKind` (`'Default'`), `SoftKeyboardStyleDarkKind` (`'Dark'`) | unsupported — `setSoftKeyboardStyle` is a no-op |
72
+ | Accessory bar (iOS) | — | unsupported — query returns `false`, setter is a no-op |
73
+ | Scroll-assist | — | unsupported — query returns `false`, setter is a no-op |
74
+
75
+ ## Keyboard-aware layout recipe
76
+
77
+ Animate your content in sync with the keyboard slide by listening on the **will** phase and using the transition's `durationSeconds` and target `height`. Fall back to the **did** phase for backends (the web default) that report only the settled state.
78
+
79
+ ```ts
80
+ import { attachSoftKeyboard, createSoftKeyboard, disposeSoftKeyboard } from '@flighthq/keyboard';
81
+ import { connectSignal } from '@flighthq/signals';
82
+
83
+ const keyboard = createSoftKeyboard();
84
+
85
+ // Will phase: a native host that knows its timing lets you start the matching animation early.
86
+ connectSignal(keyboard.onWillShow, (transition) => {
87
+ // transition.height is the settled keyboard height; transition.durationSeconds is the slide time.
88
+ animateContentInset(transition.height, transition.durationSeconds);
89
+ });
90
+ connectSignal(keyboard.onWillHide, (transition) => {
91
+ animateContentInset(0, transition.durationSeconds);
92
+ });
93
+
94
+ // Did phase / web fallback: snap to the settled height (durationSeconds is 0 here).
95
+ connectSignal(keyboard.onDidResize, (height) => {
96
+ animateContentInset(height, 0);
97
+ });
98
+
99
+ attachSoftKeyboard(keyboard);
100
+
101
+ // Later, when the surface is torn down:
102
+ disposeSoftKeyboard(keyboard);
103
+ ```
104
+
105
+ `durationSeconds` is `0` on the web default, so an app that wants a uniform feel can supply its own duration there and reserve the reported duration for native hosts that emit the will phase. Easing the animation is left to the app (or a future `@flighthq/easing` extension of this recipe); this package reports timing and geometry only.
@@ -0,0 +1,2 @@
1
+ export * from './keyboard';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './keyboard';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { SoftKeyboard, SoftKeyboardBackend, SoftKeyboardInfo, SoftKeyboardResizeMode, SoftKeyboardStyleKind, SoftKeyboardTransition } from '@flighthq/types';
2
+ export declare function attachSoftKeyboard(keyboard: SoftKeyboard): void;
3
+ export declare function createSoftKeyboard(): SoftKeyboard;
4
+ export declare function createSoftKeyboardInfo(): SoftKeyboardInfo;
5
+ export declare function createSoftKeyboardTransition(): SoftKeyboardTransition;
6
+ export declare function createWebSoftKeyboardBackend(): SoftKeyboardBackend;
7
+ export declare function detachSoftKeyboard(keyboard: SoftKeyboard): void;
8
+ export declare function disposeSoftKeyboard(keyboard: SoftKeyboard): void;
9
+ export declare function getSoftKeyboardBackend(): SoftKeyboardBackend;
10
+ export declare function getSoftKeyboardHeight(): number;
11
+ export declare function getSoftKeyboardInfo(out: SoftKeyboardInfo): SoftKeyboardInfo;
12
+ export declare function getSoftKeyboardResizeMode(): SoftKeyboardResizeMode;
13
+ export declare function hideSoftKeyboard(): void;
14
+ export declare function isSoftKeyboardAccessoryBarVisible(): boolean;
15
+ export declare function isSoftKeyboardScrollAssistEnabled(): boolean;
16
+ export declare function setSoftKeyboardAccessoryBarVisible(visible: boolean): void;
17
+ export declare function setSoftKeyboardBackend(backend: SoftKeyboardBackend | null): void;
18
+ export declare function setSoftKeyboardResizeMode(mode: SoftKeyboardResizeMode): void;
19
+ export declare function setSoftKeyboardScrollAssistEnabled(enabled: boolean): void;
20
+ export declare function setSoftKeyboardStyle(style: SoftKeyboardStyleKind): void;
21
+ export declare function showSoftKeyboard(): void;
22
+ //# sourceMappingURL=keyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../src/keyboard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAEhB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,iBAAiB,CAAC;AAQzB,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAkC/D;AAGD,wBAAgB,kBAAkB,IAAI,YAAY,CAYjD;AAGD,wBAAgB,sBAAsB,IAAI,gBAAgB,CAEzD;AAGD,wBAAgB,4BAA4B,IAAI,sBAAsB,CAErE;AAQD,wBAAgB,4BAA4B,IAAI,mBAAmB,CAkDlE;AAGD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAM/D;AAID,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAEhE;AAGD,wBAAgB,sBAAsB,IAAI,mBAAmB,CAG5D;AAGD,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAGD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,CAE3E;AAID,wBAAgB,yBAAyB,IAAI,sBAAsB,CAGlE;AAID,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAID,wBAAgB,iCAAiC,IAAI,OAAO,CAE3D;AAGD,wBAAgB,iCAAiC,IAAI,OAAO,CAE3D;AAID,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAEzE;AAGD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAEhF;AAID,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,sBAAsB,GAAG,IAAI,CAE5E;AAID,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAEzE;AAID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,qBAAqB,GAAG,IAAI,CAEvE;AAID,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
@@ -0,0 +1,232 @@
1
+ import { createSignal, emitSignal } from '@flighthq/signals';
2
+ import { SoftKeyboardResizeNoneKind } from '@flighthq/types';
3
+ // Begins delivering on-screen keyboard changes to `keyboard`'s signals by subscribing to the active
4
+ // backend. On each change it reads fresh info, emits will/did signal pairs (with onShow/onHide/onResize
5
+ // aliases firing alongside the did-phase edge), and tracks visibility-edge transitions for
6
+ // onWillShow/onWillHide and onDidShow/onDidHide pairs. Idempotent: a prior subscription is torn
7
+ // down first. Pair with detachSoftKeyboard/disposeSoftKeyboard.
8
+ export function attachSoftKeyboard(keyboard) {
9
+ detachSoftKeyboard(keyboard);
10
+ const backend = getSoftKeyboardBackend();
11
+ let wasVisible = backend.getInfo(_scratch).visible;
12
+ const unsubscribe = backend.subscribe((phase, transition) => {
13
+ const prevVisible = wasVisible;
14
+ const info = backend.getInfo(_scratch);
15
+ const nowVisible = info.visible;
16
+ if (phase === 'will') {
17
+ // Will-phase: emit will-signals before animation begins.
18
+ if (nowVisible && !prevVisible) {
19
+ emitSignal(keyboard.onWillShow, transition);
20
+ }
21
+ else if (!nowVisible && prevVisible) {
22
+ emitSignal(keyboard.onWillHide, transition);
23
+ }
24
+ else {
25
+ emitSignal(keyboard.onWillResize, transition);
26
+ }
27
+ }
28
+ else {
29
+ // Did-phase: emit did-signals (and simple-path aliases) after animation ends.
30
+ emitSignal(keyboard.onDidResize, info.height);
31
+ emitSignal(keyboard.onResize, info.height);
32
+ if (nowVisible !== prevVisible) {
33
+ wasVisible = nowVisible;
34
+ if (nowVisible) {
35
+ emitSignal(keyboard.onDidShow, info.height);
36
+ emitSignal(keyboard.onShow, info.height);
37
+ }
38
+ else {
39
+ emitSignal(keyboard.onDidHide);
40
+ emitSignal(keyboard.onHide);
41
+ }
42
+ }
43
+ }
44
+ });
45
+ _subscriptions.set(keyboard, unsubscribe);
46
+ }
47
+ // Allocates a SoftKeyboard event entity with inert signals; call attachSoftKeyboard to start delivery.
48
+ export function createSoftKeyboard() {
49
+ return {
50
+ onShow: createSignal(),
51
+ onHide: createSignal(),
52
+ onResize: createSignal(),
53
+ onWillShow: createSignal(),
54
+ onWillHide: createSignal(),
55
+ onWillResize: createSignal(),
56
+ onDidShow: createSignal(),
57
+ onDidHide: createSignal(),
58
+ onDidResize: createSignal(),
59
+ };
60
+ }
61
+ // Allocates a zeroed SoftKeyboardInfo, suitable as the `out` for getSoftKeyboardInfo.
62
+ export function createSoftKeyboardInfo() {
63
+ return { visible: false, height: 0, x: 0, y: 0, width: 0 };
64
+ }
65
+ // Allocates a zeroed SoftKeyboardTransition with durationSeconds 0 and height 0.
66
+ export function createSoftKeyboardTransition() {
67
+ return { durationSeconds: 0, height: 0 };
68
+ }
69
+ // Builds the default web backend over window.visualViewport, inferring keyboard height from the
70
+ // viewport shrink relative to window.innerHeight. Also uses the Chromium VirtualKeyboard API
71
+ // (navigator.virtualKeyboard) when available: geometry comes from geometrychange, and
72
+ // showSoftKeyboard/hideSoftKeyboard become real operations instead of no-ops.
73
+ // Degrades to height 0 and a no-op subscription where the API is absent. All did-phase only
74
+ // (durationSeconds: 0); native backends may emit will-phase with timing.
75
+ export function createWebSoftKeyboardBackend() {
76
+ return {
77
+ getInfo(out) {
78
+ const geo = getWebKeyboardGeometry();
79
+ out.height = geo.height;
80
+ out.visible = geo.height > 0;
81
+ out.x = geo.x;
82
+ out.y = geo.y;
83
+ out.width = geo.width;
84
+ return out;
85
+ },
86
+ subscribe(listener) {
87
+ if (typeof window === 'undefined')
88
+ return () => { };
89
+ // transition.height stays 0 on web: this backend fires the 'did' phase only, where consumers
90
+ // read the settled geometry from getInfo(); the will-phase target height that transition.height
91
+ // is meant to carry is animation metadata the web viewport APIs never expose. Native hosts that
92
+ // emit a real 'will' phase must populate transition.height with the incoming keyboard height.
93
+ const transition = { durationSeconds: 0, height: 0 };
94
+ const fire = () => listener('did', transition);
95
+ // Use Chromium VirtualKeyboard API when available for geometry.
96
+ const virtualKeyboard = getVirtualKeyboard();
97
+ if (virtualKeyboard !== null) {
98
+ virtualKeyboard.addEventListener('geometrychange', fire);
99
+ return () => virtualKeyboard.removeEventListener('geometrychange', fire);
100
+ }
101
+ // Fall back to visualViewport resize/scroll.
102
+ const viewport = window.visualViewport;
103
+ if (viewport === undefined || viewport === null)
104
+ return () => { };
105
+ viewport.addEventListener('resize', fire);
106
+ viewport.addEventListener('scroll', fire);
107
+ return () => {
108
+ viewport.removeEventListener('resize', fire);
109
+ viewport.removeEventListener('scroll', fire);
110
+ };
111
+ },
112
+ show() {
113
+ const vk = getVirtualKeyboard();
114
+ if (vk !== null) {
115
+ vk.show();
116
+ }
117
+ // No-op on web without VirtualKeyboard API; the soft keyboard only opens when a field is focused.
118
+ },
119
+ hide() {
120
+ const vk = getVirtualKeyboard();
121
+ if (vk !== null) {
122
+ vk.hide();
123
+ }
124
+ // No-op on web without VirtualKeyboard API.
125
+ },
126
+ };
127
+ }
128
+ // Stops delivery to `keyboard` and forgets its subscription. Safe to call when not attached.
129
+ export function detachSoftKeyboard(keyboard) {
130
+ const unsubscribe = _subscriptions.get(keyboard);
131
+ if (unsubscribe !== undefined) {
132
+ unsubscribe();
133
+ _subscriptions.delete(keyboard);
134
+ }
135
+ }
136
+ // Releases `keyboard` for garbage collection by detaching its backend subscription. The signals
137
+ // remain plain GC-managed memory afterward.
138
+ export function disposeSoftKeyboard(keyboard) {
139
+ detachSoftKeyboard(keyboard);
140
+ }
141
+ // The active soft keyboard backend, or a lazily-created web default. There is always a backend.
142
+ export function getSoftKeyboardBackend() {
143
+ if (_backend === null)
144
+ _backend = createWebSoftKeyboardBackend();
145
+ return _backend;
146
+ }
147
+ // Returns the current on-screen keyboard height in CSS pixels without allocating. 0 when hidden.
148
+ export function getSoftKeyboardHeight() {
149
+ return getSoftKeyboardBackend().getInfo(_scratch).height;
150
+ }
151
+ // Fills `out` with the current on-screen keyboard snapshot and returns it.
152
+ export function getSoftKeyboardInfo(out) {
153
+ return getSoftKeyboardBackend().getInfo(out);
154
+ }
155
+ // Returns the current keyboard resize mode. Delegates to the backend; returns
156
+ // SoftKeyboardResizeNoneKind when the backend does not support resize-mode queries.
157
+ export function getSoftKeyboardResizeMode() {
158
+ const backend = getSoftKeyboardBackend();
159
+ return backend.getResizeMode?.() ?? SoftKeyboardResizeNoneKind;
160
+ }
161
+ // Requests that the host dismiss the on-screen keyboard. A no-op on web unless the
162
+ // navigator.virtualKeyboard API is available (Chromium with virtualKeyboard policy).
163
+ export function hideSoftKeyboard() {
164
+ getSoftKeyboardBackend().hide();
165
+ }
166
+ // Returns whether the input accessory bar (iOS toolbar above the keyboard) is visible.
167
+ // Returns false when the backend does not support this query.
168
+ export function isSoftKeyboardAccessoryBarVisible() {
169
+ return getSoftKeyboardBackend().getAccessoryBarVisible?.() ?? false;
170
+ }
171
+ // Returns whether scroll-assist is enabled. Returns false when the backend does not support this.
172
+ export function isSoftKeyboardScrollAssistEnabled() {
173
+ return getSoftKeyboardBackend().getScrollAssistEnabled?.() ?? false;
174
+ }
175
+ // Controls whether the iOS input accessory bar (the toolbar above the keyboard) is visible.
176
+ // No-op when the backend does not support this.
177
+ export function setSoftKeyboardAccessoryBarVisible(visible) {
178
+ getSoftKeyboardBackend().setAccessoryBarVisible?.(visible);
179
+ }
180
+ // Installs a native host soft keyboard backend; pass null to fall back to the web default.
181
+ export function setSoftKeyboardBackend(backend) {
182
+ _backend = backend;
183
+ }
184
+ // Controls keyboard resize behavior — how the app viewport reacts when the keyboard appears.
185
+ // No-op when the backend does not support resize-mode control.
186
+ export function setSoftKeyboardResizeMode(mode) {
187
+ getSoftKeyboardBackend().setResizeMode?.(mode);
188
+ }
189
+ // Controls whether the keyboard scroll-assist feature is enabled (scrolls the focused field into
190
+ // view when the keyboard appears). No-op when the backend does not support this.
191
+ export function setSoftKeyboardScrollAssistEnabled(enabled) {
192
+ getSoftKeyboardBackend().setScrollAssistEnabled?.(enabled);
193
+ }
194
+ // Sets the visual style / appearance of the on-screen keyboard (iOS light/dark keyboard appearance).
195
+ // No-op on web and on backends that do not support style control.
196
+ export function setSoftKeyboardStyle(style) {
197
+ getSoftKeyboardBackend().setStyle?.(style);
198
+ }
199
+ // Requests that the host present the on-screen keyboard. A no-op on web unless the
200
+ // navigator.virtualKeyboard API is available (Chromium with virtualKeyboard policy).
201
+ export function showSoftKeyboard() {
202
+ getSoftKeyboardBackend().show();
203
+ }
204
+ let _backend = null;
205
+ const _scratch = createSoftKeyboardInfo();
206
+ const _subscriptions = new WeakMap();
207
+ function getVirtualKeyboard() {
208
+ if (typeof navigator === 'undefined')
209
+ return null;
210
+ const nav = navigator;
211
+ return nav.virtualKeyboard ?? null;
212
+ }
213
+ function getWebKeyboardGeometry() {
214
+ if (typeof window === 'undefined')
215
+ return { height: 0, width: 0, x: 0, y: 0 };
216
+ // Prefer the Chromium VirtualKeyboard API for precise geometry.
217
+ const vk = getVirtualKeyboard();
218
+ if (vk !== null) {
219
+ const rect = vk.boundingRect;
220
+ return { height: rect.height, width: rect.width, x: rect.x, y: rect.y };
221
+ }
222
+ // Fall back to inferring height from visualViewport shrink.
223
+ const viewport = window.visualViewport;
224
+ if (viewport === undefined || viewport === null)
225
+ return { height: 0, width: 0, x: 0, y: 0 };
226
+ const shrink = window.innerHeight - viewport.height;
227
+ const height = shrink > 0 ? shrink : 0;
228
+ const width = height > 0 ? window.innerWidth : 0;
229
+ const y = height > 0 ? viewport.height : 0;
230
+ return { height, width, x: 0, y };
231
+ }
232
+ //# sourceMappingURL=keyboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.js","sourceRoot":"","sources":["../src/keyboard.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/keyboard",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/signals": "0.1.0",
31
+ "@flighthq/types": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "On-screen (soft) keyboard visibility, height, and show/hide/resize signals over a swappable web/native backend",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,918 @@
1
+ import { cancelSignal, connectSignal } from '@flighthq/signals';
2
+ import type {
3
+ SoftKeyboardBackend,
4
+ SoftKeyboardInfo,
5
+ SoftKeyboardPhase,
6
+ SoftKeyboardResizeMode,
7
+ SoftKeyboardTransition,
8
+ } from '@flighthq/types';
9
+ import {
10
+ SoftKeyboardResizeBodyKind,
11
+ SoftKeyboardResizeNoneKind,
12
+ SoftKeyboardStyleDarkKind,
13
+ SoftKeyboardStyleDefaultKind,
14
+ } from '@flighthq/types';
15
+
16
+ import {
17
+ attachSoftKeyboard,
18
+ createSoftKeyboard,
19
+ createSoftKeyboardInfo,
20
+ createSoftKeyboardTransition,
21
+ createWebSoftKeyboardBackend,
22
+ detachSoftKeyboard,
23
+ disposeSoftKeyboard,
24
+ getSoftKeyboardBackend,
25
+ getSoftKeyboardHeight,
26
+ getSoftKeyboardInfo,
27
+ getSoftKeyboardResizeMode,
28
+ hideSoftKeyboard,
29
+ isSoftKeyboardAccessoryBarVisible,
30
+ isSoftKeyboardScrollAssistEnabled,
31
+ setSoftKeyboardAccessoryBarVisible,
32
+ setSoftKeyboardBackend,
33
+ setSoftKeyboardResizeMode,
34
+ setSoftKeyboardScrollAssistEnabled,
35
+ setSoftKeyboardStyle,
36
+ showSoftKeyboard,
37
+ } from './keyboard';
38
+
39
+ type BackendListener = (phase: SoftKeyboardPhase, transition: Readonly<SoftKeyboardTransition>) => void;
40
+
41
+ function fakeBackend(): SoftKeyboardBackend & {
42
+ visible: boolean;
43
+ height: number;
44
+ shown: boolean;
45
+ hidden: boolean;
46
+ resizeMode: string;
47
+ accessoryBarVisible: boolean;
48
+ scrollAssistEnabled: boolean;
49
+ style: string;
50
+ fire(phase?: SoftKeyboardPhase, durationSeconds?: number): void;
51
+ } {
52
+ let listener: BackendListener | null = null;
53
+ return {
54
+ visible: false,
55
+ height: 0,
56
+ shown: false,
57
+ hidden: false,
58
+ resizeMode: SoftKeyboardResizeNoneKind,
59
+ accessoryBarVisible: false,
60
+ scrollAssistEnabled: false,
61
+ style: SoftKeyboardStyleDefaultKind,
62
+ getInfo(out) {
63
+ out.visible = this.visible;
64
+ out.height = this.height;
65
+ out.x = 0;
66
+ out.y = 0;
67
+ out.width = this.visible ? 375 : 0;
68
+ return out;
69
+ },
70
+ subscribe(l) {
71
+ listener = l;
72
+ return () => {
73
+ listener = null;
74
+ };
75
+ },
76
+ show() {
77
+ this.shown = true;
78
+ },
79
+ hide() {
80
+ this.hidden = true;
81
+ },
82
+ getResizeMode(): SoftKeyboardResizeMode {
83
+ return this.resizeMode as SoftKeyboardResizeMode;
84
+ },
85
+ setResizeMode(mode) {
86
+ this.resizeMode = mode;
87
+ },
88
+ setStyle(s) {
89
+ this.style = s;
90
+ },
91
+ getAccessoryBarVisible() {
92
+ return this.accessoryBarVisible;
93
+ },
94
+ setAccessoryBarVisible(v) {
95
+ this.accessoryBarVisible = v;
96
+ },
97
+ getScrollAssistEnabled() {
98
+ return this.scrollAssistEnabled;
99
+ },
100
+ setScrollAssistEnabled(v) {
101
+ this.scrollAssistEnabled = v;
102
+ },
103
+ fire(phase: SoftKeyboardPhase = 'did', durationSeconds = 0) {
104
+ listener?.call(null, phase, { durationSeconds, height: this.height });
105
+ },
106
+ };
107
+ }
108
+
109
+ type VirtualKeyboardStub = {
110
+ boundingRect: DOMRect;
111
+ addEventListener(type: string, fn: () => void): void;
112
+ removeEventListener(type: string, fn: () => void): void;
113
+ show(): void;
114
+ hide(): void;
115
+ };
116
+
117
+ // Overrides window.innerHeight / innerWidth for the duration of one test; callers restore via the
118
+ // surrounding stubVisualViewport restore (window metrics reset on the next jsdom test anyway).
119
+ function stubWindowMetrics(innerHeight: number, innerWidth: number): void {
120
+ Object.defineProperty(window, 'innerHeight', { value: innerHeight, configurable: true });
121
+ Object.defineProperty(window, 'innerWidth', { value: innerWidth, configurable: true });
122
+ }
123
+
124
+ // Installs (or clears, with null) window.visualViewport and returns a restore function.
125
+ function stubVisualViewport(viewport: Readonly<VisualViewport> | { height: number } | null): () => void {
126
+ const had = Object.getOwnPropertyDescriptor(window, 'visualViewport');
127
+ Object.defineProperty(window, 'visualViewport', { value: viewport, configurable: true });
128
+ return () => {
129
+ if (had !== undefined) Object.defineProperty(window, 'visualViewport', had);
130
+ else Object.defineProperty(window, 'visualViewport', { value: undefined, configurable: true });
131
+ };
132
+ }
133
+
134
+ // Installs navigator.virtualKeyboard (the Chromium API) and returns a restore function.
135
+ function stubVirtualKeyboard(vk: VirtualKeyboardStub): () => void {
136
+ const nav = navigator as Navigator & { virtualKeyboard?: VirtualKeyboardStub };
137
+ const had = nav.virtualKeyboard;
138
+ Object.defineProperty(nav, 'virtualKeyboard', { value: vk, configurable: true });
139
+ return () => {
140
+ if (had !== undefined) Object.defineProperty(nav, 'virtualKeyboard', { value: had, configurable: true });
141
+ else delete nav.virtualKeyboard;
142
+ };
143
+ }
144
+
145
+ afterEach(() => setSoftKeyboardBackend(null));
146
+
147
+ describe('attachSoftKeyboard', () => {
148
+ it('emits onResize and onShow when the keyboard becomes visible (did phase)', () => {
149
+ const backend = fakeBackend();
150
+ setSoftKeyboardBackend(backend);
151
+ const keyboard = createSoftKeyboard();
152
+ let resizes = 0;
153
+ let shows = 0;
154
+ connectSignal(keyboard.onResize, () => resizes++);
155
+ connectSignal(keyboard.onShow, () => shows++);
156
+ attachSoftKeyboard(keyboard);
157
+ backend.visible = true;
158
+ backend.height = 300;
159
+ backend.fire('did');
160
+ expect(resizes).toBe(1);
161
+ expect(shows).toBe(1);
162
+ });
163
+
164
+ it('emits onWillShow before the animation on will phase', () => {
165
+ const backend = fakeBackend();
166
+ setSoftKeyboardBackend(backend);
167
+ const keyboard = createSoftKeyboard();
168
+ const willTransitions: Readonly<SoftKeyboardTransition>[] = [];
169
+ connectSignal(keyboard.onWillShow, (t) => willTransitions.push(t));
170
+ attachSoftKeyboard(keyboard);
171
+ backend.visible = true;
172
+ backend.height = 300;
173
+ backend.fire('will', 0.25);
174
+ expect(willTransitions).toHaveLength(1);
175
+ expect(willTransitions[0].durationSeconds).toBe(0.25);
176
+ expect(willTransitions[0].height).toBe(300);
177
+ });
178
+
179
+ it('emits onWillHide on will phase when transitioning hidden', () => {
180
+ const backend = fakeBackend();
181
+ setSoftKeyboardBackend(backend);
182
+ const keyboard = createSoftKeyboard();
183
+ backend.visible = true;
184
+ backend.height = 300;
185
+ const willHides: Readonly<SoftKeyboardTransition>[] = [];
186
+ connectSignal(keyboard.onWillHide, (t) => willHides.push(t));
187
+ attachSoftKeyboard(keyboard);
188
+ // keyboard is currently visible, now hide
189
+ backend.visible = false;
190
+ backend.height = 0;
191
+ backend.fire('will', 0.3);
192
+ expect(willHides).toHaveLength(1);
193
+ });
194
+
195
+ it('emits onWillResize (not will show/hide) when visible stays the same', () => {
196
+ const backend = fakeBackend();
197
+ setSoftKeyboardBackend(backend);
198
+ const keyboard = createSoftKeyboard();
199
+ backend.visible = true;
200
+ backend.height = 300;
201
+ const willResizes: Readonly<SoftKeyboardTransition>[] = [];
202
+ const willShows: Readonly<SoftKeyboardTransition>[] = [];
203
+ connectSignal(keyboard.onWillResize, (t) => willResizes.push(t));
204
+ connectSignal(keyboard.onWillShow, (t) => willShows.push(t));
205
+ attachSoftKeyboard(keyboard);
206
+ // stay visible but change height
207
+ backend.height = 350;
208
+ backend.fire('will', 0.1);
209
+ expect(willResizes).toHaveLength(1);
210
+ expect(willShows).toHaveLength(0);
211
+ });
212
+
213
+ it('emits did aliases alongside simple-path aliases', () => {
214
+ const backend = fakeBackend();
215
+ setSoftKeyboardBackend(backend);
216
+ const keyboard = createSoftKeyboard();
217
+ let didShows = 0;
218
+ let onShows = 0;
219
+ connectSignal(keyboard.onDidShow, () => didShows++);
220
+ connectSignal(keyboard.onShow, () => onShows++);
221
+ attachSoftKeyboard(keyboard);
222
+ backend.visible = true;
223
+ backend.height = 300;
224
+ backend.fire('did');
225
+ expect(didShows).toBe(1);
226
+ expect(onShows).toBe(1);
227
+ });
228
+
229
+ it('emits onHide and onDidHide when keyboard hides', () => {
230
+ const backend = fakeBackend();
231
+ setSoftKeyboardBackend(backend);
232
+ const keyboard = createSoftKeyboard();
233
+ backend.visible = true;
234
+ backend.height = 300;
235
+ let hides = 0;
236
+ let didHides = 0;
237
+ connectSignal(keyboard.onHide, () => hides++);
238
+ connectSignal(keyboard.onDidHide, () => didHides++);
239
+ attachSoftKeyboard(keyboard);
240
+ backend.visible = false;
241
+ backend.height = 0;
242
+ backend.fire('did');
243
+ expect(hides).toBe(1);
244
+ expect(didHides).toBe(1);
245
+ });
246
+
247
+ it('is idempotent: prior subscription torn down on re-attach', () => {
248
+ const backend = fakeBackend();
249
+ setSoftKeyboardBackend(backend);
250
+ const keyboard = createSoftKeyboard();
251
+ let resizes = 0;
252
+ connectSignal(keyboard.onResize, () => resizes++);
253
+ attachSoftKeyboard(keyboard);
254
+ attachSoftKeyboard(keyboard);
255
+ backend.visible = true;
256
+ backend.height = 300;
257
+ backend.fire('did');
258
+ expect(resizes).toBe(1);
259
+ });
260
+
261
+ it('dispatches multiple listeners on a single did-show edge', () => {
262
+ const backend = fakeBackend();
263
+ setSoftKeyboardBackend(backend);
264
+ const keyboard = createSoftKeyboard();
265
+ let a = 0;
266
+ let b = 0;
267
+ let c = 0;
268
+ connectSignal(keyboard.onDidShow, () => a++);
269
+ connectSignal(keyboard.onDidShow, () => b++);
270
+ connectSignal(keyboard.onDidShow, () => c++);
271
+ attachSoftKeyboard(keyboard);
272
+ backend.visible = true;
273
+ backend.height = 300;
274
+ backend.fire('did');
275
+ expect([a, b, c]).toEqual([1, 1, 1]);
276
+ });
277
+
278
+ it('honors listener priority on the onWillShow edge', () => {
279
+ const backend = fakeBackend();
280
+ setSoftKeyboardBackend(backend);
281
+ const keyboard = createSoftKeyboard();
282
+ const order: string[] = [];
283
+ connectSignal(keyboard.onWillShow, () => order.push('low'), { priority: 0 });
284
+ connectSignal(keyboard.onWillShow, () => order.push('high'), { priority: 10 });
285
+ attachSoftKeyboard(keyboard);
286
+ backend.visible = true;
287
+ backend.height = 300;
288
+ backend.fire('will', 0.25);
289
+ expect(order).toEqual(['high', 'low']);
290
+ });
291
+
292
+ it('stops the onWillShow chain when an earlier listener cancels', () => {
293
+ const backend = fakeBackend();
294
+ setSoftKeyboardBackend(backend);
295
+ const keyboard = createSoftKeyboard();
296
+ let reached = false;
297
+ connectSignal(keyboard.onWillShow, () => cancelSignal(keyboard.onWillShow), { priority: 10 });
298
+ connectSignal(keyboard.onWillShow, () => (reached = true), { priority: 0 });
299
+ attachSoftKeyboard(keyboard);
300
+ backend.visible = true;
301
+ backend.height = 300;
302
+ backend.fire('will', 0.25);
303
+ expect(reached).toBe(false);
304
+ });
305
+
306
+ it('keeps the will→did ordering across a full show transition', () => {
307
+ const backend = fakeBackend();
308
+ setSoftKeyboardBackend(backend);
309
+ const keyboard = createSoftKeyboard();
310
+ const order: string[] = [];
311
+ connectSignal(keyboard.onWillShow, () => order.push('will'));
312
+ connectSignal(keyboard.onDidShow, () => order.push('did'));
313
+ attachSoftKeyboard(keyboard);
314
+ backend.visible = true;
315
+ backend.height = 300;
316
+ backend.fire('will', 0.25);
317
+ backend.fire('did');
318
+ expect(order).toEqual(['will', 'did']);
319
+ });
320
+
321
+ it('tracks visibility correctly across rapid show/hide bursts', () => {
322
+ const backend = fakeBackend();
323
+ setSoftKeyboardBackend(backend);
324
+ const keyboard = createSoftKeyboard();
325
+ let shows = 0;
326
+ let hides = 0;
327
+ connectSignal(keyboard.onDidShow, () => shows++);
328
+ connectSignal(keyboard.onDidHide, () => hides++);
329
+ attachSoftKeyboard(keyboard);
330
+ for (let i = 0; i < 5; i++) {
331
+ backend.visible = true;
332
+ backend.height = 300;
333
+ backend.fire('did');
334
+ backend.visible = false;
335
+ backend.height = 0;
336
+ backend.fire('did');
337
+ }
338
+ expect(shows).toBe(5);
339
+ expect(hides).toBe(5);
340
+ });
341
+
342
+ it('does not re-emit show/hide edges when visibility is unchanged', () => {
343
+ const backend = fakeBackend();
344
+ setSoftKeyboardBackend(backend);
345
+ const keyboard = createSoftKeyboard();
346
+ let shows = 0;
347
+ let resizes = 0;
348
+ connectSignal(keyboard.onDidShow, () => shows++);
349
+ connectSignal(keyboard.onDidResize, () => resizes++);
350
+ attachSoftKeyboard(keyboard);
351
+ backend.visible = true;
352
+ backend.height = 300;
353
+ backend.fire('did');
354
+ // stay visible across further did edges: no new show, but resize fires each time
355
+ backend.height = 320;
356
+ backend.fire('did');
357
+ backend.height = 340;
358
+ backend.fire('did');
359
+ expect(shows).toBe(1);
360
+ expect(resizes).toBe(3);
361
+ });
362
+
363
+ it('survives re-entrant detach from inside a listener', () => {
364
+ const backend = fakeBackend();
365
+ setSoftKeyboardBackend(backend);
366
+ const keyboard = createSoftKeyboard();
367
+ let resizes = 0;
368
+ connectSignal(keyboard.onDidShow, () => detachSoftKeyboard(keyboard));
369
+ connectSignal(keyboard.onResize, () => resizes++);
370
+ attachSoftKeyboard(keyboard);
371
+ backend.visible = true;
372
+ backend.height = 300;
373
+ expect(() => backend.fire('did')).not.toThrow();
374
+ // detach during emit forgets the subscription; further fires deliver nothing
375
+ backend.fire('did');
376
+ expect(resizes).toBe(1);
377
+ });
378
+
379
+ it('survives re-entrant re-attach from inside a listener', () => {
380
+ const backend = fakeBackend();
381
+ setSoftKeyboardBackend(backend);
382
+ const keyboard = createSoftKeyboard();
383
+ connectSignal(keyboard.onDidShow, () => attachSoftKeyboard(keyboard));
384
+ attachSoftKeyboard(keyboard);
385
+ backend.visible = true;
386
+ backend.height = 300;
387
+ expect(() => backend.fire('did')).not.toThrow();
388
+ });
389
+ });
390
+
391
+ describe('createSoftKeyboard', () => {
392
+ it('creates an entity with nine signals', () => {
393
+ const keyboard = createSoftKeyboard();
394
+ expect(keyboard.onShow).toBeDefined();
395
+ expect(keyboard.onHide).toBeDefined();
396
+ expect(keyboard.onResize).toBeDefined();
397
+ expect(keyboard.onWillShow).toBeDefined();
398
+ expect(keyboard.onWillHide).toBeDefined();
399
+ expect(keyboard.onWillResize).toBeDefined();
400
+ expect(keyboard.onDidShow).toBeDefined();
401
+ expect(keyboard.onDidHide).toBeDefined();
402
+ expect(keyboard.onDidResize).toBeDefined();
403
+ });
404
+ });
405
+
406
+ describe('createSoftKeyboardInfo', () => {
407
+ it('allocates a zeroed info including rect fields', () => {
408
+ expect(createSoftKeyboardInfo()).toEqual({ visible: false, height: 0, x: 0, y: 0, width: 0 });
409
+ });
410
+ });
411
+
412
+ describe('createSoftKeyboardTransition', () => {
413
+ it('allocates a zeroed transition', () => {
414
+ expect(createSoftKeyboardTransition()).toEqual({ durationSeconds: 0, height: 0 });
415
+ });
416
+ });
417
+
418
+ describe('createWebSoftKeyboardBackend', () => {
419
+ it('reads info without throwing', () => {
420
+ const out = createSoftKeyboardInfo();
421
+ expect(typeof createWebSoftKeyboardBackend().getInfo(out).visible).toBe('boolean');
422
+ });
423
+
424
+ it('returns rect fields with height 0 when no keyboard is present', () => {
425
+ const out = createSoftKeyboardInfo();
426
+ createWebSoftKeyboardBackend().getInfo(out);
427
+ expect(out.height).toBe(0);
428
+ expect(out.x).toBe(0);
429
+ expect(out.y).toBe(0);
430
+ expect(out.width).toBe(0);
431
+ });
432
+
433
+ it('subscribes and unsubscribes without throwing', () => {
434
+ const unsubscribe = createWebSoftKeyboardBackend().subscribe(() => {});
435
+ expect(() => unsubscribe()).not.toThrow();
436
+ });
437
+
438
+ it('show and hide are no-ops without throwing', () => {
439
+ const backend = createWebSoftKeyboardBackend();
440
+ expect(() => {
441
+ backend.show();
442
+ backend.hide();
443
+ }).not.toThrow();
444
+ });
445
+
446
+ it('infers height from a visualViewport shrink relative to window.innerHeight', () => {
447
+ const restore = stubVisualViewport({ height: 600 });
448
+ try {
449
+ stubWindowMetrics(900, 375);
450
+ const out = createSoftKeyboardInfo();
451
+ createWebSoftKeyboardBackend().getInfo(out);
452
+ expect(out.visible).toBe(true);
453
+ expect(out.height).toBe(300);
454
+ expect(out.width).toBe(375);
455
+ expect(out.y).toBe(600);
456
+ } finally {
457
+ restore();
458
+ }
459
+ });
460
+
461
+ it('reports no keyboard when the visualViewport has not shrunk', () => {
462
+ const restore = stubVisualViewport({ height: 900 });
463
+ try {
464
+ stubWindowMetrics(900, 375);
465
+ const out = createSoftKeyboardInfo();
466
+ createWebSoftKeyboardBackend().getInfo(out);
467
+ expect(out.visible).toBe(false);
468
+ expect(out.height).toBe(0);
469
+ expect(out.width).toBe(0);
470
+ expect(out.y).toBe(0);
471
+ } finally {
472
+ restore();
473
+ }
474
+ });
475
+
476
+ it('subscribes to visualViewport resize/scroll and fires a did transition', () => {
477
+ const events = new Map<string, () => void>();
478
+ const viewport = {
479
+ height: 600,
480
+ addEventListener(type: string, fn: () => void) {
481
+ events.set(type, fn);
482
+ },
483
+ removeEventListener(type: string) {
484
+ events.delete(type);
485
+ },
486
+ };
487
+ const restore = stubVisualViewport(viewport as unknown as VisualViewport);
488
+ try {
489
+ let phase: SoftKeyboardPhase | null = null;
490
+ const unsubscribe = createWebSoftKeyboardBackend().subscribe((p) => (phase = p));
491
+ expect(events.has('resize')).toBe(true);
492
+ expect(events.has('scroll')).toBe(true);
493
+ events.get('resize')!();
494
+ expect(phase).toBe('did');
495
+ unsubscribe();
496
+ expect(events.size).toBe(0);
497
+ } finally {
498
+ restore();
499
+ }
500
+ });
501
+
502
+ it('returns a no-op subscription when visualViewport is absent', () => {
503
+ const restore = stubVisualViewport(null);
504
+ try {
505
+ const unsubscribe = createWebSoftKeyboardBackend().subscribe(() => {});
506
+ expect(() => unsubscribe()).not.toThrow();
507
+ } finally {
508
+ restore();
509
+ }
510
+ });
511
+
512
+ it('prefers the VirtualKeyboard API for geometry when present', () => {
513
+ const restore = stubVirtualKeyboard({
514
+ boundingRect: { height: 280, width: 320, x: 5, y: 620 } as DOMRect,
515
+ addEventListener() {},
516
+ removeEventListener() {},
517
+ show() {},
518
+ hide() {},
519
+ });
520
+ try {
521
+ const out = createSoftKeyboardInfo();
522
+ createWebSoftKeyboardBackend().getInfo(out);
523
+ expect(out.height).toBe(280);
524
+ expect(out.width).toBe(320);
525
+ expect(out.x).toBe(5);
526
+ expect(out.y).toBe(620);
527
+ expect(out.visible).toBe(true);
528
+ } finally {
529
+ restore();
530
+ }
531
+ });
532
+
533
+ it('subscribes via the VirtualKeyboard geometrychange event when present', () => {
534
+ const events = new Map<string, () => void>();
535
+ const restore = stubVirtualKeyboard({
536
+ boundingRect: { height: 0, width: 0, x: 0, y: 0 } as DOMRect,
537
+ addEventListener(type: string, fn: () => void) {
538
+ events.set(type, fn);
539
+ },
540
+ removeEventListener(type: string) {
541
+ events.delete(type);
542
+ },
543
+ show() {},
544
+ hide() {},
545
+ });
546
+ try {
547
+ let phase: SoftKeyboardPhase | null = null;
548
+ const unsubscribe = createWebSoftKeyboardBackend().subscribe((p) => (phase = p));
549
+ expect(events.has('geometrychange')).toBe(true);
550
+ events.get('geometrychange')!();
551
+ expect(phase).toBe('did');
552
+ unsubscribe();
553
+ expect(events.has('geometrychange')).toBe(false);
554
+ } finally {
555
+ restore();
556
+ }
557
+ });
558
+
559
+ it('drives show/hide through the VirtualKeyboard API when present', () => {
560
+ let shown = false;
561
+ let hidden = false;
562
+ const restore = stubVirtualKeyboard({
563
+ boundingRect: { height: 0, width: 0, x: 0, y: 0 } as DOMRect,
564
+ addEventListener() {},
565
+ removeEventListener() {},
566
+ show() {
567
+ shown = true;
568
+ },
569
+ hide() {
570
+ hidden = true;
571
+ },
572
+ });
573
+ try {
574
+ const backend = createWebSoftKeyboardBackend();
575
+ backend.show();
576
+ backend.hide();
577
+ expect(shown).toBe(true);
578
+ expect(hidden).toBe(true);
579
+ } finally {
580
+ restore();
581
+ }
582
+ });
583
+ });
584
+
585
+ describe('detachSoftKeyboard', () => {
586
+ it('stops further delivery', () => {
587
+ const backend = fakeBackend();
588
+ setSoftKeyboardBackend(backend);
589
+ const keyboard = createSoftKeyboard();
590
+ let resizes = 0;
591
+ connectSignal(keyboard.onResize, () => resizes++);
592
+ attachSoftKeyboard(keyboard);
593
+ detachSoftKeyboard(keyboard);
594
+ backend.fire();
595
+ expect(resizes).toBe(0);
596
+ });
597
+
598
+ it('is safe to call when not attached', () => {
599
+ const keyboard = createSoftKeyboard();
600
+ expect(() => detachSoftKeyboard(keyboard)).not.toThrow();
601
+ });
602
+
603
+ it('is safe to call twice', () => {
604
+ const backend = fakeBackend();
605
+ setSoftKeyboardBackend(backend);
606
+ const keyboard = createSoftKeyboard();
607
+ attachSoftKeyboard(keyboard);
608
+ detachSoftKeyboard(keyboard);
609
+ expect(() => detachSoftKeyboard(keyboard)).not.toThrow();
610
+ });
611
+
612
+ it('re-attach after detach resumes delivery', () => {
613
+ const backend = fakeBackend();
614
+ setSoftKeyboardBackend(backend);
615
+ const keyboard = createSoftKeyboard();
616
+ let resizes = 0;
617
+ connectSignal(keyboard.onResize, () => resizes++);
618
+ attachSoftKeyboard(keyboard);
619
+ detachSoftKeyboard(keyboard);
620
+ attachSoftKeyboard(keyboard);
621
+ backend.visible = true;
622
+ backend.height = 300;
623
+ backend.fire('did');
624
+ expect(resizes).toBe(1);
625
+ });
626
+ });
627
+
628
+ describe('disposeSoftKeyboard', () => {
629
+ it('detaches the subscription', () => {
630
+ const backend = fakeBackend();
631
+ setSoftKeyboardBackend(backend);
632
+ const keyboard = createSoftKeyboard();
633
+ attachSoftKeyboard(keyboard);
634
+ expect(() => disposeSoftKeyboard(keyboard)).not.toThrow();
635
+ });
636
+
637
+ it('stops further delivery after dispose', () => {
638
+ const backend = fakeBackend();
639
+ setSoftKeyboardBackend(backend);
640
+ const keyboard = createSoftKeyboard();
641
+ let resizes = 0;
642
+ connectSignal(keyboard.onResize, () => resizes++);
643
+ attachSoftKeyboard(keyboard);
644
+ disposeSoftKeyboard(keyboard);
645
+ backend.fire();
646
+ expect(resizes).toBe(0);
647
+ });
648
+
649
+ it('is safe to call when already detached', () => {
650
+ const backend = fakeBackend();
651
+ setSoftKeyboardBackend(backend);
652
+ const keyboard = createSoftKeyboard();
653
+ attachSoftKeyboard(keyboard);
654
+ detachSoftKeyboard(keyboard);
655
+ expect(() => disposeSoftKeyboard(keyboard)).not.toThrow();
656
+ });
657
+
658
+ it('is safe to call twice', () => {
659
+ const backend = fakeBackend();
660
+ setSoftKeyboardBackend(backend);
661
+ const keyboard = createSoftKeyboard();
662
+ attachSoftKeyboard(keyboard);
663
+ disposeSoftKeyboard(keyboard);
664
+ expect(() => disposeSoftKeyboard(keyboard)).not.toThrow();
665
+ });
666
+
667
+ it('is safe to call when never attached', () => {
668
+ const keyboard = createSoftKeyboard();
669
+ expect(() => disposeSoftKeyboard(keyboard)).not.toThrow();
670
+ });
671
+ });
672
+
673
+ describe('getSoftKeyboardBackend', () => {
674
+ it('falls back to a web backend', () => {
675
+ expect(getSoftKeyboardBackend()).not.toBeNull();
676
+ });
677
+ });
678
+
679
+ describe('getSoftKeyboardHeight', () => {
680
+ it('returns the current keyboard height without allocating', () => {
681
+ const backend = fakeBackend();
682
+ backend.visible = true;
683
+ backend.height = 320;
684
+ setSoftKeyboardBackend(backend);
685
+ expect(getSoftKeyboardHeight()).toBe(320);
686
+ });
687
+
688
+ it('returns 0 when keyboard is hidden', () => {
689
+ const backend = fakeBackend();
690
+ backend.visible = false;
691
+ backend.height = 0;
692
+ setSoftKeyboardBackend(backend);
693
+ expect(getSoftKeyboardHeight()).toBe(0);
694
+ });
695
+ });
696
+
697
+ describe('getSoftKeyboardInfo', () => {
698
+ it('fills the out parameter from the backend', () => {
699
+ const backend = fakeBackend();
700
+ backend.height = 250;
701
+ backend.visible = true;
702
+ setSoftKeyboardBackend(backend);
703
+ const out = createSoftKeyboardInfo();
704
+ expect(getSoftKeyboardInfo(out)).toBe(out);
705
+ expect(out.height).toBe(250);
706
+ expect(out.visible).toBe(true);
707
+ });
708
+
709
+ it('populates rect fields', () => {
710
+ const backend = fakeBackend();
711
+ backend.height = 250;
712
+ backend.visible = true;
713
+ setSoftKeyboardBackend(backend);
714
+ const out = createSoftKeyboardInfo();
715
+ getSoftKeyboardInfo(out);
716
+ expect(out.x).toBe(0);
717
+ expect(out.y).toBe(0);
718
+ expect(out.width).toBe(375);
719
+ });
720
+ });
721
+
722
+ describe('getSoftKeyboardResizeMode', () => {
723
+ it('delegates to backend getResizeMode', () => {
724
+ const backend = fakeBackend();
725
+ backend.resizeMode = SoftKeyboardResizeBodyKind;
726
+ setSoftKeyboardBackend(backend);
727
+ expect(getSoftKeyboardResizeMode()).toBe(SoftKeyboardResizeBodyKind);
728
+ });
729
+
730
+ it('returns SoftKeyboardResizeNoneKind when backend does not support it', () => {
731
+ const backend: SoftKeyboardBackend = {
732
+ getInfo(out) {
733
+ return out;
734
+ },
735
+ subscribe() {
736
+ return () => {};
737
+ },
738
+ show() {},
739
+ hide() {},
740
+ };
741
+ setSoftKeyboardBackend(backend);
742
+ expect(getSoftKeyboardResizeMode()).toBe(SoftKeyboardResizeNoneKind);
743
+ });
744
+ });
745
+
746
+ describe('hideSoftKeyboard', () => {
747
+ it('delegates to the backend hide', () => {
748
+ const backend = fakeBackend();
749
+ setSoftKeyboardBackend(backend);
750
+ hideSoftKeyboard();
751
+ expect(backend.hidden).toBe(true);
752
+ });
753
+ });
754
+
755
+ describe('isSoftKeyboardAccessoryBarVisible', () => {
756
+ it('delegates to backend getAccessoryBarVisible', () => {
757
+ const backend = fakeBackend();
758
+ backend.accessoryBarVisible = true;
759
+ setSoftKeyboardBackend(backend);
760
+ expect(isSoftKeyboardAccessoryBarVisible()).toBe(true);
761
+ });
762
+
763
+ it('returns false when backend does not support it', () => {
764
+ const backend: SoftKeyboardBackend = {
765
+ getInfo(out) {
766
+ return out;
767
+ },
768
+ subscribe() {
769
+ return () => {};
770
+ },
771
+ show() {},
772
+ hide() {},
773
+ };
774
+ setSoftKeyboardBackend(backend);
775
+ expect(isSoftKeyboardAccessoryBarVisible()).toBe(false);
776
+ });
777
+ });
778
+
779
+ describe('isSoftKeyboardScrollAssistEnabled', () => {
780
+ it('delegates to backend getScrollAssistEnabled', () => {
781
+ const backend = fakeBackend();
782
+ backend.scrollAssistEnabled = true;
783
+ setSoftKeyboardBackend(backend);
784
+ expect(isSoftKeyboardScrollAssistEnabled()).toBe(true);
785
+ });
786
+
787
+ it('returns false when backend does not support it', () => {
788
+ const backend: SoftKeyboardBackend = {
789
+ getInfo(out) {
790
+ return out;
791
+ },
792
+ subscribe() {
793
+ return () => {};
794
+ },
795
+ show() {},
796
+ hide() {},
797
+ };
798
+ setSoftKeyboardBackend(backend);
799
+ expect(isSoftKeyboardScrollAssistEnabled()).toBe(false);
800
+ });
801
+ });
802
+
803
+ describe('setSoftKeyboardAccessoryBarVisible', () => {
804
+ it('delegates to backend setAccessoryBarVisible', () => {
805
+ const backend = fakeBackend();
806
+ setSoftKeyboardBackend(backend);
807
+ setSoftKeyboardAccessoryBarVisible(true);
808
+ expect(backend.accessoryBarVisible).toBe(true);
809
+ setSoftKeyboardAccessoryBarVisible(false);
810
+ expect(backend.accessoryBarVisible).toBe(false);
811
+ });
812
+
813
+ it('is a no-op when backend does not support it', () => {
814
+ const backend: SoftKeyboardBackend = {
815
+ getInfo(out) {
816
+ return out;
817
+ },
818
+ subscribe() {
819
+ return () => {};
820
+ },
821
+ show() {},
822
+ hide() {},
823
+ };
824
+ setSoftKeyboardBackend(backend);
825
+ expect(() => setSoftKeyboardAccessoryBarVisible(true)).not.toThrow();
826
+ });
827
+ });
828
+
829
+ describe('setSoftKeyboardBackend', () => {
830
+ it('clears back to the web fallback when passed null', () => {
831
+ setSoftKeyboardBackend(fakeBackend());
832
+ setSoftKeyboardBackend(null);
833
+ expect(getSoftKeyboardBackend()).not.toBeNull();
834
+ });
835
+ });
836
+
837
+ describe('setSoftKeyboardResizeMode', () => {
838
+ it('delegates to backend setResizeMode', () => {
839
+ const backend = fakeBackend();
840
+ setSoftKeyboardBackend(backend);
841
+ setSoftKeyboardResizeMode(SoftKeyboardResizeBodyKind);
842
+ expect(backend.resizeMode).toBe(SoftKeyboardResizeBodyKind);
843
+ });
844
+
845
+ it('is a no-op when backend does not support it', () => {
846
+ const backend: SoftKeyboardBackend = {
847
+ getInfo(out) {
848
+ return out;
849
+ },
850
+ subscribe() {
851
+ return () => {};
852
+ },
853
+ show() {},
854
+ hide() {},
855
+ };
856
+ setSoftKeyboardBackend(backend);
857
+ expect(() => setSoftKeyboardResizeMode(SoftKeyboardResizeBodyKind)).not.toThrow();
858
+ });
859
+ });
860
+
861
+ describe('setSoftKeyboardScrollAssistEnabled', () => {
862
+ it('delegates to backend setScrollAssistEnabled', () => {
863
+ const backend = fakeBackend();
864
+ setSoftKeyboardBackend(backend);
865
+ setSoftKeyboardScrollAssistEnabled(true);
866
+ expect(backend.scrollAssistEnabled).toBe(true);
867
+ setSoftKeyboardScrollAssistEnabled(false);
868
+ expect(backend.scrollAssistEnabled).toBe(false);
869
+ });
870
+
871
+ it('is a no-op when backend does not support it', () => {
872
+ const backend: SoftKeyboardBackend = {
873
+ getInfo(out) {
874
+ return out;
875
+ },
876
+ subscribe() {
877
+ return () => {};
878
+ },
879
+ show() {},
880
+ hide() {},
881
+ };
882
+ setSoftKeyboardBackend(backend);
883
+ expect(() => setSoftKeyboardScrollAssistEnabled(true)).not.toThrow();
884
+ });
885
+ });
886
+
887
+ describe('setSoftKeyboardStyle', () => {
888
+ it('delegates to backend setStyle', () => {
889
+ const backend = fakeBackend();
890
+ setSoftKeyboardBackend(backend);
891
+ setSoftKeyboardStyle(SoftKeyboardStyleDarkKind);
892
+ expect(backend.style).toBe(SoftKeyboardStyleDarkKind);
893
+ });
894
+
895
+ it('is a no-op when backend does not support it', () => {
896
+ const backend: SoftKeyboardBackend = {
897
+ getInfo(out) {
898
+ return out;
899
+ },
900
+ subscribe() {
901
+ return () => {};
902
+ },
903
+ show() {},
904
+ hide() {},
905
+ };
906
+ setSoftKeyboardBackend(backend);
907
+ expect(() => setSoftKeyboardStyle(SoftKeyboardStyleDarkKind)).not.toThrow();
908
+ });
909
+ });
910
+
911
+ describe('showSoftKeyboard', () => {
912
+ it('delegates to the backend show', () => {
913
+ const backend = fakeBackend();
914
+ setSoftKeyboardBackend(backend);
915
+ showSoftKeyboard();
916
+ expect(backend.shown).toBe(true);
917
+ });
918
+ });