Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

Overlay

Overlay показывает своих потомков поверх всего в корневом представлении, в котором он находится: окна или диалога вокруг него. Размер родителя, overflow: hidden и прокручиваемые представления его не обрезают. Во всём остальном он остаётся там, где вы его написали: он читает окружающий контекст, его обработчики принадлежат его компоненту, и он исчезает вместе с родителем. На нём построены поповеры, меню, подсказки и листы; 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(..)), чтобы открывать и закрывать. Оверлеи, показанные позже, лежат поверх более ранних, а оверлей внутри другого лежит поверх него.

#Размещение

Без anchor оверлей размещается как блок 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 }) там же: каретка поля ввода (pageBounds у onCaretMove), у которой открываются подсказки к набираемому тексту.
  • placement это сторона, с которой он располагается: "top", "bottom" (по умолчанию), "left" или "right". Если он не помещается с этой стороны, но помещается с противоположной, он располагается там; если не помещается ни с одной, он выбирает сторону, где больше места.
  • align выравнивает его вдоль этой стороны: "start", "center" (по умолчанию) или "end". Сверху и снизу от якоря начало и конец следуют направлению раскладки: начало это левый край, а при раскладке справа налево правый.
  • offset это зазор между якорем и оверлеем; margin это расстояние, которое он выдерживает до краёв безопасной области, не выходя за неё: он сдвигается вдоль своей стороны, чтобы поместиться.

Оверлей с якорем следует за ним: когда якорь прокручивается, перемещается или меняет размер, оверлей движется вместе с ним в том же кадре, в котором NSH выполняет раскладку. onPosition сообщает, где он оказался, чтобы стрелка могла указывать на якорь, а длинное меню могло ограничить свою высоту:

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

#Закрытие

Оверлей сообщает о том, что обычно закрывает всплывающий элемент, а решение оставляет вам:

  • onPressOutside срабатывает, когда палец или кнопка мыши нажимается вне оверлея, его якоря и вложенных в него оверлеев. Нажатие продолжает идти к тому, что под ним.
  • onCancel срабатывает по Escape, кнопке «Назад» в Android и жесту выхода VoiceOver в iOS, только для верхнего оверлея, который его слушает. Он перехватывает их раньше страниц и BackHandler.

Чтобы заблокировать экран за всплывающим элементом, поместите под него второй оверлей с 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Зазор между якорем и оверлеем.
margin.margin(px(..))number0Расстояние, которое он выдерживает до краёв безопасной области.
children.child(..)JSX.ElementнетЧто он показывает.
class, styleметоды стиляstring, StyleProp<ViewStyle>нетЕго внешний вид и, без якоря, его положение.

Он также принимает общие свойства представления.

#События

СобытиеRustОписание
onPressOutside.on_press_outside(|point| ..)Нажатие произошло вне него, его якоря и вложенных в него оверлеев (pagePosition, Point).
onCancel.on_cancel(|| ..)Escape, кнопка «Назад» в Android или жест выхода VoiceOver, пока он является верхним слушающим оверлеем.
onPosition.on_position(|position| ..)NSH разместил его: placement (сторона, с которой он оказался), bounds в корневом представлении и available (Size: место с этой стороны).

#Примечания по платформам

  • Каждая платформа показывает оверлей как нативное представление корневого представления окна (или диалога), которое отрисовывается и обрабатывает попадания так же, как окружающие его представления: UIKit, Android views, AppKit, GTK, Win32, GPUI и DOM.
  • Оверлей внутри Dialog показывается в этом диалоге, над его содержимым.
  • Оверлеи остаются в своём окне: меню, которое вышло бы за край окна, сдвигается обратно внутрь него.