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

Elementos

Progress

Uma barra de progresso mostra até onde chegou uma tarefa de extensão conhecida (um download, um upload, uma etapa de configuração) ou, enquanto indeterminate, que uma tarefa de extensão desconhecida está em andamento. Ela é responsável pelo que os leitores de tela recebem, o papel de barra de progresso com o intervalo de 0 a 100 e o valor, e pela largura do indicador: value por cento da largura da trilha. A aparência fica a cargo do app: um Progress não tem cores, tamanhos, raios, movimento nem rótulo próprios. Ele é composto por uma trilha com um indicador dentro e, em seguida, seus filhos (um rótulo, o valor como texto), e o app estiliza cada parte.

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 vem de nsh/base, a crate nsh-base em Rust. O valor é do app: a barra de progresso mostra o value que o app passa, limitado de 0 a 100. Em Rust, progress() cria uma com um setter por prop (.value(..), .indicator_style(..)), os métodos de estilo tipados para a própria barra de progresso e .child(..) para o seu rótulo.

#Props

Prop Rust Tipo Padrão Descrição
value .value(..) number 0 Quanto da tarefa já foi feito, como porcentagem de 0 a 100 (fora do intervalo, vale o extremo mais próximo). O indicador ocupa essa parte da largura da trilha, e os leitores de tela a anunciam.
indeterminate .indeterminate(..) boolean false A extensão da tarefa é desconhecida: os leitores de tela ouvem uma barra de progresso sem valor, e a largura do indicador fica a cargo do app (indicatorClass).
accessibilityLabel .accessibility_label(..) string nenhum O que os leitores de tela dizem: o que está em andamento, quando os filhos não têm texto que diga isso.
trackClass, trackStyle .track_style(|s| ..) string, StyleProp<ViewStyle> nenhum A trilha, a parte que o indicador preenche.
indicatorClass, indicatorStyle .indicator_style(|s| ..) string, StyleProp<ViewStyle> nenhum O indicador, dentro da trilha: sua altura, cor e movimento.
children .child(..) JSX.Element nenhum O conteúdo depois da trilha: um rótulo ou o valor como texto, que os leitores de tela anunciam.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum A própria barra de progresso: a coluna (ou linha) ao redor da trilha e do rótulo.

#Partes

Parte Dentro de Estilizada com
A barra de progresso class, style
A trilha a barra de progresso, primeiro trackClass, trackStyle
O indicador a trilha indicatorClass, indicatorStyle; sua largura é value por cento da largura da trilha
O rótulo a barra de progresso, depois da trilha seus próprios elementos (children)

O indicador começa na borda inicial da trilha; overflow-hidden e um raio na trilha arredondam a extremidade dele junto com a da trilha. Uma classe w-* ou um width em indicatorStyle substitui a largura derivada do valor, que é como uma barra indeterminada ganha a sua própria.

#Indeterminada

Enquanto a extensão é desconhecida, defina indeterminate e dê ao indicador uma largura e um movimento próprios do app. O movimento é escolha do app, e motion-reduce: o mantém parado para quem pede menos movimento:

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

#Acessibilidade

  • Uma barra de progresso é um único elemento para os leitores de tela, com o papel "progressbar", o intervalo de 0 a 100 e seu valor (o VoiceOver e o TalkBack o anunciam como porcentagem); a trilha e o indicador são partes dela. Seu rótulo é o texto de seus filhos; informe accessibilityLabel quando ela não tiver nenhum.
  • Enquanto indeterminate, ela mantém o papel e o intervalo e não tem valor (no navegador: aria-valuenow é omitido, como o ARIA define para uma barra de progresso indeterminada).
  • Ela não recebe toques nem foco do teclado: ela apenas informa, e os controles que iniciam ou interrompem a tarefa são do app.

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

#Notas sobre plataformas

  • Todas as plataformas desenham o que o app estiliza; a largura do indicador acompanha value imediatamente, e a mudança é animada quando o app a anima.
  • Leitores de tela: o VoiceOver (iOS, macOS) ouve o valor como porcentagem e a barra como atualizada com frequência; o TalkBack, o UI Automation (Windows), a acessibilidade do GTK (Linux) e o navegador (aria-valuenow) recebem o intervalo e o valor.