Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

É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 :

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);
  • 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-aligned de shadcn/ui), aussi large que le déclencheur (la largeur popupStyle / popupClass de l’app en impose une autre, comme le menu_width de gpui-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())
        })
}

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

PropRustTypeDéfautDescription
items.items(..)SelectItem[][]Les éléments, dans l’ordre.
value.value(..)string | nullaucunLa 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(..)stringaucunLa valeur initiale, sans value.
open.open(..)booleanaucunOuvert, possédé par l’app.
defaultOpen.default_open(..)booleanfalseOuvert au départ, sans open.
searchable.searchable(..)booleanfalseUn champ de recherche au-dessus des éléments les filtre par titre.
searchPlaceholder.search_placeholder(..)stringaucunLe texte indicatif du champ de recherche.
searchAccessibilityLabel.search_accessibility_label(..)stringaucunComment les lecteurs d’écran nomment le champ de recherche : ce qu’il recherche.
cleanable.cleanable(..)booleanfalseUn bouton d’effacement tant qu’un élément est choisi ; un appui efface la valeur (onChange({ value: null })).
disabled.disabled(..)booleanfalseIl reste fermé : le déclencheur ne reçoit ni appuis ni focus clavier, et les styles disabled: s’y appliquent.
accessibilityLabel.accessibility_label(..)stringaucunComment 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(..)number0Ordinateurs de bureau : l’écart entre le déclencheur et la liste.
margin.margin(..)number0Ordinateurs 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>aucunLa 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>aucunLa liste, la vue défilante des lignes.
itemClass, itemStyle.item_class(..), .item_style(..)string, StyleProp<TextStyle>aucunLa 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>aucunL’en-tête de chaque section.
searchClass, searchStyle.search_class(..), .search_style(..)string, StyleProp<InputStyle>aucunLe champ de recherche : sa boîte et son texte (color, fontSize, placeholderColor).
clearButtonClass, clearButtonStyle.clear_button_class(..), .clear_button_style(..)string, StyleProp<ViewStyle>aucunLe bouton d’effacement (cleanable) ; disabled: avec le select.
clearButton.clear_button(..)JSX.ElementaucunLe contenu du bouton d’effacement : une croix, une icône.
clearButtonAccessibilityLabel.clear_button_accessibility_label(..)stringaucunComment les lecteurs d’écran nomment le bouton d’effacement.
trigger.trigger(..)JSX.ElementaucunCe qui ouvre la liste lorsqu’on appuie dessus ; la liste se place contre lui.
empty.empty(..)JSX.ElementaucunCe que la liste affiche lorsqu’aucun élément n’est visible (une recherche sans correspondance).
renderItem.render_item(..)(item: SelectEntry) => JSX.ElementaucunLa 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.ElementaucunL’en-tête d’une section, dessiné par l’app ; sinon, son titre.
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucunLa vue autour du déclencheur.

#Événements

ÉvénementRustDescription
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

TypeMembresDescription
SelectItemvalue: string, title: string, disabled?: boolean, group?: stringUn élément de items.
SelectEntryvalue, title, disabled, group? (celui de l’élément), index: numberCe que reçoit renderItem : l’élément et son index parmi les items, jamais son état (les focus:, selected: et disabled: de la ligne).
SelectGrouptitle: stringCe 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

PartieEmplacementStylisée avec
Le déclencheurla vue du selectl’élément propre de l’app (trigger)
Le bouton d’effacementla 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 feuilleau-dessus de tout, au niveau du déclencheur (ordinateurs de bureau) ou en bas (écrans tactiles)popupClass, popupStyle
Le fond d’arrière-planderrière la feuille (écrans tactiles)backdropClass, backdropStyle
Le champ de recherchela popup, en premier (searchable)searchClass, searchStyle, searchPlaceholder, searchAccessibilityLabel
La listela popup, après le champ de recherchelistClass, listStyle
L’en-tête d’une sectionla liste, avant ses élémentsgroupClass, groupStyle, renderGroup
La ligne d’un élémentla listeitemClass, itemStyle, renderItem (group-focus:, group-selected:, group-disabled: à l’intérieur, sous group dans itemClass)
L’état videla popup, tant qu’aucun élément n’est visibleson 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 :

ToucheAction
Bas, HautL’élément activé suivant ou précédent, en bouclant aux extrémités.
Début, FinLe premier ou le dernier élément activé (ceux du champ de recherche lorsqu’il a le clavier).
Page suivante, Page précédenteUne page de lignes plus loin, ou en arrière.
EntréeChoisit l’élément en surbrillance et ferme la liste.
EspaceChoisit l’élément en surbrillance (sans champ de recherche).
Lettres, chiffresL’élément suivant dont le titre commence par ce qui a été saisi pendant la dernière seconde (sans champ de recherche).
ÉchapFerme 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 bureauTéléphones et tablettes
La listeune 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’écranune feuille en bas au-dessus d’un fond d’arrière-plan, aussi haute que ses lignes jusqu’à la barre d’état
La fermerun 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
Recherchele champ prend le clavier à l’ouverture de la listele clavier apparaît lorsqu’on appuie sur le champ
Un choixl’appui vers le bas choisit, de sorte que le champ de recherche garde le clavierun 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. disabled l’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.