このページは機械翻訳のため、誤りを含む場合があります。 英語の原文を読む

要素

Overlay

Overlay は、それが属するルートビュー(ウィンドウ、またはその周囲のダイアログ)内で、すべての要素の上に子要素を表示します。親のサイズ、overflow: hidden、スクロールビューによってクリップされることはありません。それ以外の点では、記述した位置に留まります。周囲のコンテキストを読み取り、ハンドラーはそのコンポーネントのものであり、親が消えると一緒に消えます。Popover、メニュー、ツールチップ、シートは Overlay を土台に作られています。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"))
        }))
}

TSX では Overlay は nsh から、Rust では overlay() は nsh_core::prelude から提供されます。Overlay はビューの中にある間だけ表示されます。開閉するには If(Rust では when(..))の中に置きます。後から表示された Overlay は先に表示されたものの上に重なり、別の Overlay の内側にある Overlay はその上に重なります。

#配置

anchor がない場合、Overlay はルートビューの position: fixed ボックスとしてレイアウトされます。top、left、inset、サイズ、マージンは、ウィンドウ(またはダイアログ)全体を基準にします。class="inset-0" は全体を覆うので、バックドロップやウィンドウ全面のレイヤーになります。class="bottom-0 left-0 right-0" は下端に沿ったバーです。

anchor を指定すると、NSH はアンカーの隣に Overlay を配置します。

  • 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 はセーフエリアの内側に留まり、収まるようにその側に沿ってスライドします。

アンカー付きの Overlay はアンカーに追従します。アンカーがスクロール、移動、またはサイズ変更されると、NSH がレイアウトを行う同じフレームで Overlay も一緒に動きます。onPosition は Overlay が配置された場所を伝えるため、矢印でアンカーを指したり、長いメニューの高さを制限したりできます。

<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 のエスケープジェスチャーで発火し、これを受け取るのは最上位の Overlay だけです。ページや BackHandler より先に受け取ります。

ポップアップの背後の画面をブロックするには、その下に class="inset-0" を指定した2つ目の 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(|| ..)Escape、Android の戻るボタン、または VoiceOver のエスケープジェスチャー。対象の Overlay が、これらを受け取る最上位の Overlay である場合に発生します。
onPosition.on_position(|position| ..)NSH が配置を行った。placement(配置された側)、ルートビューにおける bounds、available(Size:その側の余白)を含みます。

#プラットフォームに関する注意

  • どのプラットフォームでも、Overlay はウィンドウ(またはダイアログ)のルートビューのネイティブビューとして表示され、周囲のビューと同じように描画およびヒットテストが行われます。対象は UIKit、Android views、AppKit、GTK、Win32、GPUI、DOM です。
  • Dialog の内側にある Overlay は、そのダイアログ内でコンテンツの上に表示されます。
  • Overlay はウィンドウの外には出ません。ウィンドウの端からはみ出すメニューは、内側に戻るようにスライドします。