本頁由機器翻譯,可能有錯誤。 閱讀英文原文

元素

Overlay

Overlay 會將其子元素顯示在所在根視圖(視窗,或其外圍的對話框)中所有內容之上。父元素的大小、overflow: hidden 和捲動視圖都不會裁切它。在其他方面,它仍留在你撰寫它的位置:它會讀取周圍的 context,其處理函式屬於它所在的元件,並且會隨父元素一同消失。Popover、選單、工具提示和 sheet 都以它為基礎建構;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 在 TSX 中來自 nsh,overlay() 在 Rust 中來自 nsh_core::prelude。它在視圖中存在時就會顯示:將它放入 If(Rust:when(..))即可開啟與關閉。較晚顯示的 overlay 位於較早顯示的 overlay 之上,而位於另一個 overlay 內的 overlay 則位於其上方。

#位置

沒有 anchor 時,overlay 會作為其根視圖的 position: fixed 方塊進行佈局:它的 top、left、inset、尺寸與邊距都是相對於整個視窗(或對話框)。class="inset-0" 會覆蓋整個畫面,可用作背景遮罩或全視窗圖層;class="bottom-0 left-0 right-0" 則是沿著底部的一條列。

有 anchor 時,NSH 會將它放在錨點旁邊:

  • anchor 可以是視圖(ref)、根視圖座標中的 Point({ x, y })、觸控的 pageX / pageY(選單出現在指標所在處),或該處的 Bounds({ origin, size }):輸入框的插入符號(onCaretMove 的 pageBounds),也就是輸入內容的建議開啟之處。
  • placement 是它所在的一側:"top"、"bottom"(預設)、"left" 或 "right"。當該側放不下而另一側放得下時,它會移到另一側;兩側都放不下時,則放在空間較大的一側。
  • align 決定它沿該側如何對齊:"start"、"center"(預設)或 "end"。在錨點的上方與下方,start 與 end 會依佈局方向而定:start 是左緣,在由右至左的佈局中則是右緣。
  • offset 是錨點與 overlay 之間的間距;margin 是它與安全區域邊緣保持的距離,它會停留在安全區域內:它會沿著所在的一側滑動以符合空間。

錨點式 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 返回鍵,以及 iOS 上 VoiceOver 的 escape 手勢時觸發,且僅限於監聽它的最上層 overlay。它會先於頁面和 BackHandler 取得這些操作。

若要封鎖彈出視窗後方的畫面,請在它下方再放一個帶有 class="inset-0" 的 overlay,並搭配一個會同時關閉兩者的 onPress:Sheet 與 Popover 的手機版呈現就是這樣運作的。

#Props

PropRust型別預設值說明
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、style樣式方法string、StyleProp<ViewStyle>無它的外觀,以及在沒有錨點時它所在的位置。

它也接受視圖的通用 props。

#事件

事件Rust說明
onPressOutside.on_press_outside(|point| ..)在 overlay、其錨點以及其內部的 overlay(pagePosition、Point)之外按下。
onCancel.on_cancel(|| ..)在它是正在監聽的最上層 overlay 時,按下 Escape、Android 返回鍵或 VoiceOver 的 escape 手勢。
onPosition.on_position(|position| ..)NSH 已完成定位:placement(它所在的一側)、根視圖中的 bounds,以及 available(Size:該側的可用空間)。

#平台說明

  • 每個平台都將 overlay 顯示為視窗(或對話框)根視圖的原生視圖,其繪製與命中測試方式與周圍的視圖相同:UIKit、Android views、AppKit、GTK、Win32、GPUI 與 DOM。
  • Dialog 內的 overlay 會顯示在該對話框中,位於其內容之上。
  • Overlay 會留在其所屬的視窗內:會超出視窗邊緣的選單會滑回視窗內。