このページは機械翻訳のため、誤りを含む場合があります。 英語の原文を読む

要素

Select

Select は、アイテムのリストから 1 つの値を選ぶコンポーネントです。設定、フィルター、国、タイムゾーンなどに使います。アプリのトリガーがリストを開き、リストは各デバイスに合わせた形で表示されます。

el-select.tsx
el-select.tsxLive
import { type JSX, mount, signal, Text, View } from "nsh";
import { Button, type OpenChangeReason, Select, type SelectEntry, type SelectGroup, Switch } from "nsh/base";
import { button, buttonText, CheckMark, desktop, focusRing, hoverFill, Icon, Log, Panel, size, Split, ui } from "./palette";

// Two selects in a settings card: a plain one (typeahead jumps by letter)
// and a searchable one with sections, a disabled item and an empty state.
// The trigger is the shared button look (hover, pressed, focus ring,
// disabled) with the chosen title and a chevron that turns while the list
// is open; the list is a popup at the trigger with a pointer, a sheet on a
// touch screen. The highlighted row (the keys or the pointer) is lit with
// the accent tint, the chosen one is bolder with a check in its own lane.

const LANGUAGES = [
  { value: "en", title: "English" },
  { value: "de", title: "Deutsch" },
  { value: "es", title: "Español" },
  { value: "fr", title: "Français" },
  { value: "it", title: "Italiano" },
  { value: "nl", title: "Nederlands" },
  { value: "pl", title: "Polski" },
  { value: "pt", title: "Português" },
  { value: "sv", title: "Svenska" },
  { value: "tr", title: "Türkçe" },
  { value: "ja", title: "日本語" },
  { value: "ko", title: "한국어" },
  { value: "zh", title: "中文" },
];

const REGIONS = [
  { value: "us-east-1", title: "US East (Virginia)", group: "Americas" },
  { value: "us-west-2", title: "US West (Oregon)", group: "Americas" },
  { value: "ca-central-1", title: "Canada (Montréal)", group: "Americas" },
  { value: "sa-east-1", title: "South America (São Paulo)", group: "Americas" },
  { value: "eu-west-1", title: "Europe (Ireland)", group: "Europe" },
  { value: "eu-central-1", title: "Europe (Frankfurt)", group: "Europe" },
  { value: "eu-north-1", title: "Europe (Stockholm)", group: "Europe" },
  { value: "ap-northeast-1", title: "Asia Pacific (Tokyo)", group: "Asia Pacific" },
  { value: "ap-northeast-2", title: "Asia Pacific (Seoul)", group: "Asia Pacific", disabled: true },
  { value: "ap-southeast-1", title: "Asia Pacific (Singapore)", group: "Asia Pacific" },
  { value: "ap-east-1", title: "Asia Pacific (Hong Kong)", group: "Asia Pacific" },
  { value: "ap-southeast-2", title: "Asia Pacific (Sydney)", group: "Asia Pacific" },
];

const titleOf = (items: { value: string; title: string }[], value: string | null) => items.find((i) => i.value === value)?.title;

/** The text of a control: 13 with a pointer, the touch label size otherwise. */
const controlText = desktop ? 13 : size.label;

/** A popup at the trigger with a pointer, a sheet at the bottom on a touch screen. */
const surface = () =>
  desktop
    ? { ...ui().card, borderRadius: 12, padding: 4 }
    : { ...ui().card, borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingHorizontal: 10, paddingTop: 10, paddingBottom: 30, maxHeight: "70%" as const };

const backdrop = { backgroundColor: "rgba(0,0,0,0.45)" };

/** The trigger: the shared button's look, the chosen title at the start and a chevron in a lane as wide as it. */
function Trigger(props: { title?: string; placeholder: string; open: boolean; disabled?: boolean; clearable?: boolean }) {
  const look = button("secondary", {
    style: () => ({
      flexDirection: "row",
      justifyContent: "space-between",
      gap: 8,
      ...(desktop ? { width: 260 } : {}),
      // Open: the border stays lit while the list is up; disabled, dimmed.
      ...(props.open ? { borderColor: ui().faint } : {}),
      ...(props.disabled ? { opacity: 0.4 } : {}),
    }),
  });
  return (
    // No hover while disabled.
    <View {...look} hoverStyle={props.disabled ? undefined : look.hoverStyle}>
      <Text style={{ numberOfLines: 1, flex: 1, fontSize: controlText, color: props.title ? ui().text : ui().muted }}>
        {props.title ?? props.placeholder}
      </Text>
      {/* With a clear button over the end (`cleanable`), an empty lane as wide as the button in place of the chevron. */}
      <View
        class="transition-transform duration-150 motion-reduce:transition-none"
        style={{
          width: props.clearable ? 20 : 10,
          height: 20,
          alignItems: "center",
          justifyContent: "center",
          opacity: props.clearable ? 0 : 1,
          transform: [{ rotate: props.open ? "270deg" : "90deg" }],
        }}
      >
        <Text style={{ fontSize: 18, color: ui().muted }}>›</Text>
      </View>
    </View>
  );
}

/**
 * A row's look, on the row itself (`itemClass`): the accent tint while
 * highlighted (the keys or the pointer, as the menus' rows), dimmed while
 * disabled. The states are the row's classes (`group`), so the list draws a
 * row's content once per item and the highlight moves without drawing.
 */
const rowClass =
  "group rounded-lg border border-transparent transition-colors duration-100 motion-reduce:transition-none " +
  "focus:border-brand/30 focus:bg-brand-tint active:opacity-70 disabled:opacity-40";

/**
 * A row's content: the title (bolder while chosen), a quieter detail at the
 * end, the check in its own lane (`group-selected:`).
 */
function Row(props: { item: SelectEntry; detail?: string }) {
  return (
    // The row's 1-point border on each side.
    <View style={{ height: size.rowSm - 2, flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 7 }}>
      <Text class="font-normal group-selected:font-semibold" style={{ numberOfLines: 1, flex: 1, fontSize: controlText, color: ui().text }}>
        {props.item.title}
      </Text>
      <If when={props.detail}>
        <Text style={{ fontSize: 12, color: ui().faint }}>{props.detail}</Text>
      </If>
      <View class="opacity-0 group-selected:opacity-100" style={{ width: 16 }}>
        <CheckMark size={16} tint={ui().accent} />
      </View>
    </View>
  );
}

function Header(props: { group: SelectGroup }) {
  return (
    <Text style={{ fontSize: 12, fontWeight: "500", color: ui().muted, paddingHorizontal: 9, paddingTop: 8, paddingBottom: 4 }}>
      {props.group.title}
    </Text>
  );
}

/** A label over its control, a quieter line under the label. */
function Field(props: { label: string; detail: string; children: JSX.Element }) {
  return (
    <View style={{ gap: 8 }}>
      <View style={{ gap: 2 }}>
        <Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{props.label}</Text>
        <Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
      </View>
      {props.children}
    </View>
  );
}

function App() {
  const language = signal<string | null>("en");
  const region = signal<string | null>(null);
  const languageOpen = signal(false);
  const regionOpen = signal(false);
  const locked = signal(false);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 8));
  const opened = (name: string, open: (next: boolean) => void) => ({ open: next, reason }: { open: boolean; reason: OpenChangeReason }) => {
    open(next);
    log(`${name} ${next ? "opened" : "closed"} · ${reason}`);
  };
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>select</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click a select, or Tab to it and press Down; type a letter to jump" : "Tap a select to choose from a sheet"}
      </Text>

      <Split
        gap={14}
        inspector={
          <>
            <Panel title="value">
              <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{`language ${language() ?? "null"}`}</Text>
              <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{`region ${region() ?? "null"}`}</Text>
              <Button
                disabled={region() === null}
                onPress={() => region.set(null)}
                {...button("secondary", { size: "sm", style: { marginTop: 6 } })}
              >
                <Text style={buttonText("secondary", { size: "sm" })}>Clear region</Text>
              </Button>
              <Switch
                checked={locked()}
                onChange={({ checked }) => locked.set(checked)}
                class={`-mx-2 mt-1 flex-row-reverse items-center justify-between gap-3 rounded-lg px-2 ${desktop ? "py-1" : "py-2"} active:opacity-70`}
                hoverStyle={hoverFill()}
                focusStyle={focusRing()}
                trackClass={`${desktop ? "w-9" : "w-12"} rounded-full p-0.5 transition-colors motion-reduce:transition-none`}
                trackStyle={locked() ? ui().accentFill : ui().inset}
                thumbClass={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"}
                thumbStyle={locked() ? { backgroundColor: ui().onAccent } : ui().raised}
              >
                <Text style={{ flex: 1, fontSize: size.label, color: ui().text }}>Region disabled</Text>
              </Switch>
            </Panel>
            <Log title="onChange · onOpenChange" lines={events()} minHeight={120} />
          </>
        }
      >
        <View style={{ ...ui().card, borderRadius: 16, padding: 16, gap: 20 }}>
          <Field label="Language" detail={desktop ? "Typing a letter on the trigger chooses by title" : "Thirteen languages in a sheet"}>
            <Select
              items={LANGUAGES}
              value={language()}
              onChange={({ value: next }) => {
                language.set(next);
                log(`Language → ${titleOf(LANGUAGES, next)}`);
              }}
              onOpenChange={opened("Language", (next) => languageOpen.set(next))}
              accessibilityLabel="Language"
              offset={4}
              margin={8}
              popupStyle={surface()}
              backdropStyle={backdrop}
              itemClass={rowClass}
              trigger={<Trigger title={titleOf(LANGUAGES, language())} placeholder="Choose a language" open={languageOpen()} />}
              renderItem={(item) => <Row item={item} />}
            />
          </Field>
          <Field label="Region" detail="Search by name; one region is at capacity">
            <Select
              items={REGIONS}
              value={region()}
              onChange={({ value: next }) => {
                region.set(next);
                log(next === null ? "Region cleared" : `Region → ${titleOf(REGIONS, next)}`);
              }}
              onOpenChange={opened("Region", (next) => regionOpen.set(next))}
              disabled={locked()}
              accessibilityLabel="Region"
              searchable
              searchPlaceholder="Search regions"
              searchAccessibilityLabel="Search regions"
              searchClass={`${desktop ? "text-sm" : "text-base"} placeholder:text-[${ui().faint}] text-[${ui().text}]`}
              searchStyle={{ ...ui().inset, borderRadius: 8, height: size.control, paddingHorizontal: 8, marginBottom: 4 }}
              empty={<Text style={{ paddingVertical: 20, textAlign: "center", fontSize: 13, color: ui().muted }}>No region matches</Text>}
              offset={4}
              margin={8}
              // Wider than the trigger on the desktops: the titles and their codes side by side.
              popupStyle={desktop ? { ...surface(), width: 340 } : surface()}
              backdropStyle={backdrop}
              itemClass={rowClass}
              // As wide as the trigger, so the clear button lands over its end.
              style={desktop ? { width: 260 } : undefined}
              cleanable
              clearButtonAccessibilityLabel="Clear the region"
              clearButtonClass="absolute items-center justify-center rounded-full enabled:hover:opacity-70"
              clearButtonStyle={{ top: (size.control - 20) / 2, right: 10, width: 20, height: 20 }}
              clearButton={<Icon name="close" size={14} tint={ui().muted} />}
              trigger={<Trigger title={titleOf(REGIONS, region())} placeholder="Choose a region" open={regionOpen()} disabled={locked()} clearable={region() !== null} />}
              renderItem={(item) => <Row item={item} detail={item.disabled ? "At capacity" : item.value} />}
              renderGroup={(group) => <Header group={group} />}
            />
          </Field>
        </View>
      </Split>
    </View>
  );
}

mount(App);
  • デスクトップおよびデスクトップブラウザーでは、選択中の行をトリガーに重ねたポップアップとして表示するため、選ばれたタイトルは元の位置に留まります(shadcn/ui の item-aligned position)。幅はトリガーと同じで(アプリの popupStyle / popupClass の幅で別の幅を指定でき、gpui-component の menu_width と同様です)、高さは行の高さに合わせ、画面に収まる範囲までです。何も選択されていない場合はトリガーの下に開きます。キーボードでアイテム間を移動し、選択できます。
  • スマートフォンとタブレットでは、バックドロップの上に画面下部のシートとして表示します。

Select は動作を担います。値(アプリが持つ値、または Select 自身の値)、開閉状態、キーボード、タイプアヘッド、検索、セクション、ハイライトされた行、スクリーンリーダーが読み上げる内容です。見た目はアプリ側が決めます。トリガー、ポップアップ、リスト、行、セクションのヘッダー、検索フィールドをスタイルするか、行を自分で描画します。

use nsh_base::{SelectEntry, SelectItem, select, theme};
use nsh_core::prelude::*;

const FRUITS: [(&str, &str); 5] = [
    ("apple", "Apple"),
    ("banana", "Banana"),
    ("blueberry", "Blueberry"),
    ("grapes", "Grapes"),
    ("pineapple", "Pineapple"),
];

fn fruit_select() -> impl IntoElement {
    let fruit = signal(None::<SharedString>);
    let title = move || {
        let chosen = fruit.get();
        FRUITS
            .iter()
            .find(|(value, _)| Some(*value) == chosen.as_deref())
            .map_or("Pick a fruit", |(_, title)| *title)
            .to_string()
    };
    select()
        .items(
            FRUITS
                .iter()
                .map(|&(value, title)| SelectItem::new(value, title).disabled(value == "grapes"))
                .collect::<Vec<_>>(),
        )
        .value(move || fruit.get())
        .on_change(move |next: &Option<SharedString>| fruit.set(next.clone()))
        .accessibility_label("Fruit")
        .offset(px(4.0))
        .popup_style(|s| {
            s.p_1()
                .rounded_lg()
                .border_1()
                .border_color(move || theme().border)
                .bg(move || theme().popover)
                .text_color(move || theme().popover_foreground)
                .shadow_md()
        })
        .item_class("rounded-md focus:bg-accent selected:font-medium disabled:opacity-50")
        .trigger(
            view()
                .h_9()
                .w_48()
                .flex_row()
                .items_center()
                .justify_between()
                .px_3()
                .rounded_md()
                .border_1()
                .border_color(move || theme().input)
                .hover(|s| s.bg(move || theme().accent))
                .child(text(title))
                .child(text("▾").text_color(move || theme().muted_foreground)),
        )
        .render_item(|item: &SelectEntry| {
            view()
                .flex_row()
                .items_center()
                .px_2()
                .py_1p5()
                .child(text(item.title.clone()).text_sm())
        })
}

Select は、Rust の nsh-base クレートである nsh/base から提供されます。Rust では、select() がプロパティごとのセッター(.items(..)、.value(..)、.item_style(..))とイベントごとのリスナー(.on_change(..))を備えた Select を作り、SelectItem::new(value, title) が .disabled(..) と .group(..) を持つアイテムを作ります。

#アイテム

items はデータとしてのリストです。各アイテムは、value(アイテム間で一意。選択すると設定される値)、title(行に表示され、検索とタイプアヘッドの照合対象となり、スクリーンリーダーが読み上げる内容)、そして任意の disabled と group を持ちます。同じ group を持つ隣接アイテムは、そのタイトルのヘッダーの下で 1 つのセクションになります。

<Select
  items={[
    { value: "lisbon", title: "Lisbon", group: "Europe" },
    { value: "london", title: "London", group: "Europe" },
    { value: "tokyo", title: "Tokyo", group: "Asia" },
    { value: "seoul", title: "Seoul", group: "Asia", disabled: true },
  ]}
  groupClass="px-2 pb-1 pt-2 text-xs text-muted-foreground"
  trigger={trigger}
/>

行はポップアップのスクロールビュー内の仮想化リストです。数千件のアイテムがあっても、表示範囲の行だけが生成され、行の高さが利用可能な領域を超える場合はポップアップがスクロールします。

#値

value を指定すると、選択中のアイテムはアプリが保持します。Select はその値のアイテムを表示し、選択を onChange({ value }) で通知します。アプリは value を設定して追従するか、独自の値を保持し続けます。value={null} は何も選択されていない状態です。省略すると、Select は自身で値を保持し、最初は defaultValue の値になります。onChange は変更を通知します。選択中のアイテムをもう一度選ぶと、リストが閉じます。

cleanable を指定すると、アイテムが選択されている間、クリアボタンが追加されます。押すと値がクリアされ、onChange({ value: null }) として通知され、リストは閉じたままです。このボタンは Select のビュー内でトリガーの後ろに置かれ、トリガーの内側にはないため、押してもリストは開きません。配置と中身はアプリが決めます(absolute でトリガーの末尾に重ねると、シェブロンの位置になります)。

<Select
  items={fonts}
  value={font()}
  onChange={({ value }) => font.set(value)}
  cleanable
  clearButtonAccessibilityLabel="Clear the font"
  clearButtonClass="absolute right-2 top-2 size-5 items-center justify-center rounded-full hover:bg-accent"
  clearButton={<Text class="text-xs text-muted-foreground">✕</Text>}
  trigger={trigger}
/>

トリガーはアプリの要素なので、選択中のアイテムについて好きな内容(タイトル、アイコン、何も選択されていないときのプレースホルダー)を表示できます。TSX では、渡した要素そのものがトリガーです。この要素がコンボボックスとなり、プレス、キーボードフォーカス、展開状態、disabled を引き受けます。そのため、要素自身の active:、focus:、disabled: クラスは Select に追従し、要素自身の onPress も実行され、リストはこの要素を基準に配置されて同じ幅になります。

#開閉状態

open がない場合、Select は自身で開閉状態を保持します(初期値は defaultOpen)。open を指定すると、アプリが状態を保持し、onOpenChange({ open, reason }) で変更します。理由は "triggerPress"(トリガーのプレス、またはそのキー)、"select"(選択)、"pressOutside"、"backdropPress"、"cancel"(Escape または戻る)のいずれかです。onOpen と onClose はリストが表示されたとき、消えたときを通知します(シートの場合は、表示が完了したとき、消え終わったとき)。onCancel は、ユーザーが選択せずに閉じたことを通知します。

#検索

searchable を指定すると、アイテムの上に検索フィールドが表示されます。入力するとタイトルで大文字小文字を区別せずに絞り込まれ、一致のないセクションはヘッダーごと消えます。ハイライトは最初の一致に移り、フィールドにキーボードがある間もキーでハイライトを動かせ、Return で選択します。empty は、一致するアイテムがないときにリストに表示される内容です。

<Select
  items={zones}
  searchable
  searchPlaceholder="Search time zones"
  searchAccessibilityLabel="Search time zones"
  searchClass="mx-1 mb-1 rounded-md border border-input px-2 py-1.5 text-sm"
  empty={<Text class="px-2 py-6 text-center text-sm text-muted-foreground">No time zone found</Text>}
  trigger={trigger}
/>

デスクトップでは、リストが開くと同時に検索フィールドがキーボードを受け取ります。タッチスクリーンでは、フィールドを押したときにキーボードが表示されます。開くたびに検索は空の状態から始まります。

#アプリが描画する行

itemClass は各アイテムの行をスタイルします。renderItem が行を描画するまでは、アイテムのタイトルが表示されます。focus: はハイライトされた行(キーまたはポインターがある行)に、selected: は選択中の行に、disabled: は無効な行に適用されます。行を描画するには renderItem を指定します。これはアイテムの内容(SelectEntry)を受け取り、表示範囲の行の内容を返します。行の状態は渡されません。itemClass の group を使うと、内容は行の状態に応じて group-focus:、group-selected:、group-disabled: クラスで変化します。そのため描画はアイテムごとに 1 回で済み、ハイライトの移動や選択で再描画されることはありません。renderGroup も同じ方法でセクションのヘッダーを描画します。

<Select
  items={fonts}
  itemClass="group rounded-md focus:bg-accent disabled:opacity-50"
  renderItem={(item) => (
    <View class="flex-row items-center gap-2 px-2 py-1.5">
      <Text class="flex-1 text-sm" style={{ fontFamily: item.value }}>{item.title}</Text>
      <Text class="w-4 text-right text-sm opacity-0 group-selected:opacity-100">✓</Text>
    </View>
  )}
  renderGroup={(group) => <Text class="px-2 pt-2 text-xs font-medium text-muted-foreground">{group.title}</Text>}
  trigger={trigger}
/>

#プロパティ

プロパティRust型デフォルト説明
items.items(..)SelectItem[][]アイテム(順序どおり)。
value.value(..)string | nullなし選択中のアイテムの value。アプリが保持します。選択なしは null。省略すると Select が自身で保持します。
defaultValue.default_value(..)stringなしvalue がない場合の初期値。
open.open(..)booleanなし開閉状態(アプリが保持)。
defaultOpen.default_open(..)booleanfalseopen がない場合の初期の開閉状態。
searchable.searchable(..)booleanfalseアイテムの上の検索フィールドで、タイトルによって絞り込みます。
searchPlaceholder.search_placeholder(..)stringなし検索フィールドのプレースホルダー。
searchAccessibilityLabel.search_accessibility_label(..)stringなしスクリーンリーダーが検索フィールドを指す名前(何を検索するか)。
cleanable.cleanable(..)booleanfalseアイテムの選択中に表示されるクリアボタン。押すと値がクリアされます(onChange({ value: null }))。
disabled.disabled(..)booleanfalse閉じたままになります。トリガーはプレスもキーボードフォーカスも受け付けず、disabled: のスタイルが適用されます。
accessibilityLabel.accessibility_label(..)stringなしスクリーンリーダーが Select を指す名前(何についての選択か)。
placement.placement(..)"top" | "bottom" | "left" | "right""bottom"デスクトップ: リストを置くトリガーの側。その側にしか余白がない場合は反対側。
align.align(..)"start" | "center" | "end""start"デスクトップ: トリガーに対する揃え方。
offset.offset(..)number0デスクトップ: トリガーとリストの間隔。
margin.margin(..)number0デスクトップ: セーフエリアの端から保つ距離。
popupClass, popupStyle.popup_class(..), .popup_style(..)string, StyleProp<ViewStyle>なしリストを収めるポップアップ(デスクトップ)またはシート(タッチスクリーン)。
backdropClass, backdropStyle.backdrop_class(..), .backdrop_style(..)string, StyleProp<ViewStyle>なしタッチスクリーン: シートの背後のバックドロップ。
listClass, listStyle.list_class(..), .list_style(..)string, StyleProp<ViewStyle>なしリスト(行のスクロールビュー)。
itemClass, itemStyle.item_class(..), .item_style(..)string, StyleProp<TextStyle>なし各アイテムの行。テキストスタイルはタイトルに継承されます。ハイライト中は focus:、選択中は selected:、選択できない間は disabled:。
groupClass, groupStyle.group_class(..), .group_style(..)string, StyleProp<ViewStyle>なし各セクションのヘッダー。
searchClass, searchStyle.search_class(..), .search_style(..)string, StyleProp<InputStyle>なし検索フィールド。ボックスとテキスト(color、fontSize、placeholderColor)。
clearButtonClass, clearButtonStyle.clear_button_class(..), .clear_button_style(..)string, StyleProp<ViewStyle>なしクリアボタン(cleanable)。disabled: は Select に連動します。
clearButton.clear_button(..)JSX.Elementなしクリアボタンの内容(×印、アイコンなど)。
clearButtonAccessibilityLabel.clear_button_accessibility_label(..)stringなしスクリーンリーダーがクリアボタンを指す名前。
trigger.trigger(..)JSX.Elementなし押すとリストを開く要素。リストはこれを基準に配置されます。
empty.empty(..)JSX.Elementなし表示するアイテムがないとき(一致のない検索など)にリストに表示される内容。
renderItem.render_item(..)(item: SelectEntry) => JSX.Elementなしアプリがアイテムの内容から描画する行(状態は行のクラスとして反映)。省略時はアイテムのタイトル。
renderGroup.render_group(..)(group: SelectGroup) => JSX.Elementなしアプリが描画するセクションのヘッダー。省略時はそのタイトル。
class, styleスタイルメソッドstring, StyleProp<ViewStyle>なしトリガーを囲むビュー。

#イベント

イベントRust説明
onChange.on_change(|value| ..)ユーザーが別のアイテムを選択した(プレス、Return または Space、トリガー上のタイプアヘッド)。その value を通知します。クリアボタンでクリアされた場合は null。追従するには value を設定します。
onOpenChange.on_open_change(|e| ..)リストが開いた、または閉じた。理由も通知します。追従するには open を設定します。
onOpen.on_open(|| ..)リストが表示された。シートの場合は表示が完了したとき。
onClose.on_close(|| ..)リストが消えた。シートの場合は消え終わったとき。アプリが閉じた場合にも発生します。
onCancel.on_cancel(|| ..)ユーザーが選択せずにリストを閉じた(Escape または戻る、外側やバックドロップのプレス)。

発生順は、選択の場合は onChange、選択なしで閉じた場合は onCancel、続いて onOpenChange、最後にリストが表示された、または消えたときの onOpen または onClose です。

#型

型メンバー説明
SelectItemvalue: string, title: string, disabled?: boolean, group?: stringitems のアイテム。
SelectEntryvalue, title, disabled, group?(アイテムのもの), index: numberrenderItem が受け取る内容。アイテムと items の中でのそのインデックスであり、状態(行の focus:、selected:、disabled:)は含みません。
SelectGrouptitle: stringrenderGroup が受け取る内容。セクションの group。

Rust では、これらは nsh_base::SelectItem、SelectEntry、SelectGroup です。value は SelectValue(値があれば Controlled(Some(..))、null の場合は Controlled(None))で、.value(..) が Option<SharedString> または文字列から作ります。

#パーツ

パーツ位置スタイル指定
トリガーSelect のビューアプリ自身の要素(trigger)
クリアボタンSelect のビュー内、トリガーの後ろ(cleanable、アイテムの選択中)clearButtonClass、clearButtonStyle。内容は clearButton
ポップアップまたはシート最前面。トリガーの位置(デスクトップ)または画面下部(タッチスクリーン)popupClass、popupStyle
バックドロップシートの背後(タッチスクリーン)backdropClass、backdropStyle
検索フィールドポップアップ内の先頭(searchable)searchClass、searchStyle、searchPlaceholder、searchAccessibilityLabel
リストポップアップ内、検索フィールドの後ろlistClass、listStyle
セクションのヘッダーリスト内、そのアイテムの前groupClass、groupStyle、renderGroup
アイテムの行リストitemClass、itemStyle、renderItem(内側は itemClass の group の下で group-focus:、group-selected:、group-disabled:)
空の状態ポップアップ内、アイテムが表示されないとき専用の要素(empty)

ポップアップは縦方向のカラムで、高さは検索フィールドと行に応じて決まります。そのため popupClass に max-h-* を指定すると高さが制限され、リストはその中でスクロールします。

#キーボード

デスクトップでは、disabled でない Select のトリガーに Tab で移動できます。リストが閉じている間、Down、Up、Return、Space で選択中のアイテムの位置にリストが開きます(何も選択されていない場合、Up は最後の有効なアイテム、それ以外は最初の有効なアイテムが対象です)。また、文字と数字を入力すると、ネイティブの select と同様に、入力した文字で始まるタイトルのアイテムが選択されます。リストが開いているときの操作は次のとおりです。

キー動作
Down, Up次または前の有効なアイテム。端では反対側へ循環します。
Home, End最初または最後の有効なアイテム(検索フィールドがキーボードを持っている間は、フィールド自身の動作)。
Page Down, Page Up1 ページ分、先または前の行へ。
Returnハイライトされたアイテムを選択してリストを閉じます。
Spaceハイライトされたアイテムを選択します(検索フィールドがない場合)。
文字、数字直近 1 秒間に入力した文字で始まるタイトルの次のアイテム(検索フィールドがない場合)。
Escape選択せずにリストを閉じます。

リストが閉じると、キーボードフォーカスはトリガーに戻ります。ただし、外側のプレスによって別の場所へフォーカスが移った場合を除きます。ポインターは、通過した行をハイライトします。キーで移動したときは、ハイライトされた行が見える位置までスクロールされます。

#デバイスごとの動作

デスクトップとデスクトップブラウザースマートフォンとタブレット
リストトリガーに重なるポップアップで、選択中の行がトリガーの位置に来ます(何も選択されていない場合はトリガーの下)。幅は、popupStyle で幅を指定しない限りトリガーと同じで、高さは行の高さに合わせ、画面に収まる範囲までですバックドロップの上に表示される画面下部のシートで、高さは行の高さに合わせ、ステータスバーまでです
閉じ方選択、Escape、外側のプレス(プレスはポインター下の要素にも伝わります)選択、バックドロップ、Android の戻るボタン
検索リストが開くと同時にフィールドがキーボードを受け取りますフィールドを押すとキーボードが表示されます
選択プレスの押下時点で選択されるため、検索フィールドはキーボードを保持しますタップ

popupClass は、デスクトップではポップアップを、スマートフォンではシートをスタイルします。両者で異なるスタイルにしたい場合は、platform.idiom(nsh/device)を "desktop" と比較できます。

#アクセシビリティ

  • トリガーはスクリーンリーダー向けにはコンボボックスとして扱われます。ラベルは accessibilityLabel、値は選択された項目のタイトルで、リストが展開されているかどうかも伝えます。disabled を指定すると淡色表示になります。
  • クリアボタンは clearButtonAccessibilityLabel という名前を持つボタンです。Tab キーのフォーカス停止位置にはなりません。
  • 検索フィールドは searchAccessibilityLabel という名前を持つテキストフィールドです。プレースホルダーは、フィールドが空の間に表示されるヒントです。
  • リストはリストボックス、各行はオプションで、タイトルが名前になります。選択中のオプションは選択状態になり、無効なオプションは淡色表示になります。セクションのヘッダーはヘッダーとして扱われます。
  • キー操作でハイライトが移動すると、スクリーンリーダーのフォーカスもハイライトされた行に移動します。検索フィールドがキーボード入力を受け付けている間は、代わりにハイライトされた項目のタイトルが読み上げられるため、入力をそのまま続けられます。

一般的なラベル、ロール、読み上げについては、アクセシビリティを参照してください。