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

Éléments

Popover

Un popover affiche du contenu à côté de son déclencheur, au-dessus de tout le reste : des détails, un petit formulaire, un sélecteur de couleur. Appuyer sur le déclencheur l’ouvre et le ferme ; un appui à l’extérieur, Échap, le bouton retour d’Android et le geste d’échappement de VoiceOver le ferment. Il se place sur un côté du déclencheur, passe de l’autre côté s’il n’y a pas de place, reste dans la zone de sécurité et suit le déclencheur lorsqu’il défile. Il ne dessine rien lui-même : la popup et son contenu appartiennent à l’app.

el-popover.tsx
el-popover.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Popover } from "nsh/base";
import { button, buttonText, desktop, Inspector, Log, PopIn, Segmented, Stage, ui, wide } from "./palette";

type Placement = "top" | "bottom" | "left" | "right";

function App() {
  const placement = signal<Placement>("bottom");
  const offset = signal(8);
  const events = signal<string[]>([]);
  const presses = signal(0);
  const placements = () => (
    <Segmented label="placement" options={["top", "bottom", "left", "right"] as Placement[]} value={placement()} onChange={(v) => placement.set(v)} />
  );
  const offsets = () => <Segmented label="offset" options={[0, 8, 16]} value={offset()} onChange={(v) => offset.set(v)} />;
  const popover = () => (
    <Popover
      placement={placement()}
      offset={offset()}
      margin={12}
      onOpenChange={({ open, reason }) => events.set([`${open ? "opened" : "closed"} · ${reason}`, ...events()].slice(0, 4))}
      trigger={
        <View {...button("accent")}>
          <Text style={buttonText("accent")}>Order #1024</Text>
        </View>
      }
    >
      {/* The panel grows out of the trigger's side each time it opens. */}
      <PopIn side={placement()} class="w-56 gap-2 rounded-xl p-4" style={ui().card}>
        <Text style={{ fontSize: 15, fontWeight: "600", color: ui().text }}>Shipped yesterday</Text>
        <Text style={{ fontSize: 13, color: ui().muted }}>Arriving Friday. The trigger, a press outside and Escape close this.</Text>
        <View style={{ flexDirection: "row", gap: 6, alignItems: "center" }}>
          <View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: ui().up }} />
          <Text style={{ fontSize: 12, color: ui().up }}>In transit</Text>
        </View>
      </PopIn>
    </Popover>
  );
  const count = () => (
    <View
      onPress={() => presses.update((n) => n + 1)}
      {...button("secondary", { align: "center" })}
    >
      <Text style={buttonText()}>Count · pressed {presses()}×</Text>
    </View>
  );
  const log = () => <Log title="onOpenChange({ open, reason })" lines={events()} minHeight={92} />;

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>popover</Text>
      <Text style={{ color: ui().muted }}>
        {desktop
          ? "Open it, then click “Count” below: one click closes it and presses the button"
          : "Open it, then tap “Count” below: the tap only closes it"}
      </Text>

      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            <Stage gap={24}>
              <View style={{ height: 280, alignItems: "center", justifyContent: "center" }}>{popover()}</View>
              {count()}
            </Stage>
            <Inspector>
              {placements()}
              {offsets()}
              {log()}
            </Inspector>
          </View>
        ) : (
          <>
            <View style={{ flexDirection: "row", gap: 12 }}>
              <View style={{ flex: 3 }}>{placements()}</View>
              <View style={{ flex: 2 }}>{offsets()}</View>
            </View>
            <View style={{ flex: 1, minHeight: 200, alignItems: "center", justifyContent: "center" }}>{popover()}</View>
            {count()}
            {log()}
          </>
        )
      }
    </View>
  );
}

mount(App);
use nsh_base::{OpenChangeReason, PopoverEvent, popover};
use nsh_core::prelude::*;

fn details() -> impl IntoElement {
    popover()
        .offset(px(6.0))
        .margin(px(8.0))
        .trigger(view().px_3().py_2().rounded_md().bg(rgb(0xe5e7eb)).child(text("Details")))
        .on_open_change(|e: &PopoverEvent| {
            if let PopoverEvent::OpenChange(open, OpenChangeReason::PressOutside) = e {
                log::info!("closed by a press outside: {open}");
            }
        })
        .child(
            view()
                .w(px(240.0))
                .gap_1()
                .p_3()
                .rounded_lg()
                .bg(white())
                .shadow_lg()
                .child(text("Order #1024").font_weight(FontWeight::SEMIBOLD))
                .child(text("Shipped yesterday, arriving Friday.")),
        )
}

Popover provient de nsh/base. Son contenu est recréé à chaque ouverture, donc l’état qu’il contient repart de zéro.

#État d’ouverture

Sans open, le popover gère son propre état, à partir de defaultOpen. Avec open, c’est l’app qui le possède : modifiez-le dans onOpenChange({ open, reason }), qui signale chaque demande d’ouverture ou de fermeture avec sa raison ("triggerPress", "pressOutside", "cancel").

const open = signal(false);

<Popover open={open()} onOpenChange={(e) => open.set(e.open)} trigger={<Text>Filters</Text>}>
  <FilterForm onApply={() => open.set(false)} />
</Popover>

closeOnEscape={false} le garde ouvert lors d’Échap et du retour (onCancel les reçoit toujours, pour demander d’abord) ; closeOnPressOutside={false} le garde ouvert lors des appuis à l’extérieur.

#Téléphones et ordinateurs

Un popover est ancré à son déclencheur sur tous les appareils : ce qu’il affiche appartient à côté de ce qui l’a ouvert.

  • Sur les ordinateurs de bureau et dans les navigateurs de bureau, un appui à l’extérieur le ferme et se poursuit sur ce qui se trouve sous le pointeur, de sorte qu’un seul clic ferme le popover et appuie sur le bouton visé.
  • Sur les téléphones et les tablettes, un appui à l’extérieur ne fait que le fermer : un fond transparent capte l’appui, comme le font les popovers du système.

Pour une liste d’actions, utilisez un Menu, qui devient une feuille sur les téléphones ; pour un panneau, utilisez un Sheet.

#Entrée et sortie

Un popover apparaît et disparaît avec vos transitions. popupStartingClass est l’apparence de la popup à sa première image, et les classes transition-* de popupClass l’amènent à sa propre apparence : c’est son entrée. popupEndingClass est son apparence une fois fermé : le popover reste, sans recevoir d’appuis, pendant que ces transitions s’exécutent, puis disparaît, et onClose suit. S’il est rouvert pendant sa sortie, il revient à son apparence depuis l’état où il se trouve. popupStartingStyle et popupEndingStyle font de même avec des styles en ligne. Lorsque l’utilisateur demande moins de mouvement, il apparaît et disparaît instantanément.

<Popover
  popupClass="rounded-lg bg-white p-3 shadow-lg transition-all duration-150 ease-out"
  popupStartingClass="opacity-0 scale-95"
  popupEndingClass="opacity-0 scale-95"
  onPosition={({ placement }) => side.set(placement)}
  trigger={<Text>Details</Text>}
>
  <OrderSummary />
</Popover>

onPosition({ placement, bounds, available }) indique où la popup est allée : le côté du déclencheur (l’autre côté lorsque placement manquait de place), son cadre et l’espace disponible de ce côté, afin qu’une flèche ou une entrée puisse pointer vers le déclencheur.

#Props

PropRustTypeDéfautDescription
open.open(..)booleanaucunOuvert, contrôlé par l’app.
defaultOpen.default_open(..)booleanfalseOuvert au départ, sans open.
placement.placement(..)"top" | "bottom" | "left" | "right""bottom"Le côté du déclencheur sur lequel il se place.
align.align(..)"start" | "center" | "end""center"Son alignement avec le déclencheur le long de ce côté.
offset.offset(..)number0L’espace entre le déclencheur et la popup.
margin.margin(..)number0La distance qu’il garde par rapport aux bords de la zone de sécurité.
closeOnEscape.close_on_escape(..)booleantrueÉchap et le bouton retour d’Android le ferment.
closeOnPressOutside.close_on_press_outside(..)booleantrueUn appui à l’extérieur le ferme.
popupClass, popupStyle.popup_class(..), .popup_style(..)string, StyleProp<ViewStyle>aucunL’apparence de la popup, la boîte dans laquelle se trouve le contenu.
popupStartingClass, popupStartingStyle.popup_starting_class(..), .popup_starting_style(..)string, StyleProp<ViewStyle>aucunL’apparence de la popup à sa première image, par-dessus la sienne : ses transitions partent de là.
popupEndingClass, popupEndingStyle.popup_ending_class(..), .popup_ending_style(..)string, StyleProp<ViewStyle>aucunL’apparence de la popup une fois fermée, par-dessus la sienne : elle reste pendant que ses transitions s’exécutent.
trigger.trigger(..)JSX.ElementaucunCe qui l’ouvre ; il est placé par rapport à lui. Tant que le popover est ouvert, ses classes aria-expanded: et expandedStyle s’appliquent.
children.child(..)JSX.ElementaucunLe contenu.

#Événements

ÉvénementRustDescription
onOpenChange.on_open_change(|e| ..)Il s’est ouvert ou fermé, avec la raison : "triggerPress", "pressOutside" ou "cancel".
onOpen.on_open(|| ..)Il est affiché.
onClose.on_close(|| ..)Il a disparu, après sa sortie ; également lorsque l’app l’a fermé.
onCancel.on_cancel(|| ..)L’utilisateur a tenté de le fermer (Échap ou retour, appui à l’extérieur), qu’il se soit fermé ou non.
onPosition.on_position(|e| ..)La popup a été placée : placement (le côté choisi), bounds (son cadre), available (l’espace disponible de ce côté).

#Accessibilité

Le déclencheur indique aux lecteurs d’écran si le popover est déployé. Échap et le bouton retour le ferment, comme les utilisateurs s’y attendent d’une popup. En TSX, le déclencheur est l’élément que vous passez, un Button ou une vue de votre cru : il prend lui-même en charge les appuis et l’état déployé, de sorte que ses propres classes active: et activeStyle s’appliquent pendant l’appui, et que son propre onPress s’exécute aussi.