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

العناصر

Overlay

يعرض Overlay عناصره الفرعية فوق كل شيء في العرض الجذري (root view) الموجود فيه: النافذة، أو مربع الحوار المحيط به. لا يقصّه حجم العنصر الأب ولا overflow: hidden ولا عروض التمرير. ويبقى في موضعه الذي كتبته فيه بالنسبة لكل شيء آخر: فهو يقرأ السياق المحيط به، ومعالجاته (handlers) تابعة لمكوّنه، ويزول بزوال عنصره الأب. تُبنى عليه النوافذ المنبثقة والقوائم وتلميحات الأدوات والصفائح (Popovers وMenus وTooltips وSheets)؛ ويوفّرها nsh/base جاهزة (Popover وMenu وTooltip وSheet)، أما Overlay فهو ما تستخدمه لبناء مكوّناتك الخاصة.

el-overlay.tsx
el-overlay.tsxLive
import { mount, Overlay, signal, Text, View, type ViewRef } from "nsh";
import { button, buttonText, desktop, Inspector, Panel, Segmented, Stage, ui, wide } from "./palette";

type Placement = "top" | "bottom" | "left" | "right";
type Align = "start" | "center" | "end";

function App() {
  const open = signal(false);
  const placement = signal<Placement>("bottom");
  const align = signal<Align>("center");
  const position = signal("—");
  const closed = signal("—");
  let anchor: ViewRef | undefined;

  const placements = () => (
    <Segmented
      label="placement"
      options={["top", "bottom", "left", "right"] as Placement[]}
      value={placement()}
      onChange={(v) => (placement.set(v), open.set(true))}
    />
  );
  const aligns = () => (
    <Segmented label="align" options={["start", "center", "end"] as Align[]} value={align()} onChange={(v) => (align.set(v), open.set(true))} />
  );
  const subject = () => (
    <>
      <View
        style={{
          width: 220,
          height: 72,
          overflow: "hidden",
          borderRadius: 14,
          borderWidth: 2,
          borderStyle: "dashed",
          borderColor: ui().faint,
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <View
          ref={anchor}
          onPress={() => open.set(!open())}
          {...button("accent", { align: "center" })}
        >
          <Text style={buttonText("accent")}>{open() ? "Close" : "Open"}</Text>
        </View>
        <If when={open()}>
          <Overlay
            anchor={anchor}
            placement={placement()}
            align={align()}
            offset={8}
            margin={12}
            onPosition={(e) =>
              position.set(
                `${e.placement} at ${Math.round(e.bounds.origin.x)}, ${Math.round(e.bounds.origin.y)} · ${Math.round(e.bounds.size.width)}×${Math.round(e.bounds.size.height)} · room ${Math.round(e.available.width)}×${Math.round(e.available.height)}`,
              )
            }
            onPressOutside={(e) => {
              closed.set(`onPressOutside at ${Math.round(e.pagePosition.x)}, ${Math.round(e.pagePosition.y)}`);
              open.set(false);
            }}
            onCancel={() => {
              closed.set("onCancel");
              open.set(false);
            }}
            style={{
              ...ui().card,
              borderRadius: 12,
              padding: 14,
              gap: 4,
              // Narrow beside the button, so it fits on a phone's side.
              maxWidth: !wide() && (placement() === "left" || placement() === "right") ? 130 : 220,
            }}
          >
            <Text style={{ fontSize: 15, fontWeight: "600", color: ui().text }}>On top of everything</Text>
            <Text style={{ fontSize: 13, color: ui().muted }}>
              Its box clips the button, not this overlay. Pick a side {wide() ? "on the right" : "above"}.
            </Text>
          </Overlay>
        </If>
      </View>
      <Text style={{ marginTop: 6, fontSize: 12, color: ui().faint }}>The dashed box has overflow: hidden</Text>
    </>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>overlay</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click the button, then click outside or press Escape" : "Tap the button, then tap outside"}
      </Text>

      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            <Stage>
              <View style={{ alignItems: "center" }}>{subject()}</View>
            </Stage>
            <Inspector>
              {placements()}
              {aligns()}
              <Panel title="onPosition">
                <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{position()}</Text>
              </Panel>
              <Panel title="closed by" grow>
                <Text style={{ fontSize: 13, color: closed() === "—" ? ui().text : ui().accent }}>{closed()}</Text>
              </Panel>
            </Inspector>
          </View>
        ) : (
          <>
            <View style={{ flexDirection: "row", gap: 12 }}>
              <View style={{ flex: 3 }}>{placements()}</View>
              <View style={{ flex: 2 }}>{aligns()}</View>
            </View>

            <View style={{ flex: 1, minHeight: 160, alignItems: "center", justifyContent: "center" }}>{subject()}</View>

            <View style={{ ...ui().inset, borderRadius: 12, padding: 12, gap: 6 }}>
              <Text style={{ fontSize: 12, color: ui().muted }}>
                onPosition <Text style={{ color: ui().text }}>{position()}</Text>
              </Text>
              <Text style={{ fontSize: 12, color: ui().muted }}>
                closed by <Text style={{ color: closed() === "—" ? ui().text : ui().accent }}>{closed()}</Text>
              </Text>
            </View>
          </>
        )
      }
    </View>
  );
}

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

fn more_button() -> impl IntoElement {
    let open = signal(false);
    let anchor = element_ref();
    view()
        .child(
            view()
                .element_ref(&anchor)
                .on_press(move || open.update(|o| *o = !*o))
                .px_3()
                .py_2()
                .child(text("More")),
        )
        .child(when(move || open.get(), move || {
            overlay()
                .anchor(&anchor)
                .placement(Placement::Bottom)
                .align(Align::Start)
                .offset(px(6.0))
                .margin(px(8.0))
                .on_press_outside(move |_: &Point<Points>| open.set(false))
                .on_cancel(move || open.set(false))
                .rounded_lg()
                .bg(white())
                .p_3()
                .shadow_lg()
                .child(text("Shown above everything"))
        }))
}

يأتي Overlay من nsh في TSX، وoverlay() من nsh_core::prelude في Rust. يظهر ما دام موجودًا في العرض: ضعه داخل If (في Rust: when(..)) لفتحه وإغلاقه. تظهر عناصر Overlay التي تُعرض لاحقًا فوق التي قبلها، وأي Overlay داخل آخر يظهر فوقه.

#التموضع

بدون anchor، يُوضع Overlay كصندوق position: fixed في العرض الجذري: فقيم top وleft وinset والحجم والهوامش تُحسب بالنسبة إلى النافذة بأكملها (أو مربع الحوار). class="inset-0" يغطيها، وهذا يناسب الخلفية الداكنة أو الطبقة التي تملأ النافذة كلها؛ أما class="bottom-0 left-0 right-0" فهو شريط على امتداد الأسفل.

مع anchor، يضعه NSH بجوار نقطة الارتكاز (anchor):

  • anchor هو عرض (ref)، أو Point ({ x, y }) بإحداثيات العرض الجذري، أو pageX / pageY للمسة (قائمة تظهر حيث يكون المؤشر)، أو Bounds ({ origin, size }) فيه: مثل مؤشر الإدخال في حقل نصي (pageBounds الخاص بـ onCaretMove)، حيث تُفتح اقتراحات لما تتم كتابته.
  • placement هو الجانب الذي يوضع عليه: "top" أو "bottom" (الافتراضي) أو "left" أو "right". وإذا لم يتسع له ذلك الجانب واتسع له الجانب المقابل، ذهب إليه؛ وإذا لم يتسع له أي منهما، وُضع على الجانب الأكثر اتساعًا.
  • align يحاذيه على امتداد ذلك الجانب: "start" أو "center" (الافتراضي) أو "end". فوق نقطة الارتكاز وتحتها، يتبع start وend اتجاه التخطيط: start هو الحافة اليسرى، أو الحافة اليمنى في التخطيط من اليمين إلى اليسار.
  • offset هو الفجوة بين نقطة الارتكاز وOverlay؛ وmargin هي المسافة التي يحافظ عليها من حواف المنطقة الآمنة (safe area) التي يبقى داخلها: فينزلق على امتداد جانبه ليتسع له المكان.

يتبع Overlay المرتكز نقطةَ ارتكازه: فعندما تتمرر نقطة الارتكاز أو تتحرك أو يتغير حجمها، يتحرك Overlay معها في الإطار نفسه الذي يضع فيه NSH التخطيط. ويخبرك onPosition أين استقر، فيمكن لسهم أن يشير إلى نقطة الارتكاز، أو لقائمة طويلة أن تحدّ من ارتفاعها:

<Overlay
  anchor={anchor}
  onPosition={(e) => {
    side.set(e.placement); // "top" when it flipped
    maxHeight.set(e.available.height);
  }}
/>

#الإغلاق

يبلّغ Overlay بما يغلق النوافذ المنبثقة عادةً، ويترك القرار لك:

  • onPressOutside يُطلق عندما ينزل إصبع أو زر الفأرة خارج Overlay ونقطة ارتكازه وعناصر Overlay الموجودة داخله. ويستمر الضغط إلى ما تحته.
  • onCancel يُطلق عند الضغط على Escape، أو زر الرجوع في Android، أو إيماءة الهروب في VoiceOver على iOS، وذلك لأعلى Overlay يستمع إليه فقط. ويستقبلها قبل الصفحات وBackHandler.

لحجب الشاشة خلف نافذة منبثقة، ضع Overlay ثانيًا تحتها مع class="inset-0" وonPress يغلق كليهما: هكذا يعمل Sheet وعرض Popover على الهاتف.

#الخصائص

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
anchor.anchor(&element_ref), .anchor_point(point), .anchor_bounds(bounds)ViewRef | Point | Bounds | nullلا شيءالعرض أو النقطة أو الصندوق الذي يوضع بالنسبة إليه. بدونه يكون صندوقًا ثابتًا في عرضه الجذري.
placement.placement(..)"top" | "bottom" | "left" | "right""bottom"الجانب من نقطة الارتكاز الذي يوضع عليه.
align.align(..)"start" | "center" | "end""center"كيف يحاذي نقطة الارتكاز على امتداد ذلك الجانب.
offset.offset(px(..))number0الفجوة بين نقطة الارتكاز وOverlay.
margin.margin(px(..))number0المسافة التي يحافظ عليها من حواف المنطقة الآمنة.
children.child(..)JSX.Elementلا شيءما يعرضه.
class, stylestyle methodsstring, StyleProp<ViewStyle>لا شيءمظهره، ومكانه عند عدم وجود نقطة ارتكاز.

كما يقبل الخصائص المشتركة للعروض.

#الأحداث

الحدثRustالوصف
onPressOutside.on_press_outside(|point| ..)حدث ضغط خارج Overlay ونقطة ارتكازه وعناصر Overlay الموجودة داخله (pagePosition، وPoint).
onCancel.on_cancel(|| ..)Escape أو زر الرجوع في Android أو إيماءة الهروب في VoiceOver، عندما يكون أعلى Overlay مستمعًا.
onPosition.on_position(|position| ..)وضعه NSH: placement (الجانب الذي استقر عليه)، وbounds في العرض الجذري، وavailable (Size: المساحة المتاحة على ذلك الجانب).

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

  • تعرض كل منصة Overlay كعرض أصلي (native view) ضمن العرض الجذري للنافذة (أو مربع الحوار)، يُرسم ويُختبر لمس الضغط عليه كما تُرسم العروض المحيطة به: UIKit وعروض Android وAppKit وGTK وWin32 وGPUI وDOM.
  • يظهر Overlay داخل Dialog في ذلك الحوار، فوق محتواه.
  • تبقى عناصر Overlay داخل نافذتها: فالقائمة التي كانت ستتجاوز حافة النافذة تنزلق عائدةً إلى داخلها.