Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

Elementi

Select

Un select permette di scegliere un valore da un elenco di elementi: un’impostazione, un filtro, un paese, un fuso orario. Il trigger dell’app apre l’elenco, presentato come ogni dispositivo si aspetta:

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);
  • Sui desktop e nei browser desktop, un popup sopra il trigger con la riga selezionata sovrapposta al trigger, così il titolo scelto resta dov’era (la posizione item-aligned di shadcn/ui), largo quanto il trigger (la larghezza popupStyle / popupClass dell’app ne imposta un’altra, come menu_width di gpui-component) e alto quanto le sue righe, fino allo spazio disponibile sullo schermo; senza alcuna selezione si apre sotto il trigger. La tastiera scorre gli elementi e ne sceglie uno.
  • Su telefoni e tablet, uno sheet in fondo allo schermo sopra un backdrop.

Il select gestisce il comportamento: il valore (quello dell’app, oppure il proprio), lo stato di apertura, la tastiera, il typeahead, la ricerca, le sezioni, la riga evidenziata e ciò che sentono gli screen reader. L’aspetto è dell’app: ne stila il trigger, il popup, l’elenco, le righe, le intestazioni delle sezioni e il campo di ricerca, oppure disegna lei stessa le righe.

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 proviene da nsh/base, il crate nsh-base in Rust. In Rust, select() ne crea uno con un setter per ogni prop (.items(..), .value(..), .item_style(..)) e un listener per ogni evento (.on_change(..)); SelectItem::new(value, title) crea un elemento, con .disabled(..) e .group(..).

#Elementi

items è l’elenco sotto forma di dati: ogni elemento ha un value (univoco tra gli elementi: ciò che viene impostato scegliendolo), un title (ciò che mostra la sua riga, ciò con cui corrispondono ricerca e typeahead e ciò che dicono gli screen reader) e, facoltativamente, disabled e un group. Gli elementi adiacenti con lo stesso group formano una sezione sotto un’intestazione con quel titolo:

<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}
/>

Le righe sono un elenco virtualizzato nella scroll view del popup: un elenco di migliaia di elementi genera solo le righe in vista, e il popup scorre quando le sue righe sono più alte dello spazio disponibile.

#Valore

Con value, l’app possiede l’elemento scelto: il select mostra l’elemento con quel valore e segnala una scelta con onChange({ value }); l’app imposta value per seguirla, oppure mantiene il proprio valore. value={null} significa nessuna scelta. Se omesso, il select mantiene un proprio valore, inizialmente preso da defaultValue. onChange segnala i cambiamenti: scegliere di nuovo l’elemento già scelto chiude l’elenco.

cleanable aggiunge un pulsante di cancellazione finché un elemento è scelto: una pressione azzera il valore, segnalato come onChange({ value: null }), e l’elenco resta chiuso. Il pulsante viene dopo il trigger nella view del select, non al suo interno, quindi una pressione su di esso non apre mai l’elenco; è l’app a posizionarlo (sopra l’estremità del trigger con absolute, dove ci sarebbe un chevron) e a riempirlo:

<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}
/>

Il trigger è un elemento dell’app, quindi mostra ciò che l’app vuole per l’elemento scelto: il titolo, un’icona, un placeholder finché non è scelto nulla. In TSX è l’elemento che passi: è il combobox e gestisce da sé le pressioni, il focus da tastiera, lo stato espanso e disabled, quindi le sue classi active:, focus: e disabled: seguono il select, anche il suo onPress viene eseguito, e l’elenco è posizionato rispetto a esso ed è largo quanto lui.

#Stato di apertura

Senza open, il select mantiene un proprio stato di apertura (a partire da defaultOpen). Con open, lo possiede l’app e lo cambia in onOpenChange({ open, reason }): "triggerPress" (una pressione sul trigger, o uno dei suoi tasti), "select" (una scelta), "pressOutside", "backdropPress" o "cancel" (Escape o indietro). onOpen e onClose indicano quando l’elenco è mostrato e quando è scomparso (per uno sheet, una volta entrato e una volta uscito), onCancel che l’utente lo ha chiuso senza scegliere.

#Ricerca

searchable inserisce un campo di ricerca sopra gli elementi: digitando li filtra per titolo, ignorando maiuscole e minuscole, e una sezione senza corrispondenze sparisce insieme alla sua intestazione. L’evidenziazione si sposta sulla prima corrispondenza, i tasti continuano a spostarla mentre il campo ha la tastiera, e Invio la sceglie. empty è ciò che l’elenco mostra finché nulla corrisponde:

<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}
/>

Sui desktop il campo di ricerca prende la tastiera quando l’elenco si apre; sui touch screen la tastiera compare quando si preme il campo. Ogni apertura parte con una ricerca vuota.

#Righe disegnate dall’app

itemClass stila la riga di ogni elemento, che mostra il titolo dell’elemento finché renderItem non la disegna: focus: si applica alla riga evidenziata (dove si trovano i tasti o il puntatore), selected: a quella scelta e disabled: a una disabilitata. Per disegnare una riga, fornisci renderItem: riceve ciò che è l’elemento (SelectEntry) e restituisce il contenuto della riga, per le righe in vista. Non riceve mai lo stato della riga: con group in itemClass, il contenuto segue gli stati della riga con le classi group-focus:, group-selected: e group-disabled:, quindi il disegno avviene una sola volta per elemento e uno spostamento dell’evidenziazione o una scelta non ridisegnano nulla. renderGroup disegna allo stesso modo l’intestazione di una sezione:

<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

PropRustTipoPredefinitoDescrizione
items.items(..)SelectItem[][]Gli elementi, in ordine.
value.value(..)string | nullnessunoIl value dell’elemento scelto, gestito dall’app; null per nessuno. Se omesso, il select mantiene il proprio.
defaultValue.default_value(..)stringnessunoIl valore iniziale, senza value.
open.open(..)booleannessunoAperto, gestito dall’app.
defaultOpen.default_open(..)booleanfalseAperto inizialmente, senza open.
searchable.searchable(..)booleanfalseUn campo di ricerca sopra gli elementi li filtra per titolo.
searchPlaceholder.search_placeholder(..)stringnessunoIl placeholder del campo di ricerca.
searchAccessibilityLabel.search_accessibility_label(..)stringnessunoCome gli screen reader chiamano il campo di ricerca: ciò che cerca.
cleanable.cleanable(..)booleanfalseUn pulsante di cancellazione finché un elemento è scelto; una pressione azzera il valore (onChange({ value: null })).
disabled.disabled(..)booleanfalseResta chiuso: il trigger non accetta pressioni né focus da tastiera, e si applicano gli stili disabled:.
accessibilityLabel.accessibility_label(..)stringnessunoCome lo chiamano gli screen reader: l’oggetto della scelta.
placement.placement(..)"top" | "bottom" | "left" | "right""bottom"Desktop: il lato del trigger su cui va l’elenco, o l’altro lato se solo quello ha spazio.
align.align(..)"start" | "center" | "end""start"Desktop: come si allinea al trigger.
offset.offset(..)number0Desktop: la distanza tra il trigger e l’elenco.
margin.margin(..)number0Desktop: la distanza che mantiene dai bordi della safe area.
popupClass, popupStyle.popup_class(..), .popup_style(..)string, StyleProp<ViewStyle>nessunoIl popup (desktop) o lo sheet (touch screen) in cui si trova l’elenco.
backdropClass, backdropStyle.backdrop_class(..), .backdrop_style(..)string, StyleProp<ViewStyle>nessunoTouch screen: il backdrop dietro lo sheet.
listClass, listStyle.list_class(..), .list_style(..)string, StyleProp<ViewStyle>nessunoL’elenco, la scroll view delle righe.
itemClass, itemStyle.item_class(..), .item_style(..)string, StyleProp<TextStyle>nessunoLa riga di ogni elemento, con gli stili di testo ereditati dal suo titolo: focus: quando è evidenziata, selected: quando è scelta, disabled: quando non può esserlo.
groupClass, groupStyle.group_class(..), .group_style(..)string, StyleProp<ViewStyle>nessunoL’intestazione di ogni sezione.
searchClass, searchStyle.search_class(..), .search_style(..)string, StyleProp<InputStyle>nessunoIl campo di ricerca: il suo box e il suo testo (color, fontSize, placeholderColor).
clearButtonClass, clearButtonStyle.clear_button_class(..), .clear_button_style(..)string, StyleProp<ViewStyle>nessunoIl pulsante di cancellazione (cleanable); disabled: con il select.
clearButton.clear_button(..)JSX.ElementnessunoIl contenuto del pulsante di cancellazione: una croce, un’icona.
clearButtonAccessibilityLabel.clear_button_accessibility_label(..)stringnessunoCome gli screen reader chiamano il pulsante di cancellazione.
trigger.trigger(..)JSX.ElementnessunoCiò che apre l’elenco quando viene premuto; l’elenco è posizionato rispetto a esso.
empty.empty(..)JSX.ElementnessunoCiò che l’elenco mostra quando nessun elemento è visibile (una ricerca senza corrispondenze).
renderItem.render_item(..)(item: SelectEntry) => JSX.ElementnessunoLa riga di un elemento, disegnata dall’app a partire da ciò che l’elemento è (i suoi stati sono le classi della riga); altrimenti il titolo dell’elemento.
renderGroup.render_group(..)(group: SelectGroup) => JSX.ElementnessunoL’intestazione di una sezione, disegnata dall’app; altrimenti il suo titolo.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoLa view attorno al trigger.

#Eventi

EventoRustDescrizione
onChange.on_change(|value| ..)L’utente ha scelto un altro elemento (una pressione, Invio o Spazio, il typeahead sul trigger): il suo value; null quando il pulsante di cancellazione lo ha azzerato. Imposta value per seguirlo.
onOpenChange.on_open_change(|e| ..)L’elenco si è aperto o chiuso, con il motivo. Imposta open per seguirlo.
onOpen.on_open(|| ..)L’elenco è mostrato; come sheet, una volta entrato.
onClose.on_close(|| ..)L’elenco è scomparso; come sheet, una volta uscito. Anche quando l’app lo ha chiuso.
onCancel.on_cancel(|| ..)L’utente ha chiuso l’elenco senza scegliere (Escape o indietro, una pressione all’esterno o sul backdrop).

Arrivano in quest’ordine: onChange per una scelta, oppure onCancel per una chiusura senza scelta, poi onOpenChange, quindi onOpen o onClose quando l’elenco è mostrato o scomparso.

#Tipi

TipoMembriDescrizione
SelectItemvalue: string, title: string, disabled?: boolean, group?: stringUn elemento di items.
SelectEntryvalue, title, disabled, group? (dell’elemento), index: numberCiò che riceve renderItem: l’elemento e il suo indice tra gli items, mai il suo stato (focus:, selected: e disabled: della riga).
SelectGrouptitle: stringCiò che riceve renderGroup: il group della sezione.

In Rust sono nsh_base::SelectItem, SelectEntry e SelectGroup; value è un SelectValue (Controlled(Some(..)) per un valore, Controlled(None) per null), che .value(..) crea da un Option<SharedString> o da una stringa.

#Parti

ParteDoveStilizzata con
Il triggerla view del selectl’elemento dell’app (trigger)
Il pulsante di cancellazionela view del select, dopo il trigger (cleanable, finché un elemento è scelto)clearButtonClass, clearButtonStyle; il suo contenuto clearButton
Il popup o lo sheetsopra tutto, sul trigger (desktop) o in basso (touch screen)popupClass, popupStyle
Il backdropdietro lo sheet (touch screen)backdropClass, backdropStyle
Il campo di ricercail popup, per primo (searchable)searchClass, searchStyle, searchPlaceholder, searchAccessibilityLabel
L’elencoil popup, dopo il campo di ricercalistClass, listStyle
L’intestazione di una sezionel’elenco, prima dei suoi elementigroupClass, groupStyle, renderGroup
La riga di un elementol’elencoitemClass, itemStyle, renderItem (group-focus:, group-selected:, group-disabled: all’interno, sotto group in itemClass)
Lo stato vuotoil popup, finché nessun elemento è visibileun elemento proprio (empty)

Il popup è una colonna: la sua altezza segue il campo di ricerca e le righe, quindi un max-h-* su popupClass la limita e l’elenco scorre al suo interno.

#Tastiera

Sui desktop, Tab raggiunge il trigger di un select che non è disabled. Con l’elenco chiuso, Freccia giù, Freccia su, Invio e Spazio lo aprono sull’elemento scelto (senza alcuna scelta, Freccia su apre sull’ultimo elemento abilitato e gli altri tasti sul primo), e lettere e cifre scelgono l’elemento il cui titolo inizia con quanto digitato, come fa un select nativo. Con l’elenco aperto:

TastoAzione
Freccia giù, Freccia suL’elemento abilitato successivo o precedente, con ritorno circolare agli estremi.
Home, EndIl primo o l’ultimo elemento abilitato (quelli propri del campo di ricerca mentre questo ha la tastiera).
Pagina giù, Pagina suUna pagina di righe avanti o indietro.
InvioSceglie l’elemento evidenziato e chiude l’elenco.
SpazioSceglie l’elemento evidenziato (senza campo di ricerca).
Lettere, cifreIl successivo elemento il cui titolo inizia con quanto digitato nell’ultimo secondo (senza campo di ricerca).
EscapeChiude l’elenco senza scegliere.

Quando l’elenco si chiude, il focus da tastiera torna al trigger, a meno che una pressione all’esterno non lo abbia portato altrove. Il puntatore evidenzia la riga su cui si muove; la riga evidenziata viene resa visibile quando i tasti si spostano su di essa.

#Su ogni dispositivo

Desktop e browser desktopTelefoni e tablet
L’elencoun popup sopra il trigger, con la riga selezionata sovrapposta (sotto di esso se non ce n’è nessuna), largo quanto il trigger a meno che popupStyle non imposti una larghezza, alto quanto le sue righe fino allo spazio disponibile sullo schermouno sheet in basso sopra un backdrop, alto quanto le sue righe fino alla barra di stato
Chiusurauna scelta, Escape, una pressione all’esterno (che prosegue verso ciò che si trova sotto il puntatore)una scelta, il backdrop, il pulsante indietro di Android
Ricercail campo prende la tastiera quando l’elenco si aprela tastiera compare quando si preme il campo
Una sceltala pressione in discesa sceglie, così il campo di ricerca mantiene la tastieraun tap

popupClass stila il popup sui desktop e lo sheet su telefoni e tablet; uno stile che differisce tra i due può confrontare platform.idiom (nsh/device) con "desktop".

#Accessibilità

  • Il trigger è un combobox per gli screen reader: la sua etichetta è accessibilityLabel, il suo valore è il titolo dell’elemento scelto, e indica se l’elenco è espanso. disabled lo attenua.
  • Il pulsante di cancellazione è un pulsante denominato da clearButtonAccessibilityLabel; non è una tappa del tasto Tab.
  • Il campo di ricerca è un campo di testo denominato da searchAccessibilityLabel; il suo segnaposto è un suggerimento mostrato finché è vuoto.
  • L’elenco è una listbox e ogni riga è un’opzione, denominata dal suo titolo; l’opzione scelta è selezionata e quella disabilitata è attenuata. Le intestazioni delle sezioni sono intestazioni.
  • Mentre i tasti spostano l’evidenziazione, il focus dello screen reader si sposta sulla riga evidenziata; mentre il campo di ricerca ha la tastiera, viene invece annunciato il titolo dell’elemento evidenziato, così la digitazione può continuare.

Vedi Accessibilità per etichette, ruoli e annunci in generale.