Skip to content

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

react-native-text-input-with-layout

React Native's TextInput with onTextLayout: the lines it lays out, measured natively and reported the way <Text onTextLayout> reports them. Draw behind, around or on top of the text as it's typed — a background per line for story captions, highlights, line counts.

  • It's React Native's own TextInput, so every prop, event and ref method works as usual.
  • Measured from the platform's own text layout as it changes: TextKit 2 on iOS, the EditText's Layout on Android. No hidden copy of the text to keep in sync.
  • Each line also comes with contentX/contentWidth: its text without the space the line was wrapped at.
  • iOS and Android, New Architecture, no other dependencies.

Requirements

  • React Native with the New Architecture (a Fabric component; tested with React Native 0.86 and Expo SDK 57).
  • iOS 15.1 or newer, Android 7.0 (API 24) or newer.

Installation

npx expo install react-native-text-input-with-layout
# or
npm install react-native-text-input-with-layout

Then rebuild the app (npx expo run:ios / run:android, or a new development build): the library has native code, so it doesn't run in Expo Go. No config plugin is needed. In a React Native app without Expo, run pod install in ios/.

Usage

import { useState } from 'react';
import { View } from 'react-native';
import {
  TextInputWithLayout,
  type TextInputLayoutLine,
} from 'react-native-text-input-with-layout';

function Caption() {
  const [text, setText] = useState('');
  const [lines, setLines] = useState<TextInputLayoutLine[]>([]);

  return (
    <View>
      {/* A background behind each line's text, under the input. */}
      {lines.map((line, index) => (
        <View
          key={index}
          style={{
            position: 'absolute',
            left: line.contentX - 6,
            top: line.y,
            width: line.contentWidth + 12,
            height: line.height,
            borderRadius: 6,
            backgroundColor: '#e2486b',
          }}
        />
      ))}
      <TextInputWithLayout
        multiline
        value={text}
        onChangeText={setText}
        onTextLayout={(event) => setLines(event.nativeEvent.lines)}
        style={{ padding: 16, fontSize: 24, color: '#fff' }}
      />
    </View>
  );
}

Line frames are relative to the input's top-left corner, padding included, so a layer positioned over the input (as above, or with StyleSheet.absoluteFill in the input's parent) takes them as they are.

API

<TextInputWithLayout>

Takes every TextInput prop, plus onTextLayout. Its ref is the TextInput's (focus(), blur(), clear(), setSelection(), …).

The input is wrapped in a native view that measures it. Styles that place the input — margin*, position and the insets, flex*, alignSelf, width/height and their min/max, aspectRatio, zIndex, transform — go to that view; all the others (padding, border, background, font, …) stay on the input.

onTextLayout(event)

event.nativeEvent.lines holds one TextInputLayoutLine per rendered line, in order:

Field Description
text The line's characters, with the space or line break it ends with.
x, y Top-left corner of the line, relative to the input's top-left corner.
width Width of the line. On a wrapped line, this includes the space it was broken at, as in Text.
height Height of the line.
contentX Left edge of the line's text without its trailing whitespace. On a right-to-left line, it's moved right.
contentWidth Width of the line's text without its trailing whitespace. Use these two to draw behind the text.
ascender Distance from the top of the line to its baseline.
descender Distance from the baseline to the bottom of the line.
capHeight Height of a capital letter in the line's font.
xHeight Height of a lowercase "x" in the line's font.

All values are in points (dp). The fields are those of Text#onTextLayout, plus contentX and contentWidth.

It fires once the input is laid out, and again whenever its lines change: as text is typed or set from JS, and when a style or the width changes them. When the lines come out the same, it doesn't fire.

An empty input, and the line after a trailing line break, are reported as one empty line where the cursor goes.

Lines are in the coordinates of the text, not of the visible part of the input: when a multiline input with a fixed height scrolls, subtract its scroll offset (from onScroll) to place them on screen. A single-line input reports one line, which can be wider than the input when its text is.

<TextInputLayoutView>

The native view TextInputWithLayout is made of, for wrapping another TextInput-based component (say, one from a gesture or keyboard library). It takes ViewProps, onTextLayout, and value, the input's controlled value, which iOS needs to measure text set from JS. It measures the first TextInput inside it.

<TextInputLayoutView value={text} onTextLayout={onTextLayout}>
  <SomeTextInput value={text} onChangeText={setText} />
</TextInputLayoutView>

How it works

  • iOS: the view finds the UITextView (multiline) or UITextField of the input inside it, follows its text storage edits, and measures in its next layout pass. A UITextView is read through TextKit 2's NSTextLayoutManager and is never switched over to TextKit 1 (on iOS 15, or if something else already switched it, TextKit 1 is read instead). A UITextField doesn't wrap, so its single line is measured from its text.
  • Android: right before each draw, the view checks a small key (the Layout, its width, a hash of the text, the padding) and reads the EditText's Layout when it changed. React Native doesn't tell TextWatchers about text set from JS, so this catches every change; frames where nothing changed cost nothing. capHeight and xHeight are measured from "T" and "x", as React Native does for Text.

The values come from each platform's own text layout, so they differ between iOS and Android as much as the text does. On the web, the input renders as usual and onTextLayout doesn't fire.

Migrating from 0.1

0.2 replaces the input 0.1 drew itself, with Nitro, by React Native's own TextInput:

  • react-native-nitro-modules isn't needed anymore.
  • Every TextInput prop works. Events are TextInput's: onInputFocus/onInputBlur are onFocus/onBlur, and onSubmitEditing, onContentSizeChange and onSelectionChange get TextInput's events (the text is event.nativeEvent.text).
  • onTextLayout gets a native event: the lines are event.nativeEvent.lines (were event.lines). baseline is gone (it's line.y + line.ascender); contentX and contentWidth are new.
  • The ref is the TextInput's. measureLines() is gone: keep the lines from the last onTextLayout.
  • Types: TextLayout is TextInputLayoutLine, TextLayoutEvent is TextInputLayoutEvent, and TextInputWithLayoutInstance is ComponentRef<typeof TextInput>. TextInputWithLayoutView is gone; TextInputLayoutView is the wrapper described above.

Contributing

License

MIT


Made with create-react-native-library

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages