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.
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"))
}))
}import { Overlay, signal, Text, View, type ViewRef } from "nsh";
function MoreButton() {
const open = signal(false);
let anchor: ViewRef | undefined;
return (
<View>
<View ref={anchor} onPress={() => open.set(!open())} class="px-3 py-2">
<Text>More</Text>
</View>
<If when={open()}>
<Overlay
anchor={anchor}
placement="bottom"
align="start"
offset={6}
margin={8}
onPressOutside={() => open.set(false)}
onCancel={() => open.set(false)}
class="rounded-lg bg-white p-3 shadow-lg"
>
<Text>Shown above everything</Text>
</Overlay>
</If>
</View>
);
}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:
anchores una vista (unref), unPoint({ x, y }) en las coordenadas de la vista raíz, elpageX/pageYde un toque (un menú donde está el puntero), o unBounds({ origin, size }) en ellas: el cursor de un input (pageBoundsdeonCaretMove), donde se abren las sugerencias para lo que se está escribiendo.placementes 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.alignlo 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.offsetes el espacio entre el ancla y el overlay;margines 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:
onPressOutsidese 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.onCancelse 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 yBackHandler.
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.