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

Éléments

Overlay

Un overlay affiche ses enfants au-dessus de tout le reste dans la vue racine où il se trouve : la fenêtre, ou la boîte de dialogue qui l’entoure. La taille de son parent, overflow: hidden et les vues défilantes ne le rognent pas. Pour tout le reste, il demeure là où vous l’avez écrit : il lit le contexte qui l’entoure, ses gestionnaires sont ceux de son composant, et il disparaît avec son parent. Les popovers, menus, infobulles et feuilles sont construits dessus ; nsh/base les propose prêts à l’emploi (Popover, Menu, Tooltip, Sheet), et un overlay sert à construire les vôtres.

el-overlay.tsx
el-overlay.tsxLive
import { mount, Overlay, signal, Text, View, type ViewRef } from "nsh";
import { button, buttonText, desktop, Inspector, Panel, Segmented, Stage, ui, wide } from "./palette";

type Placement = "top" | "bottom" | "left" | "right";
type Align = "start" | "center" | "end";

function App() {
  const open = signal(false);
  const placement = signal<Placement>("bottom");
  const align = signal<Align>("center");
  const position = signal("—");
  const closed = signal("—");
  let anchor: ViewRef | undefined;

  const placements = () => (
    <Segmented
      label="placement"
      options={["top", "bottom", "left", "right"] as Placement[]}
      value={placement()}
      onChange={(v) => (placement.set(v), open.set(true))}
    />
  );
  const aligns = () => (
    <Segmented label="align" options={["start", "center", "end"] as Align[]} value={align()} onChange={(v) => (align.set(v), open.set(true))} />
  );
  const subject = () => (
    <>
      <View
        style={{
          width: 220,
          height: 72,
          overflow: "hidden",
          borderRadius: 14,
          borderWidth: 2,
          borderStyle: "dashed",
          borderColor: ui().faint,
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <View
          ref={anchor}
          onPress={() => open.set(!open())}
          {...button("accent", { align: "center" })}
        >
          <Text style={buttonText("accent")}>{open() ? "Close" : "Open"}</Text>
        </View>
        <If when={open()}>
          <Overlay
            anchor={anchor}
            placement={placement()}
            align={align()}
            offset={8}
            margin={12}
            onPosition={(e) =>
              position.set(
                `${e.placement} at ${Math.round(e.bounds.origin.x)}, ${Math.round(e.bounds.origin.y)} · ${Math.round(e.bounds.size.width)}×${Math.round(e.bounds.size.height)} · room ${Math.round(e.available.width)}×${Math.round(e.available.height)}`,
              )
            }
            onPressOutside={(e) => {
              closed.set(`onPressOutside at ${Math.round(e.pagePosition.x)}, ${Math.round(e.pagePosition.y)}`);
              open.set(false);
            }}
            onCancel={() => {
              closed.set("onCancel");
              open.set(false);
            }}
            style={{
              ...ui().card,
              borderRadius: 12,
              padding: 14,
              gap: 4,
              // Narrow beside the button, so it fits on a phone's side.
              maxWidth: !wide() && (placement() === "left" || placement() === "right") ? 130 : 220,
            }}
          >
            <Text style={{ fontSize: 15, fontWeight: "600", color: ui().text }}>On top of everything</Text>
            <Text style={{ fontSize: 13, color: ui().muted }}>
              Its box clips the button, not this overlay. Pick a side {wide() ? "on the right" : "above"}.
            </Text>
          </Overlay>
        </If>
      </View>
      <Text style={{ marginTop: 6, fontSize: 12, color: ui().faint }}>The dashed box has overflow: hidden</Text>
    </>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>overlay</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click the button, then click outside or press Escape" : "Tap the button, then tap outside"}
      </Text>

      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            <Stage>
              <View style={{ alignItems: "center" }}>{subject()}</View>
            </Stage>
            <Inspector>
              {placements()}
              {aligns()}
              <Panel title="onPosition">
                <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{position()}</Text>
              </Panel>
              <Panel title="closed by" grow>
                <Text style={{ fontSize: 13, color: closed() === "—" ? ui().text : ui().accent }}>{closed()}</Text>
              </Panel>
            </Inspector>
          </View>
        ) : (
          <>
            <View style={{ flexDirection: "row", gap: 12 }}>
              <View style={{ flex: 3 }}>{placements()}</View>
              <View style={{ flex: 2 }}>{aligns()}</View>
            </View>

            <View style={{ flex: 1, minHeight: 160, alignItems: "center", justifyContent: "center" }}>{subject()}</View>

            <View style={{ ...ui().inset, borderRadius: 12, padding: 12, gap: 6 }}>
              <Text style={{ fontSize: 12, color: ui().muted }}>
                onPosition <Text style={{ color: ui().text }}>{position()}</Text>
              </Text>
              <Text style={{ fontSize: 12, color: ui().muted }}>
                closed by <Text style={{ color: closed() === "—" ? ui().text : ui().accent }}>{closed()}</Text>
              </Text>
            </View>
          </>
        )
      }
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

fn more_button() -> impl IntoElement {
    let open = signal(false);
    let anchor = element_ref();
    view()
        .child(
            view()
                .element_ref(&anchor)
                .on_press(move || open.update(|o| *o = !*o))
                .px_3()
                .py_2()
                .child(text("More")),
        )
        .child(when(move || open.get(), move || {
            overlay()
                .anchor(&anchor)
                .placement(Placement::Bottom)
                .align(Align::Start)
                .offset(px(6.0))
                .margin(px(8.0))
                .on_press_outside(move |_: &Point<Points>| open.set(false))
                .on_cancel(move || open.set(false))
                .rounded_lg()
                .bg(white())
                .p_3()
                .shadow_lg()
                .child(text("Shown above everything"))
        }))
}

Overlay provient de nsh en TSX et overlay() de nsh_core::prelude en Rust. Il est affiché tant qu’il est dans la vue : placez-le dans un If (Rust : when(..)) pour l’ouvrir et le fermer. Les overlays affichés plus tard sont au-dessus des précédents, et un overlay placé dans un autre est au-dessus de celui-ci.

#Placement

Sans anchor, un overlay est disposé comme une boîte position: fixed de sa vue racine : ses top, left, inset, sa taille et ses marges se rapportent à la fenêtre entière (ou à la boîte de dialogue). class="inset-0" la recouvre, ce qui donne un fond ou une couche plein écran ; class="bottom-0 left-0 right-0" est une barre le long du bas.

Avec anchor, NSH le place à côté de l’ancre :

  • anchor est une vue (un ref), un Point ({ x, y }) dans les coordonnées de la vue racine, le pageX / pageY d’un toucher (un menu là où se trouve le pointeur), ou un Bounds ({ origin, size }) à cet endroit : le curseur d’un champ de saisie (le pageBounds de onCaretMove), là où s’ouvrent les suggestions pour ce qui est en cours de saisie.
  • placement est le côté où il se place : "top", "bottom" (par défaut), "left" ou "right". Quand il ne tient pas de ce côté mais tient du côté opposé, il passe de l’autre côté ; quand il ne tient d’aucun des deux, il se place du côté qui a le plus de place.
  • align l’aligne le long de ce côté : "start", "center" (par défaut) ou "end". Au-dessus et en dessous de l’ancre, début et fin suivent le sens de la mise en page : le début est le bord gauche, ou le bord droit dans une mise en page de droite à gauche.
  • offset est l’écart entre l’ancre et l’overlay ; margin la distance qu’il garde par rapport aux bords de la zone de sécurité, dans laquelle il reste : il glisse le long de son côté pour s’y ajuster.

Un overlay ancré suit son ancre : quand l’ancre défile, bouge ou change de taille, l’overlay la suit dans la même frame où NSH le dispose. onPosition indique où il est allé, afin qu’une flèche puisse pointer vers l’ancre ou qu’un long menu puisse limiter sa hauteur :

<Overlay
  anchor={anchor}
  onPosition={(e) => {
    side.set(e.placement); // "top" when it flipped
    maxHeight.set(e.available.height);
  }}
/>

#Fermeture

Un overlay signale ce qui ferme habituellement une popup et vous laisse la décision :

  • onPressOutside se déclenche quand un doigt ou un bouton de souris s’appuie en dehors de l’overlay, de son ancre et des overlays qu’il contient. L’appui se poursuit vers ce qui se trouve en dessous.
  • onCancel se déclenche sur Échap, le bouton retour d’Android et le geste d’échappement de VoiceOver sur iOS, uniquement pour l’overlay du dessus qui l’écoute. Il les reçoit avant les pages et BackHandler.

Pour bloquer l’écran derrière une popup, placez en dessous un second overlay avec class="inset-0" et un onPress qui ferme les deux : c’est ainsi que fonctionnent Sheet et la présentation sur téléphone de Popover.

#Propriétés

PropriétéRustTypePar défautDescription
anchor.anchor(&element_ref), .anchor_point(point), .anchor_bounds(bounds)ViewRef | Point | Bounds | nullaucunLa vue, le point ou la boîte par rapport auxquels il est placé. Sans ancre, c’est une boîte fixe de sa vue racine.
placement.placement(..)"top" | "bottom" | "left" | "right""bottom"Le côté de l’ancre où il se place.
align.align(..)"start" | "center" | "end""center"Son alignement avec l’ancre le long de ce côté.
offset.offset(px(..))number0L’écart entre l’ancre et l’overlay.
margin.margin(px(..))number0La distance qu’il garde par rapport aux bords de la zone de sécurité.
children.child(..)JSX.ElementaucunCe qu’il affiche.
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucunSon apparence et, sans ancre, son emplacement.

Il accepte aussi les propriétés communes d’une vue.

#Événements

ÉvénementRustDescription
onPressOutside.on_press_outside(|point| ..)Un appui a eu lieu en dehors de l’overlay, de son ancre et des overlays qu’il contient (pagePosition, un Point).
onCancel.on_cancel(|| ..)Échap, le bouton retour d’Android ou le geste d’échappement de VoiceOver, tant qu’il est l’overlay du dessus à l’écoute.
onPosition.on_position(|position| ..)NSH l’a placé : placement (le côté où il est allé), bounds dans la vue racine, et available (un Size : la place disponible de ce côté).

#Notes sur les plateformes

  • Chaque plateforme affiche un overlay comme une vue native de la vue racine de la fenêtre (ou de la boîte de dialogue), dessinée et soumise au hit-testing comme les vues qui l’entourent : UIKit, vues Android, AppKit, GTK, Win32, GPUI et le DOM.
  • Un overlay dans un Dialog s’affiche dans cette boîte de dialogue, au-dessus de son contenu.
  • Les overlays restent dans leur fenêtre : un menu qui dépasserait le bord de la fenêtre glisse pour rentrer à l’intérieur.