Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ jobs:
- name: Install
working-directory: docs
run: npm ci
# The live demos run the real engines compiled to WebAssembly. Rebuild
# them here so a change to cpp/ never deploys with a stale build.
- uses: mymindstorm/setup-emsdk@v14
with:
version: latest
- name: Build the engines to WebAssembly
working-directory: docs
run: npm run build:wasm && npm run build:wasm:morph
- name: Build
working-directory: docs
run: npm run build
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jobs:
# The shared engines are host-side clang++ binaries: no toolchain to set up.
- name: C++ engines
run: bun run test:cpp
# The published lib/: ES modules, CommonJS and the declarations, for both packages.
- name: Build
run: bun run build

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

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

## Known issue: view props on Android

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).

## Credits

The input's morph is based on [Torph](https://torph.lochie.me) by [Lochie Axon](https://github.com/lochie). Thanks for building it.
Expand Down
8 changes: 8 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion docs/input/multiline.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ description: 'multiline: a field that wraps, grows with its content or scrolls p

## What a multiline field is not

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.
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.

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.
8 changes: 5 additions & 3 deletions docs/input/props.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@ The full table is in the package [README](https://github.com/ronickg/react-nativ
| `adjustsFontSizeToFit`, `minimumFontScale`, `allowFontScaling`, `maxFontSizeMultiplier` | `false`, `0.5`, `false`, `0` | Shrinking to fit a width, and following the system text size. |
| `cursorColor`, `selectionColor`, `caretHidden` | | The caret and the selection. |
| `keyboardType`, `returnKeyType`, `autoCapitalize`, `autoCorrect`, `maxLength`, `editable`, `autoFocus` | | Input traits. |
| `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. |
| `transform` | | A worklet mask, run synchronously on the UI thread. |
| `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). |
| `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). |
| `onChangeMask` | | `'mask'`: the formatted text, the extracted value, the missing tail and whether every mandatory slot is filled. |
| `value`, `defaultValue` | | Controlled and uncontrolled text. A `value` with a stale `mostRecentEventCount` is ignored, like `TextInput`. |
| `submitBehavior` | `'blurAndSubmit'` | `'submit'` keeps focus so a form can move to the next field itself. |
| `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. |
| `secureTextEntry` | `false` | Draws bullets; the field keeps the real text for autofill. |
| `textContentType` / `autoComplete` | `''` | Autofill: `'username'`, `'password'`, `'oneTimeCode'`, … |
| `keyboardAppearance`, `enablesReturnKeyAutomatically`, `showSoftInputOnFocus` | | Keyboard behaviour. |
| `selectTextOnFocus`, `clearTextOnFocus`, `contextMenuHidden`, `spellCheck`, `readOnly` | | As on `TextInput`. |
| `selection` | | `{ start, end? }` in code points, controlled. |
| `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. |

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

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).
20 changes: 13 additions & 7 deletions docs/input/react-native.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ description: 'The text-input registry, focus commands, keyboard handling and acc
`NitroInput` registers itself in React Native's text-input registry, so the
things that act on "the focused input" work on it as they do on `TextInput`:

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

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

Two consequences worth knowing:
Three consequences worth knowing:

- **`TextInput.State.focusTextInput()` / `blurTextInput()` bypass it.**
`TextInput.js` copies those two references when it loads, before the wrap
runs, so a call through them still dispatches the view command. On iOS the
component answers that command, so both work there; on Android they do not
reach a `NitroInput`. `ref.focus()`, `ref.blur()` and `Keyboard.dismiss()`
are unaffected.
- **It is not an optimisation**, but it is faster. Skipping the view command on
the way in and the batched event emitter on the way out removes about a frame
of waiting at each end, which is where the focus numbers in
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"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",
"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",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
Expand Down
2 changes: 1 addition & 1 deletion docs/rolling-number/props.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: Every prop and method of RollingNumber.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | – | The number to display. Every change rolls each digit natively. |
| `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. |
| `fractionDigits` | `number` | `0` | Digits after the decimal separator (0–9). |
| `minimumIntegerDigits` | `number` | `1` | Zero-pads the integer part (1–15). |
| `groupingSeparator` | `string` | `''` | Inserted every three integer digits. |
Expand Down
Binary file modified docs/src/engine/morph-engine.js
Binary file not shown.
Binary file modified docs/src/engine/rolling-engine.js
Binary file not shown.
32 changes: 32 additions & 0 deletions example/src/screens/DemoScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,9 @@ function MorphInputDemo() {
const [maskSel, setMaskSel] = useState('-')
const [outlinedText, setOutlinedText] = useState('')
const [multilineText, setMultilineText] = useState('')
const [multilineSubmit, setMultilineSubmit] = useState<'submit' | 'blurAndSubmit' | 'newline'>('submit')
const [multilineSubmits, setMultilineSubmits] = useState(0)
const [multilineFocused, setMultilineFocused] = useState(false)
const [maskPhone, setMaskPhone] = useState(EMPTY_MASK)
const [maskHex, setMaskHex] = useState(EMPTY_MASK)
return (
Expand Down Expand Up @@ -612,6 +615,35 @@ function MorphInputDemo() {
<Text style={styles.morphReadout} testID="morph-multiline-readout">
multiline {JSON.stringify(multilineText)}
</Text>
{/* The return key on a wrapping field: `submit` fires onSubmitEditing and
keeps focus, `blurAndSubmit` also dismisses the keyboard, `newline`
(the default) inserts a line break. */}
<NitroInput
testID="morph-multiline-submit"
multiline
numberOfLines={3}
variant="outlined"
label={`submitBehavior ${multilineSubmit}`}
placeholder="Press return"
fontSize={15}
strokeColor="#94a3b8"
focusedStrokeColor="#2563eb"
cornerRadius={10}
submitBehavior={multilineSubmit}
onFocus={() => setMultilineFocused(true)}
onBlur={() => setMultilineFocused(false)}
onSubmitEditing={() => setMultilineSubmits((n) => n + 1)}
/>
<Text style={styles.morphReadout} testID="morph-multiline-submit-readout">
submits {multilineSubmits} · {multilineFocused ? 'focused' : 'blurred'}
</Text>
<Button
title={`submitBehavior: ${multilineSubmit}`}
testID="morph-multiline-submit-toggle"
onPress={() =>
setMultilineSubmit((s) => (s === 'submit' ? 'blurAndSubmit' : s === 'blurAndSubmit' ? 'newline' : 'submit'))
}
/>
{/* lineHeight, both directions. 34 is looser than the font's own line
box at 15pt, 14 is tighter — the case RN's correction skips, which
is why a compressed lineHeight rides off-centre on a TextInput. */}
Expand Down
Loading
Loading