इस पेज का अनुवाद मशीन द्वारा किया गया है और इसमें त्रुटियाँ हो सकती हैं। मूल अंग्रेज़ी पढ़ें

एलिमेंट

Menu

Menu कार्रवाइयों की एक सूची है, जिसे उसका trigger खोलता है। एक ही संरचना हर डिवाइस पर काम करती है और हर डिवाइस की अपेक्षा के अनुसार दिखाई जाती है:

el-menu.tsx
el-menu.tsxLive
import { type JSX, mount, signal, Text, View } from "nsh";
import { ContextMenu, Menu, MenuItem, MenuSeparator, type OpenChangeReason, Submenu } from "nsh/base";
import { button, buttonText, desktop, Inspector, MenuMark, menuIndicator, Panel, PopIn, type PopupSide, Stage, tint, ui, wide } from "./palette";

/** A sheet at the bottom on phones; on desktops the popup's box keeps no
 * look, its `Items` have it. */
const surface = () =>
  desktop ? undefined : { ...ui().card, borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingHorizontal: 10, paddingTop: 10, paddingBottom: 30 };

/** A menu's items: on desktops on a surface that grows out of the trigger's
 * side each time it opens (`PopIn`), on phones in the sheet that slides up. */
function Items(props: { side?: PopupSide; children: JSX.Element }) {
  if (!desktop) return props.children;
  return (
    <PopIn side={props.side} style={{ ...ui().card, borderRadius: 12, padding: 5, minWidth: 210 }}>
      {props.children}
    </PopIn>
  );
}

const backdrop = { backgroundColor: "rgba(0,0,0,0.45)" };
const rowHeight = () => (desktop ? 32 : 46);
const row = () => ({ flexDirection: "row" as const, alignItems: "center" as const, gap: 8, height: rowHeight(), paddingHorizontal: 10, borderRadius: 8 });

/**
 * One row. A checkable row (`checked`) leads with its mark: a check for a
 * setting, a dot for one choice of a group (`radio`), which fades in as the
 * app checks the item; rows of the same menu keep the mark's room so their
 * labels line up (`inset`).
 */
function Item(props: { label: string; onSelect?: () => void; checked?: boolean; radio?: boolean; inset?: boolean; disabled?: boolean; keepOpen?: boolean; danger?: boolean }) {
  return (
    <MenuItem
      onSelect={props.onSelect}
      disabled={props.disabled}
      checked={props.checked}
      role={props.radio ? "menuitemradio" : undefined}
      indicatorClass={menuIndicator}
      indicator={props.checked === undefined ? undefined : <MenuMark kind={props.radio ? "dot" : "check"} tint={ui().accent} />}
      closeOnSelect={!props.keepOpen}
      hoverStyle={{ backgroundColor: tint(ui().accent, 0.08), borderColor: tint(ui().accent, 0.24) }}
      activeStyle={{ backgroundColor: tint(ui().accent, 0.14), borderColor: tint(ui().accent, 0.34) }}
      disabledStyle={{ opacity: 0.35 }}
      style={{ ...row(), borderWidth: 1, borderColor: "transparent" }}
    >
      <If when={props.inset}>
        <View style={{ width: 16 }} />
      </If>
      <Text style={{ numberOfLines: 1, flexGrow: 1, flexShrink: 0, fontSize: 14, color: props.danger ? ui().down : ui().text }}>{props.label}</Text>
    </MenuItem>
  );
}

function Separator() {
  return <MenuSeparator style={{ height: 1, marginVertical: 4, marginHorizontal: 6, backgroundColor: ui().border }} />;
}

function Share(props: { onSelect: (what: string) => void }) {
  return (
    <Submenu
      offset={4}
      trigger={
        <View hoverStyle={{ backgroundColor: tint(ui().accent, 0.08), borderColor: tint(ui().accent, 0.24) }} activeStyle={{ backgroundColor: tint(ui().accent, 0.14), borderColor: tint(ui().accent, 0.34) }} style={{ ...row(), borderWidth: 1, borderColor: "transparent" }}>
          <Text style={{ numberOfLines: 1, flexGrow: 1, flexShrink: 0, fontSize: 14, color: ui().text }}>Share</Text>
          <Text style={{ fontSize: 14, color: ui().faint }}>›</Text>
        </View>
      }
      back={
        <View activeStyle={{ backgroundColor: tint(ui().accent, 0.14), borderColor: tint(ui().accent, 0.34) }} style={{ ...row(), borderWidth: 1, borderColor: "transparent" }}>
          <Text style={{ fontSize: 14, fontWeight: "600", color: ui().accent }}>‹ Share</Text>
        </View>
      }
    >
      <Items side="right">
        <Item label="Mail" onSelect={() => props.onSelect("Mail")} />
        <Item label="Messages" onSelect={() => props.onSelect("Messages")} />
        <Item label="Copy link" onSelect={() => props.onSelect("Copy link")} />
      </Items>
    </Submenu>
  );
}

/** A group's title: not an item, nothing to press. */
function Label(props: { text: string }) {
  return <Text style={{ fontSize: 12, fontWeight: "600", color: ui().faint, paddingHorizontal: 10, paddingTop: 6, paddingBottom: 2 }}>{props.text}</Text>;
}

const SORTS = ["Name", "Date modified", "Size"];

function App() {
  const last = signal("—");
  const reasons = signal<string[]>([]);
  const hidden = signal(false);
  const sidebar = signal(true);
  const minimap = signal(false);
  const sort = signal("Name");
  const pinned = signal(false);
  const changed = (name: string) => ({ open, reason }: { open: boolean; reason: OpenChangeReason }) =>
    reasons.set([`${name} ${open ? "opened" : "closed"} · ${reason}`, ...reasons()].slice(0, 8));
  const choose = (what: string) => last.set(what);
  const menu = () => (
    <Menu
      offset={4}
      margin={8}
      popupStyle={surface()}
      backdropStyle={backdrop}
      onOpenChange={changed("File")}
      trigger={
        <View {...button("secondary", { align: "flex-start" })}>
          <Text style={buttonText()}>File ▾</Text>
        </View>
      }
    >
      <Items>
        <Item label="New" onSelect={() => choose("New")} />
        <Item label="Open…" onSelect={() => choose("Open…")} />
        <Item label="Paste" disabled />
        <Separator />
        <Share onSelect={(what) => choose(`Share › ${what}`)} />
        <Separator />
        <Item label="Delete" danger onSelect={() => choose("Delete")} />
      </Items>
    </Menu>
  );
  const view = () => (
    <Menu
      offset={4}
      margin={8}
      popupStyle={surface()}
      backdropStyle={backdrop}
      onOpenChange={changed("View")}
      trigger={
        <View {...button("secondary", { align: "flex-start" })}>
          <Text style={buttonText()}>View ▾</Text>
        </View>
      }
    >
      <Items>
        {/* Settings keep the menu open: each shows its new value at once. */}
        <Item label="Show sidebar" checked={sidebar()} keepOpen onSelect={() => sidebar.set(!sidebar())} />
        <Item label="Show minimap" checked={minimap()} keepOpen onSelect={() => minimap.set(!minimap())} />
        <Item label="Show hidden files" checked={hidden()} keepOpen onSelect={() => hidden.set(!hidden())} />
        <Separator />
        <Label text="Sort by" />
        {/* One choice of a group: choosing closes the menu. */}
        <Each of={SORTS}>{(by) => <Item label={by()} radio checked={sort() === by()} onSelect={() => sort.set(by())} />}</Each>
      </Items>
    </Menu>
  );
  const context = () => (
    <ContextMenu
      margin={8}
      popupStyle={surface()}
      backdropStyle={backdrop}
      onOpenChange={changed("Context")}
      trigger={
        <View
          style={{
            height: wide() ? 220 : 150,
            borderRadius: 14,
            borderWidth: 2,
            borderStyle: "dashed",
            borderColor: ui().faint,
            alignItems: "center",
            justifyContent: "center",
            gap: 4,
          }}
        >
          <Text style={{ fontSize: 15, fontWeight: "600", color: ui().text }}>{pinned() ? "ContextMenu · pinned" : "ContextMenu"}</Text>
          <Text style={{ fontSize: 13, color: ui().muted }}>{desktop ? "Right click here" : "Hold a finger here"}</Text>
        </View>
      }
    >
      <Items>
        <Item label="Pinned" checked={pinned()} onSelect={() => pinned.set(!pinned())} />
        <Item label="Copy" inset onSelect={() => choose("Copy")} />
        <Item label="Rename" inset onSelect={() => choose("Rename")} />
        <Separator />
        <Item label="Delete" inset danger onSelect={() => choose("Delete")} />
      </Items>
    </ContextMenu>
  );
  const panels = () => (
    <>
      <Panel title="onSelect · checked">
        <Text style={{ fontSize: 13, color: ui().text }}>{last()}</Text>
        <Text style={{ fontSize: 13, color: ui().muted }}>
          sidebar {sidebar() ? "on" : "off"} · minimap {minimap() ? "on" : "off"} · hidden files {hidden() ? "shown" : "hidden"} · by {sort()} · {pinned() ? "pinned" : "not pinned"}
        </Text>
      </Panel>
      <Panel title="onOpenChange({ open, reason })" grow>
        <View style={{ gap: 2 }}>
          <Each of={reasons()}>{(e, i) => <Text style={{ fontSize: 12, color: i() === 0 ? ui().text : ui().faint }}>{e()}</Text>}</Each>
        </View>
      </Panel>
    </>
  );
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>menu</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click File; hover Share to open its submenu. View has settings and a choice of one. Right click the box below" : "Tap File: a sheet; Share shows its items in place. View has settings and a choice of one"}
      </Text>

      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            <Stage>
              <View style={{ flexDirection: "row", gap: 8 }}>
                {menu()}
                {view()}
              </View>
              {context()}
            </Stage>
            <Inspector>{panels()}</Inspector>
          </View>
        ) : (
          <>
            <View style={{ flexDirection: "row", gap: 8 }}>
              {menu()}
              {view()}
            </View>
            {context()}
            {panels()}
          </>
        )
      }
    </View>
  );
}

mount(App);
  • डेस्कटॉप और डेस्कटॉप ब्राउज़र पर, trigger के पास एक popup दिखता है। Submenu अपने आइटम के बगल में cascade होता है, जब pointer उस पर आता है या उसे दबाता है।
  • फ़ोन और टैबलेट पर, स्क्रीन के नीचे backdrop के ऊपर एक sheet दिखती है। Submenu अपने आइटम menu के आइटम की जगह sheet में दिखाता है, और आपकी एक पंक्ति वापस ले जाती है।

कोई आइटम चुनने, बाहर दबाने, Escape, Android के back बटन और VoiceOver के escape gesture से menu बंद हो जाता है। ContextMenu वही menu है, जो secondary बटन (right click) से pointer पर खुलता है, या टच स्क्रीन पर उसके trigger पर उँगली दबाए रखने से खुलता है।

use nsh_base::{menu, menu_item, menu_separator, submenu};
use nsh_core::prelude::*;

fn item(label: &'static str, on_select: impl Fn() + 'static) -> impl IntoElement {
    menu_item()
        .on_select(on_select)
        .px_3()
        .py_2()
        .rounded_md()
        .child(text(label))
}

fn file_menu() -> impl IntoElement {
    menu()
        .offset(px(4.0))
        .trigger(view().px_3().py_2().child(text("File")))
        .child(item("New", || log::info!("new")))
        .child(item("Open…", || log::info!("open")))
        .child(menu_separator().h(px(1.0)).my_1().bg(rgb(0xe5e7eb)))
        .child(
            submenu()
                .trigger(view().px_3().py_2().child(text("Share ›")))
                .back(view().px_3().py_2().child(text("‹ Back")))
                .child(item("Mail", || log::info!("mail")))
                .child(item("Copy link", || log::info!("link"))),
        )
}

पाँचों nsh/base से आते हैं। आइटम अपने menu तक अपने आप पहुँच जाते हैं, इसलिए आप उन्हें अपनी मर्ज़ी से व्यवस्थित कर सकते हैं: views में समूह, लेबल, और If से आने-जाने वाले आइटम। popupClass डेस्कटॉप पर popup और फ़ोन पर sheet को स्टाइल करता है। जिस स्टाइल को दोनों पर अलग दिखना हो, वह platform.idiom (nsh) की तुलना "desktop" से कर सकता है।

जिन menus के trigger एक ही Toolbar के आइटम हैं, वे मिलकर menu bar बनाते हैं। इनमें से एक menu खुला होने पर, किसी दूसरे menu के trigger पर pointer ले जाने से वह menu उसकी जगह खुल जाता है, और arrow keys से triggers के बीच जाया जा सकता है।

<Toolbar accessibilityLabel="Menu bar">
  <Menu trigger={<View><Text>File</Text></View>}>{/* … */}</Menu>
  <Menu trigger={<View><Text>Edit</Text></View>}>{/* … */}</Menu>
</Toolbar>

#Context menus

import { ContextMenu, MenuItem } from "nsh/base";

<ContextMenu
  popupClass="min-w-40 rounded-lg bg-white p-1 shadow-lg"
  backdropClass="bg-black/40"
  trigger={<PhotoTile photo={photo} />}
>
  <MenuItem onSelect={() => share(photo)}><Text>Share</Text></MenuItem>
  <MenuItem onSelect={() => remove(photo)}><Text>Delete</Text></MenuItem>
</ContextMenu>

डेस्कटॉप पर secondary बटन इसे वहाँ खोलता है जहाँ pointer है। टच स्क्रीन पर trigger पर उँगली दबाए रखने से यह sheet के रूप में खुलता है। trigger के अपने press और आसपास के scroll views काम करते रहते हैं, और उँगली हिलाने से दबाए रखना रद्द हो जाता है।

#Checkable items

checked किसी आइटम को checkable बनाता है। यह menu में दिखने वाली एक सेटिंग (“Show sidebar”) हो सकती है, या role="menuitemradio" के साथ किसी समूह (“Sort by”) का एक विकल्प। मान आपका है: आइटम चुनने पर onSelect चलता है, आप अगला मान सेट करते हैं, और आइटम उसे दिखाता है। जब तक वह checked है, उसकी checked: classes और checkedStyle लागू रहते हैं, और उसके indicator की भी: यानी उसकी सामग्री से पहले का वह भाग (indicatorClass, indicatorStyle) जो आपके दिए निशान (indicator) को घेरता है। निशान एक बार बनता है और बना रहता है। indicator की checked: classes उसे दिखाती और छिपाती हैं, या indicatorClass में group के साथ निशान की अपनी group-checked: classes ऐसा करती हैं। closeOnSelect={false} menu को खुला रखता है, ताकि सेटिंग का नया मान तुरंत दिखे।

use nsh_base::{menu, menu_item};
use nsh_core::{element::StyleRefinement, prelude::*};

fn view_menu(sidebar: Signal<bool>, sort: Signal<&'static str>) -> impl IntoElement {
    // The indicator part shows the mark while the item is checked.
    let part = |s: StyleRefinement| s.w(px(16.0)).opacity(0.0).checked(|s| s.opacity(1.0));
    let by = move |label: &'static str| {
        menu_item()
            .role(AccessibilityRole::MenuItemRadio)
            .checked(move || Some(sort.get() == label))
            .indicator_style(part)
            .indicator(text("✓"))
            .on_select(move || sort.set(label))
            .flex_row()
            .gap_2()
            .px_3()
            .py_2()
            .child(text(label))
    };
    menu()
        .trigger(view().px_3().py_2().child(text("View")))
        .child(
            menu_item()
                .checked(move || Some(sidebar.get()))
                .close_on_select(false)
                .indicator_style(part)
                .indicator(text("✓"))
                .on_select(move || sidebar.set(!sidebar.get_untracked()))
                .flex_row()
                .gap_2()
                .px_3()
                .py_2()
                .child(text("Show sidebar")),
        )
        .child(by("Name"))
        .child(by("Date modified"))
}

पिछले किनारे पर निशान चाहिए तो वह सामग्री में जाता है, और उसे आइटम पर group के अंतर्गत group-checked: classes से स्टाइल किया जाता है।

#Entrance and exit

डेस्कटॉप पर popup आपके transitions के साथ आता-जाता है, ठीक Popover की तरह: popupStartingClass उसका पहले frame का रूप है, popupEndingClass बंद होने के बाद का रूप है, जो popupClass की transition-* classes चलने तक बना रहता है, और उसके बाद onClose आता है। Submenus यही रूप लेते हैं और अपने menu के साथ बंद होते हैं। फ़ोन की sheet अपने आप स्लाइड होकर आती और जाती है।

<Menu
  popupClass="min-w-40 rounded-lg bg-white p-1 shadow-lg transition-all duration-100"
  popupStartingClass="opacity-0 -translate-y-1"
  popupEndingClass="opacity-0"
  trigger={<Text>File</Text>}
>
  <MenuItem onSelect={newFile}><Text>New</Text></MenuItem>
</Menu>

#Open state

open के बिना, menu अपनी state खुद रखता है (Menu के लिए defaultOpen से)। open के साथ, state ऐप की होती है और वह उसे onOpenChange({ open, reason }) में बदलता है: "triggerPress", "select", "pressOutside", "backdropPress", "cancel", "contextMenu" या "longPress"।

#Props

PropRustप्रकारडिफ़ॉल्टविवरण
open.open(..)booleanकोई नहींखुला है या नहीं, ऐप द्वारा नियंत्रित।
defaultOpen.default_open(..)booleanfalseशुरू में खुला, open के बिना।
placement.placement(..)"top" | "bottom" | "left" | "right""bottom"डेस्कटॉप: trigger की किस ओर यह जाता है।
align.align(..)"start" | "center" | "end""start"डेस्कटॉप: यह trigger के साथ कैसे संरेखित होता है।
offset.offset(..)number0डेस्कटॉप: trigger और popup के बीच की दूरी।
margin.margin(..)number0डेस्कटॉप: safe area के किनारों से यह जितनी दूरी रखता है।
popupClass, popupStyle.popup_class(..), .popup_style(..)string, StyleProp<ViewStyle>कोई नहींpopup (डेस्कटॉप) या sheet (फ़ोन), और submenus के popups।
popupStartingClass, popupStartingStyle.popup_starting_class(..), .popup_starting_style(..)string, StyleProp<ViewStyle>कोई नहींडेस्कटॉप: popups का पहले frame का रूप, उनके अपने रूप के ऊपर।
popupEndingClass, popupEndingStyle.popup_ending_class(..), .popup_ending_style(..)string, StyleProp<ViewStyle>कोई नहींडेस्कटॉप: बंद होने के बाद popups का रूप, जो उनके transitions चलने तक बना रहता है।
backdropClass, backdropStyle.backdrop_class(..), .backdrop_style(..)string, StyleProp<ViewStyle>कोई नहींफ़ोन: sheet के पीछे का backdrop।
trigger.trigger(..)JSX.Elementकोई नहींजो दबाने पर इसे खोलता है। menu खुला रहने तक उसकी aria-expanded: classes और expandedStyle लागू रहते हैं।
children.child(..)JSX.Elementकोई नहींआइटम, जो हर बार खुलने पर बनते हैं।

#ContextMenu

open, popupClass / popupStyle, शुरुआती और अंतिम रूप, backdropClass / backdropStyle, trigger और children वैसे ही हैं जैसे Menu के लिए, और:

PropRustप्रकारडिफ़ॉल्टविवरण
disabled.disabled(..)booleanfalsesecondary बटन और दबाए रखी गई उँगलियाँ कुछ नहीं करतीं।
offset.offset(..)number0डेस्कटॉप: pointer और popup के बीच की दूरी।
margin.margin(..)number0डेस्कटॉप: safe area के किनारों से यह जितनी दूरी रखता है।
PropRustप्रकारडिफ़ॉल्टविवरण
disabled.disabled(..)booleanfalseचुना नहीं जा सकता; disabled: स्टाइल लागू होते हैं।
checked.checked(..)booleanकोई नहींchecked है या नहीं, जिससे यह checkable आइटम बनता है; ऐप के स्वामित्व में।
checkedStyle.checked_style(..)StyleProp<ViewStyle>कोई नहींchecked रहते हुए इसकी स्टाइल, checked: classes के रूप में।
role.role(..)AccessibilityRolechecked के साथ "menuitemcheckbox", अन्यथा "menuitem"screen readers के लिए यह क्या है; समूह के एक विकल्प के लिए "menuitemradio"।
indicatorClass, indicatorStyle.indicator_class(..), .indicator_style(..)string, StyleProp<ViewStyle>कोई नहींindicator, यानी सामग्री से पहले का भाग; उसकी checked: और disabled: classes आइटम का अनुसरण करती हैं।
indicator.indicator(..)JSX.Elementकोई नहींindicator के भीतर का निशान।
closeOnSelect.close_on_select(..)booleantrueइसे चुनने पर menu बंद हो जाता है।
accessibilityLabel.accessibility_label(..)stringकोई नहींजब सामग्री पर्याप्त न हो, तब screen readers क्या बोलें।
children, class, style.child(..), स्टाइल मेथडकोई नहींइसकी सामग्री और रूप, hover:, active: और checked: states के साथ।
PropRustप्रकारडिफ़ॉल्टविवरण
trigger.trigger(..)JSX.Elementकोई नहींवह आइटम जो इसे खोलता है।
back.back(..)JSX.Elementकोई नहींफ़ोन: इसके आइटम के ऊपर की वह पंक्ति जो आसपास के आइटम पर वापस ले जाती है।
disabled.disabled(..)booleanfalseयह नहीं खुलता।
offset.offset(..)number0डेस्कटॉप: इसके आसपास के menu और इसके popup के बीच की दूरी।
children.child(..)JSX.Elementकोई नहींइसके आइटम।

MenuSeparator एक view है जिसे आप स्टाइल करते हैं (class, style), और जो अपने स्तर के आइटम के साथ दिखाई देता है।

#Events

EventRustविवरण
onOpenChange (Menu, ContextMenu).on_open_change(|e| ..)यह खुला या बंद हुआ, कारण के साथ।
onOpen (Menu, ContextMenu).on_open(|| ..)यह दिखाया गया है; sheet के रूप में, अंदर आ जाने के बाद।
onClose (Menu, ContextMenu).on_close(|| ..)यह हट गया है: sheet के रूप में जाने के बाद, popup के रूप में अपने exit के बाद। ऐप द्वारा बंद किए जाने पर भी।
onPosition (Menu, ContextMenu).on_position(|e| ..)डेस्कटॉप: popup को रखा गया: placement (वह ओर जहाँ यह गया), bounds, available।
onCancel (Menu, ContextMenu).on_cancel(|| ..)उपयोगकर्ता ने कुछ चुने बिना इसे बंद करने की कोशिश की (Escape या back, बाहर या backdrop पर दबाना)।
onSelect (MenuItem).on_select(|| ..)आइटम चुना गया; checkable आइटम अपना दूसरा मान माँगता है।

#Accessibility

screen readers के लिए popup या sheet एक menu है और उसके आइटम menu items हैं; trigger बताता है कि menu expanded है या नहीं। checkable आइटम एक checkbox या radio menu item होता है, जिसे checked या not checked पढ़ा जाता है; उसका indicator आइटम का हिस्सा है। TSX में trigger वह element है जो आप देते हैं, यानी एक Button या आपका अपना view: वह press और expanded state खुद सँभालता है, इसलिए दबे रहने के दौरान उसकी अपनी active: classes और activeStyle दिखते हैं, और उसका अपना onPress भी चलता है। स्क्रीन पर केवल दिखाए गए स्तर के आइटम होते हैं, इसलिए screen readers वही पढ़ते हैं जो वहाँ मौजूद है।