Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

Elementi

Sheet

Un sheet è un pannello posizionato su un bordo dello schermo sopra un backdrop che copre il resto: filtri, un pannello di condivisione, un drawer laterale. Una pressione sul backdrop, Escape, il pulsante Indietro di Android e il gesto di uscita di VoiceOver lo chiudono. Viene mostrato sopra a tutto nella sua finestra, allo stesso modo su ogni dispositivo, e non disegna nulla di proprio: il pannello e il backdrop sono dell’app.

el-sheet.tsx
el-sheet.tsxLive
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"))),
        )
}

Sheet proviene da nsh/base. Il suo contenuto viene creato ogni volta che si apre. side lo posiziona in "bottom" (il valore predefinito) o in "top" rispetto allo schermo, largo quanto lo schermo, oppure a "left" o "right", alto quanto lo schermo; la sua dimensione trasversale è quella del pannello, determinata dal contenuto o da popupClass.

Il suo scorrimento segue la dimensione misurata del pannello mentre il contenuto viene creato o cresce durante l’ingresso. Il contenuto lazy e gli aggiornamenti asincroni della dimensione condividono lo stesso avanzamento dell’animazione; il pannello si assesta alla sua dimensione corrente.

#Stato di apertura

Senza open, lo sheet mantiene un proprio stato, a partire da defaultOpen. Con open, lo stato appartiene all’app: lo si modifica in onOpenChange({ open, reason }), che segnala ogni richiesta di chiusura insieme al relativo motivo ("backdropPress", "cancel", "swipe"). onOpen e onClose lo seguono in entrata e in uscita, indipendentemente da chi lo ha aperto o chiuso.

closeOnEscape={false} lo mantiene aperto con Escape e Indietro, closeOnBackdropPress={false} con le pressioni sul backdrop; onCancel le riceve comunque, per poter chiedere prima.

#Trascinamento di un bottom sheet

Imposta closeOnSwipe per fare in modo che un pannello inferiore segua il trascinamento verso il basso. Il suo backdrop sfuma in base alla distanza; rilasciando oltre il 30% dell’altezza del pannello, o verso il basso a 800 punti al secondo, viene richiesta la chiusura con il motivo "swipe". Un trascinamento più breve fa tornare il pannello alla posizione iniziale in modo fluido. Un owner controllato può mantenere open a true per far tornare il pannello dopo una richiesta di chiusura. Con il movimento ridotto, il rilascio e l’apertura/chiusura programmatiche si assestano immediatamente; il trascinamento diretto continua a seguire il dito.

Il pannello si adatta al contenuto, compresi i menu brevi; usa popupStyle o l’altezza di un figlio per limitare i contenuti più alti. L’app disegna il proprio handle, che può essere qualsiasi view all’interno del pannello. Su iOS, uno Scroll o List verticale interno consuma il trascinamento fino alla sua cima e passa al pannello il movimento verso il basso rimanente all’interno dello stesso gesto. Su Android, desktop e web, il trascinamento dell’handle o del contenuto al di fuori di uno scroll usa il supporto nativo al pan; gli scroll interni mantengono il normale arbitraggio dei gesti della loro piattaforma. Impostare closeOnSwipe a false mantiene il pannello fermo e preserva il contenuto montato e la posizione di scroll. Gli altri lati mantengono il comportamento esistente.

#Sheet nativi

Uno sheet viene disegnato da NSH nella finestra dell’app, con lo stile definito dall’app. Per lo sheet presentato dalla piattaforma stessa (un page sheet su iOS), usa un Dialog con presentation="pageSheet".

#Props

PropRustTipoPredefinitoDescrizione
open.open(..)booleannessunoAperto, controllato dall’app.
defaultOpen.default_open(..)booleanfalseAperto all’inizio, senza open.
side.side(..)"top" | "bottom" | "left" | "right""bottom"Il bordo su cui si trova.
closeOnEscape.close_on_escape(..)booleantrueEscape e il pulsante Indietro di Android lo chiudono.
closeOnBackdropPress.close_on_backdrop_press(..)booleantrueUna pressione sul backdrop lo chiude.
closeOnSwipe.close_on_swipe(..)booleanfalseUn pannello inferiore segue il trascinamento verso il basso e si chiude con un rilascio sufficiente.
popupClass, popupStyle.popup_class(..), .popup_style(..)string, StyleProp<ViewStyle>nessunoL’aspetto del pannello.
backdropClass, backdropStyle.backdrop_class(..), .backdrop_style(..)string, StyleProp<ViewStyle>nessunoL’aspetto del backdrop, ad esempio un colore traslucido.
children.child(..)JSX.ElementnessunoIl contenuto.

#Eventi

EventoRustDescrizione
onOpenChange.on_open_change(|e| ..)Una richiesta di chiusura, con il motivo: "backdropPress", "cancel" o "swipe".
onOpen.on_open(|| ..)È entrato: è tutto presente.
onClose.on_close(|| ..)È uscito: è tutto scomparso. Anche quando l’app lo ha chiuso.
onCancel.on_cancel(|| ..)L’utente ha provato a chiuderlo (Escape o Indietro, una pressione sul backdrop, uno swipe abilitato), che si sia chiuso o meno.

Sul web, l’incorporamento con dialogs: "viewport" presenta lo sheet e il suo backdrop sopra la pagina host, compreso un iframe same-origin. I frame cross-origin usano hostPlayerFrame.