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.
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 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 (umref), umPoint({ x, y }) nas coordenadas da view raiz, opageX/pageYde um toque (um menu no ponto onde está o ponteiro), ou umBounds({ origin, size }) nela: o cursor de texto de um input (pageBoundsdeonCaretMove), 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.aligno 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 deBackHandler.
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.