Éléments
Select
Un select permet de choisir une valeur dans une liste d’éléments : un réglage, un filtre, un pays, un fuseau horaire. Le déclencheur de l’app ouvre la liste, présentée comme chaque appareil l’attend :
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);- Sur les ordinateurs de bureau et dans les navigateurs de bureau, une popup au-dessus du déclencheur, avec la ligne sélectionnée superposée au déclencheur, de sorte que le titre choisi reste à sa place (la position
item-alignedde shadcn/ui), aussi large que le déclencheur (la largeurpopupStyle/popupClassde l’app en impose une autre, comme lemenu_widthdegpui-component) et aussi haute que ses lignes, dans la limite de la place disponible à l’écran ; sans sélection, elle s’ouvre sous le déclencheur. Le clavier parcourt les éléments et en choisit un. - Sur les téléphones et les tablettes, une feuille en bas de l’écran, au-dessus d’un fond d’arrière-plan.
Le select gère le comportement : la valeur (celle de l’app, ou la sienne), l’état d’ouverture, le clavier, la saisie prédictive, la recherche, les sections, la ligne en surbrillance et ce qu’entendent les lecteurs d’écran. L’apparence revient à l’app : elle stylise le déclencheur, la popup, la liste, les lignes, les en-têtes de section et le champ de recherche, ou dessine elle-même les lignes.
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 provient de nsh/base, le crate nsh-base en Rust. En Rust, select() en crée un avec un setter par prop (.items(..), .value(..), .item_style(..)) et un écouteur par événement (.on_change(..)) ; SelectItem::new(value, title) crée un élément, avec .disabled(..) et .group(..).
#Éléments
items est la liste sous forme de données : chaque élément a une value (unique parmi les éléments : ce que le choix de l’élément définit), un title (ce que montre sa ligne, ce que la recherche et la saisie prédictive comparent, et ce que disent les lecteurs d’écran), et en option disabled et un group. Les éléments adjacents d’un même group forment une section sous un en-tête portant ce titre :
<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}
/>
Les lignes forment une liste virtualisée dans la vue défilante de la popup : une liste de milliers d’éléments ne crée que les lignes visibles, et la popup défile lorsque ses lignes dépassent la place dont elle dispose.
#Valeur
Avec value, l’app possède l’élément choisi : le select affiche l’élément de cette valeur et signale un choix avec onChange({ value }) ; l’app définit value pour le suivre, ou garde sa propre valeur. value={null} signifie que rien n’est choisi. S’il est omis, le select garde sa propre valeur, à partir de defaultValue au départ. onChange signale les changements : choisir à nouveau l’élément déjà choisi ferme la liste.
cleanable ajoute un bouton d’effacement tant qu’un élément est choisi : un appui efface la valeur, signalé par onChange({ value: null }), et la liste reste fermée. Le bouton se place après le déclencheur dans la vue du select, et non à l’intérieur, de sorte qu’un appui dessus n’ouvre jamais la liste ; l’app le place (sur la fin du déclencheur avec absolute, là où serait un chevron) et le remplit :
<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}
/>
Le déclencheur est l’élément de l’app : il affiche donc ce que l’app veut pour l’élément choisi : son titre, une icône, un texte indicatif tant que rien n’est choisi. En TSX, c’est l’élément que vous passez : il est la combobox et reçoit lui-même les appuis, le focus clavier, l’état déployé et disabled, de sorte que ses propres classes active:, focus: et disabled: suivent le select, que son propre onPress s’exécute aussi, et que la liste se place contre lui et a sa largeur.
#État d’ouverture
Sans open, le select garde son propre état d’ouverture (à partir de defaultOpen). Avec open, l’app le possède et le modifie dans onOpenChange({ open, reason }) : "triggerPress" (un appui sur le déclencheur, ou l’une de ses touches), "select" (un choix), "pressOutside", "backdropPress" ou "cancel" (Échap ou retour). onOpen et onClose indiquent quand la liste est affichée et quand elle a disparu (pour une feuille, une fois qu’elle est entrée et une fois qu’elle est sortie), onCancel que l’utilisateur l’a fermée sans choisir.
#Recherche
searchable place un champ de recherche au-dessus des éléments : la saisie les filtre par titre, sans tenir compte de la casse, et une section sans correspondance disparaît avec son en-tête. La surbrillance se déplace sur la première correspondance, les touches continuent de la déplacer tant que le champ a le clavier, et Entrée la choisit. empty est ce que la liste affiche tant que rien ne correspond :
<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}
/>
Sur les ordinateurs de bureau, le champ de recherche prend le clavier à l’ouverture de la liste ; sur les écrans tactiles, le clavier apparaît lorsqu’on appuie sur le champ. Chaque ouverture commence avec une recherche vide.
#Lignes dessinées par l’app
itemClass stylise la ligne de chaque élément, qui affiche le titre de l’élément jusqu’à ce que renderItem la dessine : focus: s’applique à la ligne en surbrillance (là où se trouvent les touches ou le pointeur), selected: à la ligne choisie et disabled: à une ligne désactivée. Pour dessiner une ligne, donnez renderItem : elle reçoit ce qu’est l’élément (SelectEntry) et renvoie le contenu de la ligne, pour les lignes visibles. Elle ne reçoit jamais l’état de la ligne : avec group dans itemClass, le contenu suit les états de la ligne grâce aux classes group-focus:, group-selected: et group-disabled:, de sorte que le dessin est réalisé une seule fois par élément et qu’un déplacement de la surbrillance ou un choix ne redessine rien. renderGroup dessine l’en-tête d’une section de la même manière :
<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 | Type | Défaut | Description |
|---|---|---|---|---|
items | .items(..) | SelectItem[] | [] | Les éléments, dans l’ordre. |
value | .value(..) | string | null | aucun | La value de l’élément choisi, possédée par l’app ; null pour aucun. S’il est omis, le select garde la sienne. |
defaultValue | .default_value(..) | string | aucun | La valeur initiale, sans value. |
open | .open(..) | boolean | aucun | Ouvert, possédé par l’app. |
defaultOpen | .default_open(..) | boolean | false | Ouvert au départ, sans open. |
searchable | .searchable(..) | boolean | false | Un champ de recherche au-dessus des éléments les filtre par titre. |
searchPlaceholder | .search_placeholder(..) | string | aucun | Le texte indicatif du champ de recherche. |
searchAccessibilityLabel | .search_accessibility_label(..) | string | aucun | Comment les lecteurs d’écran nomment le champ de recherche : ce qu’il recherche. |
cleanable | .cleanable(..) | boolean | false | Un bouton d’effacement tant qu’un élément est choisi ; un appui efface la valeur (onChange({ value: null })). |
disabled | .disabled(..) | boolean | false | Il reste fermé : le déclencheur ne reçoit ni appuis ni focus clavier, et les styles disabled: s’y appliquent. |
accessibilityLabel | .accessibility_label(..) | string | aucun | Comment les lecteurs d’écran le nomment : ce que concerne le choix. |
placement | .placement(..) | "top" | "bottom" | "left" | "right" | "bottom" | Ordinateurs de bureau : le côté du déclencheur où se place la liste, l’autre côté si seul celui-là a de la place. |
align | .align(..) | "start" | "center" | "end" | "start" | Ordinateurs de bureau : son alignement par rapport au déclencheur. |
offset | .offset(..) | number | 0 | Ordinateurs de bureau : l’écart entre le déclencheur et la liste. |
margin | .margin(..) | number | 0 | Ordinateurs de bureau : la distance qu’elle conserve par rapport aux bords de la zone de sécurité. |
popupClass, popupStyle | .popup_class(..), .popup_style(..) | string, StyleProp<ViewStyle> | aucun | La popup (ordinateurs de bureau) ou la feuille (écrans tactiles) qui contient la liste. |
backdropClass, backdropStyle | .backdrop_class(..), .backdrop_style(..) | string, StyleProp<ViewStyle> | aucun | Écrans tactiles : le fond d’arrière-plan derrière la feuille. |
listClass, listStyle | .list_class(..), .list_style(..) | string, StyleProp<ViewStyle> | aucun | La liste, la vue défilante des lignes. |
itemClass, itemStyle | .item_class(..), .item_style(..) | string, StyleProp<TextStyle> | aucun | La ligne de chaque élément, dont les styles de texte sont hérités par son titre : focus: en surbrillance, selected: lorsqu’elle est choisie, disabled: lorsqu’elle ne peut pas l’être. |
groupClass, groupStyle | .group_class(..), .group_style(..) | string, StyleProp<ViewStyle> | aucun | L’en-tête de chaque section. |
searchClass, searchStyle | .search_class(..), .search_style(..) | string, StyleProp<InputStyle> | aucun | Le champ de recherche : sa boîte et son texte (color, fontSize, placeholderColor). |
clearButtonClass, clearButtonStyle | .clear_button_class(..), .clear_button_style(..) | string, StyleProp<ViewStyle> | aucun | Le bouton d’effacement (cleanable) ; disabled: avec le select. |
clearButton | .clear_button(..) | JSX.Element | aucun | Le contenu du bouton d’effacement : une croix, une icône. |
clearButtonAccessibilityLabel | .clear_button_accessibility_label(..) | string | aucun | Comment les lecteurs d’écran nomment le bouton d’effacement. |
trigger | .trigger(..) | JSX.Element | aucun | Ce qui ouvre la liste lorsqu’on appuie dessus ; la liste se place contre lui. |
empty | .empty(..) | JSX.Element | aucun | Ce que la liste affiche lorsqu’aucun élément n’est visible (une recherche sans correspondance). |
renderItem | .render_item(..) | (item: SelectEntry) => JSX.Element | aucun | La ligne d’un élément, dessinée par l’app à partir de ce qu’est l’élément (ses états sont les classes de la ligne) ; sinon, le titre de l’élément. |
renderGroup | .render_group(..) | (group: SelectGroup) => JSX.Element | aucun | L’en-tête d’une section, dessiné par l’app ; sinon, son titre. |
class, style | méthodes de style | string, StyleProp<ViewStyle> | aucun | La vue autour du déclencheur. |
#Événements
| Événement | Rust | Description |
|---|---|---|
onChange | .on_change(|value| ..) | L’utilisateur a choisi un autre élément (un appui, Entrée ou Espace, la saisie prédictive sur le déclencheur) : sa value ; null lorsque le bouton d’effacement l’a effacée. Définissez value pour le suivre. |
onOpenChange | .on_open_change(|e| ..) | La liste s’est ouverte ou fermée, avec la raison. Définissez open pour le suivre. |
onOpen | .on_open(|| ..) | La liste est affichée ; pour une feuille, une fois qu’elle est entrée. |
onClose | .on_close(|| ..) | La liste a disparu ; pour une feuille, une fois qu’elle est sortie. Aussi lorsque l’app l’a fermée. |
onCancel | .on_cancel(|| ..) | L’utilisateur a fermé la liste sans choisir (Échap ou retour, un appui à l’extérieur ou sur le fond d’arrière-plan). |
Ils arrivent dans cet ordre : onChange pour un choix, ou onCancel pour une fermeture sans choix, puis onOpenChange, puis onOpen ou onClose une fois la liste affichée ou disparue.
#Types
| Type | Membres | Description |
|---|---|---|
SelectItem | value: string, title: string, disabled?: boolean, group?: string | Un élément de items. |
SelectEntry | value, title, disabled, group? (celui de l’élément), index: number | Ce que reçoit renderItem : l’élément et son index parmi les items, jamais son état (les focus:, selected: et disabled: de la ligne). |
SelectGroup | title: string | Ce que reçoit renderGroup : le group de la section. |
En Rust, ce sont nsh_base::SelectItem, SelectEntry et SelectGroup ; value est un SelectValue (Controlled(Some(..)) pour une valeur, Controlled(None) pour null), que .value(..) construit à partir d’un Option<SharedString> ou d’une chaîne.
#Parties
| Partie | Emplacement | Stylisée avec |
|---|---|---|
| Le déclencheur | la vue du select | l’élément propre de l’app (trigger) |
| Le bouton d’effacement | la vue du select, après le déclencheur (cleanable, tant qu’un élément est choisi) | clearButtonClass, clearButtonStyle ; son contenu clearButton |
| La popup ou la feuille | au-dessus de tout, au niveau du déclencheur (ordinateurs de bureau) ou en bas (écrans tactiles) | popupClass, popupStyle |
| Le fond d’arrière-plan | derrière la feuille (écrans tactiles) | backdropClass, backdropStyle |
| Le champ de recherche | la popup, en premier (searchable) | searchClass, searchStyle, searchPlaceholder, searchAccessibilityLabel |
| La liste | la popup, après le champ de recherche | listClass, listStyle |
| L’en-tête d’une section | la liste, avant ses éléments | groupClass, groupStyle, renderGroup |
| La ligne d’un élément | la liste | itemClass, itemStyle, renderItem (group-focus:, group-selected:, group-disabled: à l’intérieur, sous group dans itemClass) |
| L’état vide | la popup, tant qu’aucun élément n’est visible | son propre élément (empty) |
La popup est une colonne : sa hauteur suit le champ de recherche et les lignes, de sorte qu’un max-h-* sur popupClass la plafonne et que la liste défile à l’intérieur.
#Clavier
Sur les ordinateurs de bureau, Tab atteint le déclencheur d’un select qui n’est pas disabled. Lorsque la liste est fermée, Bas, Haut, Entrée et Espace l’ouvrent sur l’élément choisi (sans élément choisi, Haut ouvre sur le dernier élément activé et les autres touches sur le premier), et les lettres et les chiffres choisissent l’élément dont le titre commence par ce qui a été saisi, comme le fait un select natif. Lorsque la liste est ouverte :
| Touche | Action |
|---|---|
| Bas, Haut | L’élément activé suivant ou précédent, en bouclant aux extrémités. |
| Début, Fin | Le premier ou le dernier élément activé (ceux du champ de recherche lorsqu’il a le clavier). |
| Page suivante, Page précédente | Une page de lignes plus loin, ou en arrière. |
| Entrée | Choisit l’élément en surbrillance et ferme la liste. |
| Espace | Choisit l’élément en surbrillance (sans champ de recherche). |
| Lettres, chiffres | L’élément suivant dont le titre commence par ce qui a été saisi pendant la dernière seconde (sans champ de recherche). |
| Échap | Ferme la liste sans choisir. |
Une fois la liste fermée, le focus clavier revient au déclencheur, sauf si un appui à l’extérieur l’a emmené ailleurs. Le pointeur met en surbrillance la ligne qu’il survole ; la ligne en surbrillance est rendue visible lorsque les touches s’y déplacent.
#Sur chaque appareil
| Ordinateurs de bureau et navigateurs de bureau | Téléphones et tablettes | |
|---|---|---|
| La liste | une popup au-dessus du déclencheur, avec la ligne sélectionnée dessus (en dessous tant qu’aucune ne l’est), aussi large que le déclencheur sauf si popupStyle définit une largeur, aussi haute que ses lignes dans la limite de la place disponible à l’écran | une feuille en bas au-dessus d’un fond d’arrière-plan, aussi haute que ses lignes jusqu’à la barre d’état |
| La fermer | un choix, Échap, un appui à l’extérieur (qui se poursuit sur ce qui se trouve sous le pointeur) | un choix, le fond d’arrière-plan, le bouton retour d’Android |
| Recherche | le champ prend le clavier à l’ouverture de la liste | le clavier apparaît lorsqu’on appuie sur le champ |
| Un choix | l’appui vers le bas choisit, de sorte que le champ de recherche garde le clavier | un toucher |
popupClass stylise la popup sur les ordinateurs de bureau et la feuille sur les téléphones ; un style qui diffère entre les deux peut comparer platform.idiom (nsh/device) avec "desktop".
#Accessibilité
- Le déclencheur est une combobox pour les lecteurs d’écran : son libellé est
accessibilityLabel, sa valeur est le titre de l’élément choisi, et il indique si la liste est développée.disabledl’atténue. - Le bouton d’effacement est un bouton nommé par
clearButtonAccessibilityLabel; il n’est pas un arrêt de tabulation. - Le champ de recherche est un champ de texte nommé par
searchAccessibilityLabel; son texte indicatif est une aide affichée tant qu’il est vide. - La liste est une listbox et chaque ligne une option, nommée par son titre ; l’option choisie est sélectionnée et une option désactivée est atténuée. Les en-têtes de sections sont des en-têtes.
- Lorsque les touches déplacent la surbrillance, le focus du lecteur d’écran se déplace vers la ligne mise en surbrillance ; tant que le champ de recherche a le clavier, le titre de l’élément mis en surbrillance est annoncé à la place, afin que la saisie puisse continuer.
Voir Accessibilité pour les libellés, les rôles et les annonces en général.