تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

Carousel

يعرض الـ carousel الشرائح واحدة في كل مرة: صور المنتج، أو خطوات التعريف بالتطبيق، أو لافتة تنتقل من تلقاء نفسها، أو صفًا من البطاقات. يتولى Carousel السلوك: أي شريحة تظهر، والتمرير بين الشرائح، وعنصرا التحكم السابق والتالي، والترقيم (pagination)، ومفاتيح الأسهم، والانتقال الدائري عند الأطراف، وما يسمعه قارئو الشاشة. تجد أجزاؤه الـ carousel عبر السياق (context)، فيرتبها التطبيق بحرية في تخطيطه. أما المظهر فهو من شأن التطبيق: لا ألوان ولا أحجام ولا أسهم ولا نقاط ولا تسميات من الـ carousel نفسه.

el-carousel.tsx

المحتوى عبارة عن native scroll view. يحرّك الإصبع وعجلة الفأرة ولوحة اللمس الشرائح على الخيط الرئيسي للمنصة، في الإطار نفسه الذي يتحرك فيه المؤشر، وعند الإفلات يستقر المحتوى على أقرب شريحة، بشريحة واحدة لكل swipe سريع. وعندما يستقر المحتوى يبلّغ الـ carousel عن الشريحة عبر 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 وأجزاؤه من nsh/base، وهي nsh-base crate في Rust. الشريحة المعروضة هي index الخاصة بالتطبيق: يبلّغ التمرير أو عنصر التحكم أو المفتاح عن الشريحة التالية عبر onChange، ويمرّر الـ carousel إلى index التي يعيدها التطبيق. تبقى الشريحة التي استقر عليها التمرير معروضة إلى أن يجيب التطبيق عن onChange الخاص بها: تبقى إذا أعادها التطبيق بوصفها index، ويرتد المحتوى إلى شريحة التطبيق بحركة متحركة إذا أبقى التطبيق على index التي كانت لديه. وعند غياب index يحتفظ الـ carousel بقيمته الخاصة، انطلاقًا من defaultIndex. في Rust تنشئ carousel() وcarousel_content() وcarousel_item() وcarousel_previous() وcarousel_next() وcarousel_pagination() وcarousel_pagination_item() الأجزاء، مع setter لكل خاصية (.index(..))، وlistener لكل حدث (.on_change(..))، وطرق الأنماط typed، و.child(..) لمحتواها.

#الخصائص

الخاصيةRustالنوعالافتراضيالوصف
index.index(..)numberلا شيءالشريحة المعروضة، تبدأ من 0 وتُقيَّد بعدد الشرائح الموجودة. يملكها التطبيق: يُبلَّغ عن التغييرات عبر onChange، وقيمة index جديدة تمرّر إلى هناك بحركة متحركة.
defaultIndex.default_index(..)number0الشريحة المعروضة في البداية، ما دام index غير مضبوط.
axis.axis(..)"horizontal" | "vertical""horizontal"الاتجاه الذي تتعاقب فيه الشرائح؛ يُقرأ عند إنشاء الـ carousel. مفتاحا الأسهم في الـ carousel العمودي هما Up وDown.
looping.looping(..)booleanfalseتدور الشرائح: بعد الأخيرة تأتي الأولى، وقبل الأولى تأتي الأخيرة، بسلاسة، في التمرير والسابق والتالي والمفاتيح وindex الخاص بالتطبيق على حد سواء (التكرار الدائري). يُقرأ عند إنشاء الـ carousel.
accessibilityLabel.accessibility_label(..)stringلا شيءما يسمّي به قارئو الشاشة الـ carousel، وهو منطقة (region): ما هي شرائحه ("Featured products").
children.child(..)JSX.Elementلا شيءCarouselContent وعناصر التحكم وكل ما يرتبه التطبيق حولها.
class، stylestyle methodsstring، StyleProp<ViewStyle>لا شيءتخطيط الـ carousel: أين يوضع المحتوى وعناصر التحكم.

#CarouselContent

الخاصيةRustالنوعالافتراضيالوصف
children.child(..)JSX.Elementلا شيءCarouselItem، وكل واحدة منها عنصر فرعي للمحتوى.
class، stylestyle methodsstring، StyleProp<ViewStyle>لا شيءحجم منطقة العرض (viewport)، والمسار الذي تشكّله الشرائح فيها: gap بينها، وحشوة تُظهر جزءًا من الشرائح المجاورة للشريحة المعروضة.

#CarouselItem

الخاصيةRustالنوعالافتراضيالوصف
accessibilityLabel.accessibility_label(..)stringلا شيءما يسمّي به قارئو الشاشة الشريحة، وهي مجموعة (group): موضعها بين الشرائح الأخرى وما تعرضه ("2 of 5: Ocean view").
children.child(..)JSX.Elementلا شيءمحتوى الشريحة.
class، stylestyle methodsstring، StyleProp<ViewStyle>لا شيءحجمها على امتداد المحور (w-full لشريحة واحدة في كل عرض، وbasis-1/3 لثلاث) ومظهرها. في carousel دائري تكون إزاحتها (translation) من شأن الـ carousel: ضع transform على view داخلها.

#CarouselPrevious, CarouselNext

الخاصيةRustالنوعالافتراضيالوصف
accessibilityLabel.accessibility_label(..)stringلا شيءاسم عنصر التحكم، حين لا يكون في عناصره الفرعية نص يدل عليه (أيقونة سهم).
children.child(..)JSX.Elementلا شيءأيقونة التطبيق أو تسميته.
class، stylestyle methodsstring، 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 الخاصة بالمؤشر (indicator)، وهو view على عنصر الشريحة المعروضة (قرص خلف النقاط، أو خط سفلي): مظهره وموضعه عبر الصف (inset-y-0، bottom-0 h-0.5). وعلى امتداد الصف يغطي ذلك العنصر ويقفز إلى العنصر التالي أصليًا.
indicatorStyle.indicator_style(|s| ..)StyleProp<ViewStyle>لا شيءنمط المؤشر، فوق indicatorClass. وبدون أي منهما لا يكون للترقيم مؤشر.
children.child(..)JSX.Elementلا شيءCarouselPaginationItem وكل ما يرتبه التطبيق بينها.
class، stylestyle methodsstring، 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، stylestyle methodsstring، StyleProp<ViewStyle>لا شيءمظهره؛ وفئات group-selected: داخله تتبعه ضمن group.

#الأحداث

الحدثRustالوصف
onChange (Carousel).on_change(|index: &u32| ..)انتقل المستخدم إلى شريحة أخرى: استقر التمرير أو العجلة أو لوحة اللمس عليها، أو طلبها عنصر تحكم أو عنصر ترقيم أو مفتاح. { index } هي تلك الشريحة. قيمة index جديدة من التطبيق لا تُبلِّغ عن شيء.
onDragStart (Carousel).on_drag_start(|| ..)بدأ المستخدم سحب المحتوى (بإصبع أو بلوحة اللمس).
onDragEnd (Carousel).on_drag_end(|| ..)استقر المحتوى الذي سحبه المستخدم.
onFocus (Carousel).on_focus(|| ..)دخل تركيز لوحة المفاتيح إلى الـ carousel: إلى view الخاص به، أو عنصر التحكم السابق أو التالي، أو عنصر ترقيم، أو view قابل للتركيز داخل شريحة. التنقل بينها لا يُبلِّغ عن شيء.
onBlur (Carousel).on_blur(|| ..)غادر تركيز لوحة المفاتيح الـ carousel: view الخاص به وعناصر التحكم وعناصر الترقيم ومحتوى الشرائح.

#الأجزاء

الجزءداخليُنسَّق بـالحالات
الـ carouselCarousel: class، stylefocus:
المحتوىالـ carouselCarouselContent: class، style
شريحةالمحتوىCarouselItem: class، style
السابق، التاليالـ carouselclass، style الخاصان بهماdisabled:، active:، hover:، focus:
الترقيمالـ carouselCarouselPagination: class، style
المؤشرالترقيمindicatorClass، indicatorStyle
عنصر ترقيمالترقيمclass، style الخاصان بهselected:، disabled:، active:، hover:، focus:

#عدة شرائح في كل عرض

عرض الشريحة يحدده صنفها (class) الخاص، لذا فصف من البطاقات هو عدة شرائح داخل منطقة العرض. كل شريحة نقطة التقاط (snap point) عند بدايتها: يستقر المحتوى بحيث تكون تلك الشريحة حيث تستقر الأولى، فتُحفظ الحشوة التي قبل الأولى قبل كل شريحة. وعند نهاية المدى تستقر الشرائح التي لا تبلغ ذلك الموضع مع الأخيرة، ويتوقف التالي هناك. يسمّي 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%]، جزءًا من البطاقة التالية. أما الـ carousel العمودي (axis="vertical") فيحدد أحجام شرائحه بحسب ارتفاعها.

#التكرار الدائري

مع looping تدور الشرائح بسلاسة: بعد الأخيرة تأتي الأولى، سواء بالتمرير أو بالانتقال بالتالي أو بالمفاتيح، أو بضبط index الخاص بالتطبيق، دون إرجاع للشرائح التي بينهما. يحتفظ المحتوى بمساحة دورة كاملة من الشرائح قبلها وبعدها، ويرسم كل شريحة حيث تكون أقرب إلى منتصف العرض؛ وتجري الحركة ذلك على إطارات المنصة نفسها، مع التمرير. كل شريحة موجودة مرة واحدة فقط: يراها قارئو الشاشة والترقيم مرة واحدة.

يكون التكرار سلسًا ما دامت الشرائح تغطي منطقة العرض وشريحة إضافية (ثلاث شرائح بعرض كامل، أو خمس بعرض ثلث). ومع عدد أقل، يبقى السابق والتالي والمفاتيح تدور، لكن بالتمرير راجعًا عبر الشرائح.

#شرائح تنتقل من تلقاء نفسها

اللافتة التي تتحرك تلقائيًا من شأن التطبيق: مؤقّت يضبط index. يبلّغ الـ carousel بوقت تفاعل المستخدم معه، فينتظر المؤقت حينها، كما يطلب نمط carousel في WAI-ARIA: onDragStart / onDragEnd حول السحب، وonFocus / onBlur ما دام تركيز لوحة المفاتيح داخل الـ carousel (عليه، أو على عناصر التحكم فيه، أو الترقيم، أو محتوى الشرائح)، ومؤشر الفأرة فوق view يحيط به. أوقفه عند تفعيل تقليل الحركة، وامنح المستخدمين زرًّا يوقفه (عنصر التحكم في التدوير):

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() داخل الـ effect تبدأ فترة كاملة من جديد بعد كل تغيير، بما فيه تغيير المستخدم. ومع looping، يتحرك الانتقال من الشريحة الأخيرة إلى الأولى إلى الأمام عبر الوصلة.

#إمكانية الوصول

  • الـ carousel منطقة (region) تسمّيها accessibilityLabel؛ وكل شريحة مجموعة (group) تسمّيها تسميتها الخاصة (موضعها بين الشرائح الأخرى وما تعرضه).
  • السابق والتالي زرّان (buttons)، يُعطَّلان عند الطرفين ما لم يكن الـ carousel دائريًا؛ وكل عنصر ترقيم زر، يكون محددًا (selected) ما دامت شريحته معروضة.
  • كل تسمية من التطبيق، وبلغة التطبيق.
  • كل شريحة في الـ carousel الدائري موجودة مرة واحدة، أينما رُسمت.

راجع إمكانية الوصول للاطلاع على التسميات والأدوار والإعلانات بوجه عام.

#لوحة المفاتيح

على macOS والويب، حيث يصل Tab إلى الـ views التي تستقبل الضغطات، يكون الـ carousel نقطة توقف لتركيز لوحة المفاتيح، وكذلك عناصر التحكم فيه وعناصر الترقيم. وما دام أحدها يملك التركيز:

المفتاحما يفعله
Left، Right (Up، Down عند الاتجاه العمودي)ينتقل إلى الشريحة السابقة أو التالية، مع الدوران عند الأطراف أثناء التكرار الدائري.
Home، Endينتقل إلى الشريحة الأولى أو الأخيرة.
Space، Returnيضغط عنصر التحكم أو عنصر الترقيم المركَّز عليه.

في التخطيط من اليمين إلى اليسار تتتابع شرائح الـ carousel الأفقي من اليمين، فينقل Left إلى الشريحة التالية وRight إلى السابقة؛ أما الـ carousel العمودي فيحتفظ بـ Up وDown. وعنصر التحكم الذي يصبح معطّلًا وهو يملك التركيز (التالي في الشريحة الأخيرة) يسلّم التركيز إلى الـ carousel، فتستمر المفاتيح في العمل هناك. أما الـ views القابلة للتركيز التي يضعها التطبيق داخل الشرائح فتحتفظ بمفاتيحها: أسهم حقل الإدخال تحرك مؤشر الكتابة فيه.

#الحركة

  • يحرّك التمرير والعجلة ولوحة اللمس الشرائح مع المؤشر، في الإطار نفسه: المنصة هي التي تمرّرها.
  • عند الإفلات ينساب المحتوى إلى أقرب شريحة، بشريحة واحدة لكل swipe سريع، كما تلتقط المنصة.
  • السابق والتالي وعنصر الترقيم والمفتاح وindex الخاص بالتطبيق تمرّر المحتوى إلى الشريحة بحركة متحركة؛ ومع تقليل الحركة، فورًا.
  • يقفز مؤشر الترقيم إلى عنصر الشريحة المعروضة؛ ومع تقليل الحركة ينتقل فورًا.
  • looping ينتقل من الشريحة الأخيرة إلى الأولى بخطوة إلى الأمام عبر الوصلة؛ وحين يستقر المحتوى يرجع بدورة كاملة دون حركة، وهذا لا يغيّر شيئًا على الشاشة.

#ملاحظات المنصات

  • iOS وAndroid وMac Catalyst: يمرّر الإصبع الشرائح؛ والنقرة تضغط عنصر تحكم.
  • أنظمة سطح المكتب (macOS وWindows وLinux وGPUI) والويب: تمرّر العجلة ولوحة اللمس الشرائح، والنقر يضغط عنصر تحكم. أما سحب الفأرة فوق المحتوى فمن شأن التطبيق، كما في كل scroll view هناك.
  • ترسم كل منصة ما ينسّقه التطبيق: الشرائح وعناصر التحكم والترقيم ومؤشره.