Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

Éléments

Input

Input est un champ de texte natif : un UITextField (une seule ligne) ou un UITextView (plusieurs lignes) sur iOS et Mac Catalyst, un EditText sur Android. La saisie, le curseur, la sélection, la correction automatique et le remplissage automatique sont ceux de la plateforme. En Rust, on en crée un avec nsh_base::input(), en TSX avec <Input> depuis nsh/base.

el-input.tsx
el-input.tsxLive
import { type InputRef, type JSX, mount, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { button, buttonText, field, Inspector, Panel, Stage, ui, wide } from "./palette";

function App() {
  const name = signal("");
  const password = signal("");
  const notes = signal("");
  const submitted = signal("");
  let passwordInput!: InputRef;
  const signIn = () => submitted.set(`Signed in as ${name() || "nobody"}`);
  const nameField = () => (
    <Input
      {...field()}
      value={name()}
      onChange={({ value }) => name.set(value)}
      placeholder="Ada Lovelace"
      autoCapitalize="words"
      maxLength={20}
      returnKeyType="next"
      onSubmit={() => passwordInput.focus()}
    />
  );
  const count = () => <Text style={{ fontSize: 12, color: ui().muted }}>{name().length} / 20</Text>;
  const passwordField = () => (
    <Input
      ref={passwordInput}
      {...field()}
      value={password()}
      onChange={({ value }) => password.set(value)}
      masked
      autoComplete="current-password"
      returnKeyType="done"
      onSubmit={signIn}
    />
  );
  const notesField = () => (
    <Input
      {...field({ style: { height: wide() ? 80 : 100 } })}
      multiline
      value={notes()}
      onChange={({ value }) => notes.set(value)}
      placeholder="Anything else?"
    />
  );
  const signInButton = () => (
    <View onPress={signIn} disabled={password().length === 0} {...button("primary", { style: { marginTop: 4 } })}>
      <Text style={buttonText("primary")}>Sign in</Text>
    </View>
  );
  /** A label above its field, and what describes it below. */
  const group = (label: string, input: JSX.Element, after?: JSX.Element) => (
    <View style={{ gap: 6 }}>
      <Text style={{ fontSize: 13, color: ui().muted }}>{label}</Text>
      {input}
      {after}
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, padding: 20, paddingTop: 24, gap: 14 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>input</Text>
            <Text style={{ color: ui().muted }}>Tab moves between the fields; Return in the name goes to the password</Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <Stage width={340} gap={16}>
                {group("Name (controlled, maxLength 20)", nameField(), count())}
                {group("Password", passwordField())}
                {group("Notes (multiline)", notesField())}
                {signInButton()}
              </Stage>
              <Inspector>
                <Panel title="value">
                  <Text style={{ fontSize: 13, color: ui().text }}>name {name() ? `“${name()}”` : "—"}</Text>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>password {password().length} characters</Text>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>notes {notes().length} characters</Text>
                </Panel>
                <Panel title="onSubmit" grow>
                  <Text style={{ fontSize: 13, color: ui().up }}>{submitted()}</Text>
                </Panel>
              </Inspector>
            </View>
          </View>
        ) : (
          <View style={{ flex: 1, width: "100%", maxWidth: 480, alignSelf: "center", padding: 20, paddingTop: 24, gap: 10 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>input</Text>

            <Text style={{ color: ui().muted }}>Name (controlled, maxLength 20)</Text>
            {nameField()}
            {count()}

            <Text style={{ color: ui().muted }}>Password</Text>
            {passwordField()}

            <Text style={{ color: ui().muted }}>Notes (multiline)</Text>
            {notesField()}

            {signInButton()}
            <Text style={{ color: ui().up }}>{submitted()}</Text>
          </View>
        )
      }
    </View>
  );
}

mount(App);
use nsh_base::input;
use nsh_core::prelude::*;

fn name_field() -> impl IntoElement {
    let name = signal(String::new());
    input()
        .value(name)
        .on_change(move |text: &str| name.set(text.to_string()))
        .placeholder("Your name")
        .auto_capitalize(AutoCapitalize::Words)
        .return_key_type(ReturnKeyType::Done)
        .text_base()
        .p_3()
        .border_1()
        .border_color(rgb(0xd0d7de))
        .rounded(px(8.0))
}

#Contrôlé et non contrôlé

  • Contrôlé : passez value et mettez-le à jour dans onChange. L’application peut réécrire ce que l’utilisateur a saisi (le tronquer, le formater) : le texte affiche la modification immédiatement et est corrigé à l’arrivée du nouveau value. Chaque modification native porte un compteur, et un value de l’application est appliqué lorsqu’il repose sur la dernière modification, de sorte que la saisie la plus récente l’emporte toujours sur une mise à jour lente.
  • Non contrôlé : passez defaultValue (.default_value(..), ou rien) et lisez le texte depuis onChange ou onSubmit.

Les règles qui doivent s’appliquer pendant la saisie sont des props, traitées nativement : maxLength limite la longueur et submitBehavior décide de ce que fait la touche Retour. Voir formulaires et clavier.

#Props

En plus des props communes (avec style typé comme TextStyle : la police, la couleur, l’alignement et les styles de vue s’appliquent) :

#Texte

PropRustTypeDéfautDescription
value.value(..)stringLe texte, contrôlé par l’application.
defaultValue.default_value(..)stringLe texte initial.
placeholder.placeholder(..)stringAffiché tant que le champ est vide.
multiline.multiline(..)booleanfalsePlusieurs lignes ; Retour insère un saut de ligne, sauf si submitBehavior indique le contraire.
scrollbar.scrollbar(..)boolean | ScrollbarThemetrueLa barre de défilement d’un champ multiligne : l’indicateur de la plateforme, ou la barre intégrée de l’application dès qu’elle en a défini une (Scrollbar) ; un objet constitue un thème à part entière, false aucun.
maxLength.max_length(..)numberLe nombre maximal de caractères que l’utilisateur peut saisir.
readonly.readonly(..)booleanfalseL’utilisateur peut sélectionner et copier le texte, mais pas le modifier.
masked.masked(..)booleanfalseLe texte est masqué (mots de passe).
allowFontScalingbooleantrueSuit la taille de texte du système.
maxFontSizeMultipliernumber0La borne supérieure de l’échelle du système (0 signifie aucune).

#Clavier

PropRustTypeDéfautDescription
keyboardType.keyboard_type(KeyboardType::Email)"default" | "number-pad" | "decimal-pad" | "numeric" | "numbers-and-punctuation" | "email-address" | "phone-pad" | "url""default"La disposition du clavier (les valeurs ; numbers-and-punctuation comporte un signe moins sur iOS).
returnKeyType.return_key_type(ReturnKeyType::Next)"default" | "done" | "go" | "next" | "search" | "send""default"Le libellé de la touche Retour.
autoCapitalize.auto_capitalize(AutoCapitalize::None)"none" | "sentences" | "words" | "characters""sentences"Mise en majuscules automatique.
autoCorrect.auto_correct(..)booleantrueLes corrections du système.
autoComplete.auto_fill(AutoFill::OneTimeCode)AutoComplete | TextContentTypeCe que contient le champ, pour le remplissage automatique sur iOS, Android et le web : "username", "current-password", "new-password", "one-time-code", "email", "tel", "postal-code", "cc-number", "off", …
pasteBehavior.paste_behavior(PasteBehavior::Report)"insert" | "report""insert"Ce que fait un collage : "insert" insère le texte collé, "report" n’insère rien, pour que l’application traite ce que onPaste signale. Les images et les fichiers ne sont jamais insérés.
submitBehavior.submit_behavior(SubmitBehavior::Submit)"submit" | "blurAndSubmit" | "newline""blurAndSubmit" (une ligne), "newline" (multiligne)Ce que fait Retour : "submit" signale onSubmit et conserve le focus, "blurAndSubmit" retire aussi le focus, "newline" insère un saut de ligne (multiligne ; un champ d’une seule ligne valide). Un champ multiligne qui valide accepte tout de même les sauts de ligne avec Maj+Retour ou Option / Alt+Retour sur les claviers physiques. "submit" garde le clavier affiché (par exemple pour envoyer plusieurs messages).
autoFocus.auto_focus(..)booleanfalseReçoit le focus à son apparition.

#Curseur et sélection

PropRustTypeDescription
caret.caret(..)booleanLe curseur est dessiné (true par défaut ; false le masque).
selectedRange.selected_range((start, end)){ start: number; end?: number }La sélection contrôlée, en unités UTF-16 ; end vaut par défaut start (un curseur).

Le curseur clignote tant que sa fenêtre est affichée et active. Dans les fenêtres GPUI sous Linux, il reste affiché après dix secondes sans saisie ni déplacement, comme dans les applications GTK, de sorte qu’un champ ayant le focus et laissé seul ne coûte aucune image.

#Couleurs

Les couleurs du placeholder, du curseur et de la surbrillance de la sélection sont des clés de style, avec leurs classes Tailwind, et des méthodes en Rust (UIKit teinte le curseur et la sélection avec une seule couleur, celle du curseur lorsqu’elle est définie) :

Clé de styleRustClasseDescription
placeholderColor.placeholder_color(..)placeholder:text-*La couleur du placeholder.
caretColor.caret_color(..)caret-*La couleur du curseur.
selectionColor.selection_color(..)selection:text-*La couleur de la surbrillance de la sélection.
input()
    .placeholder("Search")
    .text_color(rgb(0x18181b))
    .placeholder_color(rgb(0x9ca3af))
    .caret_color(rgb(0x2563eb))
    .selection_color(rgb(0x93c5fd))

#iOS

Clé iosRustTypeDescription
textContentType.auto_fill(..)AutoComplete | TextContentTypeLes noms d’iOS pour autoComplete ("oneTimeCode", "newPassword", …).
inputAccessoryViewID.input_accessory(id)stringLe nativeID d’un InputAccessoryView (InputAccessoryView) affiché au-dessus du clavier tant que le champ a le focus.
keyboardAppearance.keyboard_appearance(KeyboardAppearance::Dark)"default" | "light" | "dark"L’apparence du clavier ; "default" suit l’apparence du système. Les claviers d’Android suivent le thème du système.
use nsh_core::element::AutoFill;

input().auto_fill(AutoFill::OneTimeCode).keyboard_type(KeyboardType::Number)

#Événements

ÉvénementRustCharge utileQuand
onChange.on_change(|text: &str| ..){ value }L’utilisateur a modifié le texte : value est le texte actuel.
onSubmit.on_submit(|text: &str| ..){ text }La touche Retour a été pressée dans un champ qui valide (une seule ligne, ou submitBehavior "submit" / "blurAndSubmit").
onFocus / onBlur.on_focus / .on_bluraucuneLe champ a gagné / perdu le focus.
onSelectionChange.on_selection_change(|s: &Selection| ..){ start, end }Le curseur ou la sélection a bougé (unités UTF-16).
onPaste.on_paste(|e: &PasteEvent| ..){ text, html, files, uris }L’utilisateur a collé : le texte, sa mise en forme en HTML (Linux, web), les images (une capture d’écran) sous forme de File, les URL file: des fichiers locaux. Avant le onChange du texte qu’il insère (formulaires et clavier).
onCaretMove.on_caret_move(|e: &CaretEvent| ..){ bounds, pageBounds }Le curseur a bougé, avec la sélection ou lorsque le texte défile ou passe à la ligne : bounds est sa position dans le champ, pageBounds dans la fenêtre, le anchor d’un Overlay (formulaires et clavier).
onKeyPress.on_key_press(|key: &str| ..){ key }Avant que le texte ne change : key est le texte saisi, "Backspace" ou "Enter".
onContentSizeChange.on_content_size_change(|size: &Size<Points>| ..){ size }La taille du texte a changé (à la largeur du champ s’il est multiligne), au fil de la saisie ; un champ vide signale une ligne vide, quel que soit son placeholder.
onCompositionChange.on_composition_change(|e: &Composition| ..){ composing, text }Une composition de méthode de saisie a commencé, changé ou pris fin : text est ce qui est en cours de composition (pinyin avant le choix d’un caractère, kana avant la conversion), "" une fois terminée. Le texte validé parvient à onChange avant la fin.

Un champ multiligne qui s’agrandit avec son texte :

let height = signal(px(40.0));

input()
    .multiline(true)
    .on_content_size_change(move |size: &Size<Points>| {
        height.set(px((size.height.0 + 16.0).clamp(40.0, 120.0)));
    })
    .h(move || height.get())
    .px_3()
    .py_2()

#Méthodes de ref

MéthodeRustDescription
focus()focus()Donne le focus au champ et affiche le clavier.
blur()blur()Lui retire le focus.
setSelection(start, end?)set_selection(start, end)Sélectionne start..end (unités UTF-16) ; un curseur lorsqu’ils sont égaux.

Passer le focus au champ suivant avec Retour :

use nsh_core::element::AutoFill;

let password = element_ref();

view()
    .child(input().return_key_type(ReturnKeyType::Next).on_submit(move |_: &str| password.focus()))
    .child(input().element_ref(&password).masked(true).auto_fill(AutoFill::Password))

Keyboard.dismiss() depuis nsh (dismiss_keyboard() en Rust) masque le clavier, quel que soit le champ qui l’a.

#Notes par plateforme

  • Sur macOS, le texte d’une seule ligne, les placeholders et les curseurs partagent les métriques de police du champ natif et sont centrés ensemble dans la boîte de contenu. lineHeight participe à la mise en page ; les éditeurs multilignes l’utilisent aussi pour l’espacement des paragraphes.
  • Les champs placés dans une scroll view restent dégagés du clavier de façon native. Ailleurs, utilisez un padding ou une marge en bas égal à env(keyboard-inset-height).
  • disabled désactive le champ (pas de focus) en plus d’appliquer les styles désactivés ; un champ readonly conserve son focus et sa sélection.
  • inputAccessoryViewID s’applique sur iOS, où le clavier possède une barre d’accessoires.