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

Elementi

Tabs

Le tab permettono di passare da una sezione all’altra di uno stesso luogo: la panoramica e l’attività di un progetto, le viste di un documento. Ogni Tab gestisce il comportamento di una tab: pressioni, attivazione da tastiera, stato disabilitato, e il ruolo di tab con il relativo stato di selezione per gli screen reader. Un Tabs che le racchiude è la lista di tab, che gli screen reader annunciano come un unico insieme. L’aspetto è quello dell’app: una tab non ha colori, dimensioni o etichetta propri. I suoi figli sono l’etichetta dell’app, e le classi selected: sulla tab, o le classi group-selected: su ciò che contiene, seguono il suo stato. Anche il contenuto di ogni sezione è dell’app: mostra quello che il suo valore indica, accanto alla lista. L’indicatore della lista, una sottolineatura o una pillola con lo stile dell’app, si posiziona sulla tab selezionata e si sposta nativamente su quella successiva (Indicator).

el-tabs.tsx
el-tabs.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Tab, Tabs } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, size, Split, tint, ui } from "./palette";

// What each section shows: the app switches the content by its own value.
const sections = [
  {
    id: "overview",
    label: "Overview",
    rows: [
      ["Visitors", "12,480"],
      ["Sign-ups", "342"],
      ["Conversion", "2.74%"],
    ],
  },
  {
    id: "activity",
    label: "Activity",
    rows: [
      ["Deploy to production", "09:41"],
      ["Invoice paid", "08:15"],
      ["New member joined", "07:02"],
    ],
  },
  {
    id: "settings",
    label: "Settings",
    rows: [
      ["Region", "Frankfurt"],
      ["Plan", "Pro"],
      ["Seats", "8 of 10"],
    ],
  },
];
const folders = [
  { id: "inbox", label: "Inbox", count: 12 },
  { id: "archive", label: "Archive", count: 0 },
  { id: "spam", label: "Spam", count: 3, locked: true },
];

function App() {
  const section = signal("overview");
  const folder = signal("inbox");
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 3));
  const current = () => sections.find((s) => s.id === section())!;
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>tabs</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click a tab, or Tab to a list and move with the arrow keys" : "Tap a tab to show its section"}
      </Text>

      <Split gap={14} inspector={<Log title={`onPress · ${section()}, ${folder()}`} lines={events()} minHeight={76} />}>
        {/* A segmented tab list: each tab is a segment, and the list's indicator is the raised one, inside the
            list's padding, which springs to the tab the app selects. */}
        <Tabs
          style={{ flexDirection: "row", gap: 4, padding: 3, borderRadius: 10, ...ui().inset, ...(desktop ? { alignSelf: "flex-start" } : {}) }}
          indicatorStyle={{ top: 3, bottom: 3, borderRadius: 7, ...ui().raised }}
        >
          <Each of={sections}>
            {(s) => (
              <Tab
                selected={section() === s().id}
                onPress={() => {
                  section.set(s().id);
                  log(`${s().label} pressed`);
                }}
                style={
                  desktop
                    ? { height: 26, paddingHorizontal: 14, borderRadius: 7, alignItems: "center", justifyContent: "center" }
                    : { flex: 1, height: 30, borderRadius: 7, alignItems: "center", justifyContent: "center" }
                }
                class="active:opacity-70"
                hoverStyle={section() === s().id ? undefined : hoverFill()}
                focusStyle={focusRing()}
              >
                <Text style={{ fontSize: 13, fontWeight: "500", color: section() === s().id ? ui().text : ui().muted }}>
                  {s().label}
                </Text>
              </Tab>
            )}
          </Each>
        </Tabs>

        {/* The selected section's content, which the app shows by its value. */}
        <View style={{ ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          <Each of={current().rows}>
            {(row, i) => (
              <>
                {i() > 0 ? <View style={{ height: 1, marginLeft: desktop ? 12 : 16, backgroundColor: ui().line }} /> : null}
                <View
                  style={
                    desktop
                      ? { flexDirection: "row", alignItems: "center", paddingHorizontal: 12, height: size.row }
                      : { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12 }
                  }
                >
                  <Text style={{ flex: 1, fontSize: size.label, color: ui().text }}>{row()[0]}</Text>
                  <Text style={{ fontSize: 14, fontWeight: "600", fontVariant: ["tabular-nums"], color: ui().muted }}>{row()[1]}</Text>
                </View>
              </>
            )}
          </Each>
        </View>

        {/* Tabs with counts: the selected one's own fill, a disabled one dimmed. */}
        <Tabs style={{ flexDirection: "row", gap: 6 }}>
          <Each of={folders}>
            {(f) => (
              <Tab
                selected={folder() === f().id}
                disabled={f().locked}
                onPress={() => {
                  folder.set(f().id);
                  log(`${f().label} pressed`);
                }}
                class="active:opacity-70 disabled:opacity-40"
                hoverStyle={f().locked || folder() === f().id ? undefined : hoverFill()}
                focusStyle={focusRing()}
                style={{ flexDirection: "row", alignItems: "center", gap: 6, height: desktop ? size.controlSm : 30, paddingHorizontal: desktop ? 10 : 12, borderRadius: 8 }}
                selectedStyle={{ backgroundColor: tint(ui().accent, 0.16) }}
              >
                <Text style={{ fontSize: 13, fontWeight: "500", color: folder() === f().id ? ui().accent : ui().muted }}>
                  {f().label}
                </Text>
                {f().count > 0 ? (
                  <Text style={{ fontSize: 12, fontWeight: "600", fontVariant: ["tabular-nums"], color: ui().faint }}>{f().count}</Text>
                ) : null}
              </Tab>
            )}
          </Each>
        </Tabs>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{tab, tabs, theme};
use nsh_core::prelude::*;

fn sections() -> impl IntoElement {
    let section = signal("overview");
    let item = move |value: &'static str, label: &'static str| {
        let selected = move || section.get() == value;
        tab()
            .selected(selected)
            .on_press(move |_: &()| section.set(value))
            .rounded(move || theme().radius)
            .px_3()
            .py_2()
            .child(text(label).text_sm().text_color(move || {
                if selected() { theme().foreground } else { theme().muted_foreground }
            }))
    };
    view()
        .gap_4()
        .child(
            tabs()
                .flex_row()
                .self_start()
                .gap_1()
                .p_1()
                .rounded(move || theme().radius)
                .bg(move || theme().muted)
                .indicator_style(|s| {
                    s.top_1()
                        .bottom_1()
                        .rounded(move || theme().radius)
                        .bg(move || theme().background)
                })
                .child(item("overview", "Overview"))
                .child(item("activity", "Activity")),
        )
        .child(text(move || {
            if section.get() == "overview" {
                "Visitors, sign-ups and conversion this week."
            } else {
                "Deploys, invoices and new members, newest first."
            }
        }).text_color(move || theme().foreground))
}

Tab e Tabs provengono da nsh/base, il crate nsh-base in Rust. Il valore è dell’app: la lista non ne contiene alcuno. A ogni tab viene comunicato se è selezionata; una pressione riporta onPress, l’app imposta il proprio valore e mostra quella sezione, e ogni tab mostra il selected che l’app le restituisce. Gli angoli della lista e quelli delle tab sono concentrici: il padding della lista (p-1) è la differenza tra radius.lg e radius. Il riempimento della tab selezionata è l’indicatore della lista (indicatorClass, all’interno di quel padding: inset-y-1), che scatta con effetto elastico verso la tab selezionata dall’app. In Rust, tab() e tabs() li creano, con un setter per ogni prop (.selected(..)), un listener per ogni evento (.on_press(..)), i metodi di stile tipizzati e .child(..) per il loro contenuto.

#Props

#Tab

PropRustTipoPredefinitoDescrizione
selected.selected(..)booleanfalseLa tab corrente della sua lista: gli screen reader la annunciano come selezionata, e si applicano gli stili selected:. Una pressione riporta onPress; la tab mostra il valore che l’app le restituisce.
disabled.disabled(..)booleanfalseNessuna pressione e nessun feedback di pressione; si applicano gli stili disabled:, e gli screen reader la annunciano come attenuata.
selectedStyle.selected_style(|s| ..)StyleProp<ViewStyle>nessunoLo stile della tab quando è selezionata e non disabilitata, come classi selected:.
accessibilityLabel.accessibility_label(..)stringnessunoCiò che dicono gli screen reader: il nome della tab, quando i suoi figli non hanno testo che lo esprima (un’icona).
children.child(..)JSX.ElementnessunoL’etichetta, l’icona o il badge dell’app, che gli screen reader leggono.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoLa tab stessa: la superficie attorno alla sua etichetta.
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>nessunoLo stile della tab quando è premuta, con il puntatore sopra, con il focus o disabilitata, come classi <state>:.

#Tabs

PropRustTipoPredefinitoDescrizione
indicatorClass.indicator_class(..)stringnessunoLe classi Tailwind dell’indicatore sulla tab selezionata: il suo aspetto e la sua posizione attraverso la lista (bottom-0 h-0.5 per una sottolineatura in una riga, inset-y-1 per una pillola). Lungo la lista copre la tab selezionata, entro i propri margini su quell’asse. Con indicatorClass o indicatorStyle la lista ha un indicatore.
indicatorStyle.indicator_style(|s| ..)StyleProp<ViewStyle>nessunoLo stile dell’indicatore, sopra indicatorClass.
children.child(..)JSX.ElementnessunoLe tab della lista, e tutto ciò che l’app dispone tra di esse.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoIl layout e l’aspetto della lista: una colonna per impostazione predefinita, flex-row per una riga.

#Eventi

EventoRustDescrizione
onPress (Tab).on_press(|_: &()| ..)La tab è stata premuta: un tap, un clic, Spazio o Invio mentre ha il focus da tastiera, oppure l’attivazione tramite screen reader; oppure un tasto freccia, Home o End della sua lista ha spostato il focus da tastiera su di essa. Viene eseguito prima che cambi qualsiasi cosa: la tab mostra il selected che l’app le restituisce.

Anche la tab selezionata riporta le proprie pressioni, così l’app può reagire a una seconda pressione (ad esempio riportare la sua sezione in cima). Una tab disabilitata non riporta nulla.

#Parti

ParteDentroStilizzata conStati
La listaTabs class, style
L’indicatorela lista, dietro le tabindicatorClass, indicatorStylesulla tab selezionata
La tabla listaTab class, styleselected:, disabled:, active:, hover:, focus: (o selectedStyle, disabledStyle, …)
L’etichettala tabi suoi elementi (children)group-selected:, group-disabled: e le altre varianti group-*: con group sul class della tab
Il contenuto della sezioneaccanto alla listagli elementi dell’appil valore dell’app

Mostra la tab selezionata attraverso la sua superficie: un riempimento, un’etichetta più marcata, uno spessore maggiore (vedi Design guides).

#Indicator

Una sottolineatura sotto la tab selezionata, una pillola dietro di essa o il segmento in rilievo di un controllo segmentato è l’indicatore della lista: una vista che l’app stilizza con indicatorClass e indicatorStyle, disegnata dietro le tab e posizionata in modo assoluto nella lista. Native Shell la posiziona sulla tab selezionata dopo ogni layout. Lungo la lista (la larghezza di una riga, l’altezza di una colonna) copre quella tab, entro i margini dell’indicatore stesso su quell’asse (mx-3 per una sottolineatura più corta della sua tab); attraverso la lista sono le sue classi a posizionarla:

// An underline at the bottom of a row of tabs.
<Tabs class="flex-row gap-4 border-b border-border" indicatorClass="bottom-0 h-0.5 bg-foreground">
  {item("overview", "Overview")}
  {item("activity", "Activity")}
</Tabs>

// A pill behind the selected tab, inside the list's padding.
<Tabs class="flex-row self-start gap-1 rounded-(--radius-lg) bg-muted p-1" indicatorClass="inset-y-1 rounded-(--radius) bg-background shadow-sm">
  {item("overview", "Overview")}
  {item("activity", "Activity")}
</Tabs>

Quando l’app seleziona un’altra tab, l’indicatore vi scatta nativamente con effetto elastico, come fa l’indicatore della tab bar di GPUI Kit; con il movimento ridotto si sposta all’istante. Finché nessuna tab è selezionata è nascosto; se ne sono selezionate diverse, sta sulla prima. Un ridimensionamento o una variazione della larghezza di una tab lo sposta nello stesso frame delle tab. L’indicatore non richiede eventi di layout né codice da parte dell’app: la tab selezionata è quella il cui selected è true. In una lista che scorre, la vista di scorrimento va attorno a Tabs, e l’indicatore scorre insieme alle tab.

#Disabled

Una tab disabilitata non accetta pressioni, e gli screen reader la annunciano come attenuata. Le sue classi disabled: si applicano sopra quelle selected:, e le classi group-disabled: al suo interno ne seguono lo stato:

<Tab
  selected={section() === "billing"}
  disabled={!isOwner()}
  onPress={() => section.set("billing")}
  class="group rounded-(--radius) px-3 py-2 selected:bg-background disabled:opacity-50"
>
  <Text class="text-sm text-muted-foreground group-selected:text-foreground">Billing</Text>
</Tab>

#Accessibilità

  • Una tab è un unico elemento per gli screen reader, con il ruolo "tab" e il suo stato di selezione. La sua etichetta è il testo dei suoi figli, oppure accessibilityLabel.
  • Tabs ha il ruolo "tablist": gli screen reader sentono le tab al suo interno come un unico insieme, ciascuna comunque un elemento a sé.
  • disabled viene annunciato come lo annuncia la piattaforma (VoiceOver, TalkBack, UI Automation, l’albero di accessibilità del browser).
  • Su macOS e sul web la lista è un’unica sosta del focus da tastiera, e i tasti freccia si spostano tra le sue tab (vedi sotto).
  • L’indicatore è nascosto agli screen reader e non riceve il focus: solo le tab sono gli elementi della lista.

Vedi Accessibility per etichette, ruoli e annunci in generale.

#Tastiera

Su macOS e sul web, dove Tab raggiunge le viste che accettano pressioni, una lista Tabs segue il pattern WAI-ARIA per le tab:

TastoCosa fa
Tab, Shift+TabEntra nella lista sulla tab selezionata (la prima tab non disabilitata se nessuna è selezionata), e ne esce in un solo passaggio.
Destra, GiùSposta il focus sulla tab successiva, dall’ultima alla prima.
Sinistra, SuSposta il focus sulla tab precedente, dalla prima all’ultima.
Home, EndSposta il focus sulla prima o sull’ultima tab.
Spazio, InvioPreme la tab con il focus.

I tasti freccia saltano le tab disabilitate, e la tab su cui si spostano riporta onPress, così l’app la seleziona mentre il focus si sposta: la sezione segue il focus. Entrambe le coppie di frecce si spostano, qualunque sia il layout della lista, che è una classe dell’app. In un layout da destra a sinistra la riga parte da destra, quindi Sinistra e Destra si scambiano: Sinistra passa alla tab successiva e Destra a quella precedente; Su, Giù, Home e End mantengono il proprio comportamento. Gli stili focus: mostrano dove si trova il focus. Un Tab al di fuori di un Tabs è una sosta a sé stante.

#Note sulle piattaforme

  • iOS, Android e Mac Catalyst: una pressione è un tap che termina sulla tab, etichetta inclusa.
  • Desktop (macOS, Windows, Linux, GPUI) e web: una pressione è un clic. Su macOS e sul web, Spazio e Invio premono anche la tab che ha il focus da tastiera, e i tasti freccia, Home e End lo spostano (Tastiera).
  • Ogni piattaforma disegna ciò che l’app stilizza: le tab, la superficie di quella selezionata, l’indicatore e il contenuto delle sezioni. Lo spostamento dell’indicatore è un’animazione di layout nativa su ogni piattaforma.