تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

لوحة المفاتيح

env(keyboard-inset-height) هو ارتفاع لوحة المفاتيح البرمجية فوق السطح، وقيمته 0 عندما تكون مخفية. عند استخدامه كحشو (padding) سفلي أو هامش (margin) سفلي للعرض (view)، فإنه يُبقي المحتوى فوق لوحة المفاتيح: عندما تظهر لوحة المفاتيح أو تختفي، يفسح العرض مساحة بمقدار ارتفاعها، ويتحرك التغيير بحركة متزامنة مع لوحة المفاتيح بشكل أصلي. استخدمه للمحتوى الواقع خارج عروض التمرير (scroll views)، مثل شريط الرسائل في أسفل شاشة الدردشة؛ أما حقول الإدخال داخل عرض تمرير أو List فيتم تمريرها إلى مجال الرؤية بشكل أصلي. ويأتي معه Keyboard.dismiss() الذي يخفي لوحة المفاتيح، وارتفاع لوحة المفاتيح، لاستخدامهما في التخطيطات الخاصة بك.

el-keyboard.tsx
el-keyboard.tsxLive
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(),
                ),
        )
}

يناسب ارتفاع لوحة المفاتيح الكامل عرضًا يصل إلى أسفل الشاشة، كما هو الحال مع محتوى الشاشة. وعندما يكون هناك شيء أسفله، مثل شريط علامات التبويب (tab bar)، اطرح ذلك بواسطة طول: 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))
}

هنا يغطي شريط علامات التبويب البالغ ارتفاعه 49 pt أسفل العرض جزءًا من ارتفاع لوحة المفاتيح أصلًا. لإيقاف إفساح المساحة، احذف المتغير من النمط، مثلًا بواسطة شرط على إشارة (signal).

#إخفاء لوحة المفاتيح

Keyboard.dismiss() من nsh يزيل التركيز عن حقل الإدخال الذي عليه التركيز، مما يخفي لوحة المفاتيح. وإذا لم يكن أي حقل إدخال عليه التركيز، فإنه يترك كل شيء كما هو. تستدعي مكونات 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()))
}

#ارتفاع لوحة المفاتيح

TSXRustالقيمة المُعادة
keyboardFrame()keyboard_height()، keyboard_duration()ارتفاع لوحة المفاتيح البرمجية فوق سطح NSH بالنقاط (0 عندما تكون مخفية)، ومدة حركة ظهورها أو اختفائها بالميلي ثانية.

كلاهما تفاعلي (reactive): أي تأثير (effect) أو تعبير JSX أو دالة نمط (style closure) تقرؤهما يُعاد تشغيلها عندما تظهر لوحة المفاتيح أو تختفي أو يتغير ارتفاعها.

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

الارتفاع هو الجزء من لوحة المفاتيح الذي يتداخل مع سطح NSH. وعندما تتقلص النافذة نفسها بسبب لوحة المفاتيح (نشاط Android يغيّر حجمه)، أو عندما يكون NSH مضمّنًا في عرض ينتهي فوق لوحة المفاتيح، يكون الارتفاع 0.

#ملاحظات حول المنصات

  • iOS: تتحرك المساحة المفسوحة بمدة لوحة المفاتيح نفسها ومنحنى تسارع وتباطؤ (ease-in-ease-out).
  • Android: حيثما تتقلص النافذة بسبب لوحة المفاتيح (نشاط يغيّر حجمه)، ينتهي المحتوى أصلًا فوق لوحة المفاتيح ويكون المتغير 0. وحيثما تتداخل لوحة المفاتيح مع العرض، فإنه يفسح المساحة كما في iOS.
  • أنظمة سطح المكتب وMac Catalyst تعتمد الكتابة بلوحة مفاتيح فعلية: يكون المتغير 0 ويحتفظ العرض بتخطيطه.
  • الويب: المتغير هو المتغير الخاص بالمتصفح (الخاص بواجهة VirtualKeyboard API).

راجع النماذج ولوحة المفاتيح لمعرفة ترتيب التركيز ومفاتيح الإرجاع وملحقات الإدخال، والمنطقة الآمنة لمتغيرات البيئة الأخرى.