Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

Elementi

Input

Input è un campo di testo nativo: una UITextField (singola riga) o una UITextView (multiriga) su iOS e Mac Catalyst, una EditText su Android. La digitazione, il cursore, la selezione, la correzione automatica e l’autofill sono quelli della piattaforma. In Rust se ne crea uno con nsh_base::input(), in TSX con <Input> da 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))
}

#Controllato e non controllato

  • Controllato: passa value e aggiornalo in onChange. L’app può riscrivere ciò che l’utente ha digitato (rimuovere gli spazi, formattarlo): il testo mostra subito la modifica e viene corretto quando arriva il nuovo value. Ogni modifica nativa porta con sé un contatore, e un value proveniente dall’app viene applicato quando si basa sull’ultima modifica, così la digitazione più recente prevale sempre su un aggiornamento lento.
  • Non controllato: passa defaultValue (.default_value(..), oppure niente) e leggi il testo da onChange o onSubmit.

Le regole che devono valere mentre l’utente digita sono prop, gestite in modo nativo: maxLength limita la lunghezza e submitBehavior decide cosa fa il tasto di invio. Vedi moduli e tastiera.

#Prop

Oltre alle prop comuni (con style di tipo TextStyle: si applicano font, colore, allineamento e gli stili della vista):

#Testo

PropRustTipoPredefinitoDescrizione
value.value(..)stringIl testo, controllato dall’app.
defaultValue.default_value(..)stringIl testo iniziale.
placeholder.placeholder(..)stringMostrato mentre il campo è vuoto.
multiline.multiline(..)booleanfalsePiù righe; l’invio inserisce un a capo, a meno che submitBehavior non indichi diversamente.
scrollbar.scrollbar(..)boolean | ScrollbarThemetrueLa barra di scorrimento di un campo multiriga: l’indicatore della piattaforma, oppure la barra integrata dell’app una volta che ne ha impostata una (Scrollbar); un oggetto è un tema a sé, false nessuna.
maxLength.max_length(..)numberIl massimo numero di caratteri che l’utente può inserire.
readonly.readonly(..)booleanfalseL’utente può selezionare e copiare il testo ma non modificarlo.
masked.masked(..)booleanfalseIl testo è nascosto (password).
allowFontScalingbooleantrueSegue la dimensione del testo di sistema.
maxFontSizeMultipliernumber0Il limite superiore della scala di sistema (0 significa nessun limite).

#Tastiera

PropRustTipoPredefinitoDescrizione
keyboardType.keyboard_type(KeyboardType::Email)"default" | "number-pad" | "decimal-pad" | "numeric" | "numbers-and-punctuation" | "email-address" | "phone-pad" | "url""default"Il layout della tastiera (i valori; numbers-and-punctuation ha il segno meno su iOS).
returnKeyType.return_key_type(ReturnKeyType::Next)"default" | "done" | "go" | "next" | "search" | "send""default"L’etichetta del tasto di invio.
autoCapitalize.auto_capitalize(AutoCapitalize::None)"none" | "sentences" | "words" | "characters""sentences"Maiuscole automatiche.
autoCorrect.auto_correct(..)booleantrueLe correzioni del sistema.
autoComplete.auto_fill(AutoFill::OneTimeCode)AutoComplete | TextContentTypeCosa contiene il campo, per l’autofill su iOS, Android e web: "username", "current-password", "new-password", "one-time-code", "email", "tel", "postal-code", "cc-number", "off", …
pasteBehavior.paste_behavior(PasteBehavior::Report)"insert" | "report""insert"Cosa fa un incolla: "insert" inserisce il testo incollato, "report" non inserisce nulla, lasciando all’app la gestione di ciò che onPaste riporta. Immagini e file non vengono mai inseriti.
submitBehavior.submit_behavior(SubmitBehavior::Submit)"submit" | "blurAndSubmit" | "newline""blurAndSubmit" (singola riga), "newline" (multiriga)Cosa fa l’invio: "submit" riporta onSubmit e mantiene il focus, "blurAndSubmit" toglie anche il focus, "newline" inserisce un a capo (multiriga; una singola riga invia). Un campo multiriga che invia accetta comunque gli a capo con Maiusc+Invio oppure Option / Alt+Invio sulle tastiere hardware. "submit" mantiene la tastiera visibile (ad esempio per inviare più messaggi).
autoFocus.auto_focus(..)booleanfalseRiceve il focus quando appare.

#Cursore e selezione

PropRustTipoDescrizione
caret.caret(..)booleanIl cursore viene disegnato (predefinito true; false lo nasconde).
selectedRange.selected_range((start, end)){ start: number; end?: number }La selezione controllata, in unità UTF-16; end ha come valore predefinito start (un cursore).

Il cursore lampeggia finché la sua finestra è visibile e attiva. Nelle finestre GPUI su Linux resta visibile dopo dieci secondi senza digitare né spostarlo, come nelle app GTK, così un campo con focus lasciato a sé non consuma frame.

#Colori

I colori del placeholder, del cursore e dell’evidenziazione della selezione sono chiavi di stile, con le relative classi Tailwind, e metodi in Rust (UIKit colora il cursore e la selezione con un unico colore, quello del cursore se impostato):

Chiave di stileRustClasseDescrizione
placeholderColor.placeholder_color(..)placeholder:text-*Il colore del placeholder.
caretColor.caret_color(..)caret-*Il colore del cursore.
selectionColor.selection_color(..)selection:text-*Il colore dell’evidenziazione della selezione.
input()
    .placeholder("Search")
    .text_color(rgb(0x18181b))
    .placeholder_color(rgb(0x9ca3af))
    .caret_color(rgb(0x2563eb))
    .selection_color(rgb(0x93c5fd))

#iOS

Chiave iosRustTipoDescrizione
textContentType.auto_fill(..)AutoComplete | TextContentTypeI nomi di iOS per autoComplete ("oneTimeCode", "newPassword", …).
inputAccessoryViewID.input_accessory(id)stringIl nativeID di un InputAccessoryView (InputAccessoryView) mostrato sopra la tastiera mentre il campo ha il focus.
keyboardAppearance.keyboard_appearance(KeyboardAppearance::Dark)"default" | "light" | "dark"L’aspetto della tastiera; "default" segue l’aspetto del sistema. Le tastiere di Android seguono il tema del sistema.
use nsh_core::element::AutoFill;

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

#Eventi

EventoRustPayloadQuando
onChange.on_change(|text: &str| ..){ value }L’utente ha modificato il testo: value è il testo attuale.
onSubmit.on_submit(|text: &str| ..){ text }È stato premuto il tasto di invio in un campo che invia (singola riga, oppure submitBehavior "submit" / "blurAndSubmit").
onFocus / onBlur.on_focus / .on_blurnessunoIl campo ha ottenuto / perso il focus.
onSelectionChange.on_selection_change(|s: &Selection| ..){ start, end }Il cursore o la selezione si sono spostati (unità UTF-16).
onPaste.on_paste(|e: &PasteEvent| ..){ text, html, files, uris }L’utente ha incollato: il testo, la sua formattazione come HTML (Linux, web), le immagini (uno screenshot) come File, gli URL file: dei file locali. Prima del onChange del testo che inserisce (moduli e tastiera).
onCaretMove.on_caret_move(|e: &CaretEvent| ..){ bounds, pageBounds }Il cursore si è spostato, con la selezione oppure mentre il testo scorreva o andava a capo: bounds è la sua posizione nel campo, pageBounds nella finestra, il anchor di un Overlay (moduli e tastiera).
onKeyPress.on_key_press(|key: &str| ..){ key }Prima che il testo cambi: key è il testo digitato, "Backspace" o "Enter".
onContentSizeChange.on_content_size_change(|size: &Size<Points>| ..){ size }La dimensione del testo è cambiata (alla larghezza del campo se multiriga), mentre viene digitato; un campo vuoto riporta una riga vuota, indipendentemente dal suo placeholder.
onCompositionChange.on_composition_change(|e: &Composition| ..){ composing, text }La composizione di un input method è iniziata, cambiata o terminata: text è ciò che si sta componendo (pinyin prima che un carattere sia scelto, kana prima della conversione), "" quando termina. Il testo confermato raggiunge onChange prima della fine.

Un campo multiriga che cresce con il suo testo:

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

#Metodi del ref

MetodoRustDescrizione
focus()focus()Dà il focus al campo e mostra la tastiera.
blur()blur()Gli toglie il focus.
setSelection(start, end?)set_selection(start, end)Seleziona start..end (unità UTF-16); un cursore quando coincidono.

Spostare il focus al campo successivo all’invio:

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() da nsh (dismiss_keyboard() in Rust) nasconde la tastiera, qualunque sia il campo che la sta usando.

#Note sulle piattaforme

  • Su macOS, il testo a riga singola, i placeholder e i cursori condividono le metriche del font del campo nativo e sono centrati insieme all’interno del riquadro del contenuto. lineHeight partecipa al layout; gli editor multiriga lo usano anche per la spaziatura tra paragrafi.
  • Gli input all’interno di una scroll view vengono tenuti liberi dalla tastiera in modo nativo. Altrove usa un padding o un margin inferiore pari a env(keyboard-inset-height).
  • disabled disabilita il campo (senza focus) oltre ad applicare gli stili di disabilitazione; un campo readonly mantiene il focus e la selezione.
  • inputAccessoryViewID si applica su iOS, dove la tastiera ha una barra accessoria.