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

Éléments

Accordion

Un accordion empile des sections qui se déploient et se replient sous leurs en-têtes : les questions d’une FAQ, les groupes d’une page de réglages, les étapes d’un paiement. Il se compose de cinq parties, dont chacune porte un seul aspect du comportement et aucun du rendu :

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, la pile : elle contient le value de l’app, les valeurs des éléments ouverts, et signale le suivant avec onChange. Un seul élément s’ouvre à la fois, ou plusieurs avec multiple.
  • AccordionItem, une section, identifiée par sa value. Elle est ouverte tant que le value de l’accordion la contient.
  • AccordionHeader, l’en-tête de l’élément, d’un level que les lecteurs d’écran annoncent.
  • AccordionTrigger, le bouton de l’en-tête qui déploie et replie son élément, et indique s’il est déployé.
  • AccordionPanel, le contenu de l’élément, affiché tant qu’il est ouvert ; avec reveal, sa hauteur s’ouvre et se ferme avec un effet de ressort.
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
}

Les parties viennent de nsh/base, le crate nsh-base en Rust. La valeur appartient à l’app : l’appui sur un déclencheur signale le value suivant avec onChange, et l’accordion ouvre les éléments dont l’app lui renvoie les valeurs. Chaque partie trouve seule celle qui l’entoure, si bien que l’app les agence librement : des séparateurs entre les éléments, un badge dans un en-tête, une icône avant un titre. En Rust, accordion(), accordion_item(), accordion_header(), accordion_trigger() et accordion_panel() les construisent, avec un setter par prop et .child(..) pour leur contenu.

#Props

#Accordion

PropRustTypeValeur par défautDescription
value.value(..)string[][]Les values des éléments ouverts. Ils appartiennent à l’app : l’appui sur un déclencheur signale les suivants avec onChange, et l’accordion affiche ceux que l’app lui renvoie.
multiple.multiple(..)booleanfalsePlusieurs éléments ouverts à la fois : en ouvrir un laisse les autres ouverts. Sans cette prop, ouvrir un élément ferme celui qui l’était.
disabled.disabled(..)booleanfalseTous les éléments sont désactivés : aucun déclencheur ne reçoit d’appuis, et les lecteurs d’écran indiquent que chacun est grisé.
children.child(..)JSX.ElementaucuneLes éléments, et tout ce que l’app place entre eux (séparateurs).
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucuneLa mise en page et le rendu de la pile : une carte autour des éléments, l’espace entre eux.

#AccordionItem

PropRustTypeValeur par défautDescription
value.value(..)stringrequisCe qui identifie l’élément dans le value de l’accordion : l’identifiant propre à l’app, jamais son titre, qui change avec la langue.
disabled.disabled(..)booleanfalseSon déclencheur ne reçoit aucun appui, et les lecteurs d’écran indiquent qu’il est grisé. Il reste ouvert ou fermé selon le value de l’accordion.
children.child(..)JSX.ElementaucuneL’en-tête et le panneau de l’élément, et tout ce que l’app y place d’autre.
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucuneLa section : un séparateur en dessous, une carte autour.

#AccordionHeader

PropRustTypeValeur par défautDescription
level.level(..)number3Le niveau de l’en-tête parmi les titres de la page, pour les lecteurs d’écran (1 étant le plus haut).
children.child(..)JSX.ElementaucuneLe déclencheur de l’élément, et tout ce que l’en-tête contient d’autre (un badge, une action qui lui est propre).
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucuneLa mise en page de l’en-tête.

#AccordionTrigger

PropRustTypeValeur par défautDescription
expandedStyle.expanded_style(|s| ..)StyleProp<ViewStyle>aucuneLe style du déclencheur tant que son élément est ouvert, sous forme de classes aria-expanded:.
accessibilityLabel.accessibility_label(..)stringaucuneCe que disent les lecteurs d’écran, lorsque les enfants ne contiennent aucun texte qui le dise.
children.child(..)JSX.ElementaucuneLe titre et le chevron ; les classes group-aria-expanded: à l’intérieur suivent le déclencheur grâce à group sur son class.
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucuneLe déclencheur lui-même : la ligne sur laquelle l’utilisateur appuie.
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>aucuneLe style du déclencheur lorsqu’il est pressé, survolé, ciblé par le focus ou désactivé, sous forme de classes <state>:.

#AccordionPanel

PropRustTypeValeur par défautDescription
reveal.reveal(..)booleanfalseS’ouvre et se ferme avec un effet de révélation, comme celui de Collapsible : le panneau rogne son contenu et sa hauteur passe de zéro à celle du contenu, et inversement, avec un amortissement critique (environ 265 ms, sans dépassement), et les éléments qui le suivent se déplacent avec lui. Avec la réduction des animations, il s’ouvre et se ferme instantanément.
keepMounted.keep_mounted(..)booleanfalseLe contenu reste dans l’arbre tant que l’élément est fermé, hors de vue, du focus clavier et des lecteurs d’écran : une vidéo garde sa position, une web view sa page. Sans cette prop, le contenu quitte l’arbre à la fermeture du panneau (une fois l’effet de révélation terminé) et revient tel qu’il était.
accessibilityLabel.accessibility_label(..)stringaucuneLe nom que les lecteurs d’écran donnent à la région : en général le titre de l’élément, tel que son en-tête l’énonce.
children.child(..)JSX.ElementaucuneLe contenu.
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucuneLa mise en page et le rendu de la région. Avec reveal, le panneau reste dans la mise en page lorsqu’il est fermé, avec une hauteur nulle : son remplissage et ses bordures vont donc sur une vue de son contenu.

#Événements

ÉvénementRustDescription
onChange({ value }) (Accordion).on_change(|value: &Vec<SharedString>| ..)Un déclencheur a été pressé : un toucher, un clic, Espace ou Entrée lorsqu’il a le focus clavier, ou une activation par un lecteur d’écran. value contient les valeurs des éléments ouverts telles qu’elles seraient : la valeur d’un élément ouvert la quitte, celle d’un élément fermé la rejoint à la fin ou, sans multiple, la remplace. Il s’exécute avant tout changement : définissez value pour le suivre.

#Parties

PartieDansStylée avecÉtats
La pileAccordion : class, style
Un élémentla pileAccordionItem : class, stylela valeur de l’app
L’en-têtel’élémentAccordionHeader : class, style
Le déclencheurl’en-têteAccordionTrigger : class, stylearia-expanded:, disabled:, active:, hover:, focus: (ou expandedStyle, disabledStyle, …)
Le titre et le chevronle déclencheurleurs propres éléments (children)group-aria-expanded:, group-disabled: avec group sur le class du déclencheur
Le panneaul’élément, après l’en-têteAccordionPanel : class, style
Le contenule panneauses propres éléments (children)

Sans reveal, un panneau fermé est hors de la mise en page et son contenu hors de l’arbre. Avec reveal, le changement est une animation de mise en page du commit qui modifie value (comme LayoutAnimation.configureNext, exécutée nativement : aucun JavaScript ne s’exécute pendant le mouvement) : le panneau qui s’ouvre passe de zéro à la hauteur de son contenu, celui qui se ferme se rétracte, tous deux avec le même ressort, et les éléments qui les suivent se déplacent avec eux. Une transition propre à l’app l’accompagne : le chevron pivote (transition-transform group-aria-expanded:rotate-180).

#Désactivation

Le déclencheur d’un élément désactivé ne reçoit aucun appui, et les lecteurs d’écran l’annoncent comme grisé ; les classes disabled: sur le déclencheur et group-disabled: à l’intérieur suivent. La prop disabled de l’accordion désactive chaque élément. Un élément désactivé affiche toujours ce que dit le value de l’app : l’app peut l’ouvrir.

#Accessibilité

  • La pile est un groupe, chaque en-tête un titre de son level, et chaque déclencheur un bouton qui s’y trouve et indique si son élément est déployé. Les lecteurs d’écran peuvent passer d’un en-tête à l’autre.
  • Un panneau ouvert est une région, nommée par son accessibilityLabel ; un panneau fermé est masqué aux lecteurs d’écran, y compris son contenu conservé.
  • Le rendu de l’élément ouvert relève de l’app, et ne devrait pas reposer sur la seule couleur : faites pivoter le chevron, comme le font les exemples.

Voir Accessibilité pour les libellés, les rôles et les annonces en général.

#Clavier

Là où les vues qui reçoivent des appuis sont des arrêts du focus clavier (macOS et le web), chaque déclencheur est un arrêt, dans l’ordre de la page, et Espace et Entrée l’activent. Les déclencheurs désactivés sont ignorés, tout comme le contenu d’un panneau fermé. Tab passe d’un déclencheur aux contrôles de son panneau ouvert, puis au déclencheur suivant.

#Notes par plateforme

  • Chaque plateforme affiche le panneau d’un élément ouvert sous son en-tête et laisse un panneau fermé hors de la mise en page (hauteur nulle avec reveal).
  • Avec reveal, chaque plateforme anime la hauteur des panneaux avec un ressort sur son propre animateur (iOS, Android, macOS, Windows, Linux, GPUI et le web), et les ouvre et les ferme instantanément lorsque le système demande de réduire les animations.
  • Le lecteur d’écran de chaque plateforme lit l’en-tête et son niveau comme il lit les titres, et le déclencheur qu’il contient comme un bouton avec son état déployé ; sur iOS, VoiceOver lit le déclencheur lui-même.
  • iOS, Android et Mac Catalyst : l’appui sur un déclencheur est un toucher. Ordinateurs (macOS, Windows, Linux, GPUI) et web : un clic, ainsi qu’Espace ou Entrée sur le déclencheur ciblé par le focus sous macOS et sur le web.