Diese Seite wurde maschinell übersetzt und kann Fehler enthalten. Das englische Original lesen

Elemente

Popover

Ein Popover zeigt Inhalte neben seinem Trigger und über allem anderen an: Details, ein kleines Formular, einen Farbwähler. Ein Druck auf den Trigger öffnet und schließt es; ein Druck außerhalb, Escape, die Android-Zurück-Taste und die Escape-Geste von VoiceOver schließen es. Es wird an einer Seite des Triggers platziert, wechselt auf die andere Seite, wenn dort kein Platz ist, bleibt innerhalb der Safe Area und folgt dem Trigger beim Scrollen. Es zeichnet nichts selbst: Das Popup und sein Inhalt gehören der 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 stammt aus nsh/base. Sein Inhalt wird bei jedem Öffnen neu erzeugt, daher beginnt der Zustand darin jedes Mal von vorn.

#Offener Zustand

Ohne open verwaltet das Popover seinen eigenen Zustand, beginnend bei defaultOpen. Mit open gehört der Zustand der App: Sie ändert ihn in onOpenChange({ open, reason }), das jede Anfrage zum Öffnen oder Schließen samt Grund meldet ("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} hält es bei Escape und Zurück offen (onCancel bekommt beides weiterhin mit, um vorher nachzufragen); closeOnPressOutside={false} hält es bei Drücken außerhalb offen.

#Smartphones und Desktops

Ein Popover ist auf jedem Gerät an seinem Trigger verankert: Was es zeigt, gehört neben das, was es geöffnet hat.

  • Auf Desktops und in Desktop-Browsern schließt ein Druck außerhalb das Popover und wird an das weitergegeben, was unter dem Zeiger liegt. Ein Klick schließt also das Popover und drückt zugleich die Schaltfläche, auf die er traf.
  • Auf Smartphones und Tablets schließt ein Druck außerhalb das Popover nur: Ein transparenter Hintergrund fängt den Druck ab, wie bei den Popovers des Systems.

Für eine Liste von Aktionen verwenden Sie ein Menu, das auf Smartphones zu einem Sheet wird; für ein Panel verwenden Sie ein Sheet.

#Ein- und Ausblenden

Ein Popover kommt und geht mit Ihren Transitions. popupStartingClass ist das Aussehen des Popups im ersten Frame, und die transition-*-Klassen von popupClass führen es zu seinem eigenen Aussehen: dem Einblenden. popupEndingClass ist sein Aussehen im geschlossenen Zustand: Das Popover bleibt bestehen und nimmt keine Drücke an, solange diese Transitions laufen, und verschwindet dann; danach folgt onClose. Wird es beim Ausblenden erneut geöffnet, kehrt es von seinem aktuellen Stand aus zu seinem Aussehen zurück. popupStartingStyle und popupEndingStyle leisten dasselbe mit Inline-Styles. Wenn der Benutzer weniger Bewegung wünscht, erscheint und verschwindet es sofort.

<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 }) gibt an, wohin das Popup gesetzt wurde: die Seite des Triggers (die andere, wenn dort für placement kein Platz war), sein Frame und den Platz auf dieser Seite, sodass ein Pfeil oder eine Einblendung auf den Trigger zeigen kann.

#Props

Prop Rust Typ Standard Beschreibung
open .open(..) boolean keiner Geöffnet, von der App gesteuert.
defaultOpen .default_open(..) boolean false Anfangs geöffnet, ohne open.
placement .placement(..) "top" | "bottom" | "left" | "right" "bottom" Die Seite des Triggers, auf der es erscheint.
align .align(..) "start" | "center" | "end" "center" Wie es sich entlang dieser Seite am Trigger ausrichtet.
offset .offset(..) number 0 Der Abstand zwischen Trigger und Popup.
margin .margin(..) number 0 Der Abstand, den es zu den Rändern der Safe Area einhält.
closeOnEscape .close_on_escape(..) boolean true Escape und die Android-Zurück-Taste schließen es.
closeOnPressOutside .close_on_press_outside(..) boolean true Ein Druck außerhalb schließt es.
popupClass, popupStyle .popup_class(..), .popup_style(..) string, StyleProp<ViewStyle> keiner Das Aussehen des Popups, der Box, in der der Inhalt liegt.
popupStartingClass, popupStartingStyle .popup_starting_class(..), .popup_starting_style(..) string, StyleProp<ViewStyle> keiner Das Aussehen des Popups im ersten Frame, zusätzlich zu seinem eigenen: Seine Transitions laufen von dort aus.
popupEndingClass, popupEndingStyle .popup_ending_class(..), .popup_ending_style(..) string, StyleProp<ViewStyle> keiner Das Aussehen des Popups im geschlossenen Zustand, zusätzlich zu seinem eigenen: Es bleibt bestehen, solange seine Transitions laufen.
trigger .trigger(..) JSX.Element keiner Was es öffnet; das Popover wird daran ausgerichtet. Solange das Popover geöffnet ist, gelten dessen aria-expanded:-Klassen und expandedStyle.
children .child(..) JSX.Element keiner Der Inhalt.

#Ereignisse

Ereignis Rust Beschreibung
onOpenChange .on_open_change(|e| ..) Es wurde geöffnet oder geschlossen, mit dem Grund: "triggerPress", "pressOutside" oder "cancel".
onOpen .on_open(|| ..) Es wird angezeigt.
onClose .on_close(|| ..) Es ist verschwunden, nach dem Ausblenden; auch dann, wenn die App es geschlossen hat.
onCancel .on_cancel(|| ..) Der Benutzer hat versucht, es zu schließen (Escape oder Zurück, ein Druck außerhalb), unabhängig davon, ob es sich geschlossen hat.
onPosition .on_position(|e| ..) Das Popup wurde platziert: placement (die Seite, auf der es erscheint), bounds (sein Frame), available (der Platz auf dieser Seite).

#Barrierefreiheit

Der Trigger teilt Screenreadern mit, ob das Popover erweitert ist. Escape und die Zurück-Taste schließen es, wie es Benutzer von einem Popup erwarten. In TSX ist der Trigger das Element, das Sie übergeben, ein Button oder eine eigene View: Er übernimmt die Drücke und den Erweitert-Zustand selbst, sodass seine eigenen active:-Klassen und activeStyle gelten, solange er gedrückt wird, und sein eigenes onPress ebenfalls ausgeführt wird.