Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

Elementos

Dialog

Un diálogo presenta contenido sobre el resto de la app en su propia capa de ventana nativa: un view controller presentado en iOS y Mac Catalyst, un Dialog en Android, una capa sobre toda la ventana en los escritorios y la web. Se usa para flujos a pantalla completa, sheets y diálogos. Los gestos propios del sistema (deslizar una sheet hacia abajo, el botón atrás de Android, Escape) funcionan y se deciden de forma nativa. Rust crea uno con nsh_base::dialog(), TSX con <Dialog> desde 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);

El diálogo se encarga de su comportamiento y deja su aspecto a la app: sus estilos dan estilo al popup (la vista que contiene a los hijos), y en TSX backdropClass / backdropStyle añaden un backdrop detrás de él.

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

En TSX, el diálogo se muestra mientras open no sea false y se cierra cuando pasa a ser false. En Rust, se presenta mientras está en la vista y se descarta cuando sale de ella, por lo que when lo conserva. En ambos casos entrega a su contenido los insets de safe area del diálogo, que leen las variables env(safe-area-inset-*) y safeAreaInsets() (cx.insets() en un componente de Rust) dentro de él. Button, también de nsh/base, es un objetivo de pulsación al que la app da estilo.

nsh_core::prelude también tiene una función dialog más antigua y obsoleta: importa nsh_base::dialog por nombre, como arriba, y la importación explícita ocupa su lugar. Para la alerta o la action sheet del sistema, usa AlertDialog.

#Props

Prop Rust Tipo Valor por defecto Descripción
open en la vista mientras está abierto (when(open, ..)) boolean true Se muestra mientras sea true.
animation .animation(ModalAnimation::Fade) "slide" | "fade" | "none" "slide" Cómo entra y sale.
transitionShader .transition_shader(TransitionShader::new(program)) { source: string; uniforms?: Record<string, ShaderUniform>; duration?: number } ninguno Entra y sale mediante un shader WGSL en lugar de su animation: shade(uv) lee lo que hay detrás como sample_from y el diálogo como sample_to mientras nsh.progress va de 0 a 1 durante duration milisegundos (350 por defecto), y de 1 de vuelta a 0 al cerrarse. Cuando el sistema pide menos movimiento, usa su propio animation. Consulta Shader.
presentation .presentation(ModalPresentation::PageSheet) "fullScreen" | "pageSheet" | "formSheet" "fullScreen" Su forma en iOS: fullScreen cubre la pantalla con un fondo transparente; pageSheet y formSheet son sheets del sistema que mantienen visible lo que hay detrás (formSheet es una sheet centrada más pequeña en pantallas grandes).
closeOnEscape .close_on_escape(..) boolean true Escape y el botón atrás de Android lo cierran de forma nativa.
closeOnBackdropPress .close_on_backdrop_press(..) boolean true Una pulsación en el backdrop lo cierra.
closeOnSwipe .close_on_swipe(..) boolean true Deslizar una sheet hacia abajo (iOS, navegadores de teléfono) la cierra de forma nativa.
statusBar .status_bar(..) { foreground?, hidden?, animated? } ninguno iOS y Android: la barra de estado mientras el diálogo está abierto, por encima de la de las páginas (Barra de estado).
onOpen .on_open(..) () => void Terminó de aparecer.
onClose .on_close(..) () => void Desapareció: el usuario lo cerró o la app lo quitó.
onCancel .on_cancel(..) () => void El usuario intentó cerrarlo (Escape, atrás, una pulsación en el backdrop, un deslizamiento hacia abajo), haya permitido o no un flag closeOn* que se cerrara.
onOpenChange .on_cancel(..) (e: { open: boolean; reason: "cancel" | "backdropPress" | "swipe" }) => void El usuario lo cerró: mantén open sincronizado aquí. En Rust, on_cancel quita el diálogo de la vista.
class, style métodos Styled string, StyleProp<ViewStyle> El popup: la vista que contiene a los hijos.
backdropClass, backdropStyle una vista hija que llena el diálogo string, StyleProp<ViewStyle> Un backdrop que llena el diálogo detrás del popup, presente cuando se define cualquiera de los dos.
children .child(..) JSX.Element El contenido.
Clave de ios Rust Tipo Valor por defecto Descripción
supportedOrientations .supported_orientations(..) ("portrait" | "portrait-upside-down" | "landscape" | "landscape-left" | "landscape-right")[] las del presentador Las orientaciones que permite el diálogo.
Clave de android Rust Tipo Valor por defecto Descripción
statusBarTranslucent .status_bar_translucent(..) boolean false El contenido llega por debajo de la barra de estado; si no, empieza debajo de ella.

#Apertura y cierre

El estado del diálogo pertenece a la app. Cuando el usuario cierra el diálogo (Escape, el botón atrás de Android, una pulsación en el backdrop, una sheet deslizada hacia abajo), este se cierra de forma nativa y se lo comunica a la app, que ajusta su estado para que coincida: onOpenChange({ open: false, reason }) en TSX, on_cancel en Rust. En TSX, mientras open siga siendo true el diálogo permanece cerrado, y se vuelve a abrir cuando open pasa de false a true.

Cada flag closeOn* se lee de forma nativa cuando el usuario actúa, ya que el hilo principal nunca espera al código de la app. Para pedir confirmación antes de cerrar, establece los flags en false: el intento llama entonces a onCancel, y la app cierra el diálogo por sí misma (en TSX estableciendo open en false, en Rust quitándolo de la vista):

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 se llama en cada intento, ya sea que los flags permitan o no que el diálogo se cierre.

#Un diálogo con backdrop

Un diálogo con la presentación fullScreen por defecto muestra lo que dibuja la app: aquí un backdrop atenuado y una tarjeta centrada sobre él. Una pulsación en el backdrop lo cierra:

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

Para la alerta o la action sheet del sistema, usa AlertDialog de nsh/base.

#Métodos de ref

Ninguno.

#Notas de plataforma

  • iOS y Mac Catalyst: presentation elige la forma. fullScreen se presenta sobre la pantalla completa con un fondo transparente; pageSheet y formSheet son sheets del sistema con el fondo del sistema, que siguen al dedo al deslizarlas hacia abajo (closeOnSwipe).
  • Android: el diálogo llena la ventana y muestra el popup y el backdrop de la app. El botón atrás se dirige primero al diálogo más superior, antes que a las pilas y a los listeners de BackHandler.
  • Escritorios (macOS, Windows, Linux, GPUI) y la web: el diálogo es una capa transparente sobre toda la ventana que muestra el popup y el backdrop de la app. Escape es un intento de cierre.
  • supportedOrientations es de iOS; statusBarTranslucent es de Android. El contenido de un diálogo de Android empieza debajo de la barra de estado a menos que se defina statusBarTranslucent.