このページは機械翻訳のため、誤りを含む場合があります。 英語の原文を読む

要素

Carousel

カルーセルは、スライドを一度に一つずつ表示します。商品の写真、オンボーディングの各ステップ、自動で切り替わるバナー、カードの列などに使えます。Carouselが動作を担います。どのスライドを表示するか、スライド間のスワイプ、前へ・次へのコントロール、ページネーション、矢印キー、端での循環、スクリーンリーダーが読み上げる内容です。各パーツはコンテキストを通じてカルーセルを見つけるので、アプリは自分のレイアウトの中で自由に配置できます。見た目はアプリ側が決めます。色、サイズ、矢印、ドット、ラベルは一切持ちません。

el-carousel.tsx

コンテンツはネイティブのスクロールビューです。指、ホイール、タッチパッドの操作は、プラットフォームのメインスレッド上でポインターと同じフレームでスライドを動かし、離すと最も近いスライドにスナップします。1回のフリングで動くのは1スライドです。静止すると、カルーセルはonChangeでそのスライドを通知します。コントロール、ページネーション、キー、アプリのindexは、コンテンツをネイティブにそのスライドまでアニメーション付きでスクロールさせます。

use nsh_base::{
    carousel, carousel_content, carousel_item, carousel_next, carousel_pagination,
    carousel_pagination_item, carousel_previous, theme,
};
use nsh_core::prelude::*;

fn photos() -> impl IntoElement {
    let index = signal(0u32);
    let names = ["Harbor", "Dunes", "Glacier"];
    carousel()
        .index(move || Some(index.get()))
        .on_change(move |i: &u32| index.set(*i))
        .accessibility_label("Photos")
        .gap_3()
        .child(
            carousel_content().w_full().h(px(180.0)).gap_3().child(
                names
                    .iter()
                    .enumerate()
                    .map(|(i, name)| {
                        carousel_item()
                            .accessibility_label(format!("{} of 3: {name}", i + 1))
                            .w_full()
                            .rounded(move || theme().radius)
                            .bg(move || theme().muted)
                            .items_center()
                            .justify_center()
                            .child(text(*name))
                    })
                    .collect::<Vec<_>>(),
            ),
        )
        .child(
            view()
                .flex_row()
                .items_center()
                .gap_2()
                .child(carousel_previous().accessibility_label("Previous photo").child(text("‹")))
                .child(
                    carousel_pagination()
                        .flex_row()
                        .gap_1()
                        .child(
                            (0..3)
                                .map(|i| {
                                    carousel_pagination_item()
                                        .index(i)
                                        .accessibility_label(format!("Photo {}", i + 1))
                                        .size_2()
                                        .rounded_full()
                                        .bg(move || theme().border)
                                        .selected_style(|s| s.bg(move || theme().primary))
                                })
                                .collect::<Vec<_>>(),
                        ),
                )
                .child(carousel_next().accessibility_label("Next photo").child(text("›"))),
        )
}

Carouselとそのパーツは、Rust のnsh-baseクレートであるnsh/baseから提供されます。表示中のスライドはアプリのindexです。スワイプ、コントロール、キーのいずれかが次のスライドをonChangeで通知し、カルーセルはアプリが返すindexまでスクロールします。スワイプで止まったスライドは、アプリがonChangeに応答するまで表示され続けます。アプリがそれをindexとして返せばそのまま残り、アプリが元のindexを保持した場合は、コンテンツがアニメーション付きでアプリのスライドに戻ります。indexがなければ、カルーセルはdefaultIndexをもとに自身で値を保持します。Rust では、carousel()、carousel_content()、carousel_item()、carousel_previous()、carousel_next()、carousel_pagination()、carousel_pagination_item()がパーツを作り、プロパティごとのセッター(.index(..))、イベントごとのリスナー(.on_change(..))、型付きのスタイルメソッド、そしてコンテンツ用の.child(..)を備えています。

#Props

プロパティRust型デフォルト説明
index.index(..)numberなし表示中のスライド。0から始まり、スライド数の範囲に収められます。アプリが所有し、変更はonChangeで通知されます。新しいindexを渡すと、そこへアニメーション付きでスクロールします。
defaultIndex.default_index(..)number0indexが未設定のときに、最初に表示するスライド。
axis.axis(..)"horizontal" | "vertical""horizontal"スライドが並ぶ方向。カルーセルの作成時に読み取られます。縦方向のカルーセルでは、矢印キーは上と下です。
looping.looping(..)booleanfalseスライドが循環します。最後の次は最初、最初の前は最後になり、スワイプ、前へ、次へ、キー、アプリのindexのいずれでも途切れなくつながります(ループ)。カルーセルの作成時に読み取られます。
accessibilityLabel.accessibility_label(..)stringなしスクリーンリーダーがカルーセルを呼ぶ名前。リージョンとして、スライドが何であるかを示します("Featured products")。
children.child(..)JSX.ElementなしCarouselContent、コントロール、およびアプリがそれらの周りに配置するもの。
class、styleスタイルメソッドstring、StyleProp<ViewStyle>なしカルーセルのレイアウト。コンテンツとコントロールの配置先。

#CarouselContent

プロパティRust型デフォルト説明
children.child(..)JSX.ElementなしCarouselItem。それぞれがコンテンツの子になります。
class、styleスタイルメソッドstring、StyleProp<ViewStyle>なしビューポートのサイズと、その中でスライドが作るトラック。スライド間のgapや、表示中のスライドの隣のスライドをのぞかせるパディングを指定できます。

#CarouselItem

プロパティRust型デフォルト説明
accessibilityLabel.accessibility_label(..)stringなしスクリーンリーダーがスライドを呼ぶ名前。グループとして、他のスライドの中での位置と表示内容を示します("2 of 5: Ocean view")。
children.child(..)JSX.Elementなしスライドの内容。
class、styleスタイルメソッドstring、StyleProp<ViewStyle>なし軸方向のサイズ(1ビューに1スライドならw-full、3スライドならbasis-1/3)と見た目。ループするカルーセルでは、その平行移動(translation)はカルーセルが管理します。内側のビューにtransformを指定してください。

#CarouselPrevious, CarouselNext

プロパティRust型デフォルト説明
accessibilityLabel.accessibility_label(..)stringなしコントロールの名前。子要素にそれを表すテキストがない場合(矢印アイコンなど)に使います。
children.child(..)JSX.Elementなしアプリのアイコンまたはラベル。
class、styleスタイルメソッドstring、StyleProp<ViewStyle>なし見た目と配置。コンテンツの横に置くか、その端に重ねてabsoluteで配置します。
activeStyle、hoverStyle、focusStyle、disabledStyle.active(|s| ..)、.hover(..)、.focus(..)、.disabled_style(..)StyleProp<ViewStyle>なし押下中、ホバー中、フォーカス中、端で無効のときのスタイル。<state>:クラスとして指定します。

#CarouselPagination

プロパティRust型デフォルト説明
accessibilityLabel.accessibility_label(..)stringなしスクリーンリーダーがページネーション項目のグループを呼ぶ名前("Choose a slide")。
indicatorClass.indicator_class(..)stringなしインジケーターの Tailwind クラス。インジケーターは、表示中のスライドの項目に重なるビューです(ドットの背後のピル形や下線など)。見た目と、行を横切る方向の配置(inset-y-0、bottom-0 h-0.5)を指定します。行に沿った方向では、その項目の幅に広がり、次の項目へネイティブにバネのように移動します。
indicatorStyle.indicator_style(|s| ..)StyleProp<ViewStyle>なしindicatorClassに重ねて適用される、インジケーターのスタイル。どちらも指定しない場合、ページネーションにインジケーターは表示されません。
children.child(..)JSX.ElementなしCarouselPaginationItemと、アプリがそれらの間に配置するもの。
class、styleスタイルメソッドstring、StyleProp<ViewStyle>なし行のレイアウト(flex-row gap-2)。

#CarouselPaginationItem

プロパティRust型デフォルト説明
index.index(..)number必須この項目が示すスライド。0から始まります。最後のスライドを超えるインデックスは無効になります。
selectedStyle.selected_style(|s| ..)StyleProp<ViewStyle>なし対応するスライドが表示されている間のスタイル。selected:クラスとして指定します。
accessibilityLabel.accessibility_label(..)stringなしどのスライドを示すか("Slide 3")。子要素にそれを表すテキストがない場合(ドットなど)に使います。
children.child(..)JSX.Elementなしアプリのドット、サムネイル、または番号。
class、styleスタイルメソッドstring、StyleProp<ViewStyle>なし見た目。その内側のgroup-selected:クラスは、groupのもとでこの項目に追従します。

#Events

イベントRust説明
onChange (Carousel).on_change(|index: &u32| ..)ユーザーが別のスライドに移動しました。スワイプ、ホイール、タッチパッドの操作がそのスライドで静止したか、コントロール、ページネーション項目、キーがそれを要求したときです。{ index }がそのスライドです。アプリから新しいindexを渡しても、何も通知されません。
onDragStart (Carousel).on_drag_start(|| ..)ユーザーがコンテンツのドラッグを開始しました(指、タッチパッド)。
onDragEnd (Carousel).on_drag_end(|| ..)ユーザーがドラッグしたコンテンツが静止しました。
onFocus (Carousel).on_focus(|| ..)キーボードフォーカスがカルーセルに入りました。対象は、カルーセル自身のビュー、前へ・次へのコントロール、ページネーション項目、スライド内のフォーカス可能なビューです。これらの間の移動では何も通知されません。
onBlur (Carousel).on_blur(|| ..)キーボードフォーカスがカルーセルから離れました。対象は、カルーセル自身のビュー、コントロール、ページネーション項目、スライドの内容です。

#Parts

パーツ内側スタイル指定状態
カルーセルCarouselのclass、stylefocus:
コンテンツカルーセルCarouselContentのclass、style
スライドコンテンツCarouselItemのclass、style
前へ、次へカルーセルそれぞれのclass、styledisabled:、active:、hover:、focus:
ページネーションカルーセルCarouselPaginationのclass、style
インジケーターページネーションindicatorClass、indicatorStyle
ページネーション項目ページネーションそのclass、styleselected:、disabled:、active:、hover:、focus:

#Several slides per view

スライドの幅は、そのスライド自身のクラスで決まるため、カードの列のようにビューポート内に複数のスライドを並べられます。各スライドは、その開始位置がスナップポイントになります。コンテンツは、最初のスライドが静止する位置に、そのスライドが来る形で静止するため、最初のスライドの前に置いたパディングは、各スライドの前にも保たれます。範囲の終端では、その位置に届かないスライドは最後のスライドとともに静止し、次へはそこで止まります。indexは最初の位置にあるスライドを指します。最後のスライドとともに静止するスライドは同じ位置を示し、そこでのスワイプは、それらのうち最初のスライドを通知します。

<Carousel accessibilityLabel="Collections">
  <CarouselContent class="w-full gap-4 px-4">
    <Each of={collections()}>
      {(c) => (
        <CarouselItem class="basis-1/3">
          <Card collection={c()} />
        </CarouselItem>
      )}
    </Each>
  </CarouselContent>
</Carousel>

スマートフォンの幅でのbasis-1/2やbasis-[85%]では、次のカードが少しのぞきます。縦方向のカルーセル(axis="vertical")では、スライドのサイズは高さで決まります。

#Looping

loopingを指定すると、スライドは途切れなく循環します。スワイプ、次へやキーによるステップ、アプリのindexによる設定のいずれでも、最後の次は最初になり、間のスライドを巻き戻すことはありません。コンテンツは、スライドの前後にそれぞれ1周分の余白を確保し、各スライドを、ビューの中央に最も近い位置に描画します。アニメーションは、スクロールとともに、プラットフォーム自身のフレームでこれを実行します。各スライドは1つだけ存在するため、スクリーンリーダーとページネーションからも1つに見えます。

ループが途切れないのは、スライドがビューポートに加えてさらに1スライド分以上を満たす場合です(全幅のスライドなら3枚、幅が3分の1のスライドなら5枚)。それより少ない場合でも、前へ、次へ、キーは循環しますが、間のスライドを横切って戻るようにスクロールします。

#Slides that move on by themselves

自動で進むバナーはアプリ側の実装です。indexを設定するタイマーを用意します。カルーセルはユーザーが操作中であることを通知するので、WAI-ARIA カルーセルパターンが求めるとおり、タイマーはそのあいだ待機します。ドラッグの前後にはonDragStartとonDragEnd、キーボードフォーカスがカルーセル内(カルーセル自身、コントロール、ページネーション、スライドの内容)にあるあいだはonFocusとonBlur、さらにその周囲のビュー上のポインターを使います。モーション低減が有効な場合はオフにし、停止するためのボタン(ローテーションコントロール)をユーザーに用意してください。

import { cleanup, effect, reduceMotion, signal, Text, View } from "nsh";
import { Button, Carousel, CarouselContent } from "nsh/base";

const index = signal(0);
const playing = signal(true);
const held = signal(0);
const hold = (on: boolean) => held.set(held() + (on ? 1 : -1));

effect(() => {
  index();
  if (!playing() || held() > 0 || reduceMotion()) return;
  const timer = setInterval(() => index.set((index() + 1) % SLIDES.length), 4000);
  cleanup(() => clearInterval(timer));
});

<View onMouseEnter={() => hold(true)} onMouseLeave={() => hold(false)}>
  <Carousel
    looping
    index={index()}
    onChange={({ index: next }) => index.set(next)}
    onDragStart={() => hold(true)}
    onDragEnd={() => hold(false)}
    onFocus={() => hold(true)}
    onBlur={() => hold(false)}
    accessibilityLabel="Highlights"
  >
    <CarouselContent class="w-full">{/* the slides */}</CarouselContent>
    <Button onPress={() => playing.set(!playing())} class="self-start px-3 py-1">
      <Text>{playing() ? "Pause slides" : "Play slides"}</Text>
    </Button>
  </Carousel>
</View>

エフェクト内でindex()を読み取ると、ユーザーによる変更を含め、変更のたびにインターバルが最初からやり直されます。loopingを指定すると、最後のスライドから最初のスライドへは、継ぎ目を越えて前方へ移動します。

#Accessibility

  • カルーセルはaccessibilityLabelで名前が付けられたリージョンです。各スライドは、それぞれ固有の名前(他のスライドの中での位置と表示内容)を持つグループです。
  • 前へと次へはボタンで、カルーセルがループしない限り、端では無効になります。各ページネーション項目もボタンで、対応するスライドが表示されている間は選択状態になります。
  • すべてのラベルはアプリが用意し、アプリの言語で表示されます。
  • ループするカルーセルのスライドは、どこに描画されていても、それぞれ1つだけ存在します。

ラベル、ロール、通知に関する一般的な内容は、アクセシビリティを参照してください。

#Keyboard

macOS と Web では、Tab が押下を受け付けるビューに到達するため、カルーセルはキーボードフォーカスの停止位置となり、そのコントロールやページネーション項目も同様です。いずれかにフォーカスがあるあいだは、次のキーが使えます。

キー動作
左、右(縦方向では上、下)前または次のスライドに移動します。ループ時は端を越えて循環します。
Home、End最初または最後のスライドに移動します。
Space、Returnフォーカス中のコントロールまたはページネーション項目を押します。

右から左へのレイアウトでは、横方向のカルーセルのスライドは右から並ぶため、左キーで次のスライドへ、右キーで前のスライドへ移動します。縦方向のカルーセルでは、上と下のままです。フォーカス中に無効になったコントロール(最後のスライドでの次へ)は、フォーカスをカルーセルに渡すので、キー操作はそこで引き続き機能します。スライド内にあるアプリのフォーカス可能なビューは、それぞれ独自のキー操作を保ちます。たとえば入力欄の矢印キーはキャレットを動かします。

#Motion

  • スワイプ、ホイール、タッチパッドの操作は、ポインターと同じフレームでスライドを動かします。スクロールするのはプラットフォームです。
  • 離すと、プラットフォームのスナップによって、コンテンツは最も近いスライドへ滑らかに移動します。1回のフリングで動くのは1スライドです。
  • 前へ、次へ、ページネーション項目、キー、アプリのindexは、コンテンツをそのスライドまでアニメーション付きでスクロールします。モーション低減が有効な場合は、即座に移動します。
  • ページネーションのインジケーターは、表示中のスライドの項目へバネのように移動します。モーション低減が有効な場合は、即座に移動します。
  • loopingは、最後のスライドから最初のスライドへ、継ぎ目を越えて1ステップ前方へ進みます。コンテンツが静止すると、アニメーションなしで1周分戻りますが、画面上の見た目は変わりません。

#Platform notes

  • iOS、Android、Mac Catalyst: 指でスライドをスワイプし、タップでコントロールを押します。
  • デスクトップ(macOS、Windows、Linux、GPUI)と Web: ホイールとタッチパッドでスライドをスクロールし、クリックでコントロールを押します。コンテンツ上のマウスドラッグはアプリ側の扱いで、これらのプラットフォームのすべてのスクロールビューと同様です。
  • どのプラットフォームでも、アプリがスタイルしたものが描画されます。スライド、コントロール、ページネーション、そのインジケーターです。