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

Elementi

Popover

Un popover mostra un contenuto accanto al suo trigger, sopra a tutto il resto: dettagli, un piccolo modulo, un selettore di colore. Premere il trigger lo apre e lo chiude; una pressione all’esterno, Escape, il pulsante indietro di Android e il gesto di uscita di VoiceOver lo chiudono. Viene posizionato su un lato del trigger, passa sull’altro lato quando non c’è spazio, resta dentro la safe area e segue il trigger durante lo scorrimento. Non disegna nulla di suo: il popup e il suo contenuto sono dell’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 proviene da nsh/base. Il suo contenuto viene creato a ogni apertura, quindi lo stato al suo interno riparte da zero.

#Stato di apertura

Senza open, il popover mantiene un proprio stato, che parte da defaultOpen. Con open, lo stato è dell’app: lo si modifica in onOpenChange({ open, reason }), che segnala ogni richiesta di apertura o chiusura con il relativo motivo ("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} lo mantiene aperto su Escape e indietro (onCancel li riceve comunque, per chiedere prima); closeOnPressOutside={false} lo mantiene aperto sulle pressioni all’esterno.

#Telefoni e desktop

Un popover è ancorato al suo trigger su ogni dispositivo: ciò che mostra appartiene a ciò che lo ha aperto.

  • Sui desktop e nei browser desktop, una pressione all’esterno lo chiude e prosegue verso ciò che si trova sotto il puntatore, quindi un solo clic chiude il popover e preme il pulsante su cui è caduto.
  • Su telefoni e tablet, una pressione all’esterno si limita a chiuderlo: un backdrop trasparente intercetta la pressione, come fanno i popover di sistema.

Per un elenco di azioni usa un Menu, che sui telefoni diventa uno sheet; per un pannello usa uno Sheet.

#Ingresso e uscita

Un popover compare e scompare con le tue transizioni. popupStartingClass è l’aspetto del popup nel suo primo frame, e le classi transition-* di popupClass lo portano al suo aspetto definitivo: il suo ingresso. popupEndingClass è il suo aspetto una volta chiuso: il popover resta, senza ricevere pressioni, mentre queste transizioni sono in corso, poi scompare, e onClose segue. Se viene riaperto durante l’uscita, torna al suo aspetto a partire da dove si trova. popupStartingStyle e popupEndingStyle fanno lo stesso con gli stili inline. Quando l’utente chiede meno movimento, compare e scompare immediatamente.

<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 }) indica dove è finito il popup: il lato del trigger (l’altro quando placement non aveva spazio), il suo frame e lo spazio disponibile su quel lato, così una freccia o un ingresso possono puntare al trigger.

#Props

PropRustTipoPredefinitoDescrizione
open.open(..)booleannessunoAperto, controllato dall’app.
defaultOpen.default_open(..)booleanfalseAperto all’inizio, senza open.
placement.placement(..)"top" | "bottom" | "left" | "right""bottom"Il lato del trigger su cui si posiziona.
align.align(..)"start" | "center" | "end""center"Come si allinea al trigger lungo quel lato.
offset.offset(..)number0La distanza tra il trigger e il popup.
margin.margin(..)number0La distanza che mantiene dai bordi della safe area.
closeOnEscape.close_on_escape(..)booleantrueEscape e il pulsante indietro di Android lo chiudono.
closeOnPressOutside.close_on_press_outside(..)booleantrueUna pressione all’esterno lo chiude.
popupClass, popupStyle.popup_class(..), .popup_style(..)string, StyleProp<ViewStyle>nessunoL’aspetto del popup, il box in cui si trova il contenuto.
popupStartingClass, popupStartingStyle.popup_starting_class(..), .popup_starting_style(..)string, StyleProp<ViewStyle>nessunoL’aspetto del popup nel suo primo frame, sovrapposto al proprio: le sue transizioni partono da qui.
popupEndingClass, popupEndingStyle.popup_ending_class(..), .popup_ending_style(..)string, StyleProp<ViewStyle>nessunoL’aspetto del popup una volta chiuso, sovrapposto al proprio: resta finché le sue transizioni sono in corso.
trigger.trigger(..)JSX.ElementnessunoCiò che lo apre; il popover viene posizionato rispetto ad esso. Mentre il popover è aperto, si applicano le sue classi aria-expanded: e expandedStyle.
children.child(..)JSX.ElementnessunoIl contenuto.

#Eventi

EventoRustDescrizione
onOpenChange.on_open_change(|e| ..)Si è aperto o chiuso, con il motivo: "triggerPress", "pressOutside" o "cancel".
onOpen.on_open(|| ..)È mostrato.
onClose.on_close(|| ..)Non c’è più, dopo la sua uscita; anche quando è stato chiuso dall’app.
onCancel.on_cancel(|| ..)L’utente ha provato a chiuderlo (Escape o indietro, una pressione all’esterno), che si sia chiuso o meno.
onPosition.on_position(|e| ..)Il popup è stato posizionato: placement (il lato su cui è finito), bounds (il suo frame), available (lo spazio disponibile su quel lato).

#Accessibilità

Il trigger indica agli screen reader se il popover è espanso. Escape e il pulsante indietro lo chiudono, come gli utenti si aspettano da un popup. In TSX il trigger è l’elemento che passi, un Button o una vista tua: riceve da sé le pressioni e lo stato espanso, quindi le sue classi active: e activeStyle si applicano mentre è premuto, e anche il suo onPress viene eseguito.