Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

Éléments

Dialog

Un dialog présente du contenu par-dessus le reste de l’app dans sa propre couche de fenêtre native : un view controller présenté sur iOS et Mac Catalyst, un Dialog sur Android, une couche au-dessus de toute la fenêtre sur les desktops et le web. Il sert aux parcours plein écran, aux sheets et aux boîtes de dialogue. Les gestes propres au système (glisser une sheet vers le bas, le bouton retour d’Android, Escape) fonctionnent et sont gérés nativement. En Rust, on en crée un avec nsh_base::dialog(), en TSX avec <Dialog> depuis 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);

Le dialog gère son comportement et laisse son apparence à l’app : ses styles stylent la popup (la vue qui contient les enfants), et en TSX backdropClass / backdropStyle ajoutent un fond derrière elle.

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, le dialog est affiché tant que open n’est pas false et fermé lorsqu’il devient false. En Rust, il est présenté tant qu’il est dans la vue et retiré lorsqu’il la quitte, c’est pourquoi when le conserve. Dans les deux cas, il transmet à son contenu les insets de zone sûre du dialog, que lisent les variables env(safe-area-inset-*) et safeAreaInsets() (cx.insets() dans un composant Rust) à l’intérieur. Button, également issu de nsh/base, est une cible de pression stylée par l’app.

nsh_core::prelude possède aussi une ancienne fonction dialog, dépréciée : importez nsh_base::dialog par son nom, comme ci-dessus, et l’import explicite prend sa place. Pour l’alerte ou l’action sheet du système, utilisez AlertDialog.

#Props

PropRustTypeDéfautDescription
opendans la vue tant qu’il est ouvert (when(open, ..))booleantrueAffiché tant que la valeur est true.
animation.animation(ModalAnimation::Fade)"slide" | "fade" | "none""slide"La manière dont il entre et sort.
transitionShader.transition_shader(TransitionShader::new(program)){ source: string; uniforms?: Record<string, ShaderUniform>; duration?: number }aucunIl entre et sort via un shader WGSL à la place de son animation : shade(uv) lit ce qui est derrière sous le nom sample_from et le dialog sous le nom sample_to pendant que nsh.progress va de 0 à 1 sur duration millisecondes (350 par défaut), puis de 1 à 0 à la fermeture. Lorsque le système demande moins de mouvement, il utilise son propre animation. Voir Shader.
presentation.presentation(ModalPresentation::PageSheet)"fullScreen" | "pageSheet" | "formSheet""fullScreen"Sa forme sur iOS : fullScreen couvre l’écran avec un fond transparent ; pageSheet et formSheet sont des sheets système qui laissent visible ce qui est derrière (formSheet est une sheet centrée plus petite sur les grands écrans).
closeOnEscape.close_on_escape(..)booleantrueEscape et le bouton retour d’Android le ferment nativement.
closeOnBackdropPress.close_on_backdrop_press(..)booleantrueUn appui sur le fond le ferme.
closeOnSwipe.close_on_swipe(..)booleantrueGlisser une sheet vers le bas (iOS, navigateurs sur téléphone) la ferme nativement.
statusBar.status_bar(..){ foreground?, hidden?, animated? }aucuniOS et Android : la barre d’état tant que le dialog est ouvert, par-dessus celle des pages (Status bar).
onOpen.on_open(..)() => voidIl a fini d’apparaître.
onClose.on_close(..)() => voidIl a disparu : l’utilisateur l’a fermé, ou l’app l’a retiré.
onCancel.on_cancel(..)() => voidL’utilisateur a tenté de le fermer (Escape, retour, un appui sur le fond, un glissement vers le bas), que l’indicateur closeOn* l’ait autorisé ou non.
onOpenChange.on_cancel(..)(e: { open: boolean; reason: "cancel" | "backdropPress" | "swipe" }) => voidL’utilisateur l’a fermé : gardez open synchronisé ici. En Rust, on_cancel retire le dialog de la vue.
class, styleméthodes Styledstring, StyleProp<ViewStyle>La popup : la vue qui contient les enfants.
backdropClass, backdropStyleune vue enfant remplissant le dialogstring, StyleProp<ViewStyle>Un fond qui remplit le dialog derrière la popup, présent dès que l’un des deux est défini.
children.child(..)JSX.ElementLe contenu.
Clé iosRustTypeDéfautDescription
supportedOrientations.supported_orientations(..)("portrait" | "portrait-upside-down" | "landscape" | "landscape-left" | "landscape-right")[]celles du présentateurLes orientations que le dialog autorise.
Clé androidRustTypeDéfautDescription
statusBarTranslucent.status_bar_translucent(..)booleanfalseLe contenu passe sous la barre d’état ; sinon il commence en dessous.

#Ouverture et fermeture

L’état du dialog appartient à l’app. Lorsque l’utilisateur ferme le dialog (Escape, le bouton retour d’Android, un appui sur le fond, une sheet glissée vers le bas), celui-ci se ferme nativement et en informe l’app, qui met son état à jour en conséquence : onOpenChange({ open: false, reason }) en TSX, on_cancel en Rust. En TSX, tant que open reste true, le dialog reste fermé, et il se rouvre lorsque open passe de false à true.

Chaque indicateur closeOn* est lu nativement au moment de l’action de l’utilisateur, car le thread principal n’attend jamais le code de l’app. Pour demander une confirmation avant la fermeture, mettez les indicateurs à false : la tentative appelle alors onCancel, et l’app ferme elle-même le dialog (en TSX en passant open à false, en Rust en le retirant de la vue) :

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 est appelé à chaque tentative, que les indicateurs autorisent la fermeture du dialog ou non.

#Un dialog avec un fond

Un dialog avec la présentation fullScreen par défaut affiche ce que l’app dessine : ici un fond assombri et une carte centrée par-dessus. Un appui sur le fond le ferme :

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

Pour l’alerte ou l’action sheet du système, utilisez AlertDialog depuis nsh/base.

#Méthodes de ref

Aucune.

#Notes par plateforme

  • iOS et Mac Catalyst : presentation choisit la forme. fullScreen présente sur tout l’écran avec un fond transparent ; pageSheet et formSheet sont des sheets système avec le fond du système, qui suivent le doigt lorsqu’on les glisse vers le bas (closeOnSwipe).
  • Android : le dialog remplit la fenêtre et affiche la popup et le fond de l’app. Le bouton retour s’adresse d’abord au dialog le plus haut, avant les stacks et les listeners BackHandler.
  • Desktops (macOS, Windows, Linux, GPUI) et web : le dialog est une couche transparente au-dessus de toute la fenêtre, qui affiche la popup et le fond de l’app. Escape constitue une tentative de fermeture.
  • supportedOrientations est propre à iOS ; statusBarTranslucent est propre à Android. Le contenu d’un dialog Android commence sous la barre d’état, sauf si statusBarTranslucent est défini.