Skip to content

Commit ed9afa2

Browse files
ronickgRonald Goedekeclaude
authored
Review cleanup: one value limit, dark mode, per-frame work, submitBehavior, packaging (#11)
* Rolling number: one value limit, dark mode, no per-frame allocations The engine clamped the scaled magnitude at 10^15 for a jump and 10^17 for a roll, so `jumpTo` and `animateTo` showed different digits above that; both now share one limit, the stagger's longest delay is stored once per roll, and the unused `revealFraction` is gone. Both views follow a light/dark switch when no `color` is set: iOS rasterizes the glyphs with the trait-resolved label color and rebuilds them on a trait or display-scale change (the scale now comes from the view's traits, not `UIScreen.main`), Android rebuilds its paints on a configuration change. Per frame, iOS reuses its wheel and element buffers, keys the glyph caches by a value type and compares layers in place; Android pools its layout elements, splits the affixes once per format and slides one cached shimmer gradient. Both format the accessibility text only when a screen reader asks for it, and Android reads the animator scale once behind a settings observer. The wrapper keeps one `ref` handle per mount and tests the right-to-left resolution; `lib/` ships ES module, CommonJS and declaration builds; the Android Gradle plugin is pinned only for a standalone build, Java 17. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Input: worklet ids on Android, dark mode on iOS, submitBehavior on multiline Android never marked five worklet-id props dirty, so a changed handler kept calling a dead id; they flush like the other three now. The view also re-feeds after `secureTextEntry` or `morph` toggles, applies `maxLength` only in text mode, sets tabular figures in every mode, honours `showSoftInputOnFocus`, resolves its theme colours once and again on a configuration change, and caches the animator scale. iOS re-rasterizes glyphs and re-resolves frame, label and caret colours on a trait or scale change (no more `UIScreen.main`), the Fabric focus/blur commands find multiline fields, `maxIntegerDigits` allows 30 as on Android, a `morph` toggled back on re-feeds, and a frame no longer allocates per glyph. `submitBehavior` gains `'newline'`, resolved as `TextInput` resolves it, and both platforms honour `submit` / `blurAndSubmit` on a multiline field. The wrapper no longer re-renders on every keystroke of an uncontrolled field, memoises `maskNotations` and the colours, exports the missing types, drops affixes on multiline with a warning, and gains 43 tests; the README props table is complete and guarded by a test. The native `affixAlign` prop is gone (the wrapper folds it), dead engine, mask and worklet-bridge methods are removed, the outline geometry writes into a reused buffer, and packaging mirrors the rolling number's dual build with `react-native-worklets` as an optional peer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Repo: build step in CI, wasm rebuilt on deploy, the Nitro patch documented The docs workflow rebuilds both engines to WebAssembly before deploying, since a change under cpp/ used to trigger a deploy with a stale build (the committed builds are refreshed here too, and the rolling engine's script links with em++). The package workflow builds `lib/`. The README's test command was `bun test`, which runs Bun's own runner against the Jest files; it is `bun run test`. It also documents the Android view-props patch for Nitro 0.37 that consumers need until the upstream fix ships. The demo gains a multiline field with a `submitBehavior` toggle, the row the device check of that feature used. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Ronald Goedeke <ronald@margelo.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent df7c449 commit ed9afa2

92 files changed

Lines changed: 2227 additions & 709 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/docs.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ jobs:
3333
- name: Install
3434
working-directory: docs
3535
run: npm ci
36+
# The live demos run the real engines compiled to WebAssembly. Rebuild
37+
# them here so a change to cpp/ never deploys with a stale build.
38+
- uses: mymindstorm/setup-emsdk@v14
39+
with:
40+
version: latest
41+
- name: Build the engines to WebAssembly
42+
working-directory: docs
43+
run: npm run build:wasm && npm run build:wasm:morph
3644
- name: Build
3745
working-directory: docs
3846
run: npm run build

‎.github/workflows/packages.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ jobs:
3939
# The shared engines are host-side clang++ binaries: no toolchain to set up.
4040
- name: C++ engines
4141
run: bun run test:cpp
42+
# The published lib/: ES modules, CommonJS and the declarations, for both packages.
43+
- name: Build
44+
run: bun run build
4245

4346
android:
4447
name: Android libraries

‎README.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,9 @@ import { NitroInput } from 'react-native-nitro-input'
114114
```sh
115115
bun install # hoisted linker, see bunfig.toml
116116
bun specs # re-run nitrogen after editing src/specs/*.nitro.ts
117-
bun test # jest tests for the JS wrapper
118-
bun --cwd packages/react-native-nitro-rolling-number run test:cpp # engine tests (same for packages/react-native-nitro-input)
117+
bun run test # jest tests for the JS wrappers (plain `bun test` would run Bun's own runner against them)
118+
bun run test:cpp # C++ engine tests for both packages (host clang++)
119+
bun run build # lib/ for both packages: ES modules, CommonJS and declarations
119120
bun example ios # or: bun example android
120121
cd docs && npm install && npm start # docs site
121122
```
@@ -124,6 +125,10 @@ Releasing: `bun --cwd packages/<package> release <patch|minor|major>` runs the t
124125

125126
The example's Android Gradle files point at the workspace root `node_modules`, and Metro watches the whole repo.
126127

128+
## Known issue: view props on Android
129+
130+
Nitro Modules 0.37 never fills a Hybrid View's raw props on Android from React Native 0.86 on, so `backgroundColor`, `border*`, `opacity`, `transform`, `testID` and the accessibility props you pass to `<RollingNumber>` or `<NitroInput>` are silently ignored there. iOS is unaffected. The fix is filed upstream as [margelo/nitro#1655](https://github.com/margelo/nitro/pull/1655) (issue [#1656](https://github.com/margelo/nitro/issues/1656)); until a Nitro release carries it, apply the patch this repo uses: copy [`patches/react-native-nitro-modules@0.37.1.patch`](patches/react-native-nitro-modules@0.37.1.patch) into your app and register it under `patchedDependencies` in `package.json` (Bun) or with [patch-package](https://github.com/ds300/patch-package) (npm / Yarn).
131+
127132
## Credits
128133

129134
The input's morph is based on [Torph](https://torph.lochie.me) by [Lochie Axon](https://github.com/lochie). Thanks for building it.

‎bun.lock‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎docs/input/multiline.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ description: 'multiline: a field that wraps, grows with its content or scrolls p
1919

2020
## What a multiline field is not
2121

22-
A multiline field is always drawn by the system view and is always `mode="text"`. The glyph engine lays one run out on one baseline, so it cannot morph wrapped text, and an amount or a mask is a single-line idea. `morph`, `mode="number"` and `mode="mask"` are ignored alongside `multiline`, with one warning in development. Everything else applies: a [frame](/input/frames) with a floating label, `maxLength`, autocorrect and the edit callbacks.
22+
A multiline field is always drawn by the system view and is always `mode="text"`. The glyph engine lays one run out on one baseline, so it cannot morph wrapped text, and an amount, a mask and their affixes are single-line ideas. `morph`, `mode="number"`, `mode="mask"`, `prefix` and `suffix` are ignored alongside `multiline`, with one warning each in development. Everything else applies: a [frame](/input/frames) with a floating label, `maxLength`, autocorrect and the edit callbacks.
23+
24+
The return key inserts a line break, as it does on a multiline `TextInput`: `submitBehavior` defaults to `'newline'` there. Pass `submitBehavior="blurAndSubmit"` (or the older `blurOnSubmit`) to have it fire `onSubmitEditing` and dismiss the keyboard instead, or `'submit'` to fire it and keep focus.

‎docs/input/props.mdx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,19 @@ The full table is in the package [README](https://github.com/ronickg/react-nativ
2525
| `adjustsFontSizeToFit`, `minimumFontScale`, `allowFontScaling`, `maxFontSizeMultiplier` | `false`, `0.5`, `false`, `0` | Shrinking to fit a width, and following the system text size. |
2626
| `cursorColor`, `selectionColor`, `caretHidden` | | The caret and the selection. |
2727
| `keyboardType`, `returnKeyType`, `autoCapitalize`, `autoCorrect`, `maxLength`, `editable`, `autoFocus` | | Input traits. |
28+
| `inputMode`, `enterKeyHint` | | React Native's HTML-style aliases for `keyboardType` and `returnKeyType`, mapped with its tables; the explicit prop wins. `inputMode="none"` focuses without a keyboard. |
2829
| `transform` | | A worklet mask, run synchronously on the UI thread. |
29-
| `onChangeText`, `onChangeValue`, `onFocus`, `onBlur`, `onSubmitEditing`, `onEndEditing`, `onSelectionChange`, `onKeyPress` | | `TextInput`'s edit callbacks, plus `onChangeValue`. Any of them marked `'worklet'` runs on the UI thread instead of the JS one — see [Worklets](/input/worklets). |
30+
| `onChangeText`, `onChange`, `onChangeValue`, `onFocus`, `onBlur`, `onSubmitEditing`, `onEndEditing`, `onSelectionChange`, `onKeyPress` | | `TextInput`'s edit callbacks, plus `onChangeValue`. Each event carries `nativeEvent` as `TextInput`'s does, with the same fields repeated at the top level. Any of them marked `'worklet'` runs on the UI thread instead of the JS one — see [Worklets](/input/worklets). |
3031
| `onChangeMask` | | `'mask'`: the formatted text, the extracted value, the missing tail and whether every mandatory slot is filled. |
3132
| `value`, `defaultValue` | | Controlled and uncontrolled text. A `value` with a stale `mostRecentEventCount` is ignored, like `TextInput`. |
32-
| `submitBehavior` | `'blurAndSubmit'` | `'submit'` keeps focus so a form can move to the next field itself. |
33+
| `submitBehavior` | `'blurAndSubmit'`, `'newline'` when `multiline` | `'submit'` keeps focus so a form can move to the next field itself; `'newline'` inserts a line break (`multiline` only). `blurOnSubmit` is the deprecated alias, resolved as `TextInput` resolves it. |
3334
| `secureTextEntry` | `false` | Draws bullets; the field keeps the real text for autofill. |
3435
| `textContentType` / `autoComplete` | `''` | Autofill: `'username'`, `'password'`, `'oneTimeCode'`, … |
3536
| `keyboardAppearance`, `enablesReturnKeyAutomatically`, `showSoftInputOnFocus` | | Keyboard behaviour. |
3637
| `selectTextOnFocus`, `clearTextOnFocus`, `contextMenuHidden`, `spellCheck`, `readOnly` | | As on `TextInput`. |
3738
| `selection` | | `{ start, end? }` in code points, controlled. |
39+
| `id`, `aria-label`, `testID`, `accessibilityLabel` | | `id` and `aria-label` are resolved like `TextInput`'s and win over the older spellings; `testID` and the label are forwarded to the system field, the element VoiceOver, TalkBack and e2e tools interact with. |
3840

39-
Imperative handle: `focus()`, `blur()`, `clear()`, `setText()`, `setValue()`, `getText()`, `getValue()`, `isFocused()`.
41+
Imperative handle: `focus()`, `blur()`, `clear()`, `setText()`, `setValue()`, `getText()`, `getValue()`, `isFocused()`, `setSelection()`, and `native`, the Nitro object.
4042

4143
Hook: `useNitroInputState(useSharedValue)` — the field's `text`, `value`, `focused` and `selection` as shared values, plus the worklet `handlers` that keep them current. See [the field's state as shared values](/input/worklets#the-fields-state-as-shared-values).

‎docs/input/react-native.mdx‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ description: 'The text-input registry, focus commands, keyboard handling and acc
77
`NitroInput` registers itself in React Native's text-input registry, so the
88
things that act on "the focused input" work on it as they do on `TextInput`:
99

10-
- `Keyboard.dismiss()`, `TextInput.State.currentlyFocusedInput()` and
11-
`TextInput.State.blurTextInput()`
10+
- `Keyboard.dismiss()` and `TextInput.State.currentlyFocusedInput()`
1211
- a `ScrollView`'s `keyboardShouldPersistTaps` / auto-blur
1312
- `react-native-keyboard-controller`: the focused-input observer sees it,
1413
`KeyboardAwareScrollView` scrolls it into view, and `KeyboardToolbar`'s
@@ -18,13 +17,20 @@ things that act on "the focused input" work on it as they do on `TextInput`:
1817

1918
React Native focuses an input by dispatching a codegen `focus` / `blur` **view
2019
command**. A Nitro view has no such command on Android, so that call would be
21-
dropped and the keyboard would stay up. `NitroInput` therefore wraps
22-
`TextInput.State.focusTextInput` / `blurTextInput` once and routes a
23-
`NitroInput` to its own native focus and blur; every other input is passed
24-
straight through untouched.
20+
dropped and the keyboard would stay up. `NitroInput` therefore wraps the
21+
registry's `focusTextInput` / `blurTextInput` once and routes a `NitroInput` to
22+
its own native focus and blur; every other input is passed straight through
23+
untouched. That reaches everything that reads the two off the registry when it
24+
calls them, which `Keyboard.dismiss()` and `ScrollView` do.
2525

26-
Two consequences worth knowing:
26+
Three consequences worth knowing:
2727

28+
- **`TextInput.State.focusTextInput()` / `blurTextInput()` bypass it.**
29+
`TextInput.js` copies those two references when it loads, before the wrap
30+
runs, so a call through them still dispatches the view command. On iOS the
31+
component answers that command, so both work there; on Android they do not
32+
reach a `NitroInput`. `ref.focus()`, `ref.blur()` and `Keyboard.dismiss()`
33+
are unaffected.
2834
- **It is not an optimisation**, but it is faster. Skipping the view command on
2935
the way in and the batched event emitter on the way out removes about a frame
3036
of waiting at each end, which is where the focus numbers in

‎docs/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"docusaurus": "docusaurus",
77
"start": "docusaurus start",
88
"build": "docusaurus build",
9-
"build:wasm": "emcc -O3 -std=c++20 --bind -sMODULARIZE=1 -sEXPORT_ES6=1 -sSINGLE_FILE=1 -sENVIRONMENT=web -sEXPORT_NAME=createRollingEngine -sFILESYSTEM=0 -sALLOW_MEMORY_GROWTH=1 --emit-tsd rolling-engine.d.ts -I ../packages/react-native-nitro-rolling-number/cpp ../packages/react-native-nitro-rolling-number/cpp/RollingEngine.cpp wasm/bindings.cpp -o src/engine/rolling-engine.js",
9+
"build:wasm": "em++ -O3 -std=c++20 --bind -sMODULARIZE=1 -sEXPORT_ES6=1 -sSINGLE_FILE=1 -sENVIRONMENT=web -sEXPORT_NAME=createRollingEngine -sFILESYSTEM=0 -sALLOW_MEMORY_GROWTH=1 --emit-tsd rolling-engine.d.ts -I ../packages/react-native-nitro-rolling-number/cpp ../packages/react-native-nitro-rolling-number/cpp/RollingEngine.cpp wasm/bindings.cpp -o src/engine/rolling-engine.js",
1010
"swizzle": "docusaurus swizzle",
1111
"deploy": "docusaurus deploy",
1212
"clear": "docusaurus clear",

‎docs/rolling-number/props.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ description: Every prop and method of RollingNumber.
1414

1515
| Prop | Type | Default | Description |
1616
| --- | --- | --- | --- |
17-
| `value` | `number` | – | The number to display. Every change rolls each digit natively. |
17+
| `value` | `number` | – | The number to display. Every change rolls each digit natively. At most 18 digits are shown: `|value| × 10^fractionDigits` is clamped at 10^17, and a JS number carries exact integers only up to 2^53. |
1818
| `fractionDigits` | `number` | `0` | Digits after the decimal separator (0–9). |
1919
| `minimumIntegerDigits` | `number` | `1` | Zero-pads the integer part (1–15). |
2020
| `groupingSeparator` | `string` | `''` | Inserted every three integer digits. |

‎docs/src/engine/morph-engine.js‎

2.44 KB
Binary file not shown.

0 commit comments

Comments
 (0)