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

Elementos

Scrollbar

Uma barra de rolagem dá a uma scroll view ou a uma lista uma barra com o visual próprio do app: uma pílula fina que se alarga sob o ponteiro, um trilho no tom da marca, uma barra ao lado de um cabeçalho fixo ou ao longo de dois painéis. Definida uma vez como tema do app, cada scroll view e cada lista desenha uma no lugar do indicador da plataforma (Barras integradas); quando posicionada pelo app, ela nomeia a scroll view que move e fica onde o app a colocar. Ela tem comportamento próprio: o comprimento e a posição do thumb acompanham o conteúdo, a viewport e o offset (inclusive enquanto uma lista cresce ou a janela é redimensionada); arrastar o thumb rola o conteúdo, e um toque no trilho leva o meio do thumb até ali; ela aparece enquanto o conteúdo rola, enquanto o ponteiro está sobre ela, ou sempre, e surge e desaparece com fade de forma nativa. O visual e o tempo ficam por conta do app: uma Scrollbar não tem cores, tamanhos nem raios próprios, e seus fades não levam tempo algum até que o app defina algum.

el-scrollbar.tsx
el-scrollbar.tsxLive
import { mount, Scroll, signal, Text, View, type ScrollRef } from "nsh";
import { Scrollbar, type ScrollbarMode } from "nsh/base";
import { desktop, Panel, Segmented, size, Split, ui } from "./palette";

// A list of messages in a card whose own indicator is off, with the app's
// bar over its right edge, timed as gpui-component's: a 6-point pill in the
// palette's muted tone that widens to 8 under the pointer (`hover:`) and
// while dragged (`active:`), fading in over 300 ms and out over 500 ms, 2 s
// after the last scroll (`motion`); pointing at its hidden thumb in the
// `hover` mode slides it in. The inspector switches when it shows and
// follows the offset the list reports.
const MESSAGES = Array.from({ length: 60 }, (_, i) => ({
  from: ["Ada", "Grace", "Linus", "Ken", "Barbara"][i % 5],
  text: ["Shipped the build", "Review when you can", "Tests are green", "Moved the meeting", "Draft is ready"][i % 5],
  at: `${9 + Math.floor(i / 6)}:${String((i * 7) % 60).padStart(2, "0")}`,
}));

function App() {
  const mode = signal<ScrollbarMode>("scrolling");
  const offset = signal(0);
  let inbox: ScrollRef | undefined;
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>scrollbar</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Scroll the list, drag the thumb or click the track" : "Scroll the list, or drag the thumb"}
      </Text>
      <Split
        gap={14}
        grow
        inspector={
          <Panel title="scrollbar">
            <Segmented
              label="mode"
              options={["scrolling", "hover", "always"] as const}
              value={mode()}
              onChange={(v) => mode.set(v)}
            />
            <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>
              {`offset ${Math.round(offset())}`}
            </Text>
          </Panel>
        }
      >
        <View style={{ ...ui().card, flex: 1, minHeight: 320, borderRadius: 16, overflow: "hidden" }}>
          <Scroll
            ref={inbox}
            scrollbar={false}
            style={{ flex: 1 }}
            onScroll={(e) => offset.set(e.offset.y)}
          >
            {MESSAGES.map((m) => (
              <View
                class={desktop ? "flex-row gap-3 px-3 py-2.5" : "flex-row gap-3 px-4 py-3"}
                style={{ borderBottomWidth: 1, borderColor: ui().line }}
              >
                <View style={{ flex: 1, gap: 2 }}>
                  <Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{m.from}</Text>
                  <Text style={{ fontSize: size.detail, color: ui().muted }}>{m.text}</Text>
                </View>
                <Text style={{ fontSize: size.detail, color: ui().muted, fontVariant: ["tabular-nums"] }}>{m.at}</Text>
              </View>
            ))}
          </Scroll>
          <Scrollbar
            scroll={inbox}
            mode={mode()}
            motion={{ idle: 2000, enter: 300, exit: 500, entrance: "fade", thumbHoverEntrance: "slideAndFade" }}
            accessibilityLabel="Messages"
            class="absolute inset-y-0 right-0 w-4 items-end p-1"
            thumbClass="w-1.5 rounded-full opacity-60 transition-all duration-300 hover:w-2 hover:opacity-90 active:w-2 active:opacity-100"
            thumbStyle={{ backgroundColor: ui().muted }}
          />
        </View>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{ScrollbarEntrance, ScrollbarMode, ScrollbarMotion, scrollbar, theme};
use nsh_core::prelude::*;

fn inbox(rows: impl IntoElement) -> impl IntoElement {
    let list = element_ref();
    // In over 300 ms and out over 500 ms, 2 s after the last scroll; pointing
    // at the hidden thumb slides it in from the edge.
    let mut motion = ScrollbarMotion::default();
    motion.enter = 300.0;
    motion.exit = 500.0;
    motion.thumb_hover_entrance = ScrollbarEntrance::SlideAndFade;
    view()
        .relative()
        .flex_1()
        // The scroll view's own indicator gives way to the app's bar.
        .child(scroll().element_ref(&list).flex_1().scrollbar(false).child(rows))
        .child(
            scrollbar()
                .scroll(list)
                .mode(ScrollbarMode::Hover)
                .motion(motion)
                .accessibility_label("Messages")
                // The track over the right edge; the thumb right-aligned in it.
                .absolute()
                .top_0()
                .bottom_0()
                .right_0()
                .w(px(16.0))
                .items_end()
                .p(px(4.0))
                .thumb_style(|s| {
                    s.w(px(6.0))
                        .rounded_full()
                        .bg(move || theme().muted_foreground)
                        .opacity(0.6)
                        .group_hover(|s| s.w(px(8.0)).opacity(0.9))
                }),
        )
}

Scrollbar vem de nsh/base, a crate nsh-base em Rust. Em Rust, scrollbar() cria uma com um setter por prop (.scroll(..), .thumb_style(..)) e os métodos de estilo tipados para a própria barra; seu scroll recebe um ElementRef (ou um ScrollbarTarget).

#Barras integradas

Toda scroll view e toda lista pode desenhar uma Scrollbar no lugar do indicador da plataforma, sem nenhuma barra para posicionar: Scrollbar.setTheme dá o visual do app à barra de cada view cujo scrollbar é true, uma única vez, onde quer que o app inicie. Cada view recebe sua própria barra sobre a viewport, que permanece ali enquanto o conteúdo rola (na thread principal, como as sticky views) e é desenhada acima do conteúdo; ela acompanha, rola e faz fade como uma Scrollbar posicionada faria.

use nsh_base::{ScrollbarLook, ScrollbarMode, ScrollbarTheme, set_scrollbar_theme};

fn use_app_scrollbars() {
    let mut vertical = ScrollbarLook::default();
    vertical.class = Some("absolute inset-y-0 right-0 w-4 items-end p-1".into());
    vertical.thumb_class = Some("w-1.5 rounded-full bg-muted-foreground opacity-60 hover:w-2".into());
    let mut horizontal = ScrollbarLook::default();
    horizontal.class = Some("absolute inset-x-0 bottom-0 h-4 items-end p-1".into());
    horizontal.thumb_class = Some("h-1.5 rounded-full bg-muted-foreground opacity-60 hover:h-2".into());
    let mut theme = ScrollbarTheme::default();
    theme.mode = ScrollbarMode::Hover;
    theme.motion.enter = 300.0;
    theme.motion.exit = 500.0;
    theme.vertical = Some(vertical);
    theme.horizontal = Some(horizontal);
    set_scrollbar_theme(Some(theme));
}

Um ScrollbarTheme tem as props de uma barra que são iguais para toda view, e o visual de cada eixo:

Campo Tipo Padrão Descrição
mode "scrolling" | "hover" | "always" "scrolling" Quando as barras aparecem: Quando aparece.
motion ScrollbarMotion troca instantânea, idle 2000 Como aparecem e desaparecem: Movimento.
minThumbLength number 48 O menor comprimento que um thumb pode ter.
vertical, horizontal ScrollbarLook nenhum O visual das barras das views verticais e das horizontais: class e style para o trilho, posicionado na viewport da view; thumbClass e thumbStyle; accessibilityLabel. Uma view cujo eixo não tem visual mantém o indicador da plataforma.

Um setTheme posterior reestiliza as barras, e Scrollbar.setTheme(null) devolve às views o indicador da plataforma. Uma view pode receber um tema próprio como sua prop scrollbar (<Scroll scrollbar={{ mode: "always", vertical: { … } }}>), tenha o app definido um ou não; scrollbar={false} deixa uma view sem barra. Uma lista disposta na viewport de seu pai (viewport="parent") rola junto com essa view e com sua barra. Um Input de várias linhas também recebe a barra do seu visual vertical, sobre o texto, que ele rola como o indicador próprio da view de texto faz.

#A scroll view que ela move

scroll nomeia a scroll view ou a lista que a barra acompanha: seu ref. A barra a encontra assim que ela está na tela, então o ref pode ser definido por um elemento posterior na mesma renderização; escreva a barra depois de sua scroll view (ou mantenha o ref em um signal). Desative o indicador próprio da scroll view com scrollbar={false} e posicione a barra com class: sobre a borda da scroll view, em uma caixa relative ao redor de ambas, ou em qualquer outro lugar da tela.

  • O thumb tem o comprimento da fração do conteúdo que a viewport mostra, ao longo da content box do trilho (dentro do seu padding), e não é menor que minThumbLength. Sua posição indica o quanto o conteúdo foi rolado; enquanto o conteúdo faz bounce além de uma extremidade, o thumb permanece nessa extremidade.
  • Arrastar o thumb rola o conteúdo na mesma medida em que o thumb se move. Um toque no trilho leva o meio do thumb para baixo do ponteiro, e o arrasto continua a partir dali.
  • Uma barra vertical acompanha uma scroll view vertical; axis="horizontal" cria uma barra para uma horizontal (posicione-a ao longo da borda inferior). Uma barra por eixo.
  • As listas funcionam da mesma forma: o thumb acompanha o comprimento total da lista à medida que as linhas são medidas e adicionadas.

#Quando aparece

mode Aparece
"scrolling" (padrão) Enquanto o conteúdo rola, e até motion.idle depois que ele para; o ponteiro sobre a barra enquanto ela aparece a mantém visível.
"hover" Enquanto o ponteiro está sobre a barra (onde ela aparece, mesmo oculta), e como "scrolling" faz.
"always" Sempre.

Em todos os modos, ela aparece enquanto seu thumb é arrastado e permanece motion.idle após o último scroll, arrasto ou hover. Enquanto o conteúdo cabe na viewport, ela permanece oculta. Oculta, a barra fica em opacity: 0 e deixa os toques passarem para o que está embaixo dela.

#Movimento

motion define o tempo dos fades da barra, que são executados de forma nativa, como o ScrollbarMotion de gpui-base faz:

Campo Padrão Descrição
idle 2000 Por quanto tempo a barra permanece após o último scroll, arrasto ou hover, em milissegundos.
enter 0 Quanto tempo leva para aparecer: sua opacidade sobe linearmente, e um slide usa ease-out.
exit 0 Quanto tempo leva para desaparecer depois que idle termina, com ease-in; ela desliza para fora do trilho enquanto some.
entrance "fade" Como ela entra: "fade" no lugar, ou "slideAndFade" a partir da borda em que está (a direita, a inferior).
thumbHoverEntrance "fade" Como ela entra quando o ponteiro sobre seu thumb oculto a exibe ("hover").

Sem durações, a barra aparece e desaparece instantaneamente. Um fade revertido na metade mantém sua velocidade: ele dura a fração de sua duração que ainda falta percorrer. Uma barra "always" nunca é animada, nem qualquer barra enquanto o usuário pede menos movimento. O trilho recorta o que desliza. O movimento do próprio thumb, como uma largura que cresce em hover:, pertence ao estilo dele (transition-all em thumbClass).

#Props

Prop Rust Tipo Padrão Descrição
scroll .scroll(..) YNode nenhum A scroll view ou lista que a barra acompanha e move (seu ref).
axis .axis(..) "horizontal" | "vertical" "vertical" O eixo da scroll view: o trilho corre ao longo dele. Lido quando a barra é criada.
mode .mode(..) "scrolling" | "hover" | "always" "scrolling" Quando a barra aparece.
motion .motion(..) ScrollbarMotion troca instantânea, idle 2000 Como ela aparece e desaparece: Movimento.
minThumbLength .min_thumb_length(..) number 48 O menor comprimento do thumb em um conteúdo longo, para que continue fácil de agarrar.
thumbClass, thumbStyle .thumb_style(|s| ..) string, StyleProp<ViewStyle> nenhum O thumb.
accessibilityLabel .accessibility_label(..) string nenhum O que os leitores de tela dizem sobre a barra.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum A barra: seu trilho, onde o app a posiciona.

#Partes

Parte Dentro de Estilizada com
O trilho onde o app o posiciona class, style
O thumb a content box do trilho thumbClass, thumbStyle

A barra dimensiona o thumb ao longo do eixo e o move; sua espessura e sua posição transversal ao trilho ficam por conta do app (w-1.5 e items-end no trilho colocam um thumb de 6 pontos à direita). O padding do trilho mantém o thumb afastado de suas extremidades e laterais.

Estados que as partes exibem:

  • O trilho: hover: sob o ponteiro; ele é um group, então o thumb recebe group-hover: a partir dele (um thumb que se alarga quando o ponteiro se aproxima).
  • O thumb: hover: sob o ponteiro, active: enquanto é arrastado.

A visibilidade da barra é sua opacidade: aplique a transparência às cores do fundo do trilho ou do thumb. Um thumb que se alarga recebe transition-all (isso nunca atrasa a posição do thumb, que a barra define separadamente dos estilos do thumb), e motion-reduce: o mantém parado para as pessoas que pedem menos movimento.

#Acessibilidade

  • A scroll view continua sendo o que os leitores de tela rolam: eles a movem com seus próprios gestos e leem seu conteúdo.
  • A barra é rotulada por accessibilityLabel para os leitores de tela que chegam até ela.

Veja Acessibilidade para rótulos, papéis e ações em geral.

#Notas de plataforma

  • Todas as plataformas desenham o que o app estiliza, e o thumb acompanha o offset que cada plataforma informa para a scroll view. Arrastar o thumb e tocar no trilho funcionam com mouse, caneta ou dedo.
  • "hover" acompanha o ponteiro de um mouse ou de um touchpad (os desktops e a web); em uma tela sensível ao toque, a barra aparece enquanto o conteúdo rola, e seu trilho oculto deixa os toques passarem.