Éléments
Clavier
env(keyboard-inset-height) est la hauteur du clavier logiciel au-dessus de la surface, 0 tant qu’il est masqué. Utilisée comme padding ou margin du bas d’une vue, elle maintient le contenu au-dessus du clavier : lorsque le clavier s’affiche ou se masque, la vue fait de la place de sa hauteur, et le changement s’anime nativement avec le clavier. Utilisez-la pour le contenu en dehors des scroll views, comme une barre de message en bas d’un écran de discussion ; les champs de saisie à l’intérieur d’une scroll view ou d’une List sont amenés à l’écran nativement par défilement. Elle s’accompagne de Keyboard.dismiss(), qui masque le clavier, et de la hauteur du clavier, pour vos propres mises en page.
import { mount, Scroll, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { Button, Switch } from "nsh/base";
import { Keyboard, keyboardFrame } from "nsh";
import { button, buttonText, desktop, field, tint, ui } from "./palette";
function App() {
const draft = signal("");
const messages = signal(["Type below: the bar stays above the keyboard."]);
const behavior = signal<"padding" | "height">("padding");
const enabled = signal(true);
const send = () => {
if (!draft().trim()) return;
messages.update((list) => [...list, draft().trim()]);
draft.set("");
};
const chip = (on: () => boolean) => button("secondary", { on, size: "sm" });
const chipText = (on: () => boolean) => buttonText("secondary", { on });
const isPadding = () => behavior() === "padding";
const isHeight = () => behavior() === "height";
return (
<View
style={{
flex: 1,
backgroundColor: behavior() === "height" ? tint(ui().info, 0.12) : ui().page,
// Bottom padding (or margin) by the keyboard's height, while enabled.
...(!enabled() ? {} : behavior() === "padding" ? { paddingBottom: "env(keyboard-inset-height)" } : { marginBottom: "env(keyboard-inset-height)" }),
}}
>
<View style={{ padding: 20, paddingTop: 24, paddingBottom: 8, gap: 8 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>keyboard inset</Text>
<Text style={{ color: ui().muted }}>
{desktop
? "Desktops type with a hardware keyboard: its height stays 0 and the view keeps its layout. Open this on a phone to see it move."
: "Tap the message field: the bar moves up with the keyboard."}
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 6 }}>
<Button onPress={() => behavior.set("padding")} selected={isPadding()} {...chip(isPadding)}>
<Text style={chipText(isPadding)}>padding</Text>
</Button>
<Button onPress={() => behavior.set("height")} selected={isHeight()} {...chip(isHeight)}>
<Text style={chipText(isHeight)}>height</Text>
</Button>
<Switch
checked={enabled()}
onChange={({ checked }) => enabled.set(checked)}
class="flex-row items-center gap-2 px-1 active:opacity-70"
trackClass="w-9 rounded-full p-0.5 transition-colors motion-reduce:transition-none"
trackStyle={enabled() ? ui().accentFill : ui().inset}
thumbClass="size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4"
thumbStyle={enabled() ? { backgroundColor: ui().onAccent } : ui().raised}
>
<Text style={{ color: ui().text, fontSize: 13, fontWeight: "600" }}>enabled {String(enabled())}</Text>
</Switch>
</View>
<Text style={{ color: ui().accent, fontSize: 13 }}>
keyboardFrame(): height {Math.round(keyboardFrame().height)} · duration {Math.round(keyboardFrame().duration)} ms
</Text>
</View>
<Scroll style={{ flex: 1 }} keyboardDismissMode="interactive">
<View style={{ padding: 20, paddingTop: 4, gap: 8 }}>
<Each of={messages()}>
{(text) => (
<View style={{ ...ui().row, alignSelf: "flex-start", maxWidth: 320, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8 }}>
<Text style={{ color: ui().text }}>{text()}</Text>
</View>
)}
</Each>
</View>
</Scroll>
<View style={{ flexDirection: "row", gap: 8, padding: 12, paddingHorizontal: desktop ? 20 : 12, borderTopWidth: 1, borderTopColor: ui().border, backgroundColor: ui().page }}>
<Input
value={draft()}
onChange={({ value }) => draft.set(value)}
onSubmit={send}
testID="message"
returnKeyType="send"
placeholder="Message"
{...field({ style: { flex: 1 } })}
/>
<Button onPress={send} {...button("primary")}>
<Text style={buttonText("primary")}>Send</Text>
</Button>
<Button onPress={() => Keyboard.dismiss()} {...button()}>
<Text style={buttonText()}>Done</Text>
</Button>
</View>
</View>
);
}
mount(App);use nsh_core::prelude::*;
fn chat() -> impl IntoElement {
let draft = signal(String::new());
view()
.flex_1()
.bg(white())
.pb_keyboard()
.child(scroll().flex_1().child(text("Messages…").p_4()))
.child(
view()
.flex_row()
.gap_2()
.p_2()
.border_t_1()
.border_color(rgb(0xe5e5e5))
.child(
input()
.value(draft)
.on_change(move |v: &str| draft.set(v.to_owned()))
.placeholder("Message")
.flex_1()
.p_2(),
),
)
}import { mount, Scroll, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
function App() {
const draft = signal("");
return (
<View style={{ flex: 1, backgroundColor: "#ffffff", paddingBottom: "env(keyboard-inset-height)" }}>
<Scroll style={{ flex: 1 }} keyboardDismissMode="interactive">
<Text style={{ padding: 16, paddingTop: 48 }}>Messages…</Text>
</Scroll>
<View style={{ flexDirection: "row", gap: 8, padding: 8, borderTopWidth: 1, borderTopColor: "#e5e5e5" }}>
<Input
value={draft()}
onChange={({ value }) => draft.set(value)}
placeholder="Message"
style={{ flex: 1, fontSize: 16, padding: 8 }}
/>
</View>
</View>
);
}
mount(App);La hauteur complète du clavier convient à une vue qui atteint le bas de l’écran, comme le contenu d’un écran. Lorsque quelque chose se trouve en dessous, comme une barre d’onglets, soustrayez-le avec une longueur : calc(env(keyboard-inset-height) + -49px) maintient une vue au-dessus d’une barre d’onglets de 49 pt, et calc(env(keyboard-inset-height) + 12px) ajoute un espacement au-dessus du clavier. Un padding vaut au minimum 0.
#Padding ou margin
| Style | Comment la place est faite |
|---|---|
paddingBottom: "env(keyboard-inset-height)" (Tailwind pb-[env(keyboard-inset-height)], Rust .pb_keyboard()) | Le contenu au-dessus rétrécit ; l’arrière-plan atteint toujours le bas de l’écran. |
marginBottom: "env(keyboard-inset-height)" (Tailwind mb-[env(keyboard-inset-height)], Rust .mb_keyboard()) | La vue entière, arrière-plan compris, s’arrête en haut du clavier. |
La hauteur du clavier n’est ajoutée à la longueur que dans la même déclaration ; une vue la combine donc avec son propre espacement dans calc(). En Rust, .pb_keyboard_plus(px(..)) et .mb_keyboard_plus(px(..)) ajoutent une longueur.
use nsh_core::prelude::*;
fn sign_in() -> impl IntoElement {
view()
.mb_keyboard_plus(px(-49.0))
.flex_1()
.p_4()
.gap_3()
.child(input().placeholder("E-mail").keyboard_type(KeyboardType::Email))
}<View class="flex-1 gap-3 p-4 mb-[calc(env(keyboard-inset-height)+-49px)]">
<Input placeholder="E-mail" keyboardType="email-address" class="rounded-lg border border-neutral-300 px-3 py-2" />
</View>Ici, une barre d’onglets de 49 pt située sous la vue couvre déjà une partie de la hauteur du clavier. Pour désactiver cette place, laissez la variable en dehors du style, par exemple avec une condition sur un signal.
#Masquer le clavier
Keyboard.dismiss(), provenant de nsh, retire le focus du champ de saisie ayant le focus, ce qui masque le clavier. Si aucun champ n’a le focus, rien ne change. Les composants Rust appellent dismiss_keyboard().
use nsh_base::button;
use nsh_core::prelude::*;
fn done() -> impl IntoElement {
button()
.on_press(|| dismiss_keyboard())
.rounded_lg()
.bg(rgb(0x2563eb))
.px_4()
.py_3()
.child(text("Done").text_color(white()))
}import { Text, Keyboard } from "nsh";
import { Button } from "nsh/base";
function Done() {
return (
<Button onPress={() => Keyboard.dismiss()} class="rounded-lg bg-blue-600 px-4 py-3 active:bg-blue-700">
<Text class="text-center font-semibold text-white">Done</Text>
</Button>
);
}#La hauteur du clavier
| TSX | Rust | Retourne |
|---|---|---|
keyboardFrame() | keyboard_height(), keyboard_duration() | La hauteur du clavier logiciel au-dessus de la surface NSH, en points (0 tant qu’il est masqué), et la durée de son animation d’affichage ou de masquage, en millisecondes. |
Les deux sont réactives : un effet, une expression JSX ou une closure de style qui les lit s’exécute à nouveau lorsque le clavier s’affiche, se masque ou change de hauteur.
use nsh_core::prelude::*;
fn message_bar() -> impl IntoElement {
view()
.flex_row()
.p_2()
.pb(move || {
// The home indicator's inset only matters while the keyboard is hidden.
let home = if keyboard_height() > Points::ZERO {
Points::ZERO
} else {
safe_area().bottom
};
px(8.0) + home
})
.child(input().placeholder("Message").flex_1())
}import { View, keyboardFrame, safeArea } from "nsh";
import { Input } from "nsh/base";
function MessageBar() {
const insets = safeArea();
return (
<View
style={{
flexDirection: "row",
padding: 8,
// The home indicator's inset only matters while the keyboard is hidden.
paddingBottom: 8 + (keyboardFrame().height > 0 ? 0 : insets().bottom),
}}
>
<Input placeholder="Message" style={{ flex: 1 }} />
</View>
);
}La hauteur correspond à la partie du clavier qui recouvre la surface NSH. Lorsque la fenêtre elle-même se réduit pour laisser place au clavier (une activité Android qui se redimensionne), ou lorsque NSH est intégré dans une vue qui se termine au-dessus du clavier, la hauteur vaut 0.
#Notes par plateforme
- iOS : la place s’anime avec la durée propre au clavier et une courbe ease-in-ease-out.
- Android : lorsque la fenêtre se réduit pour laisser place au clavier (une activité qui se redimensionne), le contenu se termine déjà au-dessus du clavier et la variable vaut 0. Lorsque le clavier recouvre la vue, elle fait de la place comme sur iOS.
- Ordinateurs de bureau et Mac Catalyst : la saisie se fait avec un clavier physique ; la variable vaut 0 et la vue conserve sa mise en page.
- Web : la variable est celle du navigateur (celle de l’API VirtualKeyboard).
Consultez Formulaires et clavier pour l’ordre du focus, les touches de retour et les accessoires de saisie, et Safe area pour les autres variables d’environnement.