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

Elemente

Dialog

Ein Dialog stellt Inhalte über dem Rest der App in einer eigenen nativen Fensterebene dar: ein präsentierter View Controller unter iOS und Mac Catalyst, ein Dialog unter Android, eine Ebene über dem gesamten Fenster auf den Desktop-Plattformen und im Web. Er wird für Vollbild-Abläufe, Sheets und Dialoge verwendet. Die systemeigenen Gesten (ein Sheet nach unten wischen, die Zurück-Taste unter Android, Escape) funktionieren und werden nativ entschieden. In Rust erzeugt man einen Dialog mit nsh_base::dialog(), in TSX mit <Dialog> aus nsh/base.

el-dialog.tsx
el-dialog.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Button, Dialog, Switch } from "nsh/base";
import { button, buttonText, desktop, Inspector, Panel, Stage, tint, ui, wide } from "./palette";

const track = `${desktop ? "w-9" : "w-12"} rounded-full p-0.5 transition-colors motion-reduce:transition-none`;
const thumb = desktop ? "size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4" : "size-5.5 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-5";

function App() {
  const card = signal(false);
  const sheet = signal(false);
  const backdropCloses = signal(true);
  const cancels = signal(0);
  const log = signal("–");
  const openCard = () => (
    <Button onPress={() => card.set(true)} {...button("primary")}>
      <Text style={buttonText("primary")}>Open a card</Text>
    </Button>
  );
  const openSheet = () => (
    <Button onPress={() => sheet.set(true)} {...button()}>
      <Text style={buttonText()}>Open as a page sheet</Text>
    </Button>
  );
  const backdrop = () => (
    <Switch
      checked={backdropCloses()}
      onChange={({ checked }) => backdropCloses.set(checked)}
      class={wide() ? "flex-row items-center gap-2 self-center active:opacity-70" : "flex-row items-center gap-3 rounded-xl p-3 active:opacity-70"}
      style={wide() ? undefined : ui().row}
      trackClass={track}
      trackStyle={backdropCloses() ? ui().accentFill : ui().inset}
      thumbClass={thumb}
      thumbStyle={backdropCloses() ? { backgroundColor: ui().onAccent } : ui().raised}
    >
      <Text style={{ color: ui().text, flex: wide() ? undefined : 1, fontSize: desktop ? 13 : undefined }}>closeOnBackdropPress {String(backdropCloses())}</Text>
    </Switch>
  );
  const hint = desktop
    ? "Close with Escape or a click on the backdrop."
    : "Close with a tap on the backdrop, a swipe down or the back button. On iOS the sheet is a system pageSheet.";
  const state = () => `open: card ${String(card())} · sheet ${String(sheet())} · onCancel ${cancels()}×`;
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 12 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>dialog</Text>

      {() =>
        wide() ? (
          <>
            <Text style={{ color: ui().muted }}>{hint}</Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <Stage gap={16}>
                <View style={{ flexDirection: "row", justifyContent: "center", gap: 8 }}>
                  {openCard()}
                  {openSheet()}
                </View>
                {backdrop()}
              </Stage>
              <Inspector>
                <Panel title="open · onCancel">
                  <Text style={{ fontSize: 13, color: ui().text }}>{state()}</Text>
                </Panel>
                <Panel title="onOpenChange({ open, reason }) · onOpen" grow>
                  <Text style={{ fontSize: 13, fontWeight: "600", color: ui().accent }}>{log()}</Text>
                </Panel>
              </Inspector>
            </View>
          </>
        ) : (
          <View style={{ gap: 10 }}>
            {openCard()}
            {openSheet()}

            {backdrop()}

            <View style={{ ...ui().inset, borderRadius: 12, padding: 14, gap: 4 }}>
              <Text style={{ color: ui().muted, fontSize: 13 }}>{state()}</Text>
              <Text style={{ color: ui().accent, fontSize: 15, fontWeight: "600" }}>{log()}</Text>
            </View>
            <Text style={{ color: ui().faint, fontSize: 13 }}>{hint}</Text>
          </View>
        )
      }

      <Dialog
        open={card()}
        animation="fade"
        closeOnBackdropPress={backdropCloses()}
        onOpenChange={({ open, reason }) => {
          card.set(open);
          log.set(`onOpenChange({ open: ${open}, reason: "${reason}" })`);
        }}
        onCancel={() => cancels.update((n) => n + 1)}
        onOpen={() => log.set("onOpen: card")}
        backdropTestID="card-backdrop"
        backdropStyle={{ backgroundColor: tint("#000000", 0.45) }}
        style={{ ...ui().card, margin: "auto", width: 320, maxWidth: "90%", borderRadius: 18, padding: 20, gap: 12 }}
      >
        <Text style={{ color: ui().text, fontSize: 20, fontWeight: "700" }}>Delete this item?</Text>
        <Text style={{ color: ui().muted }}>
          {backdropCloses()
            ? "A press on the dimmed backdrop closes it."
            : "The backdrop only calls onCancel: close with a button."}
        </Text>
        <View style={{ flexDirection: "row", gap: 8, justifyContent: desktop ? "flex-end" : undefined }}>
          <Button
            onPress={() => {
              card.set(false);
              log.set("closed by the app: Keep");
            }}
            {...button("secondary", { style: desktop ? {} : { flex: 1 } })}
          >
            <Text style={buttonText()}>Keep</Text>
          </Button>
          <Button
            onPress={() => {
              card.set(false);
              log.set("closed by the app: Delete");
            }}
            {...button("danger", { style: desktop ? {} : { flex: 1 } })}
          >
            <Text style={buttonText("danger")}>Delete</Text>
          </Button>
        </View>
      </Dialog>

      <Dialog
        open={sheet()}
        presentation="pageSheet"
        onOpenChange={({ open, reason }) => {
          sheet.set(open);
          log.set(`onOpenChange({ open: ${open}, reason: "${reason}" })`);
        }}
        onCancel={() => cancels.update((n) => n + 1)}
        onOpen={() => log.set("onOpen: sheet")}
        backdropStyle={{ backgroundColor: tint("#000000", 0.3) }}
        // As tall as its content: a panel in the middle on the desktop, a
        // sheet along the bottom edge on a phone.
        style={
          desktop
            ? { backgroundColor: ui().page, margin: "auto", width: 480, maxWidth: "90%", maxHeight: "90%", borderRadius: 16 }
            : {
                backgroundColor: ui().page,
                marginTop: "auto",
                width: "100%",
                maxWidth: 560,
                maxHeight: "90%",
                alignSelf: "center",
                borderTopLeftRadius: 16,
                borderTopRightRadius: 16,
              }
        }
      >
        <View style={{ padding: 20, paddingTop: "calc(env(safe-area-inset-top) + 20px)", paddingRight: "calc(env(safe-area-inset-right) + 20px)", paddingBottom: "calc(env(safe-area-inset-bottom) + 20px)", paddingLeft: "calc(env(safe-area-inset-left) + 20px)", gap: 12 }}>
          <Text style={{ color: ui().text, fontSize: 22, fontWeight: "700" }}>A sheet</Text>
          <Text style={{ color: ui().muted }}>
            presentation="pageSheet": a system sheet on iOS that follows the finger when swiped down. Elsewhere the
            dialog is a layer over the window, and this panel is the app's own style.
          </Text>
          <Button onPress={() => sheet.set(false)} {...button("primary", { align: "flex-end" })}>
            <Text style={buttonText("primary")}>Close</Text>
          </Button>
        </View>
      </Dialog>
    </View>
  );
}

mount(App);

Der Dialog bestimmt sein Verhalten selbst und überlässt sein Aussehen der App: Seine Styles gestalten das Popup (die View, die die Kinder enthält), und in TSX fügen backdropClass / backdropStyle dahinter einen Backdrop hinzu.

use nsh_base::{button, dialog};
use nsh_core::prelude::*;

/// A blue button with a white label.
fn primary(label: &'static str, on_press: impl Fn() + 'static) -> impl IntoElement {
    button()
        .on_press(on_press)
        .items_center()
        .rounded_lg()
        .bg(rgb(0x2563eb))
        .px_4()
        .py_3()
        .active(|s| s.opacity(0.7))
        .child(text(label).font_weight(FontWeight::SEMIBOLD).text_color(white()))
}

fn example() -> impl IntoElement {
    let open = signal(false);
    view()
        .flex_1()
        .justify_center()
        .p_6()
        .child(primary("Open", move || open.set(true)))
        .child(when(open, move || {
            dialog()
                .presentation(ModalPresentation::PageSheet)
                .on_cancel(move || open.set(false))
                .flex_1()
                .bg(white())
                .child(
                    view()
                        .flex_1()
                        .px_5()
                        .pt_safe_area_plus(px(20.0))
                        .pb_safe_area_plus(px(20.0))
                        .gap_3()
                        .child(text("A sheet").text_2xl().font_weight(FontWeight::BOLD))
                        .child(primary("Close", move || open.set(false))),
                )
        }))
}

In TSX wird der Dialog angezeigt, solange open nicht false ist, und geschlossen, sobald es false wird. In Rust wird er präsentiert, solange er in der View ist, und entfernt, sobald er sie verlässt, sodass when ihn hält. In beiden Fällen gibt er seinem Inhalt die Safe-Area-Insets des Dialogs, die die env(safe-area-inset-*)-Variablen und safeAreaInsets() (cx.insets() in einer Rust-Komponente) darin auslesen. Button, ebenfalls aus nsh/base, ist ein von der App gestaltetes Press-Ziel.

nsh_core::prelude hat außerdem eine ältere, veraltete Funktion dialog: Importiere nsh_base::dialog wie oben beim Namen, dann ersetzt der explizite Import sie. Für den System-Alert oder das Action Sheet verwende AlertDialog.

#Props

Prop Rust Typ Standard Beschreibung
open in der View, solange offen (when(open, ..)) boolean true Wird angezeigt, solange der Wert true ist.
animation .animation(ModalAnimation::Fade) "slide" | "fade" | "none" "slide" Wie er eingeblendet und ausgeblendet wird.
transitionShader .transition_shader(TransitionShader::new(program)) { source: string; uniforms?: Record<string, ShaderUniform>; duration?: number } keine Er wird anstelle seines animation über einen WGSL-Shader ein- und ausgeblendet: shade(uv) liest den Hintergrund als sample_from und den Dialog als sample_to, während nsh.progress über duration Millisekunden (standardmäßig 350) von 0 bis 1 läuft und beim Schließen von 1 zurück auf 0. Wenn das System weniger Bewegung anfordert, verwendet er sein eigenes animation. Siehe Shader.
presentation .presentation(ModalPresentation::PageSheet) "fullScreen" | "pageSheet" | "formSheet" "fullScreen" Seine Form unter iOS: fullScreen bedeckt den Bildschirm mit einem transparenten Hintergrund; pageSheet und formSheet sind System-Sheets, die den Hintergrund sichtbar lassen (formSheet ist auf großen Bildschirmen ein kleineres, zentriertes Sheet).
closeOnEscape .close_on_escape(..) boolean true Escape und die Zurück-Taste unter Android schließen ihn nativ.
closeOnBackdropPress .close_on_backdrop_press(..) boolean true Ein Druck auf den Backdrop schließt ihn.
closeOnSwipe .close_on_swipe(..) boolean true Ein Sheet nach unten zu wischen (iOS, Browser auf Smartphones) schließt ihn nativ.
statusBar .status_bar(..) { foreground?, hidden?, animated? } keine iOS und Android: die Statusleiste, solange der Dialog offen ist, mit Vorrang vor der der Seiten (Status bar).
onOpen .on_open(..) () => void Das Einblenden ist abgeschlossen.
onClose .on_close(..) () => void Er ist verschwunden: Der Benutzer hat ihn geschlossen, oder die App hat ihn entfernt.
onCancel .on_cancel(..) () => void Der Benutzer hat versucht, ihn zu schließen (Escape, Zurück, ein Druck auf den Backdrop, ein Wischen nach unten), unabhängig davon, ob ein closeOn*-Flag das Schließen erlaubt hat.
onOpenChange .on_cancel(..) (e: { open: boolean; reason: "cancel" | "backdropPress" | "swipe" }) => void Der Benutzer hat ihn geschlossen: Halte open hier synchron. In Rust entfernt on_cancel den Dialog aus der View.
class, style Styled-Methoden string, StyleProp<ViewStyle> Das Popup: die View, die die Kinder enthält.
backdropClass, backdropStyle eine Kind-View, die den Dialog ausfüllt string, StyleProp<ViewStyle> Ein Backdrop, der den Dialog hinter dem Popup ausfüllt und vorhanden ist, sobald eines von beiden gesetzt ist.
children .child(..) JSX.Element Der Inhalt.
ios-Schlüssel Rust Typ Standard Beschreibung
supportedOrientations .supported_orientations(..) ("portrait" | "portrait-upside-down" | "landscape" | "landscape-left" | "landscape-right")[] die des Presenters Die Ausrichtungen, die der Dialog erlaubt.
android-Schlüssel Rust Typ Standard Beschreibung
statusBarTranslucent .status_bar_translucent(..) boolean false Der Inhalt reicht unter die Statusleiste; andernfalls beginnt er darunter.

#Öffnen und Schließen

Der Zustand des Dialogs gehört der App. Wenn der Benutzer den Dialog schließt (Escape, die Zurück-Taste unter Android, ein Druck auf den Backdrop, ein nach unten gewischtes Sheet), schließt er sich nativ und teilt es der App mit, die ihren Zustand entsprechend setzt: in TSX onOpenChange({ open: false, reason }), in Rust on_cancel. In TSX bleibt der Dialog geschlossen, solange open auf true bleibt, und er öffnet sich erneut, wenn open von false auf true wechselt.

Jedes closeOn*-Flag wird nativ gelesen, wenn der Benutzer handelt, da der Main-Thread nie auf den Code der App wartet. Um vor dem Schließen eine Bestätigung einzuholen, setze die Flags auf false: Der Versuch ruft dann onCancel auf, und die App schließt den Dialog selbst (in TSX, indem sie open auf false setzt, in Rust, indem sie ihn aus der View nimmt):

dialog()
    .presentation(ModalPresentation::PageSheet)
    .close_on_escape(false)
    .close_on_backdrop_press(false)
    .close_on_swipe(false)
    .on_cancel(move || ask_to_discard.set(true))
    .flex_1()
    .bg(white())
    .child(form())

onCancel wird bei jedem Versuch aufgerufen, unabhängig davon, ob die Flags das Schließen des Dialogs erlauben.

#Ein Dialog mit Backdrop

Ein Dialog mit der Standard-Präsentation fullScreen zeigt, was die App zeichnet: hier einen abgedunkelten Backdrop und eine darüber zentrierte Karte. Ein Druck auf den Backdrop schließt ihn:

when(confirm, move || {
    dialog()
        .animation(ModalAnimation::Fade)
        .on_cancel(move || confirm.set(false))
        .flex_1()
        .child(
            // The backdrop: a press on it closes the dialog.
            view()
                .absolute()
                .inset_0()
                .bg(rgba(0x00000066))
                .on_press(move || confirm.set(false)),
        )
        .child(
            view()
                .m(auto())
                .w_80()
                .gap_3()
                .rounded_2xl()
                .bg(white())
                .p_5()
                .child(text("Delete this item?").text_xl().font_weight(FontWeight::BOLD))
                .child(
                    button()
                        .on_press(remove)
                        .items_center()
                        .rounded_lg()
                        .bg(rgb(0xdc2626))
                        .px_4()
                        .py_3()
                        .active(|s| s.opacity(0.7))
                        .child(text("Delete").font_weight(FontWeight::SEMIBOLD).text_color(white())),
                ),
        )
})

Für den System-Alert oder das Action Sheet verwende AlertDialog aus nsh/base.

#Ref-Methoden

Keine.

#Plattformhinweise

  • iOS und Mac Catalyst: presentation bestimmt die Form. fullScreen präsentiert über dem gesamten Bildschirm mit einem transparenten Hintergrund; pageSheet und formSheet sind System-Sheets mit dem Systemhintergrund, die beim Wischen nach unten dem Finger folgen (closeOnSwipe).
  • Android: Der Dialog füllt das Fenster und zeigt das Popup und den Backdrop der App. Die Zurück-Taste geht zuerst an den obersten Dialog, noch vor Stacks und BackHandler-Listenern.
  • Desktop-Plattformen (macOS, Windows, Linux, GPUI) und das Web: Der Dialog ist eine transparente Ebene über dem gesamten Fenster, die das Popup und den Backdrop der App zeigt. Escape ist ein Schließversuch.
  • supportedOrientations gehört zu iOS, statusBarTranslucent zu Android. Der Inhalt eines Android-Dialogs beginnt unterhalb der Statusleiste, sofern nicht statusBarTranslucent gesetzt ist.