元素
Select
Select 讓使用者從一列項目中選出一個值,例如設定、篩選條件、國家或時區。應用程式的觸發元素會開啟列表,並按各種裝置的慣例呈現:
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定位)。視窗寬度與觸發元素相同(應用程式的popupStyle/popupClass寬度可另行指定,一如gpui-component的menu_width),高度隨各列而定,上限為螢幕可用的空間。未選取任何項目時,視窗會在觸發元素下方開啟。鍵盤可在項目之間移動並選取其中一項。 - 在手機及平板電腦上,列表以螢幕底部的面板呈現,並置於背景遮罩之上。
Select 負責行為:值(由應用程式持有,或由 Select 自行持有)、開啟狀態、鍵盤、輸入即選(typeahead)、搜尋、分組、高亮列,以及螢幕閱讀器讀出的內容。外觀則由應用程式負責:它為觸發元素、彈出視窗、列表、各列、分組標題和搜尋欄設定樣式,或自行繪製各列。
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())
})
}import { mount, signal, Text, View } from "nsh";
import { Select } from "nsh/base";
const FRUITS = [
{ value: "apple", title: "Apple" },
{ value: "banana", title: "Banana" },
{ value: "blueberry", title: "Blueberry" },
{ value: "grapes", title: "Grapes", disabled: true },
{ value: "pineapple", title: "Pineapple" },
];
function App() {
const fruit = signal<string | null>(null);
const title = () => FRUITS.find((f) => f.value === fruit())?.title;
return (
<Select
items={FRUITS}
value={fruit()}
onChange={({ value }) => fruit.set(value)}
accessibilityLabel="Fruit"
offset={4}
popupClass="rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-md"
backdropClass="bg-black/40"
itemClass="rounded-md px-2 py-1.5 text-sm focus:bg-accent focus:text-accent-foreground disabled:opacity-50"
trigger={
<View class="h-9 w-48 flex-row items-center justify-between rounded-md border border-input px-3 hover:bg-accent active:bg-accent focus-visible:ring-2 focus-visible:ring-ring">
<Text class={fruit() ? "text-foreground" : "text-muted-foreground"}>{title() ?? "Pick a fruit"}</Text>
<Text class="text-muted-foreground">▾</Text>
</View>
}
/>
);
}
mount(App);Select 來自 nsh/base,即 Rust 中的 nsh-base crate。在 Rust 中,select() 會建立一個 Select,每個 prop 各有一個 setter(.items(..)、.value(..)、.item_style(..)),每個事件各有一個監聽器(.on_change(..));SelectItem::new(value, title) 會建立一個項目,並附帶 .disabled(..) 和 .group(..)。
#項目
items 是以資料表示的列表:每個項目都有 value(在所有項目中必須唯一,即選取該項目時所設定的值)、title(該列顯示的內容,也是搜尋和輸入即選所比對的文字,以及螢幕閱讀器讀出的內容),以及可選的 disabled 和 group。屬於同一個 group 的相鄰項目會組成一個分組,並置於以該標題命名的分組標題之下:
<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 中,它就是你傳入的元素:它本身即是 combobox,並自行接收按壓、鍵盤焦點、展開狀態和 disabled,因此它自己的 active:、focus: 和 disabled: class 會跟隨 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: class 跟隨列的狀態,因此每個項目只會繪製一次,高亮移動或作出選取時不會重新繪製。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}
/>
#Props
| Prop | Rust | 類型 | 預設值 | 說明 |
|---|---|---|---|---|
items |
.items(..) |
SelectItem[] |
[] |
項目,按順序排列。 |
value |
.value(..) |
string | null |
無 | 所選項目的 value,由應用程式持有;null 表示未選取。省略時,Select 自行保存。 |
defaultValue |
.default_value(..) |
string |
無 | 沒有 value 時的起始值。 |
open |
.open(..) |
boolean |
無 | 開啟狀態,由應用程式持有。 |
defaultOpen |
.default_open(..) |
boolean |
false |
沒有 open 時,是否一開始就開啟。 |
searchable |
.searchable(..) |
boolean |
false |
項目上方的搜尋欄,按標題篩選項目。 |
searchPlaceholder |
.search_placeholder(..) |
string |
無 | 搜尋欄的佔位文字。 |
searchAccessibilityLabel |
.search_accessibility_label(..) |
string |
無 | 螢幕閱讀器對搜尋欄的稱呼:即它所搜尋的內容。 |
cleanable |
.cleanable(..) |
boolean |
false |
已選取項目時顯示清除按鈕;按下後會清除該值(onChange({ value: null }))。 |
disabled |
.disabled(..) |
boolean |
false |
保持關閉:觸發元素不接受按壓和鍵盤焦點,並套用 disabled: 樣式。 |
accessibilityLabel |
.accessibility_label(..) |
string |
無 | 螢幕閱讀器對它的稱呼:即這項選擇與什麼有關。 |
placement |
.placement(..) |
"top" | "bottom" | "left" | "right" |
"bottom" |
桌面平台:列表位於觸發元素的哪一側;若只有另一側有足夠空間,則改用另一側。 |
align |
.align(..) |
"start" | "center" | "end" |
"start" |
桌面平台:與觸發元素的對齊方式。 |
offset |
.offset(..) |
number |
0 |
桌面平台:觸發元素與列表之間的間距。 |
margin |
.margin(..) |
number |
0 |
桌面平台:與安全區域邊緣保持的距離。 |
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 |
無 | 項目的列,由應用程式根據項目資料繪製(其狀態即列的 class);否則顯示項目的標題。 |
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。
#類型
| 類型 | 成員 | 說明 |
|---|---|---|
SelectItem |
value: string、title: string、disabled?: boolean、group?: string |
items 的一個項目。 |
SelectEntry |
value、title、disabled、group?(項目的),index: number |
renderItem 所取得的內容:項目及其在 items 中的索引,絕不包含項目的狀態(列的 focus:、selected: 和 disabled:)。 |
SelectGroup |
title: string |
renderGroup 所取得的內容:分組的 group。 |
在 Rust 中,它們分別是 nsh_base::SelectItem、SelectEntry 和 SelectGroup;value 是一個 SelectValue(Controlled(Some(..)) 表示有值,Controlled(None) 表示 null),.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(內部為 group-focus:、group-selected:、group-disabled:,位於 itemClass 中的 group 之下) |
| 空狀態 | 彈出視窗內,沒有項目顯示時 | 其自身的元素(empty) |
彈出視窗是一個直欄:其高度隨搜尋欄和各列而定,因此在 popupClass 上設定 max-h-* 會限制其高度,列表則在其中捲動。
#鍵盤
在桌面平台上,未 disabled 的 Select 可用 Tab 鍵移到其觸發元素。列表關閉時,按 Down、Up、Return 和 Space 會在已選取的項目上開啟列表(未選取任何項目時,Up 會移到最後一個可用項目,其他按鍵則移到第一個),而字母和數字鍵則會像原生 select 一樣,選取標題以所輸入內容開頭的項目。列表開啟時:
| 按鍵 | 動作 |
|---|---|
| Down、Up | 移到下一個或上一個可用項目,到達盡頭時會循環。 |
| Home、End | 移到第一個或最後一個可用項目(搜尋欄擁有鍵盤時,則由搜尋欄自行處理)。 |
| Page Down、Page Up | 向前或向後移動一頁的列數。 |
| Return | 選取高亮的項目並關閉列表。 |
| Space | 選取高亮的項目(沒有搜尋欄時)。 |
| 字母、數字 | 移到下一個標題以最近一秒內所輸入內容開頭的項目(沒有搜尋欄時)。 |
| Escape | 不作選取並關閉列表。 |
列表關閉後,鍵盤焦點會回到觸發元素,除非在外部按壓時已將焦點移到別處。指標移過的列會被高亮;按鍵移動時,高亮的列會被捲動至可見位置。
#各種裝置
| 桌面平台及桌面瀏覽器 | 手機及平板電腦 | |
|---|---|---|
| 列表 | 位於觸發元素之上的彈出視窗,已選取的列疊在觸發元素上(未選取任何項目時則在其下方),寬度與觸發元素相同(除非 popupStyle 指定了寬度),高度隨各列而定,上限為螢幕可用的空間 |
位於底部、置於背景遮罩之上的面板,高度隨各列而定,上限為狀態列 |
| 關閉方式 | 作出選取、Escape、在外部按壓(該按壓會繼續傳到指標下方的元素) | 作出選取、背景遮罩、Android 返回按鈕 |
| 搜尋 | 列表開啟時,搜尋欄會取得鍵盤 | 按下搜尋欄後才會彈出鍵盤 |
| 選取 | 按下的瞬間即選取,因此搜尋欄會保持鍵盤焦點 | 輕按 |
popupClass 在桌面平台上為彈出視窗設定樣式,在手機上則為面板設定樣式;若兩者的樣式不同,可以用 "desktop" 比較 platform.idiom(nsh/device)。
#無障礙設計
- 對螢幕閱讀器而言,觸發器是一個組合框(combobox):其標籤為
accessibilityLabel,其值為所選項目的標題,並會說明清單是否已展開。disabled會令它變暗。 - 清除按鈕是一個按鈕,名稱由
clearButtonAccessibilityLabel指定;它不是 Tab 鍵的停駐點。 - 搜尋欄位是一個文字欄位,名稱由
searchAccessibilityLabel指定;其預留位置文字(placeholder)是在欄位為空時顯示的提示。 - 清單是一個 listbox,每一列是一個 option,以其標題命名;已選取的選項會標示為選取狀態,已停用的選項則會變暗。各區段的標頭屬於標題(header)。
- 當按鍵移動醒目標示時,螢幕閱讀器的焦點會移到被標示的列;當搜尋欄位正在接收鍵盤輸入時,則會改為朗讀被標示項目的標題,讓使用者可以繼續輸入。
有關標籤、角色及朗讀內容的一般說明,請參閱無障礙設計。