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

العناصر

Combobox

يختار الـ Combobox قيمًا من قائمة عناصر، قيمة واحدة أو، مع multiple، عدة قيم: وسوم منشور، أو أعضاء قناة، أو لغات مستند. زر التشغيل فيه حقل إدخال: الكتابة تفتح القائمة وتصفّيها، كما يفعل combobox في shadcn/ui. وهو شقيق Select ويشاركه قائمته، ويعرضها بالشكل الذي يتوقعه كل جهاز:

el-combobox.tsx
el-combobox.tsxLive
import { type JSX, mount, signal, Text, View } from "nsh";
import { Combobox, type OpenChangeReason, type SelectEntry, type SelectGroup } from "nsh/base";
import { CheckMark, desktop, Icon, Log, Panel, size, Split, ui } from "./palette";

// Two comboboxes in a card: labels for an issue, several picked in one go
// from a searchable list with sections, a clear button and a footer; and
// an assignee, one person replacing the last. The trigger is the
// field: the input typing filters the list with, and a chevron that
// turns while the list is open; the list is a popup at the trigger with a pointer, a sheet
// on a touch screen. Selected rows show a check in its own lane.

const LABELS = [
  { value: "bug", title: "Bug", group: "Type" },
  { value: "feature", title: "Feature", group: "Type" },
  { value: "docs", title: "Docs", group: "Type" },
  { value: "ios", title: "iOS", group: "Platform" },
  { value: "android", title: "Android", group: "Platform" },
  { value: "macos", title: "macOS", group: "Platform" },
  { value: "windows", title: "Windows", group: "Platform" },
  { value: "linux", title: "Linux", group: "Platform" },
  { value: "web", title: "Web", group: "Platform" },
  { value: "wontfix", title: "Won't fix", group: "Status", disabled: true },
  { value: "blocked", title: "Blocked", group: "Status" },
];

const PEOPLE = [
  { value: "ada", title: "Ada Lovelace" },
  { value: "grace", title: "Grace Hopper" },
  { value: "linus", title: "Linus Torvalds" },
  { value: "margaret", title: "Margaret Hamilton" },
];

const titlesOf = (items: { value: string; title: string }[], values: string[]) =>
  values.map((v) => items.find((i) => i.value === v)?.title ?? v);

/** The text of a control: 13 with a pointer, the touch label size otherwise. */
const controlText = desktop ? 13 : size.label;

/** A popup at the trigger with a pointer, a sheet at the bottom on a touch screen. */
const surface = () =>
  desktop
    ? { ...ui().card, borderRadius: 12, padding: 4 }
    : { ...ui().card, borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingHorizontal: 10, paddingTop: 10, paddingBottom: 30, maxHeight: "70%" as const };

const backdrop = { backgroundColor: "rgba(0,0,0,0.45)" };

/** The field: a bordered box around the input, the clear button and the chevron. */
const fieldStyle = (open: boolean) => ({
  flexDirection: "row" as const,
  alignItems: "center" as const,
  minHeight: size.control,
  paddingLeft: 10,
  paddingRight: 4,
  borderRadius: 8,
  borderWidth: 1,
  borderColor: open ? ui().faint : ui().border,
  backgroundColor: ui().card.backgroundColor,
  ...(desktop ? { width: 260 } : {}),
});
const inputClass = () => `${desktop ? "text-sm" : "text-base"} placeholder:text-[${ui().faint}] text-[${ui().text}]`;
const inputStyle = { flex: 1, minWidth: 40, height: size.control - 2 };

/** The trigger button: a chevron that turns while the list is open. */
function Chevron(props: { open: boolean }) {
  return (
    <View
      class="transition-transform duration-150 motion-reduce:transition-none"
      style={{ width: 24, height: 24, alignItems: "center", justifyContent: "center", transform: [{ rotate: props.open ? "270deg" : "90deg" }] }}
    >
      <Text style={{ fontSize: 18, color: ui().muted }}>›</Text>
    </View>
  );
}

/**
 * A row's look, on the row itself (`itemClass`): the accent tint while
 * highlighted (the keys or the pointer, as the menus' rows), dimmed while
 * disabled. The states are the row's classes (`group`), so the list draws a
 * row's content once per item and the highlight moves without drawing.
 */
const rowClass =
  "group rounded-lg border border-transparent transition-colors duration-100 motion-reduce:transition-none " +
  "focus:border-brand/30 focus:bg-brand-tint active:opacity-70 disabled:opacity-40";

/** A row's content: the title, the check in its own lane (`group-selected:`). */
function Row(props: { item: SelectEntry }) {
  return (
    // The row's 1-point border on each side.
    <View style={{ height: size.rowSm - 2, flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 7 }}>
      <Text style={{ numberOfLines: 1, flex: 1, fontSize: controlText, color: ui().text }}>{props.item.title}</Text>
      <View class="opacity-0 group-selected:opacity-100" style={{ width: 16 }}>
        <CheckMark size={16} tint={ui().accent} />
      </View>
    </View>
  );
}

function Header(props: { group: SelectGroup }) {
  return (
    <Text style={{ fontSize: 12, fontWeight: "500", color: ui().muted, paddingHorizontal: 9, paddingTop: 8, paddingBottom: 4 }}>
      {props.group.title}
    </Text>
  );
}

/** A label over its control, a quieter line under the label. */
function Field(props: { label: string; detail: string; children: JSX.Element }) {
  return (
    <View style={{ gap: 8 }}>
      <View style={{ 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>
      {props.children}
    </View>
  );
}

function App() {
  const labels = signal<string[]>(["bug", "ios"]);
  const assignee = signal<string[]>([]);
  const labelsOpen = signal(false);
  const assigneeOpen = signal(false);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 8));
  const opened = (open: (next: boolean) => void) => ({ open: next }: { open: boolean; reason: OpenChangeReason }) => open(next);
  const labelTitle = () => {
    const titles = titlesOf(LABELS, labels());
    return titles.length === 0 ? undefined : titles.length <= 2 ? titles.join(", ") : `${titles.length} labels`;
  };
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>combobox</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Pick several labels without the list closing; Cmd+Return confirms" : "Tap labels to toggle them; tap outside to finish"}
      </Text>

      <Split
        gap={14}
        inspector={
          <>
            <Panel title="value">
              <Text style={{ fontSize: 13, color: ui().text }}>{`labels [${labels().join(", ")}]`}</Text>
              <Text style={{ fontSize: 13, color: ui().text }}>{`assignee [${assignee().join(", ")}]`}</Text>
            </Panel>
            <Log title="onChange · onConfirm" lines={events()} minHeight={120} />
          </>
        }
      >
        <View style={{ ...ui().card, borderRadius: 16, padding: 16, gap: 20 }}>
          <Field label="Labels" detail="Several at once, with search and sections">
            <Combobox
              items={LABELS}
              multiple
              value={labels()}
              onChange={({ value }) => {
                labels.set(value);
                log(`Labels → ${titlesOf(LABELS, value).join(", ") || "none"}`);
              }}
              onConfirm={({ value }) => log(`Confirmed ${value.length} labels`)}
              onOpenChange={opened((next) => labelsOpen.set(next))}
              accessibilityLabel="Labels"
              style={fieldStyle(labelsOpen())}
              placeholder={labelTitle() ?? "Add labels"}
              inputClass={inputClass()}
              inputStyle={inputStyle}
              triggerButton={<Chevron open={labelsOpen()} />}
              empty={<Text style={{ paddingVertical: 20, textAlign: "center", fontSize: 13, color: ui().muted }}>No label matches</Text>}
              offset={4}
              margin={8}
              popupStyle={surface()}
              backdropStyle={backdrop}
              itemClass={rowClass}
              cleanable
              clearButtonAccessibilityLabel="Clear the labels"
              clearButtonClass="items-center justify-center rounded-full enabled:hover:opacity-70"
              clearButtonStyle={{ width: 20, height: 20 }}
              clearButton={<Icon name="close" size={14} tint={ui().muted} />}
              footerStyle={{ borderTopWidth: 1, borderColor: ui().border, marginTop: 4, paddingTop: 4 }}
              footer={
                <View style={{ paddingHorizontal: 8, paddingVertical: 6 }}>
                  <Text style={{ fontSize: 12, color: ui().muted }}>{`${labels().length} selected`}</Text>
                </View>
              }
              renderItem={(item) => <Row item={item} />}
              renderGroup={(group) => <Header group={group} />}
            />
          </Field>
          <Field label="Assignee" detail="One person: a choice replaces the last">
            <Combobox
              items={PEOPLE}
              value={assignee()}
              onChange={({ value }) => {
                assignee.set(value);
                log(`Assignee → ${titlesOf(PEOPLE, value)[0] ?? "none"}`);
              }}
              onOpenChange={opened((next) => assigneeOpen.set(next))}
              accessibilityLabel="Assignee"
              offset={4}
              margin={8}
              popupStyle={surface()}
              backdropStyle={backdrop}
              itemClass={rowClass}
              style={fieldStyle(assigneeOpen())}
              placeholder="Assign someone"
              inputClass={inputClass()}
              inputStyle={inputStyle}
              triggerButton={<Chevron open={assigneeOpen()} />}
              renderItem={(item) => <Row item={item} />}
            />
          </Field>
        </View>
      </Split>
    </View>
  );
}

mount(App);
  • على أجهزة سطح المكتب ومتصفحات سطح المكتب، نافذة منبثقة تحت الحقل، بعرض الحقل (ويمكن لعرض popupStyle / popupClass في التطبيق أن يحدد عرضًا آخر) وبارتفاع صفوفها حتى حدّ المساحة المتاحة بجانبها.
  • على الهواتف والأجهزة اللوحية، ورقة سفلية (sheet) في أسفل الشاشة فوق خلفية معتمة (backdrop).

يملك الـ Combobox السلوك: القيمة (للتطبيق أو الخاصة به)، وحالة الفتح، ولوحة المفاتيح، والبحث، والأقسام، والصف المميَّز، وما يسمعه قارئو الشاشة. مع multiple، يبدّل الاختيار العنصر في القيمة وتبقى القائمة مفتوحة، فيمكن اختيار عدة عناصر دفعة واحدة. أما المظهر فللتطبيق: فهو ينسّق الحقل والنافذة المنبثقة والقائمة والصفوف وعناوين الأقسام وحقل الإدخال والتذييل، أو يرسم الصفوف بنفسه.

use nsh_base::{SelectEntry, SelectItem, combobox, theme};
use nsh_core::prelude::*;

const TAGS: [(&str, &str); 5] = [
    ("bug", "Bug"),
    ("design", "Design"),
    ("docs", "Docs"),
    ("perf", "Performance"),
    ("security", "Security"),
];

fn tag_picker() -> impl IntoElement {
    let tags = signal(Vec::<SharedString>::new());
    let title = move || match tags.get().len() {
        0 => "Add tags".to_string(),
        n => format!("{n} tags"),
    };
    combobox()
        .items(
            TAGS.iter()
                .map(|&(value, title)| SelectItem::new(value, title))
                .collect::<Vec<_>>(),
        )
        .multiple(true)
        .placeholder(title)
        .value(move || Some(tags.get()))
        .on_change(move |next: &Vec<SharedString>| tags.set(next.clone()))
        .accessibility_label("Tags")
        .offset(px(4.0))
        .popup_style(|s| {
            s.p_1()
                .rounded_lg()
                .border_1()
                .border_color(move || theme().border)
                .bg(move || theme().popover)
                .shadow_md()
        })
        .h_9()
        .w_48()
        .px_3()
        .flex_row()
        .items_center()
        .rounded_md()
        .border_1()
        .border_color(move || theme().input)
        .input_style(|s| s.flex_1().text_sm())
        .trigger_button(text("⌄"))
        .item_class("rounded-md focus:bg-accent selected:font-medium disabled:opacity-50")
        .render_item(|item: &SelectEntry| view().px_2().py_1p5().child(text(item.title.clone()).text_sm()))
}

Combobox يأتي من nsh/base، حزمة nsh-base في Rust. في Rust، combobox() ينشئ واحدًا بدالة ضبط (setter) لكل خاصية (.items(..)، .value(..)، .multiple(..)) ومستمع لكل حدث (.on_change(..)). عناصره هي عناصر Select: SelectItem::new(value, title)، مع .disabled(..) و.group(..)، في أقسام كما في Select (انظر العناصر).

#القيمة

القيمة مصفوفة بقيم العناصر المحددة. مع value يملكها التطبيق: يعرض الـ Combobox تلك العناصر محددة ويبلغ عن كل تغيير عبر onChange({ value })؛ ويضبط التطبيق value ليتبعه، أو يحتفظ بقيمته الخاصة. وإن حُذف ذلك، احتفظ الـ Combobox بقيمته الخاصة، ابتداءً من defaultValue.

  • مع multiple، تبدّل الضغطة أو Return أو Space العنصر المميَّز: تدخله في القيمة إن كان خارجها، وتخرجه إن كان فيها. وتبقى القائمة مفتوحة.
  • بدونه، يستبدل الاختيار القيمة بقيمة ذلك العنصر ويغلق القائمة؛ واختيار العنصر المحدد مرة أخرى يغلقها فقط.

onConfirm({ value }) يخبر بالقيمة عند إغلاق القائمة: مرة واحدة لكل فتح، مهما كانت طريقة الإغلاق (اختيار، Escape، ضغطة خارج القائمة، زر التشغيل)، مع القيمة كما تركها المستخدم. و Cmd+Return على Mac (Ctrl+Return في غيره) يغلق القائمة ويؤكد.

cleanable يضيف زر مسح أثناء وجود عنصر محدد: ضغطة عليه تفرّغ القيمة (onChange({ value: [] })). يأتي الزر بعد حقل الإدخال وقبل زر التشغيل؛ والضغط عليه لا يفتح القائمة أبدًا. ويملؤه التطبيق، كما في زر المسح في Select.

#حقل الإدخال

زر التشغيل حقل نصي في صف الـ Combobox (class وstyle: حدّ الحقل وتعبئته)، يسبقه prefix الخاص بالتطبيق (العناصر المختارة كرقائق chips، أو أيقونة) ويليه زر المسح وtriggerButton الخاص بالتطبيق (سهم chevron) وsuffix.

  • الكتابة تفتح القائمة وتصفّي العناصر بحسب العنوان دون مراعاة حالة الأحرف؛ ويظهر empty ما دام لا شيء يطابق. ويفتحها أيضًا Down وUp، وضغطة على زر التشغيل تفتحها وتغلقها، وعلى شاشات اللمس تفتحها نقرة على حقل الإدخال.
  • مع قيمة واحدة، يعرض حقل الإدخال عنوان العنصر المختار (placeholder ما دام لا يوجد). ويكون محددًا عند حصوله على التركيز، فما يُكتب يحلّ محله.
  • مع multiple، يعرض حقل الإدخال الاستعلام؛ ويعرض التطبيق العناصر المختارة، في prefix أو بجوار الـ Combobox.

الاستعلام خاص بالـ Combobox، يبدأ كل فتح من defaultQuery (وإلا فارغًا)، أو خاص بالتطبيق مع query: onQueryChange({ query }) يبلغ بما كُتب، ويضبط التطبيق query ليتبعه، أو ليصفّي على طريقته، أو ليقترح نقطة بداية.

#التذييل

footer محتوى تحت الصفوف، في جزء خاص به (footerClass، footerStyle): إجراء («إدارة الوسوم»)، أو عدّاد، أو تلميح. يبقى في مكانه بينما تتمرر الصفوف.

#الخصائص

الخاصيةRustالنوعالافتراضيالوصف
items.items(..)SelectItem[][]العناصر، بالترتيب.
multiple.multiple(..)booleanfalseالاختيار يبدّل العنصر في القيمة وتبقى القائمة مفتوحة.
value.value(..)string[]لا شيءقيم العناصر المحددة، يملكها التطبيق.
defaultValue.default_value(..)string[][]القيمة الأولية، بدون value.
open، defaultOpen.open(..)، .default_open(..)booleanfalseحالة الفتح، للتطبيق أو الخاصة بالـ Combobox.
placeholder.placeholder(..)stringلا شيءالنص البديل لحقل الإدخال، ما دام فارغًا.
query.query(..)stringلا شيءالاستعلام، يملكه التطبيق.
defaultQuery.default_query(..)stringلا شيءالاستعلام الذي يبدأ منه كل فتح، بدون query.
cleanable.cleanable(..)booleanfalseزر مسح أثناء وجود عنصر محدد؛ والضغط عليه يفرّغ القيمة.
disabled.disabled(..)booleanfalseيبقى مغلقًا ولا يقبل ضغطات ولا تركيز لوحة المفاتيح.
accessibilityLabel.accessibility_label(..)stringلا شيءموضوع الاختيار، لقارئي الشاشة.
placement، align، offset، margin.placement(..), …مثل Select"bottom"، "start"، 0، 0أجهزة سطح المكتب: موضع النافذة المنبثقة بالنسبة إلى الحقل.
popupClass، popupStyle.popup_class(..)، .popup_style(..)string، StyleProp<ViewStyle>لا شيءالنافذة المنبثقة (أجهزة سطح المكتب) أو الورقة السفلية (شاشات اللمس).
backdropClass، backdropStyle.backdrop_class(..)، .backdrop_style(..)string، StyleProp<ViewStyle>لا شيءالخلفية المعتمة خلف الورقة السفلية.
listClass، listStyle.list_class(..)، .list_style(..)string، StyleProp<ViewStyle>لا شيءالقائمة، عرض التمرير للصفوف.
itemClass، itemStyle.item_class(..)، .item_style(..)string، StyleProp<TextStyle>لا شيءصف كل عنصر: focus: أثناء التمييز، selected: أثناء التحديد، disabled: حين لا يمكن تحديده.
groupClass، groupStyle.group_class(..)، .group_style(..)string، StyleProp<ViewStyle>لا شيءعنوان كل قسم.
inputClass، inputStyle.input_class(..)، .input_style(..)string، StyleProp<InputStyle>لا شيءحقل الإدخال: focus: أثناء التحرير، disabled: مع الـ Combobox.
footerClass، footerStyle.footer_class(..)، .footer_style(..)string، StyleProp<ViewStyle>لا شيءالتذييل، تحت الصفوف.
clearButtonClass، clearButtonStyle.clear_button_class(..)، .clear_button_style(..)string، StyleProp<ViewStyle>لا شيءزر المسح (cleanable).
clearButton.clear_button(..)JSX.Elementلا شيءمحتوى زر المسح.
clearButtonAccessibilityLabel.clear_button_accessibility_label(..)stringلا شيءالاسم الذي يسمّي به قارئو الشاشة زر المسح.
triggerButton.trigger_button(..)JSX.Elementلا شيءزر بعد حقل الإدخال (سهم chevron): الضغط عليه يفتح القائمة ويغلقها.
prefix.prefix(..)JSX.Elementلا شيءمحتوى قبل حقل الإدخال: رقائق chips، أيقونة.
suffix.suffix(..)JSX.Elementلا شيءمحتوى بعد زر التشغيل.
empty.empty(..)JSX.Elementلا شيءما تعرضه القائمة ما دام لا يظهر فيها عنصر.
footer.footer(..)JSX.Elementلا شيءما يظهر تحت الصفوف.
renderItem.render_item(..)(item: SelectEntry) => JSX.Elementلا شيءصف العنصر، يرسمه التطبيق مما هو عليه العنصر لا من حالته: يتبع المحتوى group-focus: وgroup-selected: وgroup-disabled: الخاصة بالصف (group في itemClass)، فلا يؤدي انتقال التمييز أو التبديل إلى إعادة رسم شيء؛ وإلا فعنوان العنصر.
renderGroup.render_group(..)(group: SelectGroup) => JSX.Elementلا شيءعنوان القسم، يرسمه التطبيق؛ وإلا فعنوانه.
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءالحقل: صف البادئة وحقل الإدخال والأزرار واللاحقة؛ وتأخذ القائمة عرضه.

#الأحداث

الحدثRustالوصف
onChange.on_change(|value| ..)تغيّرت القيمة (اختيار، تبديل، زر المسح): { value }، قيم العناصر المحددة. اضبط value لتتبعه.
onConfirm.on_confirm(|value| ..)أُغلقت القائمة: { value } كما هي حينئذ، مرة واحدة لكل فتح.
onQueryChange.on_query_change(|query| ..)كتب المستخدم في حقل الإدخال: { query }. اضبط query لتتبعه.
onOpenChange.on_open_change(|e| ..)فُتحت القائمة أو أُغلقت، مع السبب ({ open, reason }). اضبط open لتتبعه.
onOpen، onClose.on_open(..)، .on_close(..)ظهرت القائمة، أو زالت.
onCancel.on_cancel(..)أغلق المستخدم القائمة دون اختيار يغلقها (Escape أو الرجوع، ضغطة خارجها أو على الخلفية المعتمة).

تأتي بهذا الترتيب: onChange عند الاختيار، أو onCancel عند الإغلاق دون اختيار، ثم onOpenChange، ثم onConfirm عند إغلاق القائمة، ثم onOpen أو onClose بعد ظهور القائمة أو زوالها.

#الأجزاء

الجزءالموضعالتنسيق بواسطة
الحقلعرض الـ Combobox، صفclass، style
البادئةالحقل، أولًاعنصر خاص بالتطبيق (prefix)
حقل الإدخالالحقل، بعد البادئةinputClass، inputStyle، placeholder
زر المسحالحقل، بعد حقل الإدخال (cleanable، أثناء وجود عنصر محدد)clearButtonClass، clearButtonStyle؛ ومحتواه clearButton
زر التشغيلالحقل، بعد زر المسحعنصر خاص بالتطبيق (triggerButton)
اللاحقةالحقل، أخيرًاعنصر خاص بالتطبيق (suffix)
النافذة المنبثقة أو الورقة السفليةفوق كل شيء، تحت الحقل (أجهزة سطح المكتب) أو في الأسفل (شاشات اللمس)popupClass، popupStyle
الخلفية المعتمةخلف الورقة السفلية (شاشات اللمس)backdropClass، backdropStyle
القائمةالنافذة المنبثقةlistClass، listStyle
عنوان القسمالقائمة، قبل عناصرهgroupClass، groupStyle، renderGroup
صف العنصرالقائمةitemClass، itemStyle، renderItem (group-focus:، group-selected:، group-disabled: في داخله، تحت group في itemClass)
الحالة الفارغةالنافذة المنبثقة، ما دام لا يظهر عنصرعنصر خاص بها (empty)
التذييلالنافذة المنبثقة، أخيرًاfooterClass، footerStyle؛ ومحتواه footer

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

مفاتيح الـ Combobox هي مفاتيح Select (انظر لوحة المفاتيح) ما دام حقل الإدخال محتفظًا بالتركيز، مع هذه الفروق: تُكتب الأحرف والأرقام في حقل الإدخال فتصفّي القائمة بدل البحث بالكتابة (typeahead)؛ و Down و Up يفتحان القائمة المغلقة؛ و Return يبدّل العنصر المميَّز مع multiple ويُبقي القائمة مفتوحة؛ و Cmd+Return على Mac (Ctrl+Return في غيره) يغلقها ويؤكد. عند إغلاق القائمة على أجهزة سطح المكتب، يحتفظ حقل الإدخال بالتركيز.

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

  • حقل الإدخال هو combobox بالنسبة إلى قارئي الشاشة: تسميته accessibilityLabel، وقيمته عناوين العناصر المحددة مفصولة بفواصل، ويخبر بما إذا كانت القائمة موسّعة. وdisabled يعتّمه.
  • القائمة listbox وكل صف option، يُسمّى بعنوانه؛ والخيارات المحددة تُعلَن محددة، والمعطّل منها معتَّم. وعناوين الأقسام عناوين (headers).
  • زر المسح زر يُسمّى بـ clearButtonAccessibilityLabel؛ وليس محطة توقف في Tab. أما زر التشغيل فمخفي عن قارئي الشاشة، لأن حقل الإدخال يخبر بما إذا كانت القائمة مفتوحة، وهو كذلك ليس محطة توقف في Tab.

انظر إمكانية الوصول للتسميات والأدوار والإعلانات عمومًا.