Elementi
Overlay
Un overlay mostra i suoi figli sopra a tutto nella vista radice in cui si trova: la finestra, o il dialog che la circonda.
Né la dimensione del genitore, né overflow: hidden, né le scroll view lo ritagliano. Per tutto il resto rimane dove lo hai scritto:
legge il contesto che lo circonda, i suoi handler sono quelli del suo componente e scompare insieme al suo genitore. Popover,
menu, tooltip e sheet sono costruiti su di esso; nsh/base li offre già pronti
(Popover, Menu, Tooltip, Sheet), mentre l’overlay è ciò che
usi per costruire i tuoi.
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 proviene da nsh in TSX e overlay() da nsh_core::prelude in Rust. Viene mostrato finché è presente nella
vista: mettilo in un If (Rust: when(..)) per aprirlo e chiuderlo. Gli overlay mostrati dopo stanno sopra a quelli precedenti, e
un overlay dentro un altro sta sopra a quest’ultimo.
#Posizionamento
Senza anchor, un overlay viene disposto come un box position: fixed della sua vista radice: i suoi top, left, inset,
la dimensione e i margini sono riferiti all’intera finestra (o al dialog). class="inset-0" la ricopre, ed è un backdrop o un
livello a tutta finestra; class="bottom-0 left-0 right-0" è una barra lungo il bordo inferiore.
Con anchor, NSH lo posiziona accanto all’ancora:
anchorè una vista (unref), unPoint({ x, y }) nelle coordinate della vista radice, le coordinatepageX/pageYdi un tocco (un menu dove si trova il puntatore), oppure unBounds({ origin, size }) in quelle coordinate: il cursore di un input (pageBoundsdionCaretMove), dove si aprono i suggerimenti per ciò che si sta digitando.placementè il lato su cui si posiziona:"top","bottom"(il valore predefinito),"left"o"right". Quando non c’entra da quel lato ma c’entra sul lato opposto, va sull’altro lato; quando non c’entra in nessuno dei due, va sul lato con più spazio.alignlo allinea lungo quel lato:"start","center"(il valore predefinito) o"end". Sopra e sotto l’ancora, start ed end seguono la direzione del layout: start è il bordo sinistro, oppure il bordo destro in un layout da destra a sinistra.offsetè lo spazio tra l’ancora e l’overlay;marginè la distanza che mantiene dai bordi della safe area, all’interno della quale resta: scorre lungo il proprio lato per adattarsi.
Un overlay ancorato segue la sua ancora: quando l’ancora scorre, si sposta o cambia dimensione, l’overlay si muove con essa nello
stesso frame in cui NSH lo dispone. onPosition indica dove è finito, così una freccia può puntare all’ancora o un menu lungo può
limitare la propria altezza:
<Overlay
anchor={anchor}
onPosition={(e) => {
side.set(e.placement); // "top" when it flipped
maxHeight.set(e.available.height);
}}
/>
#Chiusura
Un overlay segnala ciò che di solito chiude un popup e lascia a te la decisione:
onPressOutsideviene emesso quando un dito o un pulsante del mouse viene premuto fuori dall’overlay, dalla sua ancora e dagli overlay al suo interno. La pressione prosegue verso ciò che si trova sotto.onCancelviene emesso con Escape, il pulsante indietro di Android e il gesto di escape di VoiceOver su iOS, solo per l’overlay più in alto che lo ascolta. Li intercetta prima delle pagine e diBackHandler.
Per bloccare lo schermo dietro un popup, metti sotto di esso un secondo overlay con class="inset-0" e un onPress che chiuda
entrambi: è così che funzionano Sheet e la presentazione su telefono di Popover.
#Props
| Prop | Rust | Tipo | Default | Descrizione |
|---|---|---|---|---|
anchor | .anchor(&element_ref), .anchor_point(point), .anchor_bounds(bounds) | ViewRef | Point | Bounds | null | nessuno | La vista, il punto o il box rispetto a cui viene posizionato. Senza di esso è un box fisso della sua vista radice. |
placement | .placement(..) | "top" | "bottom" | "left" | "right" | "bottom" | Il lato dell’ancora su cui si posiziona. |
align | .align(..) | "start" | "center" | "end" | "center" | Come si allinea all’ancora lungo quel lato. |
offset | .offset(px(..)) | number | 0 | Lo spazio tra l’ancora e l’overlay. |
margin | .margin(px(..)) | number | 0 | La distanza che mantiene dai bordi della safe area. |
children | .child(..) | JSX.Element | nessuno | Ciò che mostra. |
class, style | metodi di stile | string, StyleProp<ViewStyle> | nessuno | Il suo aspetto e, senza un’ancora, la sua posizione. |
Accetta anche le prop comuni di una vista.
#Eventi
| Evento | Rust | Descrizione |
|---|---|---|
onPressOutside | .on_press_outside(|point| ..) | Una pressione è avvenuta fuori da esso, dalla sua ancora e dagli overlay al suo interno (pagePosition, un Point). |
onCancel | .on_cancel(|| ..) | Escape, il pulsante indietro di Android o il gesto di escape di VoiceOver, mentre è l’overlay più in alto in ascolto. |
onPosition | .on_position(|position| ..) | NSH lo ha posizionato: placement (il lato su cui è andato), bounds nella vista radice e available (un Size: lo spazio disponibile su quel lato). |
#Note sulle piattaforme
- Ogni piattaforma mostra un overlay come una vista nativa della vista radice della finestra (o del dialog), disegnata e sottoposta a hit-test come le viste che la circondano: UIKit, Android views, AppKit, GTK, Win32, GPUI e il DOM.
- Un overlay all’interno di un Dialog viene mostrato in quel dialog, sopra al suo contenuto.
- Gli overlay restano nella propria finestra: un menu che supererebbe il bordo della finestra scorre all’indietro per rientrarvi.