要素
キーボード
env(keyboard-inset-height) は、サーフェス上に表示されるソフトウェアキーボードの高さです。キーボードが非表示のときは 0 になります。ビューの下パディングまたは下マージンとして使うと、コンテンツがキーボードの上に保たれます。キーボードが表示または非表示になると、ビューはその高さ分の余白を確保し、その変化はネイティブにキーボードと一緒にアニメーションします。チャット画面下部のメッセージバーのように、スクロールビューの外にあるコンテンツに使ってください。スクロールビューや List の内側にある入力欄は、ネイティブに表示位置までスクロールされます。あわせて、キーボードを非表示にする Keyboard.dismiss() と、独自のレイアウト向けにキーボードの高さも利用できます。
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);キーボードの高さ全体は、画面コンテンツのように画面の下端まで届くビューに適しています。タブバーなど、ビューの下に別の要素がある場合は、長さを使ってその分を差し引きます。calc(env(keyboard-inset-height) + -49px) は、高さ 49 pt のタブバーの上にビューを保ち、calc(env(keyboard-inset-height) + 12px) はキーボードの上に余白を加えます。パディングは 0 未満にはなりません。
#パディングかマージンか
| スタイル | 余白の確保方法 |
|---|---|
paddingBottom: "env(keyboard-inset-height)"(Tailwind では pb-[env(keyboard-inset-height)]、Rust では .pb_keyboard()) | 上側のコンテンツが縮みます。背景は画面の下端まで届いたままです。 |
marginBottom: "env(keyboard-inset-height)"(Tailwind では mb-[env(keyboard-inset-height)]、Rust では .mb_keyboard()) | 背景を含むビュー全体が、キーボードの上端で終わります。 |
キーボードの高さは、同じ宣言内の長さにのみ加算されます。そのため、ビュー自身の余白と組み合わせる場合は calc() に記述します。Rust では、.pb_keyboard_plus(px(..)) と .mb_keyboard_plus(px(..)) が長さを加算します。
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>この例では、ビューの下にある 49 pt のタブバーが、すでにキーボードの高さの一部を覆っています。余白の確保をオフにするには、たとえばシグナルの条件を使うなどして、スタイルから変数を外します。
#キーボードを非表示にする
nsh の Keyboard.dismiss() は、フォーカス中の入力欄のフォーカスを外し、キーボードを非表示にします。フォーカス中の入力欄がない場合は、何も変更しません。Rust コンポーネントでは 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>
);
}#キーボードの高さ
| TSX | Rust | 戻り値 |
|---|---|---|
keyboardFrame() | keyboard_height()、keyboard_duration() | NSH サーフェス上のソフトウェアキーボードの高さ(ポイント単位。非表示の間は 0)と、その表示・非表示アニメーションの所要時間(ミリ秒単位)。 |
どちらもリアクティブです。これらを読み取るエフェクト、JSX 式、スタイルのクロージャは、キーボードが表示・非表示になったとき、または高さが変わったときに再実行されます。
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>
);
}この高さは、キーボードのうち NSH サーフェスと重なる部分です。キーボードに合わせてウィンドウ自体が縮む場合(サイズ変更を行う Android のアクティビティ)や、キーボードの上で終わるビューに NSH が埋め込まれている場合、高さは 0 になります。
#プラットフォームに関する注意
- iOS: 余白は、キーボード自身の所要時間と ease-in-ease-out カーブでアニメーションします。
- Android: キーボードに合わせてウィンドウが縮む場合(サイズ変更を行うアクティビティ)、コンテンツはすでにキーボードの上で終わっており、変数は 0 です。キーボードがビューに重なる場合は、iOS と同様に余白を確保します。
- デスクトップと Mac Catalyst: ハードウェアキーボードで入力するため、変数は 0 のままで、ビューのレイアウトは変わりません。
- Web: 変数はブラウザ自身の値(VirtualKeyboard API の値)です。
フォーカス順序、リターンキー、入力アクセサリについてはフォームとキーボードを、その他の環境変数についてはセーフエリアを参照してください。