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

Elementos

Accordion

Um accordion empilha seções que se expandem e se recolhem sob seus títulos: as perguntas de um FAQ, os grupos de uma página de configurações, as etapas de um checkout. Cinco partes o compõem, cada uma responsável por um pedaço do comportamento e nenhuma pela aparência:

el-accordion.tsx
el-accordion.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionTrigger } from "nsh/base";
import { desktop, focusRing, hoverFill, Icon, Log, Panel, Segmented, size, Split, ui } from "./palette";

// An FAQ: each item a heading whose trigger (a button that says whether it
// is expanded) opens its answer. The app owns which items are open
// (`value`) and sets what `onChange` reports; one at a time, or several
// with `multiple`. The panels spring open and shut (`reveal`) and the
// items below move with them; the chevron turns with `group-aria-expanded:`.
// The pointer's hover and the keyboard's focus ring come from the palette;
// the reveal and the chevron stay still with reduced motion.
const pad = desktop ? "px-3" : "px-4";

const FAQ: { value: string; question: string; answer: string; disabled?: boolean }[] = [
  { value: "shipping", question: "How long does shipping take?", answer: "Orders leave the warehouse within a day and arrive in two to five working days." },
  { value: "returns", question: "Can I return an item?", answer: "Within 30 days, unused and in its box: print the label from your order and drop it off." },
  { value: "warranty", question: "What does the warranty cover?", answer: "Two years against defects in materials and workmanship, parts and labour included." },
  { value: "gift", question: "Can I send it as a gift?", answer: "Gift wrapping opens with the holiday season.", disabled: true },
];

const MODES = ["one at a time", "several"] as const;

function App() {
  const open = signal<string[]>(["shipping"]);
  const mode = signal<(typeof MODES)[number]>("one at a time");
  const events = signal<string[]>([]);
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>accordion</Text>
      <Text style={{ color: ui().muted }}>{desktop ? "Click a question, or Tab to it and press Space" : "Tap a question to read its answer"}</Text>

      <Split
        gap={14}
        inspector={
          <>
            <Panel title="multiple">
              <Segmented
                options={MODES}
                value={mode()}
                onChange={(value) => {
                  mode.set(value);
                  // One at a time keeps the item opened last.
                  if (value === "one at a time") open.set(open().slice(-1));
                }}
              />
            </Panel>
            <Panel title="value">
              <Text style={{ fontSize: 13, color: ui().text }}>{open().length ? open().join(", ") : "none open"}</Text>
            </Panel>
            <Log title="onChange({ value })" lines={events()} minHeight={92} />
          </>
        }
      >
        <Accordion
          value={open()}
          multiple={mode() === "several"}
          onChange={({ value }) => {
            open.set(value);
            events.set([`[${value.join(", ")}]`, ...events()].slice(0, 4));
          }}
          style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}
        >
          {FAQ.map((item, i) => (
            <AccordionItem value={item.value} disabled={item.disabled}>
              <View style={{ height: i === 0 ? 0 : 1, marginLeft: desktop ? 12 : 16, backgroundColor: ui().line }} />
              <AccordionHeader>
                <AccordionTrigger
                  hoverStyle={item.disabled ? undefined : hoverFill()}
                  focusStyle={focusRing()}
                  class={`group flex-row items-center gap-3 ${pad} active:opacity-70 disabled:opacity-40`}
                  style={{ minHeight: size.row + 8 }}
                >
                  <Text style={{ flex: 1, fontSize: size.label, fontWeight: "500", color: ui().text }}>{item.question}</Text>
                  <View class="transition-transform duration-200 motion-reduce:transition-none group-aria-expanded:rotate-180">
                    <Icon name="chevron-down" size={14} tint={ui().muted} />
                  </View>
                </AccordionTrigger>
              </AccordionHeader>
              <AccordionPanel reveal accessibilityLabel={item.question}>
                <View class={`${pad} pb-3`}>
                  <Text style={{ fontSize: size.label, lineHeight: size.label * 1.45, color: ui().muted }}>{item.answer}</Text>
                </View>
              </AccordionPanel>
            </AccordionItem>
          ))}
        </Accordion>
      </Split>
    </View>
  );
}

mount(App);
  • Accordion, a pilha: guarda os value do app, os valores dos itens abertos, e informa o próximo com onChange. Um item abre por vez, ou vários com multiple.
  • AccordionItem, uma seção, identificada por seu value. Fica aberta enquanto o value do accordion a contiver.
  • AccordionHeader, o título do item, de um level que os leitores de tela anunciam.
  • AccordionTrigger, o botão no título que expande e recolhe seu item, e informa se está expandido.
  • AccordionPanel, o conteúdo do item, exibido enquanto ele está aberto; com reveal, sua altura se abre e se fecha como uma mola.
use nsh_base::{accordion, accordion_header, accordion_item, accordion_panel, accordion_trigger, theme};
use nsh_core::prelude::*;

const FAQ: [(&str, &str, &str); 2] = [
    ("shipping", "When will my order ship?", "Orders placed before 2 pm leave the same day."),
    ("returns", "How do I return an item?", "Print the label from your order within 30 days."),
];

fn faq() -> impl IntoElement {
    let open = signal(vec![SharedString::from("shipping")]);
    let mut list = accordion()
        .value(open)
        .on_change(move |value: &Vec<SharedString>| open.set(value.clone()));
    for (value, question, answer) in FAQ {
        list = list.child(
            accordion_item()
                .value(value)
                .border_b_1()
                .border_color(move || theme().border)
                .child(
                    accordion_header().child(
                        accordion_trigger()
                            .flex_row()
                            .items_center()
                            .justify_between()
                            .py_3()
                            .child(text(question).font_weight(FontWeight::MEDIUM))
                            .child(text("⌄").transition_transform().rotate(move || {
                                let open = open.get().iter().any(|v| &**v == value);
                                if open { deg(180.0) } else { deg(0.0) }
                            })),
                    ),
                )
                .child(
                    accordion_panel()
                        .reveal(true)
                        .accessibility_label(question)
                        .child(view().pb_3().child(text(answer).text_color(move || theme().muted_foreground))),
                ),
        );
    }
    list
}

As partes vêm de nsh/base, o crate nsh-base em Rust. O valor pertence ao app: o toque em um trigger informa o próximo value com onChange, e o accordion abre os itens cujos valores o app devolve. Cada parte encontra sozinha a que a envolve, então o app as organiza livremente: divisores entre os itens, um badge em um título, um ícone antes de um título de texto. Em Rust, accordion(), accordion_item(), accordion_header(), accordion_trigger() e accordion_panel() as criam, com um setter por prop e .child(..) para o que fica dentro.

#Props

#Accordion

Prop Rust Tipo Padrão Descrição
value .value(..) string[] [] Os values dos itens abertos. O app é o dono deles: o toque em um trigger informa os próximos com onChange, e o accordion mostra os que o app devolve.
multiple .multiple(..) boolean false Vários itens abertos ao mesmo tempo: abrir um deixa os outros abertos. Sem isso, abrir um item fecha o que estava aberto.
disabled .disabled(..) boolean false Todos os itens ficam desabilitados: nenhum trigger recebe toques, e os leitores de tela informam que cada um está esmaecido.
children .child(..) JSX.Element nenhum Os itens, e o que o app dispuser entre eles (divisores).
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum O layout e a aparência da pilha: um cartão em volta dos itens, o espaço entre eles.

#AccordionItem

Prop Rust Tipo Padrão Descrição
value .value(..) string obrigatório O que identifica o item no value do accordion: o id do próprio app, nunca o título, que muda com o idioma.
disabled .disabled(..) boolean false Seu trigger não recebe toques, e os leitores de tela informam que está esmaecido. O item continua aberto ou fechado conforme o value do accordion.
children .child(..) JSX.Element nenhum O header e o panel do item, e o que mais o app dispuser dentro dele.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum A seção: um divisor abaixo dela, um cartão em volta.

#AccordionHeader

Prop Rust Tipo Padrão Descrição
level .level(..) number 3 O nível do título entre os títulos da página, para os leitores de tela (1 é o mais alto).
children .child(..) JSX.Element nenhum O trigger do item, e o que mais o header contiver (um badge, uma ação própria).
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum O layout do título.

#AccordionTrigger

Prop Rust Tipo Padrão Descrição
expandedStyle .expanded_style(|s| ..) StyleProp<ViewStyle> nenhum O estilo do trigger enquanto seu item está aberto, como classes aria-expanded:.
accessibilityLabel .accessibility_label(..) string nenhum O que os leitores de tela dizem, quando os filhos não têm texto que o diga.
children .child(..) JSX.Element nenhum O título e o chevron; as classes group-aria-expanded: dentro dele acompanham o trigger com group em seu class.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum O próprio trigger: a linha que o usuário pressiona.
activeStyle, hoverStyle, focusStyle, disabledStyle .active(|s| ..), .hover(..), .focus(..), .disabled_style(..) StyleProp<ViewStyle> nenhum O estilo do trigger enquanto pressionado, com o ponteiro sobre ele, com foco ou desabilitado, como as classes <state>:.

#AccordionPanel

Prop Rust Tipo Padrão Descrição
reveal .reveal(..) boolean false Abre e fecha com uma revelação, como a de Collapsible: o panel recorta seu conteúdo e sua altura vai de zero até a do conteúdo e volta, como uma mola criticamente amortecida (cerca de 265 ms, sem overshoot), e os itens seguintes se movem junto. Com movimento reduzido, abre e fecha de uma vez.
keepMounted .keep_mounted(..) boolean false O conteúdo permanece na árvore enquanto o item está fechado, fora da vista, do foco do teclado e dos leitores de tela: um vídeo mantém sua posição, uma web view mantém sua página. Sem isso, o conteúdo sai da árvore quando o panel fecha (depois que a revelação o fechou) e volta como estava.
accessibilityLabel .accessibility_label(..) string nenhum Como os leitores de tela chamam a região: em geral o título do item, como diz o seu header.
children .child(..) JSX.Element nenhum O conteúdo.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum O layout e a aparência da região. Com reveal, o panel fica no layout enquanto fechado, com altura zero, então seu padding e suas bordas vão em uma view do conteúdo.

#Eventos

Evento Rust Descrição
onChange({ value }) (Accordion) .on_change(|value: &Vec<SharedString>| ..) Um trigger foi pressionado: um toque, um clique, Space ou Return enquanto ele tem o foco do teclado, ou a ativação por um leitor de tela. value são os valores dos itens abertos como ficariam: o valor de um item aberto sai da lista, o de um fechado entra no fim dela ou, sem multiple, a substitui. Roda antes de qualquer mudança: defina value para acompanhá-lo.

#Partes

Parte Dentro de Estilizada com Estados
A pilha Accordion de class, style
Um item a pilha AccordionItem de class, style o valor do app
O título o item AccordionHeader de class, style
O trigger o título AccordionTrigger de class, style aria-expanded:, disabled:, active:, hover:, focus: (ou expandedStyle, disabledStyle, …)
O título de texto e o chevron o trigger seus próprios elementos (children) group-aria-expanded:, group-disabled: com group no class do trigger
O panel o item, depois do título AccordionPanel de class, style
O conteúdo o panel seus próprios elementos (children)

Sem reveal, um panel fechado fica fora do layout e seu conteúdo fora da árvore. Com reveal, a mudança é uma animação de layout do commit que altera value (como LayoutAnimation.configureNext, executada nativamente, então nenhum JavaScript roda enquanto ela se move): o panel que abre cresce de zero até a altura de seu conteúdo, o que fecha encolhe de volta, ambos na mesma mola, e os itens seguintes se movem junto. Uma transição do app acompanha: o chevron gira (transition-transform group-aria-expanded:rotate-180).

#Desabilitado

O trigger de um item desabilitado não recebe toques, e os leitores de tela o anunciam como esmaecido; as classes disabled: no trigger e group-disabled: dentro dele acompanham. O disabled do accordion desabilita todos os itens. Um item desabilitado ainda mostra o que o value do app diz: o app pode abri-lo.

#Acessibilidade

  • A pilha é um grupo, cada header é um título de seu level, e cada trigger é um botão dentro dele que informa se seu item está expandido. Os leitores de tela podem ir de título em título.
  • Um panel aberto é uma região, nomeada por seu accessibilityLabel; um fechado fica oculto dos leitores de tela, inclusive o conteúdo mantido.
  • A aparência do item aberto é do app, e não deve depender só da cor: gire o chevron, como fazem os exemplos.

Veja Acessibilidade para rótulos, papéis e anúncios em geral.

#Teclado

Onde as views que recebem toques são paradas do foco do teclado (macOS e a web), cada trigger é uma parada, na ordem da página, e Space e Return o pressionam. Os triggers desabilitados são pulados, assim como o conteúdo de um panel fechado. Tab vai de um trigger para os controles de seu panel aberto e segue para o próximo trigger.

#Notas por plataforma

  • Todas as plataformas mostram o panel de um item aberto sob seu título e deixam um fechado fora do layout (altura zero com reveal).
  • Com reveal, todas as plataformas animam como mola a altura dos panels em seu próprio animador (iOS, Android, macOS, Windows, Linux, GPUI e a web), e os abrem e fecham de uma vez enquanto o sistema pede movimento reduzido.
  • O leitor de tela de cada plataforma ouve o título e seu nível ao ler os títulos, e o trigger dentro dele como um botão com seu estado expandido; no iOS, o VoiceOver lê o próprio trigger.
  • iOS, Android e Mac Catalyst: o acionamento de um trigger é um toque. Desktops (macOS, Windows, Linux, GPUI) e a web: um clique, e Space ou Return no trigger com foco no macOS e na web.