Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

Éléments

Sheet

Une sheet est un panneau placé sur un bord de l’écran, au-dessus d’un fond (backdrop) qui recouvre le reste : filtres, panneau de partage, tiroir latéral. Un appui sur le backdrop, la touche Échap, le bouton retour d’Android et le geste d’échappement de VoiceOver la ferment. Elle s’affiche au-dessus de tout le contenu de sa fenêtre, de la même façon sur tous les appareils, et ne dessine rien par elle-même : le panneau et le backdrop sont ceux de l’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 provient de nsh/base. Son contenu est recréé à chaque ouverture. side la place en "bottom" (par défaut) ou en "top" de l’écran, sur toute la largeur de l’écran, ou à "left" ou "right", sur toute la hauteur de l’écran ; sa taille dans l’autre dimension est celle du panneau, déterminée par son contenu ou par popupClass.

Son glissement suit la taille mesurée du panneau pendant que le contenu est créé ou grandit durant l’entrée. Le contenu paresseux (lazy) et les mises à jour de taille asynchrones partagent la même progression d’animation ; le panneau se stabilise à sa taille courante.

#État d’ouverture

Sans open, la sheet conserve son propre état, en partant de defaultOpen. Avec open, c’est l’app qui le possède : modifiez-le dans onOpenChange({ open, reason }), qui signale chaque demande de fermeture avec son motif ("backdropPress", "cancel", "swipe"). onOpen et onClose l’accompagnent à l’entrée et à la sortie, que ce soit l’utilisateur ou l’app qui l’ait ouverte ou fermée.

closeOnEscape={false} la maintient ouverte face à Échap et au retour, closeOnBackdropPress={false} face aux appuis sur le backdrop ; onCancel les reçoit quand même, afin de pouvoir demander confirmation avant.

#Glisser une bottom sheet

Définissez closeOnSwipe pour qu’un panneau du bas suive un glissement vers le bas. Son backdrop s’estompe selon la distance ; un relâchement au-delà de 30 % de la hauteur du panneau, ou vers le bas à 800 points par seconde, demande la fermeture avec le motif "swipe". Un glissement plus court ramène le panneau en douceur. Un propriétaire en mode contrôlé peut laisser open à true pour ramener le panneau après une demande de fermeture. Avec la réduction des animations, le relâchement et l’ouverture/fermeture programmatiques se stabilisent immédiatement ; le glissement direct suit toujours le doigt.

Le panneau s’adapte à son contenu, y compris pour les menus courts ; utilisez popupStyle ou la hauteur d’un enfant pour contraindre un contenu plus haut. L’app dessine sa propre poignée, qui peut être n’importe quelle vue à l’intérieur du panneau. Sur iOS, un Scroll ou List vertical interne consomme le glissement jusqu’à son sommet, puis transmet le mouvement descendant restant au panneau au sein du même geste. Sur Android, les ordinateurs de bureau et le web, le glissement de la poignée ou du contenu en dehors d’un défilement utilise leur prise en charge native du déplacement (pan) ; les défilements internes conservent l’arbitrage de gestes normal de leur plateforme. Définir closeOnSwipe à false maintient le panneau immobile et préserve le contenu monté ainsi que la position de défilement. Les autres côtés conservent leur comportement existant.

#Sheets natives

Une sheet est dessinée par NSH dans la fenêtre de l’app, avec le style défini par l’app. Pour la sheet présentée par la plateforme elle-même (une page sheet sur iOS), utilisez un Dialog avec presentation="pageSheet".

#Props

PropRustTypeDéfautDescription
open.open(..)booleanaucunOuverte, contrôlée par l’app.
defaultOpen.default_open(..)booleanfalseOuverte au départ, sans open.
side.side(..)"top" | "bottom" | "left" | "right""bottom"Le bord où elle se place.
closeOnEscape.close_on_escape(..)booleantrueÉchap et le bouton retour d’Android la ferment.
closeOnBackdropPress.close_on_backdrop_press(..)booleantrueUn appui sur le backdrop la ferme.
closeOnSwipe.close_on_swipe(..)booleanfalseUn panneau du bas suit un glissement vers le bas et se ferme si le relâchement est suffisant.
popupClass, popupStyle.popup_class(..), .popup_style(..)string, StyleProp<ViewStyle>aucunL’apparence du panneau.
backdropClass, backdropStyle.backdrop_class(..), .backdrop_style(..)string, StyleProp<ViewStyle>aucunL’apparence du backdrop, par exemple une couleur translucide.
children.child(..)JSX.ElementaucunLe contenu.

#Événements

ÉvénementRustDescription
onOpenChange.on_open_change(|e| ..)Une demande de fermeture, avec son motif : "backdropPress", "cancel" ou "swipe".
onOpen.on_open(|| ..)Elle est entrée : tout est en place.
onClose.on_close(|| ..)Elle est partie : tout a disparu. Également lorsque l’app l’a fermée.
onCancel.on_cancel(|| ..)L’utilisateur a essayé de la fermer (Échap ou retour, appui sur le backdrop, balayage activé), qu’elle se soit fermée ou non.

Sur le web, l’intégration avec dialogs: "viewport" présente la sheet et son backdrop par-dessus la page hôte, y compris dans une iframe de même origine. Les frames cross-origin utilisent hostPlayerFrame.