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

Elementi

Dialog

Un dialog presenta contenuto sopra il resto dell’app nel proprio livello di finestra nativo: un view controller presentato su iOS e Mac Catalyst, un Dialog su Android, un livello sopra l’intera finestra sui desktop e sul web. Si usa per flussi a schermo intero, sheet e dialog. I gesti del sistema (trascinare uno sheet verso il basso, il pulsante indietro di Android, Escape) funzionano e vengono decisi nativamente. In Rust se ne crea uno con nsh_base::dialog(), in TSX con <Dialog> da 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);

Il dialog gestisce il proprio comportamento e lascia l’aspetto all’app: i suoi stili definiscono lo stile del popup (la view che contiene i figli), e in TSX backdropClass / backdropStyle aggiungono un backdrop dietro di esso.

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, il dialog viene mostrato finché open non è false e chiuso quando diventa false. In Rust, viene presentato finché è nella view e chiuso quando ne esce, quindi when lo contiene. In entrambi i casi fornisce al proprio contenuto gli inset della safe area del dialog, che le variabili env(safe-area-inset-*) e safeAreaInsets() (cx.insets() in un componente Rust) al suo interno leggono. Button, anch’esso da nsh/base, è un target di pressione stilizzato dall’app.

nsh_core::prelude ha anche una funzione dialog più vecchia e deprecata: importa nsh_base::dialog per nome, come sopra, e l’import esplicito ne prende il posto. Per l’alert o l’action sheet del sistema, usa AlertDialog.

#Props

PropRustTipoPredefinitoDescrizione
opennella view finché è aperto (when(open, ..))booleantrueMostrato finché è true.
animation.animation(ModalAnimation::Fade)"slide" | "fade" | "none""slide"Come entra ed esce.
transitionShader.transition_shader(TransitionShader::new(program)){ source: string; uniforms?: Record<string, ShaderUniform>; duration?: number }nessunoEntra ed esce tramite uno shader WGSL al posto della sua animation: shade(uv) legge ciò che sta dietro come sample_from e il dialog come sample_to mentre nsh.progress va da 0 a 1 in duration millisecondi (350 per impostazione predefinita), e da 1 a 0 alla chiusura. Quando il sistema richiede meno movimento, usa la propria animation. Vedi Shader.
presentation.presentation(ModalPresentation::PageSheet)"fullScreen" | "pageSheet" | "formSheet""fullScreen"La sua forma su iOS: fullScreen copre lo schermo con uno sfondo trasparente; pageSheet e formSheet sono sheet di sistema che mantengono visibile ciò che sta dietro (formSheet è uno sheet centrato più piccolo sugli schermi grandi).
closeOnEscape.close_on_escape(..)booleantrueEscape e il pulsante indietro di Android lo chiudono nativamente.
closeOnBackdropPress.close_on_backdrop_press(..)booleantrueUna pressione sul backdrop lo chiude.
closeOnSwipe.close_on_swipe(..)booleantrueTrascinare uno sheet verso il basso (iOS, browser per telefono) lo chiude nativamente.
statusBar.status_bar(..){ foreground?, hidden?, animated? }nessunoiOS e Android: la barra di stato mentre il dialog è aperto, al posto di quella delle pagine (Status bar).
onOpen.on_open(..)() => voidHa terminato di apparire.
onClose.on_close(..)() => voidÈ scomparso: l’utente lo ha chiuso, oppure l’app lo ha rimosso.
onCancel.on_cancel(..)() => voidL’utente ha tentato di chiuderlo (Escape, indietro, una pressione sul backdrop, un trascinamento verso il basso), indipendentemente dal fatto che un flag closeOn* lo abbia permesso.
onOpenChange.on_cancel(..)(e: { open: boolean; reason: "cancel" | "backdropPress" | "swipe" }) => voidL’utente lo ha chiuso: mantieni qui open sincronizzato. In Rust, on_cancel rimuove il dialog dalla view.
class, stylemetodi Styledstring, StyleProp<ViewStyle>Il popup: la view che contiene i figli.
backdropClass, backdropStyleuna view figlia che riempie il dialogstring, StyleProp<ViewStyle>Un backdrop che riempie il dialog dietro il popup, presente quando uno dei due è impostato.
children.child(..)JSX.ElementIl contenuto.
Chiave iosRustTipoPredefinitoDescrizione
supportedOrientations.supported_orientations(..)("portrait" | "portrait-upside-down" | "landscape" | "landscape-left" | "landscape-right")[]quello del presenterGli orientamenti consentiti dal dialog.
Chiave androidRustTipoPredefinitoDescrizione
statusBarTranslucent.status_bar_translucent(..)booleanfalseIl contenuto si estende sotto la barra di stato; altrimenti inizia sotto di essa.

#Apertura e chiusura

Lo stato del dialog appartiene all’app. Quando l’utente chiude il dialog (Escape, il pulsante indietro di Android, una pressione sul backdrop, uno sheet trascinato verso il basso), questo si chiude nativamente e lo comunica all’app, che imposta il proprio stato di conseguenza: onOpenChange({ open: false, reason }) in TSX, on_cancel in Rust. In TSX, finché open rimane true il dialog resta chiuso, e si riapre quando open passa da false a true.

Ogni flag closeOn* viene letto nativamente quando l’utente agisce, poiché il thread principale non attende mai il codice dell’app. Per chiedere conferma prima di chiudere, imposta i flag su false: il tentativo chiama allora onCancel, e l’app chiude il dialog da sé (in TSX impostando open su false, in Rust rimuovendolo dalla view):

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 viene chiamato a ogni tentativo, che i flag permettano o meno la chiusura del dialog.

#Un dialog con backdrop

Un dialog con la presentazione fullScreen predefinita mostra ciò che l’app disegna: qui un backdrop oscurato e una card centrata sopra di esso. Una pressione sul backdrop lo chiude:

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())),
                ),
        )
})

Per l’alert o l’action sheet del sistema, usa AlertDialog da nsh/base.

#Metodi del ref

Nessuno.

#Note sulle piattaforme

  • iOS e Mac Catalyst: presentation sceglie la forma. fullScreen presenta a schermo intero con uno sfondo trasparente; pageSheet e formSheet sono sheet di sistema con lo sfondo di sistema, che seguono il dito quando vengono trascinati verso il basso (closeOnSwipe).
  • Android: il dialog riempie la finestra e mostra il popup e il backdrop dell’app. Il pulsante indietro va prima al dialog più in alto, prima degli stack e dei listener BackHandler.
  • Desktop (macOS, Windows, Linux, GPUI) e web: il dialog è un livello trasparente sopra l’intera finestra che mostra il popup e il backdrop dell’app. Escape è un tentativo di chiusura.
  • supportedOrientations è di iOS; statusBarTranslucent è di Android. Il contenuto di un dialog Android inizia sotto la barra di stato a meno che statusBarTranslucent sia impostato.