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

العناصر

Accordion

يكدّس الأكورديون (Accordion) أقسامًا تتوسع وتنطوي تحت عناوينها: أسئلة الأسئلة الشائعة، ومجموعات صفحة الإعدادات، وخطوات الدفع. يتكوّن من خمسة أجزاء، يملك كل منها جزءًا واحدًا من السلوك ولا يملك شيئًا من المظهر:

el-accordion.tsx
el-accordion.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionTrigger } from "nsh/base";
import { desktop, focusRing, hoverFill, Icon, Log, Panel, Segmented, size, Split, ui } from "./palette";

// An FAQ: each item a heading whose trigger (a button that says whether it
// is expanded) opens its answer. The app owns which items are open
// (`value`) and sets what `onChange` reports; one at a time, or several
// with `multiple`. The panels spring open and shut (`reveal`) and the
// items below move with them; the chevron turns with `group-aria-expanded:`.
// The pointer's hover and the keyboard's focus ring come from the palette;
// the reveal and the chevron stay still with reduced motion.
const pad = desktop ? "px-3" : "px-4";

const FAQ: { value: string; question: string; answer: string; disabled?: boolean }[] = [
  { value: "shipping", question: "How long does shipping take?", answer: "Orders leave the warehouse within a day and arrive in two to five working days." },
  { value: "returns", question: "Can I return an item?", answer: "Within 30 days, unused and in its box: print the label from your order and drop it off." },
  { value: "warranty", question: "What does the warranty cover?", answer: "Two years against defects in materials and workmanship, parts and labour included." },
  { value: "gift", question: "Can I send it as a gift?", answer: "Gift wrapping opens with the holiday season.", disabled: true },
];

const MODES = ["one at a time", "several"] as const;

function App() {
  const open = signal<string[]>(["shipping"]);
  const mode = signal<(typeof MODES)[number]>("one at a time");
  const events = signal<string[]>([]);
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>accordion</Text>
      <Text style={{ color: ui().muted }}>{desktop ? "Click a question, or Tab to it and press Space" : "Tap a question to read its answer"}</Text>

      <Split
        gap={14}
        inspector={
          <>
            <Panel title="multiple">
              <Segmented
                options={MODES}
                value={mode()}
                onChange={(value) => {
                  mode.set(value);
                  // One at a time keeps the item opened last.
                  if (value === "one at a time") open.set(open().slice(-1));
                }}
              />
            </Panel>
            <Panel title="value">
              <Text style={{ fontSize: 13, color: ui().text }}>{open().length ? open().join(", ") : "none open"}</Text>
            </Panel>
            <Log title="onChange({ value })" lines={events()} minHeight={92} />
          </>
        }
      >
        <Accordion
          value={open()}
          multiple={mode() === "several"}
          onChange={({ value }) => {
            open.set(value);
            events.set([`[${value.join(", ")}]`, ...events()].slice(0, 4));
          }}
          style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}
        >
          {FAQ.map((item, i) => (
            <AccordionItem value={item.value} disabled={item.disabled}>
              <View style={{ height: i === 0 ? 0 : 1, marginLeft: desktop ? 12 : 16, backgroundColor: ui().line }} />
              <AccordionHeader>
                <AccordionTrigger
                  hoverStyle={item.disabled ? undefined : hoverFill()}
                  focusStyle={focusRing()}
                  class={`group flex-row items-center gap-3 ${pad} active:opacity-70 disabled:opacity-40`}
                  style={{ minHeight: size.row + 8 }}
                >
                  <Text style={{ flex: 1, fontSize: size.label, fontWeight: "500", color: ui().text }}>{item.question}</Text>
                  <View class="transition-transform duration-200 motion-reduce:transition-none group-aria-expanded:rotate-180">
                    <Icon name="chevron-down" size={14} tint={ui().muted} />
                  </View>
                </AccordionTrigger>
              </AccordionHeader>
              <AccordionPanel reveal accessibilityLabel={item.question}>
                <View class={`${pad} pb-3`}>
                  <Text style={{ fontSize: size.label, lineHeight: size.label * 1.45, color: ui().muted }}>{item.answer}</Text>
                </View>
              </AccordionPanel>
            </AccordionItem>
          ))}
        </Accordion>
      </Split>
    </View>
  );
}

mount(App);
  • Accordion، المكدّس: يحمل value التطبيق، وهي قيم العناصر المفتوحة، ويُبلغ بالقيمة التالية عبر onChange. يُفتح عنصر واحد في كل مرة، أو عدة عناصر باستخدام multiple.
  • AccordionItem، قسم واحد، يُعرَّف بـvalue. يكون مفتوحًا ما دامت value الأكورديون تحتوي عليه.
  • AccordionHeader، عنوان العنصر، من level تعلنه قارئات الشاشة.
  • AccordionTrigger، الزر في العنوان الذي يوسّع عنصره ويطويه، ويُعلن ما إذا كان موسّعًا.
  • AccordionPanel، محتوى العنصر، يظهر ما دام مفتوحًا؛ ومع reveal يتمدد ارتفاعه وينكمش بحركة نابضية.
use nsh_base::{accordion, accordion_header, accordion_item, accordion_panel, accordion_trigger, theme};
use nsh_core::prelude::*;

const FAQ: [(&str, &str, &str); 2] = [
    ("shipping", "When will my order ship?", "Orders placed before 2 pm leave the same day."),
    ("returns", "How do I return an item?", "Print the label from your order within 30 days."),
];

fn faq() -> impl IntoElement {
    let open = signal(vec![SharedString::from("shipping")]);
    let mut list = accordion()
        .value(open)
        .on_change(move |value: &Vec<SharedString>| open.set(value.clone()));
    for (value, question, answer) in FAQ {
        list = list.child(
            accordion_item()
                .value(value)
                .border_b_1()
                .border_color(move || theme().border)
                .child(
                    accordion_header().child(
                        accordion_trigger()
                            .flex_row()
                            .items_center()
                            .justify_between()
                            .py_3()
                            .child(text(question).font_weight(FontWeight::MEDIUM))
                            .child(text("⌄").transition_transform().rotate(move || {
                                let open = open.get().iter().any(|v| &**v == value);
                                if open { deg(180.0) } else { deg(0.0) }
                            })),
                    ),
                )
                .child(
                    accordion_panel()
                        .reveal(true)
                        .accessibility_label(question)
                        .child(view().pb_3().child(text(answer).text_color(move || theme().muted_foreground))),
                ),
        );
    }
    list
}

تأتي الأجزاء من nsh/base، وهي حزمة nsh-base في Rust. القيمة ملك التطبيق: فضغطة المشغّل تُبلغ بالقيمة value التالية عبر onChange، ويفتح الأكورديون العناصر التي تعيد إليه التطبيقُ قيمها. يجد كل جزء الجزء المحيط به من تلقاء نفسه، فيرتّبها التطبيق بحرية: فواصل بين العناصر، وشارة في عنوان، وأيقونة قبل العنوان. في Rust تُنشئ accordion() وaccordion_item() وaccordion_header() وaccordion_trigger() وaccordion_panel() هذه الأجزاء، مع دالة ضبط لكل خاصية و.child(..) لما بداخلها.

#الخصائص

#Accordion

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
value.value(..)string[][]value العناصر المفتوحة. يملكها التطبيق: فضغطة المشغّل تُبلغ بالقيم التالية عبر onChange، ويعرض الأكورديون القيم التي يعيدها التطبيق.
multiple.multiple(..)booleanfalseعدة عناصر مفتوحة في آن واحد: فتح عنصر يترك العناصر الأخرى مفتوحة. وبدونها، يُغلق فتحُ عنصرٍ العنصرَ الذي كان مفتوحًا.
disabled.disabled(..)booleanfalseجميع العناصر معطّلة: لا يقبل أي مشغّل ضغطات، وتعلن قارئات الشاشة أن كل عنصر باهت.
children.child(..)JSX.Elementلا شيءالعناصر، وما يرتّبه التطبيق بينها (فواصل).
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءتخطيط المكدّس ومظهره: بطاقة حول العناصر، والمسافة بينها.

#AccordionItem

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
value.value(..)stringمطلوبةما يُعرّف العنصر في value الأكورديون: معرّف التطبيق نفسه، وليس عنوانه أبدًا، لأن العنوان يتغير باللغة.
disabled.disabled(..)booleanfalseلا يقبل مشغّله ضغطات، وتعلن قارئات الشاشة أنه باهت. ويبقى مفتوحًا أو مغلقًا بحسب ما تقوله value الأكورديون.
children.child(..)JSX.Elementلا شيءترويسة العنصر ولوحته، وما يرتّبه التطبيق فيه.
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءالقسم: فاصل تحته، وبطاقة حوله.

#AccordionHeader

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
level.level(..)number3مستوى العنوان بين عناوين الصفحة، لقارئات الشاشة (1 هو الأعلى).
children.child(..)JSX.Elementلا شيءمشغّل العنصر، وما تحمله الترويسة أيضًا (شارة، إجراء خاص بها).
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءتخطيط العنوان.

#AccordionTrigger

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
expandedStyle.expanded_style(|s| ..)StyleProp<ViewStyle>لا شيءتنسيق المشغّل أثناء فتح عنصره، على هيئة فئات aria-expanded:.
accessibilityLabel.accessibility_label(..)stringلا شيءما تقوله قارئات الشاشة، حين لا يكون في العناصر الفرعية نص يقوله.
children.child(..)JSX.Elementلا شيءالعنوان والسهم؛ وتتبع فئات group-aria-expanded: بداخلها المشغّلَ باستخدام group على class الخاص به.
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءالمشغّل نفسه: الصف الذي يضغطه المستخدم.
activeStyle، hoverStyle، focusStyle، disabledStyle.active(|s| ..)، .hover(..)، .focus(..)، .disabled_style(..)StyleProp<ViewStyle>لا شيءتنسيق المشغّل أثناء الضغط أو التمرير أو التركيز أو التعطيل، على هيئة فئات <state>:.

#AccordionPanel

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
reveal.reveal(..)booleanfalseيُفتح ويُغلق بكشف تدريجي، كما في Collapsible: تقصّ اللوحة محتواها، ويتمدد ارتفاعها بحركة نابضية من الصفر إلى ارتفاع المحتوى وبالعكس، بتخميد حرج (نحو 265 ملّي ثانية، بلا تجاوز)، وتتحرك العناصر التي بعدها معها. ومع تقليل الحركة تُفتح وتُغلق فورًا.
keepMounted.keep_mounted(..)booleanfalseيبقى المحتوى في الشجرة أثناء إغلاق العنصر، بعيدًا عن الأنظار وعن تركيز لوحة المفاتيح وعن قارئات الشاشة: فيحتفظ الفيديو بموضعه، وتحتفظ عروض الويب بصفحتها. وبدونها يغادر المحتوى الشجرة عند إغلاق اللوحة (بعد أن يُنهي الكشف إغلاقها) ويعود كما كان.
accessibilityLabel.accessibility_label(..)stringلا شيءما تسمّي به قارئات الشاشة المنطقة: عادةً عنوان العنصر، كما تقوله ترويسته.
children.child(..)JSX.Elementلا شيءالمحتوى.
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءتخطيط المنطقة ومظهرها. ومع reveal تبقى اللوحة في التخطيط وهي مغلقة بارتفاع صفر، لذا توضع الحشوات والحدود على عرض (View) يحتوي المحتوى.

#الأحداث

الحدثRustالوصف
onChange({ value }) (Accordion).on_change(|value: &Vec<SharedString>| ..)ضُغط مشغّل: نقرة لمسية، أو نقرة بالفأرة، أو Space أو Return أثناء تركيز لوحة المفاتيح عليه، أو تفعيل من قارئ شاشة. value هي قيم العناصر المفتوحة كما ستكون: قيمة العنصر المفتوح تغادرها، وقيمة العنصر المغلق تنضم إليها في النهاية، أو، بدون multiple، تحل محلها. ويعمل قبل أن يتغير أي شيء: اضبط value لتتبعه.

#الأجزاء

الجزءداخليُنسَّق بواسطةالحالات
المكدّسAccordion الخاصة بـclass، style
عنصرالمكدّسAccordionItem الخاصة بـclass، styleقيمة التطبيق
العنوانالعنصرAccordionHeader الخاصة بـclass، style
المشغّلالعنوانAccordionTrigger الخاصة بـclass، stylearia-expanded:، disabled:، active:، hover:، focus: (أو expandedStyle، disabledStyle، …)
العنوان الفرعي والسهمالمشغّلعناصرها الخاصة (children)group-aria-expanded:، group-disabled: مع group على class الخاص بالمشغّل
اللوحةالعنصر، بعد العنوانAccordionPanel الخاصة بـclass، style
المحتوىاللوحةعناصره الخاصة (children)

بدون reveal، تخرج اللوحة المغلقة من التخطيط ويخرج محتواها من الشجرة. ومع reveal، يكون التغيير حركة تخطيط (layout animation) في الإيداع الذي يغيّر value (كما في LayoutAnimation.configureNext، وتُنفَّذ أصلية، فلا تعمل JavaScript أثناء الحركة): تنمو اللوحة التي تُفتح من الصفر إلى ارتفاع محتواها، وتنكمش التي تُغلق إلى الوراء، كلتاهما بالنابض نفسه، وتتحرك العناصر التي بعدهما معهما. وتصحبها انتقالة من التطبيق: يدور السهم (transition-transform group-aria-expanded:rotate-180).

#التعطيل

لا يقبل مشغّل العنصر المعطّل ضغطات، وتعلنه قارئات الشاشة باهتًا؛ وتتبعه فئات disabled: على المشغّل وgroup-disabled: بداخله. وتعطّل disabled الأكورديون جميع العناصر. ويظل العنصر المعطّل يعرض ما تقوله value التطبيق: إذ يستطيع التطبيق فتحه.

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

  • المكدّس مجموعة، وكل ترويسة عنوان من level الخاص بها، وكل مشغّل زر بداخلها يعلن ما إذا كان عنصره موسّعًا. وتستطيع قارئات الشاشة الانتقال من عنوان إلى عنوان.
  • اللوحة المفتوحة منطقة (region) تُسمّى بـaccessibilityLabel؛ أما المغلقة فمخفية عن قارئات الشاشة، بما في ذلك المحتوى المُبقى فيها.
  • مظهر العنصر المفتوح من شأن التطبيق، ولا ينبغي أن يعتمد على اللون وحده: أدِر السهم كما تفعل الأمثلة.

انظر Accessibility للاطلاع على التسميات والأدوار والإعلانات عمومًا.

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

حيث تكون العروض التي تقبل الضغطات محطات لتركيز لوحة المفاتيح (macOS والويب)، يكون كل مشغّل محطة، بترتيب الصفحة، ويضغطه Space وReturn. تُتخطى المشغّلات المعطّلة، وكذلك محتوى اللوحة المغلقة. وينقل Tab التركيز من مشغّل إلى عناصر التحكم في لوحته المفتوحة ثم إلى المشغّل التالي.

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

  • تعرض كل منصة لوحة العنصر المفتوح تحت عنوانه وتترك المغلقة خارج التخطيط (بارتفاع صفر مع reveal).
  • ومع reveal، تُحرّك كل منصة ارتفاعات اللوحات نابضيًا بمحرّك الحركة الخاص بها (iOS وAndroid وmacOS وWindows وLinux وGPUI والويب)، وتفتحها وتغلقها فورًا ما دام النظام يطلب تقليل الحركة.
  • تسمع قارئ الشاشة في كل منصة العنوان ومستواه أثناء قراءة العناوين، والمشغّل بداخله زرًّا مع حالته الموسّعة؛ وفي iOS يقرأ VoiceOver المشغّل نفسه.
  • iOS وAndroid وMac Catalyst: ضغطة المشغّل نقرة لمسية. أنظمة سطح المكتب (macOS وWindows وLinux وGPUI) والويب: نقرة بالفأرة، وSpace أو Return على المشغّل المركَّز عليه في macOS والويب.