Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

Elementos

Overlay

Un overlay muestra a sus hijos por encima de todo lo que hay en la vista raíz en la que está: la ventana, o el diálogo que lo rodea. Ni el tamaño de su padre, ni overflow: hidden, ni las vistas de scroll lo recortan. En todo lo demás se queda donde lo escribiste: lee el contexto que lo rodea, sus manejadores son los de su componente y desaparece junto con su padre. Los popovers, menús, tooltips y sheets se construyen sobre él; nsh/base los trae listos (Popover, Menu, Tooltip, Sheet), y un overlay es lo que usas para construir los tuyos.

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 viene de nsh en TSX y overlay() de nsh_core::prelude en Rust. Se muestra mientras está en la vista: colócalo dentro de un If (Rust: when(..)) para abrirlo y cerrarlo. Los overlays mostrados después quedan encima de los anteriores, y un overlay dentro de otro queda encima de este.

#Ubicación

Sin anchor, un overlay se dispone como una caja position: fixed de su vista raíz: sus top, left, inset, tamaño y márgenes se calculan respecto a toda la ventana (o diálogo). class="inset-0" la cubre, lo que sirve para un fondo o una capa de ventana completa; class="bottom-0 left-0 right-0" es una barra a lo largo del borde inferior.

Con anchor, NSH lo coloca junto al ancla:

  • anchor es una vista (un ref), un Point ({ x, y }) en las coordenadas de la vista raíz, el pageX / pageY de un toque (un menú donde está el puntero), o un Bounds ({ origin, size }) en ellas: el cursor de un input (pageBounds de onCaretMove), donde se abren las sugerencias para lo que se está escribiendo.
  • placement es el lado en el que se coloca: "top", "bottom" (el valor por defecto), "left" o "right". Cuando no cabe en ese lado y sí cabe en el opuesto, pasa al opuesto; cuando no cabe en ninguno, se coloca en el lado con más espacio.
  • align lo alinea a lo largo de ese lado: "start", "center" (el valor por defecto) o "end". Por encima y por debajo del ancla, start y end siguen la dirección del layout: start es el borde izquierdo, o el derecho en un layout de derecha a izquierda.
  • offset es el espacio entre el ancla y el overlay; margin es la distancia que mantiene respecto a los bordes del área segura, dentro de la cual permanece: se desliza a lo largo de su lado para encajar.

Un overlay anclado sigue a su ancla: cuando el ancla se desplaza, se mueve o cambia de tamaño, el overlay se mueve con ella en el mismo frame en que NSH lo dispone. onPosition indica dónde quedó, de modo que una flecha pueda apuntar al ancla o un menú largo pueda limitar su altura:

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

#Cierre

Un overlay informa de lo que suele cerrar un popup y te deja la decisión a ti:

  • onPressOutside se dispara cuando un dedo o un botón del ratón se presiona fuera del overlay, de su ancla y de los overlays que contiene. La pulsación continúa hacia lo que hay debajo.
  • onCancel se dispara con Escape, el botón atrás de Android y el gesto de escape de VoiceOver en iOS, solo para el overlay superior que lo escucha. Los recibe antes que las páginas y BackHandler.

Para bloquear la pantalla detrás de un popup, coloca debajo un segundo overlay con class="inset-0" y un onPress que cierre ambos: así funcionan Sheet y la presentación en teléfono de Popover.

#Props

Prop Rust Tipo Valor por defecto Descripción
anchor .anchor(&element_ref), .anchor_point(point), .anchor_bounds(bounds) ViewRef | Point | Bounds | null ninguno La vista, el punto o la caja respecto a la que se coloca. Sin ella es una caja fija de su vista raíz.
placement .placement(..) "top" | "bottom" | "left" | "right" "bottom" El lado del ancla en el que se coloca.
align .align(..) "start" | "center" | "end" "center" Cómo se alinea con el ancla a lo largo de ese lado.
offset .offset(px(..)) number 0 El espacio entre el ancla y el overlay.
margin .margin(px(..)) number 0 La distancia que mantiene respecto a los bordes del área segura.
children .child(..) JSX.Element ninguno Lo que muestra.
class, style métodos de estilo string, StyleProp<ViewStyle> ninguno Su aspecto y, sin ancla, su posición.

También acepta las props comunes de una vista.

#Eventos

Evento Rust Descripción
onPressOutside .on_press_outside(|point| ..) Una pulsación se presionó fuera de él, de su ancla y de los overlays que contiene (pagePosition, un Point).
onCancel .on_cancel(|| ..) Escape, el botón atrás de Android o el gesto de escape de VoiceOver, mientras es el overlay superior que escucha.
onPosition .on_position(|position| ..) NSH lo colocó: placement (el lado en el que quedó), bounds en la vista raíz y available (un Size: el espacio disponible en ese lado).

#Notas de plataforma

  • Cada plataforma muestra un overlay como una vista nativa de la vista raíz de la ventana (o del diálogo), dibujada y sometida a hit-testing igual que las vistas que la rodean: UIKit, Android views, AppKit, GTK, Win32, GPUI y el DOM.
  • Un overlay dentro de un Dialog se muestra en ese diálogo, por encima de su contenido.
  • Los overlays permanecen en su ventana: un menú que sobrepasaría el borde de la ventana se desliza hacia atrás para quedar dentro.