Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Elementos

Overlay

Um overlay mostra seus filhos por cima de tudo na view raiz em que está: a janela, ou o diálogo ao redor dela. O tamanho do pai, overflow: hidden e as scroll views não o cortam. Para todo o resto, ele fica onde você o escreveu: lê o contexto ao redor, seus handlers são os do seu componente e ele some junto com o pai. Popovers, menus, tooltips e sheets são construídos sobre ele; nsh/base já traz esses componentes prontos (Popover, Menu, Tooltip, Sheet), e o overlay é o que você usa para construir os seus próprios.

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 vem de nsh em TSX e overlay() vem de nsh_core::prelude em Rust. Ele é exibido enquanto estiver na view: coloque-o em um If (Rust: when(..)) para abri-lo e fechá-lo. Overlays exibidos depois ficam por cima dos anteriores, e um overlay dentro de outro fica por cima dele.

#Posicionamento

Sem anchor, um overlay é posicionado como uma caixa position: fixed da sua view raiz: seus top, left, inset, tamanho e margens são relativos à janela inteira (ou ao diálogo). class="inset-0" a cobre, o que dá um backdrop ou uma camada de janela inteira; class="bottom-0 left-0 right-0" é uma barra ao longo da parte inferior.

Com anchor, o NSH o posiciona ao lado da âncora:

  • anchor é uma view (um ref), um Point ({ x, y }) nas coordenadas da view raiz, o pageX / pageY de um toque (um menu no ponto onde está o ponteiro), ou um Bounds ({ origin, size }) nela: o cursor de texto de um input (pageBounds de onCaretMove), onde se abrem as sugestões para o que está sendo digitado.
  • placement é o lado em que ele fica: "top", "bottom" (o padrão), "left" ou "right". Quando não cabe ali e cabe do lado oposto, ele vai para lá; quando não cabe em nenhum dos dois, vai para o lado com mais espaço.
  • align o alinha ao longo desse lado: "start", "center" (o padrão) ou "end". Acima e abaixo da âncora, start e end seguem a direção do layout: start é a borda esquerda, ou a borda direita em um layout da direita para a esquerda.
  • offset é o espaço entre a âncora e o overlay; margin é a distância que ele mantém das bordas da área segura, dentro da qual ele permanece: ele desliza ao longo do seu lado para caber.

Um overlay ancorado acompanha a sua âncora: quando a âncora rola, se move ou muda de tamanho, o overlay se move junto no mesmo frame em que o NSH faz o layout. onPosition informa para onde ele foi, de modo que uma seta possa apontar para a âncora ou um menu longo possa limitar a sua altura:

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

#Fechamento

Um overlay informa o que normalmente fecha um popup e deixa a decisão para você:

  • onPressOutside é disparado quando um dedo ou um botão do mouse é pressionado fora do overlay, da sua âncora e dos overlays dentro dele. O toque segue para o que está embaixo.
  • onCancel é disparado no Escape, no botão voltar do Android e no gesto de escape do VoiceOver no iOS, apenas para o overlay do topo que o escuta. Ele os captura antes das páginas e de BackHandler.

Para bloquear a tela atrás de um popup, coloque um segundo overlay sob ele com class="inset-0" e um onPress que feche os dois: é assim que funcionam o Sheet e a apresentação do Popover no celular.

#Props

Prop Rust Tipo Padrão Descrição
anchor .anchor(&element_ref), .anchor_point(point), .anchor_bounds(bounds) ViewRef | Point | Bounds | null nenhum A view, o ponto ou a caixa em relação à qual ele é posicionado. Sem uma âncora, é uma caixa fixa da sua view raiz.
placement .placement(..) "top" | "bottom" | "left" | "right" "bottom" O lado da âncora em que ele fica.
align .align(..) "start" | "center" | "end" "center" Como ele se alinha à âncora ao longo desse lado.
offset .offset(px(..)) number 0 O espaço entre a âncora e o overlay.
margin .margin(px(..)) number 0 A distância que ele mantém das bordas da área segura.
children .child(..) JSX.Element nenhum O que ele exibe.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum Sua aparência e, sem âncora, sua posição.

Ele também aceita as props comuns de uma view.

#Eventos

Evento Rust Descrição
onPressOutside .on_press_outside(|point| ..) Um toque foi feito fora dele, da sua âncora e dos overlays dentro dele (pagePosition, um Point).
onCancel .on_cancel(|| ..) Escape, o botão voltar do Android ou o gesto de escape do VoiceOver, enquanto ele for o overlay do topo que escuta.
onPosition .on_position(|position| ..) O NSH o posicionou: placement (o lado em que ele ficou), bounds na view raiz e available (um Size: o espaço disponível nesse lado).

#Notas de plataforma

  • Todas as plataformas exibem um overlay como uma view nativa da view raiz da janela (ou do diálogo), desenhada e testada para hit-testing como as views ao redor: UIKit, views do Android, AppKit, GTK, Win32, GPUI e o DOM.
  • Um overlay dentro de um Dialog é exibido nesse diálogo, acima do seu conteúdo.
  • Os overlays permanecem na sua janela: um menu que ultrapassaria a borda da janela desliza de volta para dentro dela.