Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

Sheet

Sheet — это панель у края экрана поверх подложки, которая закрывает всё остальное: фильтры, панель «Поделиться», боковое меню. Нажатие на подложку, Escape, кнопка «Назад» на Android и жест выхода VoiceOver закрывают её. Она показывается поверх всего в своём окне, одинаково на любом устройстве, и ничего не рисует сама: панель и подложка принадлежат приложению.

el-sheet.tsx
el-sheet.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Sheet } from "nsh/base";
import { button, buttonText, desktop, Inspector, Log, Panel, size, Stage, ui, wide } from "./palette";

type Side = "bottom" | "top" | "left" | "right";

/** The panel's corners and size across, by the edge it is on. */
function panel(side: Side) {
  const r = 20;
  const across = side === "left" || side === "right" ? { width: wide() ? 320 : 280 } : {};
  const corners = {
    bottom: { borderTopLeftRadius: r, borderTopRightRadius: r, paddingBottom: 32 },
    top: { borderBottomLeftRadius: r, borderBottomRightRadius: r, paddingTop: 32 },
    left: { borderTopRightRadius: r, borderBottomRightRadius: r },
    right: { borderTopLeftRadius: r, borderBottomLeftRadius: r },
  }[side];
  return { ...ui().card, padding: 20, gap: 12, ...across, ...corners };
}

function App() {
  const open = signal(false);
  const side = signal<Side>("bottom");
  const events = signal<string[]>([]);
  const log = (e: string) => events.set([e, ...events()].slice(0, 6));
  const show = (s: Side) => {
    side.set(s);
    open.set(true);
    log(`open · side ${s}`);
  };
  /** A button that opens the sheet at side `s`. */
  const opener = (s: Side) => (
    <View onPress={() => show(s)} {...button("secondary", { on: () => s === side() })}>
      <Text style={buttonText("secondary", { on: () => s === side() })}>side="{s}"</Text>
    </View>
  );
  const changes = () => <Log title="onOpenChange({ open, reason }) · onCancel" lines={events()} minHeight={130} />;

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>sheet</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Open a side; click the backdrop or press Escape to close it" : "Open a side; tap the backdrop or Android's back to close it"}
      </Text>

      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            {/* Each button at the edge its sheet comes from. */}
            <Stage width={360} gap={12}>
              <View style={{ alignItems: "center" }}>{opener("top")}</View>
              <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
                {opener("left")}
                {opener("right")}
              </View>
              <View style={{ alignItems: "center" }}>{opener("bottom")}</View>
            </Stage>
            <Inspector>
              <Panel title="open · side">
                <Text style={{ fontSize: 13, color: ui().text }}>
                  {open() ? "open" : "closed"} · {side()}
                </Text>
              </Panel>
              {changes()}
            </Inspector>
          </View>
        ) : (
          <>
            <View style={{ flex: 1, minHeight: 200, alignItems: "center", justifyContent: "center" }}>
              <View style={{ width: 240, gap: 8 }}>
                <Each of={["top", "left", "right", "bottom"] as Side[]}>{(s) => opener(s())}</Each>
              </View>
            </View>
            {changes()}
          </>
        )
      }

      <Sheet
        open={open()}
        closeOnSwipe
        side={side()}
        onOpenChange={({ open: next, reason }) => {
          open.set(next);
          log(`onOpenChange · ${next ? "opened" : "closed"} · ${reason}`);
        }}
        onCancel={() => log("onCancel")}
        popupStyle={panel(side())}
        backdropStyle={{ backgroundColor: "rgba(0,0,0,0.45)" }}
      >
        <Text style={{ fontSize: 17, fontWeight: "700", color: ui().text }}>Filters</Text>
        <Text style={{ fontSize: 14, color: ui().muted }}>
          A panel at the {side()} edge. {desktop ? "The backdrop and Escape close it." : "The backdrop and Android's back close it."}
        </Text>
        <View
          onPress={() => {
            open.set(false);
            log("Done pressed");
          }}
          {...button("primary", { align: "flex-end" })}
        >
          <Text style={buttonText("primary")}>Done</Text>
        </View>
      </Sheet>
    </View>
  );
}

mount(App);
use nsh_base::{SheetEvent, sheet};
use nsh_core::prelude::*;

fn filters(open: Signal<bool>) -> impl IntoElement {
    sheet()
        .open(move || Some(open.get()))
        .on_open_change(move |e: &SheetEvent| {
            if let SheetEvent::OpenChange(next, _) = e {
                open.set(*next);
            }
        })
        .child(
            view()
                .gap_3()
                .p_6()
                .rounded_t(px(16.0))
                .bg(white())
                .child(text("Filters").font_weight(FontWeight::SEMIBOLD))
                .child(view().on_press(move || open.set(false)).child(text("Done"))),
        )
}

Sheet берётся из nsh/base. Его содержимое создаётся заново при каждом открытии. side размещает его у "bottom" (по умолчанию) или "top" края экрана, по всей ширине экрана, либо у "left" или "right" края, по всей высоте экрана; размер поперёк определяется самой панелью: по её содержимому или через popupClass.

Анимация выезда следует за измеренным размером панели, пока содержимое создаётся или растёт во время появления. Ленивое содержимое и асинхронные обновления размера используют общий прогресс анимации; панель останавливается в своём текущем размере.

#Состояние открытия

Без open sheet хранит собственное состояние, начиная с defaultOpen. С open состоянием владеет приложение: изменяйте его в onOpenChange({ open, reason }), который сообщает о каждом запросе на закрытие вместе с причиной ("backdropPress", "cancel", "swipe"). onOpen и onClose отслеживают его появление и скрытие независимо от того, кто открыл или закрыл sheet.

closeOnEscape={false} оставляет sheet открытым при Escape и «Назад», closeOnBackdropPress={false} — при нажатиях на подложку; onCancel всё равно получает эти события, чтобы можно было сначала спросить.

#Перетаскивание нижней панели

Задайте closeOnSwipe, чтобы нижняя панель следовала за перетаскиванием вниз. Подложка плавно гаснет по мере смещения; если отпустить после смещения более чем на 30% высоты панели или при движении вниз со скоростью более 800 точек в секунду, будет запрошено закрытие с причиной "swipe". Более короткое перетаскивание плавно возвращает панель на место. Управляющий владелец может оставить open равным true, чтобы вернуть панель после запроса на закрытие. При включённом уменьшении анимации отпускание и программное открытие и закрытие завершаются мгновенно; прямое перетаскивание по-прежнему следует за пальцем.

Панель подстраивается под содержимое, включая короткие меню; используйте popupStyle или высоту дочернего элемента, чтобы ограничить более высокое содержимое. Маркер (handle) приложение рисует само: это может быть любое представление внутри панели. На iOS вложенный вертикальный Scroll или List поглощает перетаскивание до своей верхней границы, а оставшееся движение вниз передаёт панели в рамках того же жеста. На Android, на десктопах и в вебе перетаскивание маркера или содержимого вне прокручиваемой области использует их нативную поддержку панорамирования; вложенные прокручиваемые области сохраняют обычное для своей платформы разрешение конфликтов жестов. Если задать closeOnSwipe равным false, панель остаётся неподвижной, а смонтированное содержимое и позиция прокрутки сохраняются. Остальные края сохраняют прежнее поведение.

#Нативные sheet

Sheet рисуется NSH в окне приложения и оформляется приложением. Для собственной представляемой платформой панели (page sheet на iOS) используйте Dialog с presentation="pageSheet".

#Свойства

СвойствоRustТипПо умолчаниюОписание
open.open(..)booleanнетОткрыт, управляется приложением.
defaultOpen.default_open(..)booleanfalseИзначально открыт, без open.
side.side(..)"top" | "bottom" | "left" | "right""bottom"Край, у которого он расположен.
closeOnEscape.close_on_escape(..)booleantrueEscape и кнопка «Назад» на Android закрывают его.
closeOnBackdropPress.close_on_backdrop_press(..)booleantrueНажатие на подложку закрывает его.
closeOnSwipe.close_on_swipe(..)booleanfalseНижняя панель следует за перетаскиванием вниз и закрывается при достаточном смещении.
popupClass, popupStyle.popup_class(..), .popup_style(..)string, StyleProp<ViewStyle>нетВнешний вид панели.
backdropClass, backdropStyle.backdrop_class(..), .backdrop_style(..)string, StyleProp<ViewStyle>нетВнешний вид подложки, например полупрозрачный цвет.
children.child(..)JSX.ElementнетСодержимое.

#События

СобытиеRustОписание
onOpenChange.on_open_change(|e| ..)Запрос на закрытие с причиной: "backdropPress", "cancel" или "swipe".
onOpen.on_open(|| ..)Sheet появился: он полностью отображён.
onClose.on_close(|| ..)Sheet скрылся: он полностью убран. Также срабатывает, когда его закрыло приложение.
onCancel.on_cancel(|| ..)Пользователь попытался закрыть его (Escape или «Назад», нажатие на подложку, включённый свайп), независимо от того, закрылся он или нет.

В вебе встраивание через dialogs: "viewport" показывает sheet и его подложку поверх страницы-хоста, включая iframe того же источника. Для фреймов с другого источника используйте hostPlayerFrame.