Elemente
Select
Ein Select wählt einen Wert aus einer Liste von Elementen: eine Einstellung, einen Filter, ein Land, eine Zeitzone. Der Trigger der App öffnet die Liste, dargestellt so, wie es jedes Gerät erwartet:
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);- Auf Desktops und in Desktop-Browsern ein Popup über dem Trigger, wobei die gewählte Zeile über dem Trigger liegt, sodass der gewählte Titel an seiner Stelle bleibt (die Position
item-alignedvon shadcn/ui). Es ist so breit wie der Trigger (die BreitepopupStyle/popupClassder App legt eine andere fest, wie beigpui-componentmitmenu_width) und so hoch wie seine Zeilen, bis zum verfügbaren Platz auf dem Bildschirm. Ohne Auswahl öffnet es sich unterhalb des Triggers. Die Tastatur bewegt sich durch die Elemente und wählt eines aus. - Auf Smartphones und Tablets ein Sheet am unteren Bildschirmrand über einem Backdrop.
Das Select besitzt das Verhalten: den Wert (den der App oder seinen eigenen), den Öffnungszustand, die Tastatur, Typeahead, Suche, Abschnitte, die hervorgehobene Zeile und das, was Screenreader ansagen. Das Aussehen ist Sache der App: Sie gestaltet den Trigger, das Popup, die Liste, die Zeilen, die Header der Abschnitte und das Suchfeld oder zeichnet die Zeilen selbst.
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 kommt aus nsh/base, dem nsh-base-Crate in Rust. In Rust erzeugt select() eines mit einem Setter pro Prop (.items(..), .value(..), .item_style(..)) und einem Listener pro Event (.on_change(..)); SelectItem::new(value, title) erzeugt ein Element, mit .disabled(..) und .group(..).
#Elemente
items ist die Liste als Daten: Jedes Element hat einen value (eindeutig unter den Elementen: das, was seine Auswahl setzt), einen title (was seine Zeile zeigt, worauf Suche und Typeahead passen und was Screenreader sagen) und optional disabled und einen group. Benachbarte Elemente mit demselben group bilden einen Abschnitt unter einem Header mit diesem Titel:
<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}
/>
Die Zeilen sind eine virtualisierte Liste in der Scroll-View des Popups: Eine Liste mit Tausenden Elementen erzeugt nur die Zeilen im sichtbaren Bereich, und das Popup scrollt, wenn seine Zeilen höher sind als der verfügbare Platz.
#Wert
Mit value gehört das gewählte Element der App: Das Select zeigt das Element dieses Werts und meldet eine Auswahl mit onChange({ value }); die App setzt value, um ihr zu folgen, oder behält ihren eigenen Wert. value={null} bedeutet, dass nichts gewählt ist. Wird es weggelassen, behält das Select seinen eigenen Wert, zunächst aus defaultValue. onChange meldet Änderungen: Das erneute Wählen des gewählten Elements schließt die Liste.
cleanable fügt einen Clear-Button hinzu, solange ein Element gewählt ist: Ein Druck löscht den Wert, gemeldet als onChange({ value: null }), und die Liste bleibt geschlossen. Der Button steht in der View des Selects nach dem Trigger, nicht in ihm, sodass ein Druck darauf die Liste nie öffnet. Die App platziert ihn (mit absolute über dem Ende des Triggers, wo sonst ein Chevron wäre) und füllt ihn:
<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}
/>
Der Trigger ist das Element der App, er zeigt also, was auch immer die App für das gewählte Element möchte: seinen Titel, ein Icon, einen Platzhalter, solange nichts gewählt ist. In TSX ist es das Element, das du übergibst: Es ist die Combobox und übernimmt die Drücke, den Tastaturfokus, den Expanded-Zustand und disabled selbst, sodass seine eigenen Klassen active:, focus: und disabled: dem Select folgen, sein eigenes onPress ebenfalls ausgeführt wird und die Liste an ihm ausgerichtet und genauso breit ist.
#Öffnungszustand
Ohne open behält das Select seinen eigenen Öffnungszustand (aus defaultOpen). Mit open gehört er der App, die ihn in onOpenChange({ open, reason }) ändert: "triggerPress" (ein Druck auf den Trigger oder eine seiner Tasten), "select" (eine Auswahl), "pressOutside", "backdropPress" oder "cancel" (Escape oder Zurück). onOpen und onClose melden, wann die Liste angezeigt wird und wann sie verschwunden ist (bei einem Sheet, sobald es eingeblendet bzw. ausgeblendet wurde), onCancel, dass der Nutzer sie ohne Auswahl geschlossen hat.
#Suche
searchable setzt ein Suchfeld über die Elemente: Die Eingabe filtert sie nach Titel, ohne Beachtung der Groß- und Kleinschreibung, und ein Abschnitt ohne Treffer verschwindet samt Header. Die Hervorhebung springt zum ersten Treffer, die Tasten bewegen sie weiter, solange das Suchfeld die Tastatur hat, und die Eingabetaste wählt sie. empty ist das, was die Liste zeigt, solange nichts passt:
<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}
/>
Auf Desktops übernimmt das Suchfeld die Tastatur, sobald sich die Liste öffnet; auf Touchscreens erscheint die Tastatur, wenn das Feld gedrückt wird. Jedes Öffnen beginnt mit einer leeren Suche.
#Zeilen, die die App zeichnet
itemClass gestaltet die Zeile jedes Elements, die den Titel des Elements zeigt, bis renderItem sie zeichnet: focus: gilt für die hervorgehobene Zeile (dort, wo die Tasten oder der Zeiger sind), selected: für die gewählte und disabled: für eine deaktivierte. Um eine Zeile zu zeichnen, gib renderItem an: Es erhält, was das Element ist (SelectEntry), und liefert den Inhalt der Zeile zurück, für die sichtbaren Zeilen. Es erhält nie den Zustand der Zeile: Mit group in itemClass folgt der Inhalt den Zuständen der Zeile über die Klassen group-focus:, group-selected: und group-disabled:, sodass die Darstellung einmal pro Element erstellt wird und eine Bewegung der Hervorhebung oder eine Auswahl nichts neu zeichnet. renderGroup zeichnet den Header eines Abschnitts auf dieselbe Weise:
<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 | Typ | Standard | Beschreibung |
|---|---|---|---|---|
items |
.items(..) |
SelectItem[] |
[] |
Die Elemente, in Reihenfolge. |
value |
.value(..) |
string | null |
keine | Der value des gewählten Elements, gehört der App; null für keines. Wird es weggelassen, behält das Select seinen eigenen. |
defaultValue |
.default_value(..) |
string |
keine | Der Anfangswert, ohne value. |
open |
.open(..) |
boolean |
keine | Geöffnet, gehört der App. |
defaultOpen |
.default_open(..) |
boolean |
false |
Anfangs geöffnet, ohne open. |
searchable |
.searchable(..) |
boolean |
false |
Ein Suchfeld über den Elementen filtert sie nach Titel. |
searchPlaceholder |
.search_placeholder(..) |
string |
keine | Der Platzhalter des Suchfelds. |
searchAccessibilityLabel |
.search_accessibility_label(..) |
string |
keine | Wie Screenreader das Suchfeld nennen: was es durchsucht. |
cleanable |
.cleanable(..) |
boolean |
false |
Ein Clear-Button, solange ein Element gewählt ist; ein Druck löscht den Wert (onChange({ value: null })). |
disabled |
.disabled(..) |
boolean |
false |
Es bleibt geschlossen: Der Trigger nimmt keine Drücke und keinen Tastaturfokus an, und disabled:-Stile gelten für ihn. |
accessibilityLabel |
.accessibility_label(..) |
string |
keine | Wie Screenreader es nennen: worum es bei der Auswahl geht. |
placement |
.placement(..) |
"top" | "bottom" | "left" | "right" |
"bottom" |
Desktops: die Seite des Triggers, auf der die Liste erscheint, die andere Seite, wenn nur dort Platz ist. |
align |
.align(..) |
"start" | "center" | "end" |
"start" |
Desktops: wie sie am Trigger ausgerichtet wird. |
offset |
.offset(..) |
number |
0 |
Desktops: der Abstand zwischen Trigger und Liste. |
margin |
.margin(..) |
number |
0 |
Desktops: der Abstand, den sie zu den Rändern der Safe Area einhält. |
popupClass, popupStyle |
.popup_class(..), .popup_style(..) |
string, StyleProp<ViewStyle> |
keine | Das Popup (Desktops) oder das Sheet (Touchscreens), in dem die Liste liegt. |
backdropClass, backdropStyle |
.backdrop_class(..), .backdrop_style(..) |
string, StyleProp<ViewStyle> |
keine | Touchscreens: der Backdrop hinter dem Sheet. |
listClass, listStyle |
.list_class(..), .list_style(..) |
string, StyleProp<ViewStyle> |
keine | Die Liste, die Scroll-View der Zeilen. |
itemClass, itemStyle |
.item_class(..), .item_style(..) |
string, StyleProp<TextStyle> |
keine | Die Zeile jedes Elements, ihre Textstile werden von ihrem Titel geerbt: focus: solange hervorgehoben, selected: solange gewählt, disabled: solange sie nicht wählbar ist. |
groupClass, groupStyle |
.group_class(..), .group_style(..) |
string, StyleProp<ViewStyle> |
keine | Der Header jedes Abschnitts. |
searchClass, searchStyle |
.search_class(..), .search_style(..) |
string, StyleProp<InputStyle> |
keine | Das Suchfeld: seine Box und sein Text (color, fontSize, placeholderColor). |
clearButtonClass, clearButtonStyle |
.clear_button_class(..), .clear_button_style(..) |
string, StyleProp<ViewStyle> |
keine | Der Clear-Button (cleanable); disabled: zusammen mit dem Select. |
clearButton |
.clear_button(..) |
JSX.Element |
keine | Der Inhalt des Clear-Buttons: ein Kreuz, ein Icon. |
clearButtonAccessibilityLabel |
.clear_button_accessibility_label(..) |
string |
keine | Wie Screenreader den Clear-Button nennen. |
trigger |
.trigger(..) |
JSX.Element |
keine | Was die Liste öffnet, wenn es gedrückt wird; die Liste wird daran ausgerichtet. |
empty |
.empty(..) |
JSX.Element |
keine | Was die Liste zeigt, solange kein Element angezeigt wird (eine Suche ohne Treffer). |
renderItem |
.render_item(..) |
(item: SelectEntry) => JSX.Element |
keine | Die Zeile eines Elements, von der App aus dem gezeichnet, was das Element ist (seine Zustände sind die Klassen der Zeile); sonst der Titel des Elements. |
renderGroup |
.render_group(..) |
(group: SelectGroup) => JSX.Element |
keine | Der Header eines Abschnitts, von der App gezeichnet; sonst sein Titel. |
class, style |
Stilmethoden | string, StyleProp<ViewStyle> |
keine | Die View um den Trigger. |
#Events
| Event | Rust | Beschreibung |
|---|---|---|
onChange |
.on_change(|value| ..) |
Der Nutzer hat ein anderes Element gewählt (ein Druck, Eingabetaste oder Leertaste, Typeahead am Trigger): sein value; null, wenn der Clear-Button es gelöscht hat. Setze value, um ihm zu folgen. |
onOpenChange |
.on_open_change(|e| ..) |
Die Liste wurde geöffnet oder geschlossen, mit dem Grund. Setze open, um dem zu folgen. |
onOpen |
.on_open(|| ..) |
Die Liste wird angezeigt; bei einem Sheet, sobald es eingeblendet wurde. |
onClose |
.on_close(|| ..) |
Die Liste ist verschwunden; bei einem Sheet, sobald es ausgeblendet wurde. Auch, wenn die App sie geschlossen hat. |
onCancel |
.on_cancel(|| ..) |
Der Nutzer hat die Liste ohne Auswahl geschlossen (Escape oder Zurück, ein Druck außerhalb oder auf den Backdrop). |
Sie treten in dieser Reihenfolge auf: onChange bei einer Auswahl oder onCancel bei einem Schließen ohne Auswahl, dann onOpenChange, dann onOpen oder onClose, sobald die Liste angezeigt wird bzw. verschwunden ist.
#Typen
| Typ | Mitglieder | Beschreibung |
|---|---|---|
SelectItem |
value: string, title: string, disabled?: boolean, group?: string |
Ein Element von items. |
SelectEntry |
value, title, disabled, group? (die des Elements), index: number |
Was renderItem erhält: das Element und sein Index unter den items, nie sein Zustand (focus:, selected: und disabled: der Zeile). |
SelectGroup |
title: string |
Was renderGroup erhält: der group des Abschnitts. |
In Rust sind sie nsh_base::SelectItem, SelectEntry und SelectGroup; value ist ein SelectValue (Controlled(Some(..)) für einen Wert, Controlled(None) für null), das .value(..) aus einem Option<SharedString> oder einem String erzeugt.
#Teile
| Teil | Ort | Gestaltet mit |
|---|---|---|
| Der Trigger | die View des Selects | dem eigenen Element der App (trigger) |
| Der Clear-Button | die View des Selects, nach dem Trigger (cleanable, solange ein Element gewählt ist) |
clearButtonClass, clearButtonStyle; sein Inhalt clearButton |
| Das Popup oder Sheet | über allem, am Trigger (Desktops) oder am unteren Rand (Touchscreens) | popupClass, popupStyle |
| Der Backdrop | hinter dem Sheet (Touchscreens) | backdropClass, backdropStyle |
| Das Suchfeld | das Popup, zuerst (searchable) |
searchClass, searchStyle, searchPlaceholder, searchAccessibilityLabel |
| Die Liste | das Popup, nach dem Suchfeld | listClass, listStyle |
| Der Header eines Abschnitts | die Liste, vor seinen Elementen | groupClass, groupStyle, renderGroup |
| Die Zeile eines Elements | die Liste | itemClass, itemStyle, renderItem (group-focus:, group-selected:, group-disabled: im Inneren, unter group in itemClass) |
| Der Leerzustand | das Popup, solange kein Element angezeigt wird | seinem eigenen Element (empty) |
Das Popup ist eine Spalte: Seine Höhe folgt dem Suchfeld und den Zeilen, sodass ein max-h-* auf popupClass sie begrenzt und die Liste darin scrollt.
#Tastatur
Auf Desktops erreicht Tab den Trigger eines Selects, das nicht disabled ist. Bei geschlossener Liste öffnen Pfeil nach unten, Pfeil nach oben, Eingabetaste und Leertaste sie auf dem gewählten Element (ohne Auswahl: Pfeil nach oben auf dem letzten aktivierten Element, die anderen auf dem ersten), und Buchstaben und Ziffern wählen das Element, dessen Titel mit dem Eingegebenen beginnt, wie bei einem nativen Select. Bei geöffneter Liste:
| Taste | Aktion |
|---|---|
| Pfeil nach unten, Pfeil nach oben | Das nächste bzw. vorherige aktivierte Element, an den Enden umlaufend. |
| Pos1, Ende | Das erste bzw. letzte aktivierte Element (die eigenen des Suchfelds, solange es die Tastatur hat). |
| Bild ab, Bild auf | Eine Seite Zeilen vor oder zurück. |
| Eingabetaste | Wählt das hervorgehobene Element und schließt die Liste. |
| Leertaste | Wählt das hervorgehobene Element (ohne Suchfeld). |
| Buchstaben, Ziffern | Das nächste Element, dessen Titel mit dem beginnt, was in der letzten Sekunde eingegeben wurde (ohne Suchfeld). |
| Escape | Schließt die Liste ohne Auswahl. |
Sobald die Liste schließt, kehrt der Tastaturfokus zum Trigger zurück, es sei denn, ein Druck außerhalb hat ihn woandershin gelenkt. Der Zeiger hebt die Zeile hervor, über die er fährt; die hervorgehobene Zeile wird in den sichtbaren Bereich gescrollt, wenn die Tasten sie erreichen.
#Auf jedem Gerät
| Desktops und Desktop-Browser | Smartphones und Tablets | |
|---|---|---|
| Die Liste | ein Popup über dem Trigger, die gewählte Zeile darauf (darunter, solange keine gewählt ist), so breit wie der Trigger, sofern popupStyle keine Breite festlegt, so hoch wie seine Zeilen, bis zum verfügbaren Platz auf dem Bildschirm |
ein Sheet am unteren Rand über einem Backdrop, so hoch wie seine Zeilen, bis zur Statusleiste |
| Schließen | eine Auswahl, Escape, ein Druck außerhalb (der an das weitergeht, was unter dem Zeiger liegt) | eine Auswahl, der Backdrop, die Zurück-Taste von Android |
| Suche | das Feld übernimmt die Tastatur, sobald sich die Liste öffnet | die Tastatur erscheint, wenn das Feld gedrückt wird |
| Eine Auswahl | der Druck beim Herunterdrücken wählt aus, damit das Suchfeld die Tastatur behält | ein Tipp |
popupClass gestaltet das Popup auf Desktops und das Sheet auf Smartphones; ein Stil, der sich zwischen beiden unterscheidet, kann platform.idiom (nsh/device) mit "desktop" vergleichen.
#Barrierefreiheit
- Der Trigger ist für Screenreader eine Combobox: Ihr Label ist
accessibilityLabel, ihr Wert der Titel des ausgewählten Eintrags, und sie gibt an, ob die Liste aufgeklappt ist.disabledblendet sie abgedunkelt dar. - Die Schaltfläche zum Löschen ist ein Button, der durch
clearButtonAccessibilityLabelbenannt wird; sie ist kein Tab-Stopp. - Das Suchfeld ist ein Textfeld, das durch
searchAccessibilityLabelbenannt wird; sein Platzhalter ist ein Hinweis, der angezeigt wird, solange es leer ist. - Die Liste ist eine Listbox und jede Zeile eine Option, die durch ihren Titel benannt wird; die ausgewählte Option ist selektiert und eine deaktivierte wird abgedunkelt dargestellt. Die Kopfzeilen von Abschnitten sind Überschriften.
- Während die Tasten die Hervorhebung verschieben, wandert der Fokus des Screenreaders zur hervorgehobenen Zeile; solange das Suchfeld die Tastatureingaben erhält, wird stattdessen der Titel des hervorgehobenen Eintrags angesagt, sodass die Eingabe weiterlaufen kann.
Siehe Barrierefreiheit für Labels, Rollen und Ansagen im Allgemeinen.