Diese Seite wurde maschinell übersetzt und kann Fehler enthalten. Das englische Original lesen

Elemente

Progress

Ein Fortschrittsbalken zeigt, wie weit eine Aufgabe mit bekanntem Umfang ist (ein Download, ein Upload, ein Schritt einer Einrichtung) oder, solange indeterminate, dass eine Aufgabe mit unbekanntem Umfang läuft. Er bestimmt, was Screenreader erhalten, nämlich die Rolle Progress Bar mit dem Bereich 0 bis 100 und dem Wert, sowie die Breite des Indikators: value Prozent der Breite des Tracks. Das Aussehen liegt bei der App: Progress hat keine eigenen Farben, Größen, Radien, Animationen oder Beschriftungen. Er besteht aus einem Track mit einem Indikator darin und danach seinen Children (eine Beschriftung, der Wert als Text), und die App gestaltet jeden Teil.

el-progress.tsx
el-progress.tsxLive
import { cleanup, mount, signal, Text, View } from "nsh";
import { reduceMotion } from "nsh";
import { Button, Progress, Switch } from "nsh/base";
import { button, buttonText, desktop, focusRing, hoverFill, Log, Panel, size, Split, ui } from "./palette";

// The parts: geometry and motion in classes, colors from the palette. The
// label row comes first (`flex-col-reverse`: the children follow the track),
// its value tabular and at the trailing edge. An indeterminate indicator is a
// third of the track that sweeps from end to end (the app flips its
// translation, a transition moves it), and rests in the middle with reduced
// motion. A determinate one is the whole track, slid back by what is left to
// do, so a new value glides there (the track clips it). The pointer's hover
// and the keyboard's focus ring come from the palette. Buttons are as wide as
// their labels with a pointer.
const pad = desktop ? "px-3 py-3" : "px-4 py-4";
const track = "h-1.5 overflow-hidden rounded-full";
const SWEEP = 800;
const indicator = (indeterminate: boolean, end: boolean) =>
  !indeterminate
    ? "h-full w-full rounded-full transition-transform duration-180 ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-none"
    : reduceMotion()
      ? "h-full w-1/3 rounded-full translate-x-full"
      : `h-full w-1/3 rounded-full transition-transform duration-700 ease-in-out ${end ? "translate-x-[200%]" : "translate-x-0"}`;

function Transfer(props: { label: string; detail: string; value: number; indeterminate?: boolean; end: boolean }) {
  return (
    <Progress
      value={props.value}
      indeterminate={props.indeterminate}
      class={`flex-col-reverse gap-2 ${pad}`}
      trackClass={track}
      trackStyle={ui().inset}
      indicatorClass={indicator(!!props.indeterminate, props.end)}
      indicatorStyle={{ backgroundColor: ui().accent, translate: props.indeterminate ? undefined : `${props.value - 100}% 0` }}
    >
      <View class="flex-row items-baseline gap-3">
        <View style={{ flex: 1, 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>
        <Text style={{ fontSize: size.label, color: ui().muted, fontVariant: ["tabular-nums"], textAlign: "right" }}>
          {props.indeterminate ? "—" : `${Math.round(props.value)}%`}
        </Text>
      </View>
    </Progress>
  );
}

function Action(props: { label: string; disabled?: boolean; onPress: () => void }) {
  return (
    <Button
      onPress={props.onPress}
      disabled={props.disabled}
      {...button("secondary", { style: desktop ? {} : { flex: 1 } })}
    >
      <Text style={buttonText()}>{props.label}</Text>
    </Button>
  );
}

function App() {
  const sent = signal(35);
  const running = signal(false);
  const unknown = signal(false);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 5));
  // The sweep of the indeterminate bars: still with reduced motion.
  const end = signal(false);
  const sweep = setInterval(() => {
    if (!reduceMotion()) end.set(!end());
  }, SWEEP);
  cleanup(() => clearInterval(sweep));
  let timer: ReturnType<typeof setInterval> | undefined;
  const stop = () => {
    if (timer !== undefined) clearInterval(timer);
    timer = undefined;
    running.set(false);
  };
  const start = () => {
    if (sent() >= 100) sent.set(0);
    running.set(true);
    log(`Start at ${sent()}%`);
    timer = setInterval(() => {
      sent.set(Math.min(100, sent() + 5));
      if (sent() >= 100) {
        stop();
        log("Done: value 100");
      }
    }, 200);
  };
  const line = () => <View style={{ height: 1, marginLeft: desktop ? 12 : 16, backgroundColor: ui().line }} />;
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>progress</Text>
      <Text style={{ color: ui().muted }}>{desktop ? "Click Start, or Tab to it and press Space" : "Tap Start to send the photos"}</Text>

      <Split
        gap={14}
        inspector={
          <>
            <Panel title="value · indeterminate">
              <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>
                {unknown() ? "indeterminate (no value for screen readers)" : `value ${sent()} of 100`}
              </Text>
              <View class={desktop ? "flex-row self-start gap-2" : "flex-row gap-2"}>
                <Action label={running() ? "Pause" : sent() >= 100 ? "Again" : "Start"} disabled={unknown()} onPress={() => (running() ? (stop(), log(`Pause at ${sent()}%`)) : start())} />
                <Action
                  label="Reset"
                  disabled={sent() === 0}
                  onPress={() => {
                    stop();
                    sent.set(0);
                    log("Reset: value 0");
                  }}
                />
              </View>
              <Switch
                checked={unknown()}
                onChange={({ checked }) => {
                  unknown.set(checked);
                  if (checked) stop();
                  log(checked ? "Size unknown: indeterminate" : "Size known: value " + sent());
                }}
                class={`-mx-2 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={unknown() ? 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={unknown() ? { backgroundColor: ui().onAccent } : ui().raised}
              >
                <Text style={{ flex: 1, fontSize: size.label, color: ui().text }}>Size unknown</Text>
              </Switch>
            </Panel>
            <Log title="events" lines={events()} minHeight={92} />
          </>
        }
      >
        <View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
          <Transfer
            label="Sending 24 photos"
            detail={unknown() ? "Counting what to send" : sent() >= 100 ? "Sent" : `${Math.round((sent() / 100) * 24)} of 24`}
            value={sent()}
            indeterminate={unknown()}
            end={end()}
          />
          {line()}
          <Transfer label="Preparing the backup" detail="Its size is not known yet" value={0} indeterminate end={end()} />
          {line()}
          <Transfer label="Library" detail="212 GB of 256 GB" value={83} end={end()} />
        </View>
      </Split>
    </View>
  );
}

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

fn upload(sent: Signal<f32>) -> impl IntoElement {
    progress()
        .value(sent)
        .accessibility_label("Uploading photos")
        .gap_2()
        .track_style(|s| s.h(px(8.0)).rounded_full().overflow_hidden().bg(move || theme().muted))
        .indicator_style(|s| s.h_full().rounded_full().bg(move || theme().primary))
        .child(
            text(move || format!("{:.0}%", sent.get()))
                .text_sm()
                .text_color(move || theme().muted_foreground),
        )
}

Progress stammt aus nsh/base, dem nsh-base-Crate in Rust. Der Wert gehört der App: Der Fortschrittsbalken zeigt den value, den die App übergibt, begrenzt auf 0 bis 100. In Rust erzeugt progress() einen Fortschrittsbalken mit einem Setter pro Prop (.value(..), .indicator_style(..)), den typisierten Style-Methoden für den Fortschrittsbalken selbst und .child(..) für seine Beschriftung.

#Props

Prop Rust Typ Standard Beschreibung
value .value(..) number 0 Wie weit die Aufgabe ist, als Prozentwert von 0 bis 100 (außerhalb des Bereichs gilt das nächstliegende Ende). Der Indikator nimmt diesen Anteil der Breite des Tracks ein, und Screenreader sagen ihn an.
indeterminate .indeterminate(..) boolean false Der Umfang der Aufgabe ist unbekannt: Screenreader hören einen Fortschrittsbalken ohne Wert, und die Breite des Indikators bestimmt die App (indicatorClass).
accessibilityLabel .accessibility_label(..) string keiner Was Screenreader sagen: was in Arbeit ist, wenn die Children keinen Text enthalten, der es sagt.
trackClass, trackStyle .track_style(|s| ..) string, StyleProp<ViewStyle> keiner Der Track, der Teil, den der Indikator füllt.
indicatorClass, indicatorStyle .indicator_style(|s| ..) string, StyleProp<ViewStyle> keiner Der Indikator im Track: seine Höhe, Farbe und Animation.
children .child(..) JSX.Element keiner Der Inhalt nach dem Track: eine Beschriftung oder der Wert als Text, den Screenreader ansagen.
class, style Style-Methoden string, StyleProp<ViewStyle> keiner Der Fortschrittsbalken selbst: die Spalte (oder Zeile) um den Track und die Beschriftung.

#Teile

Teil Enthalten in Gestaltet mit
Der Fortschrittsbalken class, style
Der Track dem Fortschrittsbalken, als Erstes trackClass, trackStyle
Der Indikator dem Track indicatorClass, indicatorStyle; seine Breite beträgt value Prozent der des Tracks
Die Beschriftung dem Fortschrittsbalken, nach dem Track eigenen Elementen (children)

Der Indikator beginnt an der vorderen Kante des Tracks; overflow-hidden und ein Radius am Track runden sein Ende zusammen mit dem des Tracks ab. Eine w-*-Klasse oder ein width in indicatorStyle ersetzt die Breite des Werts, und so erhält ein unbestimmter Balken seine eigene.

#Unbestimmt

Solange der Umfang unbekannt ist, setze indeterminate und gib dem Indikator eine Breite und eine Animation der App. Die Animation ist Sache der App, und motion-reduce: hält sie für Personen still, die weniger Bewegung wünschen:

<Progress
  value={0}
  indeterminate={connecting()}
  trackClass="h-2 overflow-hidden rounded-full bg-muted"
  indicatorClass={
    connecting()
      ? "h-full w-1/3 rounded-full bg-primary animate-pulse motion-reduce:animate-none"
      : "h-full rounded-full bg-primary"
  }
/>

#Barrierefreiheit

  • Ein Fortschrittsbalken ist für Screenreader ein einzelnes Element mit der Rolle "progressbar", dem Bereich 0 bis 100 und seinem Wert (VoiceOver und TalkBack sagen ihn als Prozentwert an); der Track und der Indikator sind Teile davon. Seine Beschriftung ist der Text seiner Children; gib accessibilityLabel an, wenn er keinen hat.
  • Solange indeterminate, behält er die Rolle und den Bereich und hat keinen Wert (im Browser entfällt aria-valuenow, so wie ARIA es für einen unbestimmten Fortschrittsbalken vorsieht).
  • Er nimmt keine Eingaben per Tippen oder Klicken und keinen Tastaturfokus an: Er meldet nur, und die Steuerelemente, die die Aufgabe starten oder stoppen, sind Sache der App.

Siehe Barrierefreiheit für Beschriftungen, Rollen und Ansagen im Allgemeinen.

#Hinweise zu den Plattformen

  • Jede Plattform zeichnet, was die App gestaltet; die Breite des Indikators folgt value sofort, und eine Änderung wird animiert, wenn die App sie animiert.
  • Screenreader: VoiceOver (iOS, macOS) hört den Wert als Prozentwert und den Balken als sich häufig aktualisierend; TalkBack, UI Automation (Windows), die Barrierefreiheit von GTK (Linux) und der Browser (aria-valuenow) erhalten den Bereich und den Wert.