Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Elementos

Select

Um select escolhe um valor de uma lista de itens: uma configuração, um filtro, um país, um fuso horário. O gatilho do app abre a lista, apresentada da forma que cada dispositivo espera:

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);
  • Nos desktops e nos navegadores desktop, um popup sobre o gatilho, com a linha selecionada sobreposta ao gatilho, de modo que o título escolhido permaneça onde estava (a posição item-aligned do shadcn/ui), tão largo quanto o gatilho (a largura definida por popupStyle / popupClass no app estabelece outra, como em gpui-component, com menu_width) e tão alto quanto suas linhas, até o espaço disponível na tela; sem nenhum item selecionado, ele abre abaixo do gatilho. O teclado percorre os itens e escolhe um.
  • Em celulares e tablets, uma sheet na parte inferior da tela, sobre um backdrop.

O select é dono do comportamento: o valor (o do app ou o próprio), o estado aberto, o teclado, o typeahead, a busca, as seções, a linha destacada e o que os leitores de tela anunciam. A aparência é do app: ele estiliza o gatilho, o popup, a lista, as linhas, os cabeçalhos das seções e o campo de busca, ou desenha as linhas por conta própria.

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 vem de nsh/base, o crate nsh-base em Rust. Em Rust, select() cria um com um setter por prop (.items(..), .value(..), .item_style(..)) e um listener por evento (.on_change(..)); SelectItem::new(value, title) cria um item, com .disabled(..) e .group(..).

#Itens

items é a lista como dado: cada item tem um value (único entre os itens: o que escolhê-lo define), um title (o que sua linha mostra, o que a busca e o typeahead comparam e o que os leitores de tela anunciam) e, opcionalmente, disabled e um group. Itens adjacentes com o mesmo group formam uma seção sob um cabeçalho com esse título:

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

As linhas são uma lista virtualizada na scroll view do popup: uma lista com milhares de itens cria apenas as linhas visíveis, e o popup rola quando suas linhas são mais altas que o espaço disponível.

#Valor

Com value, o app é dono do item escolhido: o select mostra o item desse valor e informa uma escolha com onChange({ value }); o app define value para acompanhá-la, ou mantém o seu próprio. value={null} significa nada escolhido. Se omitido, o select mantém seu próprio valor, a partir de defaultValue no início. onChange informa as mudanças: escolher novamente o item já escolhido fecha a lista.

cleanable adiciona um botão de limpar enquanto um item está escolhido: um toque limpa o valor, informado como onChange({ value: null }), e a lista permanece fechada. O botão vem depois do gatilho na view do select, não dentro dele, de modo que um toque nele nunca abre a lista; o app o posiciona (sobre o fim do gatilho com absolute, onde ficaria um chevron) e o preenche:

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

O gatilho é um elemento do app, então ele mostra o que o app quiser para o item escolhido: seu título, um ícone, um placeholder enquanto nada está escolhido. Em TSX, ele é o elemento que você passa: ele é o combobox e recebe os toques, o foco do teclado, o estado expandido e disabled por conta própria, então suas próprias classes active:, focus: e disabled: acompanham o select, seu próprio onPress também é executado, e a lista é posicionada em relação a ele e tem a mesma largura.

#Estado aberto

Sem open, o select mantém seu próprio estado aberto (a partir de defaultOpen). Com open, o app é dono dele e o altera em onOpenChange({ open, reason }): "triggerPress" (um toque no gatilho, ou uma de suas teclas), "select" (uma escolha), "pressOutside", "backdropPress" ou "cancel" (Escape ou voltar). onOpen e onClose indicam quando a lista é exibida e quando some (uma sheet, depois de entrar e depois de sair), onCancel indica que o usuário a fechou sem escolher.

#Busca

searchable coloca um campo de busca acima dos itens: digitar filtra-os pelo título, ignorando maiúsculas e minúsculas, e uma seção sem correspondência some junto com seu cabeçalho. O destaque vai para a primeira correspondência, as teclas continuam a movê-lo enquanto o campo tem o teclado, e Return a escolhe. empty é o que a lista mostra enquanto nada corresponde:

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

Nos desktops, o campo de busca recebe o teclado assim que a lista abre; em telas sensíveis ao toque, o teclado aparece quando o campo é tocado. Cada abertura começa com a busca vazia.

#Linhas desenhadas pelo app

itemClass estiliza a linha de cada item, que mostra o título do item até que renderItem a desenhe: focus: se aplica à linha destacada (onde estão as teclas ou o ponteiro), selected: à escolhida e disabled: à desabilitada. Para desenhar uma linha, forneça renderItem: ele recebe o que o item é (SelectEntry) e retorna o conteúdo da linha, para as linhas visíveis. Ele nunca recebe o estado da linha: com group em itemClass, o conteúdo acompanha os estados da linha por meio das classes group-focus:, group-selected: e group-disabled:, de modo que o desenho é feito uma vez por item e mover o destaque ou escolher não redesenha nada. renderGroup desenha o cabeçalho de uma seção da mesma forma:

<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 Tipo Padrão Descrição
items .items(..) SelectItem[] [] Os itens, em ordem.
value .value(..) string | null nenhum O value do item escolhido, controlado pelo app; null para nenhum. Se omitido, o select mantém o seu próprio.
defaultValue .default_value(..) string nenhum O valor inicial, sem value.
open .open(..) boolean nenhum Aberto, controlado pelo app.
defaultOpen .default_open(..) boolean false Aberto no início, sem open.
searchable .searchable(..) boolean false Um campo de busca acima dos itens os filtra pelo título.
searchPlaceholder .search_placeholder(..) string nenhum O placeholder do campo de busca.
searchAccessibilityLabel .search_accessibility_label(..) string nenhum Como os leitores de tela chamam o campo de busca: o que ele pesquisa.
cleanable .cleanable(..) boolean false Um botão de limpar enquanto um item está escolhido; um toque limpa o valor (onChange({ value: null })).
disabled .disabled(..) boolean false Permanece fechado: o gatilho não recebe toques nem foco do teclado, e os estilos disabled: se aplicam a ele.
accessibilityLabel .accessibility_label(..) string nenhum Como os leitores de tela o chamam: sobre o que é a escolha.
placement .placement(..) "top" | "bottom" | "left" | "right" "bottom" Desktops: o lado do gatilho em que a lista fica, ou o outro lado quando só ele tem espaço.
align .align(..) "start" | "center" | "end" "start" Desktops: como ela se alinha com o gatilho.
offset .offset(..) number 0 Desktops: o espaço entre o gatilho e a lista.
margin .margin(..) number 0 Desktops: a distância que ela mantém das bordas da área segura.
popupClass, popupStyle .popup_class(..), .popup_style(..) string, StyleProp<ViewStyle> nenhum O popup (desktops) ou a sheet (telas sensíveis ao toque) em que a lista fica.
backdropClass, backdropStyle .backdrop_class(..), .backdrop_style(..) string, StyleProp<ViewStyle> nenhum Telas sensíveis ao toque: o backdrop atrás da sheet.
listClass, listStyle .list_class(..), .list_style(..) string, StyleProp<ViewStyle> nenhum A lista, a scroll view das linhas.
itemClass, itemStyle .item_class(..), .item_style(..) string, StyleProp<TextStyle> nenhum A linha de cada item, com seus estilos de texto herdados pelo título: focus: enquanto destacada, selected: enquanto escolhida, disabled: enquanto não pode ser escolhida.
groupClass, groupStyle .group_class(..), .group_style(..) string, StyleProp<ViewStyle> nenhum O cabeçalho de cada seção.
searchClass, searchStyle .search_class(..), .search_style(..) string, StyleProp<InputStyle> nenhum O campo de busca: sua caixa e seu texto (color, fontSize, placeholderColor).
clearButtonClass, clearButtonStyle .clear_button_class(..), .clear_button_style(..) string, StyleProp<ViewStyle> nenhum O botão de limpar (cleanable); disabled: com o select.
clearButton .clear_button(..) JSX.Element nenhum O conteúdo do botão de limpar: um X, um ícone.
clearButtonAccessibilityLabel .clear_button_accessibility_label(..) string nenhum Como os leitores de tela chamam o botão de limpar.
trigger .trigger(..) JSX.Element nenhum O que abre a lista quando tocado; a lista é posicionada em relação a ele.
empty .empty(..) JSX.Element nenhum O que a lista mostra enquanto nenhum item aparece (uma busca sem correspondência).
renderItem .render_item(..) (item: SelectEntry) => JSX.Element nenhum A linha de um item, desenhada pelo app a partir do que o item é (seus estados são as classes da linha); caso contrário, o título do item.
renderGroup .render_group(..) (group: SelectGroup) => JSX.Element nenhum O cabeçalho de uma seção, desenhado pelo app; caso contrário, seu título.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum A view ao redor do gatilho.

#Eventos

Evento Rust Descrição
onChange .on_change(|value| ..) O usuário escolheu outro item (um toque, Return ou Space, typeahead no gatilho): seu value; null quando o botão de limpar o limpou. Defina value para acompanhá-lo.
onOpenChange .on_open_change(|e| ..) A lista abriu ou fechou, com o motivo. Defina open para acompanhá-lo.
onOpen .on_open(|| ..) A lista é exibida; como sheet, depois de entrar.
onClose .on_close(|| ..) A lista sumiu; como sheet, depois de sair. Também quando o app a fechou.
onCancel .on_cancel(|| ..) O usuário fechou a lista sem escolher (Escape ou voltar, um toque fora ou no backdrop).

Eles ocorrem nesta ordem: onChange para uma escolha, ou onCancel para um fechamento sem escolha, depois onOpenChange e, em seguida, onOpen ou onClose quando a lista é exibida ou some.

#Tipos

Tipo Membros Descrição
SelectItem value: string, title: string, disabled?: boolean, group?: string Um item de items.
SelectEntry value, title, disabled, group? (do item), index: number O que renderItem recebe: o item e seu índice entre os items, nunca seu estado (focus:, selected: e disabled: da linha).
SelectGroup title: string O que renderGroup recebe: o group da seção.

Em Rust, eles são nsh_base::SelectItem, SelectEntry e SelectGroup; value é um SelectValue (Controlled(Some(..)) para um valor, Controlled(None) para null), que .value(..) cria a partir de um Option<SharedString> ou de uma string.

#Partes

Parte Onde Estilizada com
O gatilho a view do select o próprio elemento do app (trigger)
O botão de limpar a view do select, depois do gatilho (cleanable, enquanto um item está escolhido) clearButtonClass, clearButtonStyle; seu conteúdo clearButton
O popup ou a sheet sobre tudo, no gatilho (desktops) ou na parte inferior (telas sensíveis ao toque) popupClass, popupStyle
O backdrop atrás da sheet (telas sensíveis ao toque) backdropClass, backdropStyle
O campo de busca o popup, em primeiro lugar (searchable) searchClass, searchStyle, searchPlaceholder, searchAccessibilityLabel
A lista o popup, depois do campo de busca listClass, listStyle
O cabeçalho de uma seção a lista, antes de seus itens groupClass, groupStyle, renderGroup
A linha de um item a lista itemClass, itemStyle, renderItem (group-focus:, group-selected:, group-disabled: dentro, sob group em itemClass)
O estado vazio o popup, enquanto nenhum item aparece seu próprio elemento (empty)

O popup é uma coluna: sua altura acompanha o campo de busca e as linhas, de modo que um max-h-* em popupClass a limita e a lista rola dentro dela.

#Teclado

Nos desktops, Tab chega ao gatilho de um select que não está disabled. Com a lista fechada, Down, Up, Return e Space a abrem no item escolhido (sem nenhum escolhido, Up abre no último item habilitado e as demais no primeiro), e letras e dígitos escolhem o item cujo título começa com o que foi digitado, como faz um select nativo. Com a lista aberta:

Tecla Ação
Down, Up O próximo item habilitado ou o anterior, dando a volta nas extremidades.
Home, End O primeiro ou o último item habilitado (as ações do próprio campo de busca enquanto ele tem o teclado).
Page Down, Page Up Avança ou volta uma página de linhas.
Return Escolhe o item destacado e fecha a lista.
Space Escolhe o item destacado (sem campo de busca).
Letras, dígitos O próximo item cujo título começa com o que foi digitado no último segundo (sem campo de busca).
Escape Fecha a lista sem escolher.

Quando a lista fecha, o foco do teclado volta ao gatilho, a menos que um toque fora o tenha levado para outro lugar. O ponteiro destaca a linha sobre a qual passa; a linha destacada é revelada conforme as teclas se movem até ela.

#Em cada dispositivo

Desktops e navegadores desktop Celulares e tablets
A lista um popup sobre o gatilho, com a linha selecionada sobre ele (abaixo dele enquanto não houver nenhuma), tão largo quanto o gatilho, a menos que popupStyle defina uma largura, e tão alto quanto suas linhas, até o espaço disponível na tela uma sheet na parte inferior sobre um backdrop, tão alta quanto suas linhas, até a barra de status
Fechamento uma escolha, Escape, um toque fora (que segue para o que está sob o ponteiro) uma escolha, o backdrop, o botão voltar do Android
Busca o campo recebe o teclado assim que a lista abre o teclado aparece quando o campo é tocado
Uma escolha o pressionar do botão escolhe, de modo que o campo de busca mantém o teclado um toque

popupClass estiliza o popup nos desktops e a sheet nos celulares; um estilo que difere entre eles pode comparar platform.idiom (nsh/device) com "desktop".

#Acessibilidade

  • O gatilho é um combobox para leitores de tela: seu rótulo é accessibilityLabel, seu valor é o título do item escolhido e ele informa se a lista está expandida. disabled o deixa esmaecido.
  • O botão de limpar é um botão nomeado por clearButtonAccessibilityLabel; ele não é uma parada da tecla Tab.
  • O campo de busca é um campo de texto nomeado por searchAccessibilityLabel; seu placeholder é uma dica exibida enquanto ele está vazio.
  • A lista é um listbox e cada linha é uma option, nomeada pelo seu título; a opção escolhida fica selecionada e uma desabilitada fica esmaecida. Os cabeçalhos das seções são cabeçalhos.
  • À medida que as teclas movem o destaque, o foco do leitor de tela se move para a linha destacada; enquanto o campo de busca está com o teclado, o título do item destacado é anunciado no lugar, para que a digitação continue.

Veja Acessibilidade para rótulos, funções e anúncios em geral.