Elementos
Sheet
A sheet é um painel em uma borda da tela sobre um backdrop que cobre o resto: filtros, um painel de compartilhamento, um drawer lateral. Um toque no backdrop, Escape, o botão voltar do Android e o gesto de escape do VoiceOver o fecham. Ela aparece acima de tudo na sua janela, igual em todos os dispositivos, e não desenha nada por conta própria: o painel e o backdrop são do app.
import { mount, signal, Text, View } from "nsh";
import { Sheet } from "nsh/base";
import { button, buttonText, desktop, Inspector, Log, Panel, size, Stage, ui, wide } from "./palette";
type Side = "bottom" | "top" | "left" | "right";
/** The panel's corners and size across, by the edge it is on. */
function panel(side: Side) {
const r = 20;
const across = side === "left" || side === "right" ? { width: wide() ? 320 : 280 } : {};
const corners = {
bottom: { borderTopLeftRadius: r, borderTopRightRadius: r, paddingBottom: 32 },
top: { borderBottomLeftRadius: r, borderBottomRightRadius: r, paddingTop: 32 },
left: { borderTopRightRadius: r, borderBottomRightRadius: r },
right: { borderTopLeftRadius: r, borderBottomLeftRadius: r },
}[side];
return { ...ui().card, padding: 20, gap: 12, ...across, ...corners };
}
function App() {
const open = signal(false);
const side = signal<Side>("bottom");
const events = signal<string[]>([]);
const log = (e: string) => events.set([e, ...events()].slice(0, 6));
const show = (s: Side) => {
side.set(s);
open.set(true);
log(`open · side ${s}`);
};
/** A button that opens the sheet at side `s`. */
const opener = (s: Side) => (
<View onPress={() => show(s)} {...button("secondary", { on: () => s === side() })}>
<Text style={buttonText("secondary", { on: () => s === side() })}>side="{s}"</Text>
</View>
);
const changes = () => <Log title="onOpenChange({ open, reason }) · onCancel" lines={events()} minHeight={130} />;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>sheet</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Open a side; click the backdrop or press Escape to close it" : "Open a side; tap the backdrop or Android's back to close it"}
</Text>
{() =>
wide() ? (
<View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
{/* Each button at the edge its sheet comes from. */}
<Stage width={360} gap={12}>
<View style={{ alignItems: "center" }}>{opener("top")}</View>
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
{opener("left")}
{opener("right")}
</View>
<View style={{ alignItems: "center" }}>{opener("bottom")}</View>
</Stage>
<Inspector>
<Panel title="open · side">
<Text style={{ fontSize: 13, color: ui().text }}>
{open() ? "open" : "closed"} · {side()}
</Text>
</Panel>
{changes()}
</Inspector>
</View>
) : (
<>
<View style={{ flex: 1, minHeight: 200, alignItems: "center", justifyContent: "center" }}>
<View style={{ width: 240, gap: 8 }}>
<Each of={["top", "left", "right", "bottom"] as Side[]}>{(s) => opener(s())}</Each>
</View>
</View>
{changes()}
</>
)
}
<Sheet
open={open()}
closeOnSwipe
side={side()}
onOpenChange={({ open: next, reason }) => {
open.set(next);
log(`onOpenChange · ${next ? "opened" : "closed"} · ${reason}`);
}}
onCancel={() => log("onCancel")}
popupStyle={panel(side())}
backdropStyle={{ backgroundColor: "rgba(0,0,0,0.45)" }}
>
<Text style={{ fontSize: 17, fontWeight: "700", color: ui().text }}>Filters</Text>
<Text style={{ fontSize: 14, color: ui().muted }}>
A panel at the {side()} edge. {desktop ? "The backdrop and Escape close it." : "The backdrop and Android's back close it."}
</Text>
<View
onPress={() => {
open.set(false);
log("Done pressed");
}}
{...button("primary", { align: "flex-end" })}
>
<Text style={buttonText("primary")}>Done</Text>
</View>
</Sheet>
</View>
);
}
mount(App);use nsh_base::{SheetEvent, sheet};
use nsh_core::prelude::*;
fn filters(open: Signal<bool>) -> impl IntoElement {
sheet()
.open(move || Some(open.get()))
.on_open_change(move |e: &SheetEvent| {
if let SheetEvent::OpenChange(next, _) = e {
open.set(*next);
}
})
.child(
view()
.gap_3()
.p_6()
.rounded_t(px(16.0))
.bg(white())
.child(text("Filters").font_weight(FontWeight::SEMIBOLD))
.child(view().on_press(move || open.set(false)).child(text("Done"))),
)
}import { signal, Text, View } from "nsh";
import { Sheet } from "nsh/base";
function Filters() {
const open = signal(false);
return (
<>
<View onPress={() => open.set(true)}><Text>Filters</Text></View>
<Sheet
open={open()}
onOpenChange={(e) => open.set(e.open)}
popupClass="gap-3 rounded-t-2xl bg-white p-6"
backdropClass="bg-black/40"
>
<Text class="font-semibold">Filters</Text>
<View onPress={() => open.set(false)}><Text>Done</Text></View>
</Sheet>
</>
);
}Sheet vem de nsh/base. Seu conteúdo é criado a cada abertura. side a posiciona na parte "bottom" (o padrão) ou "top" da tela, com a largura da tela, ou à "left" ou à "right", com a altura da tela; seu tamanho na transversal é o do painel, definido pelo conteúdo ou por popupClass.
Sua animação de deslize acompanha o tamanho medido do painel à medida que o conteúdo é criado ou cresce durante a entrada. Conteúdo lazy e atualizações assíncronas de tamanho compartilham o mesmo progresso de animação; o painel se estabiliza no seu tamanho atual.
#Estado de abertura
Sem open, a sheet mantém seu próprio estado, começando em defaultOpen. Com open, o app é o dono do estado: altere-o em onOpenChange({ open, reason }), que informa cada solicitação de fechamento com o motivo ("backdropPress", "cancel", "swipe").
onOpen e onClose acompanham a entrada e a saída, independentemente de quem a abriu ou fechou.
closeOnEscape={false} a mantém aberta com Escape e voltar, closeOnBackdropPress={false} com toques no backdrop;
onCancel ainda os recebe, para perguntar antes.
#Arrastando uma bottom sheet
Defina closeOnSwipe para que um painel inferior acompanhe o arrasto para baixo. Seu backdrop desaparece gradualmente conforme a distância; soltar além de 30% da altura do painel, ou para baixo a 800 pontos por segundo, solicita o fechamento com o motivo "swipe".
Um arrasto menor faz o painel voltar suavemente. Um dono controlado pode manter open como true para devolver o painel após uma solicitação de fechamento.
Com movimento reduzido, soltar e abrir/fechar programaticamente se estabilizam imediatamente; o arrasto direto ainda acompanha o dedo.
O painel se ajusta ao conteúdo, incluindo menus curtos; use popupStyle ou a altura de um filho para limitar conteúdos mais altos.
O app desenha seu próprio handle, que pode ser qualquer view dentro do painel. No iOS, um Scroll ou List vertical interno consome o arrasto até chegar ao topo e passa o movimento descendente restante ao painel dentro do mesmo gesto.
No Android, em desktops e na web, arrastar o handle ou o conteúdo fora de uma área rolável usa o suporte nativo a pan; rolagens internas mantêm a arbitragem de gestos normal da sua plataforma. Definir closeOnSwipe como false mantém o painel parado e preserva o conteúdo montado e a posição de rolagem. Os demais lados mantêm o comportamento existente.
#Sheets nativas
Uma sheet é desenhada pelo NSH na janela do app, com estilo definido pelo app. Para a sheet apresentada pela própria plataforma (uma page sheet no iOS), use um Dialog com presentation="pageSheet".
#Props
| Prop | Rust | Tipo | Padrão | Descrição |
|---|---|---|---|---|
open |
.open(..) |
boolean |
nenhum | Aberta, controlada pelo app. |
defaultOpen |
.default_open(..) |
boolean |
false |
Aberta inicialmente, sem open. |
side |
.side(..) |
"top" | "bottom" | "left" | "right" |
"bottom" |
A borda em que ela fica. |
closeOnEscape |
.close_on_escape(..) |
boolean |
true |
Escape e o botão voltar do Android a fecham. |
closeOnBackdropPress |
.close_on_backdrop_press(..) |
boolean |
true |
Um toque no backdrop a fecha. |
closeOnSwipe |
.close_on_swipe(..) |
boolean |
false |
Um painel inferior acompanha o arrasto para baixo e fecha ao ser solto o suficiente. |
popupClass, popupStyle |
.popup_class(..), .popup_style(..) |
string, StyleProp<ViewStyle> |
nenhum | A aparência do painel. |
backdropClass, backdropStyle |
.backdrop_class(..), .backdrop_style(..) |
string, StyleProp<ViewStyle> |
nenhum | A aparência do backdrop, como uma cor translúcida. |
children |
.child(..) |
JSX.Element |
nenhum | O conteúdo. |
#Eventos
| Evento | Rust | Descrição |
|---|---|---|
onOpenChange |
.on_open_change(|e| ..) |
Uma solicitação de fechamento, com o motivo: "backdropPress", "cancel" ou "swipe". |
onOpen |
.on_open(|| ..) |
Ela entrou: está tudo lá. |
onClose |
.on_close(|| ..) |
Ela saiu: sumiu por completo. Também quando o app a fechou. |
onCancel |
.on_cancel(|| ..) |
O usuário tentou fechá-la (Escape ou voltar, um toque no backdrop, um deslize habilitado), tenha ela fechado ou não. |
Na web, incorporar com dialogs: "viewport" apresenta a sheet e seu backdrop sobre a página host, incluindo um iframe de mesma origem. Frames de origem cruzada usam hostPlayerFrame.