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

العناصر

Toolbar

شريط الأدوات هو شريط من عناصر التحكم: أدوات التنسيق في محرر النصوص، وأزرار التحكم في مشغّل الوسائط، ولوحة أدوات عمودية. يتولى Toolbar أمر لوحة المفاتيح ودلالات الشريط: تعلن قارئات الشاشة عن شريط الأدوات (باسمه المحدد بواسطة accessibilityLabel)، وتصل إليه لوحة المفاتيح بوقفة واحدة وتتنقل بين عناصره بمفاتيح الأسهم الخاصة بـ axis. عناصره هي Button وToggle وLink الموجودة بداخله، على أي عمق. أما ToolbarGroup فهي سلسلة مسماة من العناصر المترابطة (التراجع والإعادة، المحاذاة)، وToolbarSeparator خط يرسمه التطبيق بين هذه السلاسل. المظهر من شأن التطبيق: ليس لشريط الأدوات ألوان أو حشوة أو فراغات خاصة به، ويحتفظ كل عنصر تحكم بحالته الخاصة.

el-toolbar.tsx
el-toolbar.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Button, Toggle, ToggleGroup, Toolbar, ToolbarGroup, ToolbarSeparator } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, Split, ui } from "./palette";

type Mark = "bold" | "italic" | "underline";
type Align = "left" | "center" | "right";
type Tool = "select" | "pen" | "shape" | "text";

const marks: { id: Mark; label: string; glyph: string }[] = [
  { id: "bold", label: "Bold", glyph: "B" },
  { id: "italic", label: "Italic", glyph: "I" },
  { id: "underline", label: "Underline", glyph: "U" },
];
const aligns: { id: Align; label: string; glyph: string }[] = [
  { id: "left", label: "Align left", glyph: "⇤" },
  { id: "center", label: "Align center", glyph: "↔" },
  { id: "right", label: "Align right", glyph: "⇥" },
];
const tools: { id: Tool; label: string; glyph: string }[] = [
  { id: "select", label: "Select", glyph: "↖" },
  { id: "pen", label: "Pen", glyph: "✎" },
  { id: "shape", label: "Rectangle", glyph: "▢" },
  { id: "text", label: "Text", glyph: "T" },
];

const side = desktop ? 28 : 34;

function App() {
  const on = signal<Mark[]>(["bold"]);
  const align = signal<Align>("left");
  const tool = signal<Tool>("select");
  const history = signal<string[]>([]);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 3));
  const edit = (what: string) => {
    history.set([...history(), what]);
    log(what);
  };
  const has = (m: Mark) => on().includes(m);
  // A tool: bare while off, raised while pressed, a hover fill, the focus ring.
  const look = (pressed: boolean) => ({
    style: { minWidth: side, height: side, paddingHorizontal: 6, borderRadius: 7, alignItems: "center", justifyContent: "center" } as const,
    class: "transition-colors active:opacity-70 disabled:opacity-35 motion-reduce:transition-none",
    pressedStyle: ui().raised,
    hoverStyle: pressed ? undefined : hoverFill(),
    focusStyle: focusRing(),
  });
  const glyph = (text: string, active: boolean) => <Text style={{ fontSize: 14, fontWeight: "600", color: active ? ui().text : ui().muted }}>{text}</Text>;
  const rule = (vertical = false) => (
    <ToolbarSeparator style={vertical ? { height: 1, alignSelf: "stretch", marginVertical: 4, backgroundColor: ui().line } : { width: 1, height: 16, marginHorizontal: 4, backgroundColor: ui().line }} />
  );
  const bar = { alignItems: "center", gap: 2, padding: 3, borderRadius: 10, ...ui().inset } as const;
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>toolbar</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Tab into a toolbar, then move with the arrow keys; Home and End jump to its ends" : "Tap the tools; a screen reader hears each bar as a toolbar"}
      </Text>

      <Split gap={14} inspector={<Log title={`${history().length} edits · ${on().join(", ") || "no marks"}, ${align()} · ${tool()}`} lines={events()} minHeight={76} />}>
        {/* A formatting bar: history, a toggle group of marks (a group of the toolbar), the alignment. */}
        <Toolbar accessibilityLabel="Formatting" style={{ ...bar, alignSelf: "flex-start" }}>
          <ToolbarGroup accessibilityLabel="History">
            <Button accessibilityLabel="Undo" disabled={history().length === 0} onPress={() => {
              history.set(history().slice(0, -1));
              log("Undo");
            }} {...look(false)}>
              {glyph("↶", true)}
            </Button>
          </ToolbarGroup>
          {rule()}
          <ToggleGroup style={{ flexDirection: "row", gap: 2 }}>
            <Each of={marks}>
              {(m) => (
                <Toggle
                  pressed={has(m().id)}
                  accessibilityLabel={m().label}
                  onChange={({ pressed }) => {
                    on.set(pressed ? [...on(), m().id] : on().filter((x) => x !== m().id));
                    edit(`${m().label}: ${pressed}`);
                  }}
                  {...look(has(m().id))}
                >
                  {glyph(m().glyph, has(m().id))}
                </Toggle>
              )}
            </Each>
          </ToggleGroup>
          {rule()}
          <ToolbarGroup accessibilityLabel="Alignment" style={{ gap: 2 }}>
            <Each of={aligns}>
              {(a) => (
                <Toggle
                  pressed={align() === a().id}
                  accessibilityLabel={a().label}
                  onChange={({ pressed }) => {
                    // Exactly one: the pressed alignment stays pressed.
                    if (pressed) align.set(a().id);
                    edit(`${a().label}`);
                  }}
                  {...look(align() === a().id)}
                >
                  {glyph(a().glyph, align() === a().id)}
                </Toggle>
              )}
            </Each>
          </ToolbarGroup>
        </Toolbar>

        <View style={{ flexDirection: "row", gap: 14, alignItems: "stretch" }}>
          {/* A vertical palette: Up and Down move along it. */}
          <Toolbar axis="vertical" accessibilityLabel="Tools" style={{ ...bar, alignSelf: "flex-start" }}>
            <Each of={tools}>
              {(t) => (
                <Toggle
                  pressed={tool() === t().id}
                  accessibilityLabel={t().label}
                  onChange={({ pressed }) => {
                    if (!pressed) return;
                    tool.set(t().id);
                    log(`Tool: ${t().label}`);
                  }}
                  {...look(tool() === t().id)}
                >
                  {glyph(t().glyph, tool() === t().id)}
                </Toggle>
              )}
            </Each>
            {rule(true)}
            <Button accessibilityLabel="Clear canvas" onPress={() => log("Clear canvas")} {...look(false)}>
              {glyph("⌫", false)}
            </Button>
          </Toolbar>
          <View style={{ flex: 1, ...ui().card, borderRadius: 14, padding: 14, gap: 6 }}>
            <Text
              style={{
                fontSize: 15,
                lineHeight: 22,
                color: ui().text,
                fontWeight: has("bold") ? "700" : "400",
                fontStyle: has("italic") ? "italic" : "normal",
                textDecorationLine: has("underline") ? "underline" : "none",
                textAlign: align(),
              }}
            >
              One stop of Tab per toolbar; its arrows move between the tools.
            </Text>
            <Text style={{ fontSize: 13, color: ui().muted }}>{`Tool: ${tools.find((t) => t.id === tool())!.label}`}</Text>
          </View>
        </View>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{button, theme, toggle, toolbar, toolbar_group, toolbar_separator};
use nsh_core::prelude::*;

fn formatting() -> impl IntoElement {
    let bold = signal(false);
    toolbar()
        .accessibility_label("Formatting")
        .items_center()
        .gap_1()
        .child(
            toolbar_group()
                .accessibility_label("History")
                .child(button().accessibility_label("Undo").size_8().child(text("↶")))
                .child(button().accessibility_label("Redo").size_8().child(text("↷"))),
        )
        .child(toolbar_separator().w(px(1.0)).h_5().bg(move || theme().border))
        .child(
            toggle()
                .pressed(bold)
                .accessibility_label("Bold")
                .on_change(move |pressed: &bool| bold.set(*pressed))
                .size_8()
                .items_center()
                .justify_center()
                .pressed_style(|s| s.bg(move || theme().accent))
                .child(text("B")),
        )
}

تأتي Toolbar وToolbarGroup وToolbarSeparator من nsh/base، وهي الحزمة nsh-base في Rust. لا يحتفظ شريط الأدوات بأي قيمة: يُبلَّغ المفتاح التبديلي بداخله بما إذا كان مضغوطًا ويُرسل onChange، أما الزر فيُرسل onPress، ويحتفظ التطبيق بالحالة. في Rust تنشئها toolbar() وtoolbar_group() و toolbar_separator()، مع دالة ضبط لكل خاصية (.axis(..)، .disabled(..))، ودوال التنسيق المطبوعة، و .child(..) لمحتواها.

#الخصائص

#Toolbar

الخاصيةRustالنوعالافتراضيالوصف
axis.axis(..)"horizontal" | "vertical""horizontal"الاتجاه الذي تمتد عليه عناصره: مفاتيح الأسهم التي تتنقل على امتداده (Left وRight، أو Up وDown)، والاتجاه الذي يرتب فيه أبناءه (صف أو عمود). تُقرأ عند إنشاء شريط الأدوات.
disabled.disabled(..)booleanfalseكل عنصر في شريط الأدوات معطَّل، مهما كانت قيمة disabled الخاصة به: لا ضغطات، ولا وقفة لتركيز لوحة المفاتيح، ويظهر باهتًا لقارئات الشاشة.
accessibilityLabel.accessibility_label(..)stringلا شيءالاسم الذي تنطق به قارئات الشاشة لشريط الأدوات (“Formatting”)، للصفحة التي تحتوي على أكثر من شريط.
children.child(..)JSX.Elementلا شيءالعناصر ومجموعاتها وفواصلها، وأي شيء آخر يرتبه التطبيق بينها (عنوان، أو فراغ مرن، أو حقل إدخال).
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءتخطيط الشريط ومظهره: المحاذاة والفراغات والحشوة وسطحه.

#ToolbarGroup

الخاصيةRustالنوعالافتراضيالوصف
disabled.disabled(..)booleanfalseكل عنصر في المجموعة معطَّل، مهما كانت قيمة disabled الخاصة به.
accessibilityLabel.accessibility_label(..)stringلا شيءالاسم الذي تنطق به قارئات الشاشة للمجموعة (“History”، “Alignment”).
children.child(..)JSX.Elementلا شيءعناصر المجموعة، وأي شيء يرتبه التطبيق بينها.
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءتخطيط المجموعة ومظهرها. تمتد على محور شريط الأدوات.

#ToolbarSeparator

الخاصيةRustالنوعالافتراضيالوصف
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءالخط: خط رفيع يعبر الصف (h-5 w-px bg-border) أو العمود (h-px self-stretch bg-border).

#العناصر

عناصر شريط الأدوات هي Button وToggle وLink الموجودة بداخله، وفي مجموعاته، وكذلك في ToggleGroup بداخله. ومشغِّل Menu أو Popover هو عنصر أيضًا: أي العرض الموجود في خانته trigger، والذي تمنحه النافذة المنبثقة ضغطاته وتركيزه وحالة expanded الخاصة به. أما الأداة التي يرسمها التطبيق بنفسه فهي Button بفئات التطبيق: لا مظهر خاصًا لـButton، فيبدو كما ينسّقه التطبيق ويكون عنصرًا (أما View مع onPress فيبقى وقفة مستقلة خارج مفاتيح الأسهم). وToggleGroup داخل شريط الأدوات هي إحدى مجموعاته: مفاتيحها التبديلية هي عناصر الشريط، وتسمع قارئات الشاشة مجموعة، وتظل disabled الخاصة بها تعطّل مفاتيحها التبديلية. أما عناصر التحكم الأخرى (Input وSelect وSlider) فتبقى وقفات مستقلة وتحتفظ بمفاتيح الأسهم الخاصة بها؛ ضعها في نهاية الشريط. وما تعرضه النافذة المنبثقة التي يفتحها عنصر (حقول النافذة المنبثقة وأزرارها، وعناصر القائمة، وروابط بطاقة التمرير) ليس جزءًا من شريط الأدوات. تمر مفاتيح الأسهم على العناصر بالترتيب الذي يرتبها به الشريط، مهما كان عمق كل منها، ويأخذ العنصر الذي يعرضه التطبيق لاحقًا مكانه بينها.

#الأجزاء

الجزءداخليُنسَّق بواسطةالحالات
الشريطToolbar: class، style
مجموعةالشريطToolbarGroup: class، style
فاصلالشريط، مجموعةToolbarSeparator: class، style
عنصرالشريط، مجموعةclass وstyle الخاصان بالعنصر نفسهالخاصة به: aria-pressed:، disabled:، hover:، focus:، active:

أظهر العنصر الذي عليه لوحة المفاتيح بأنماط focus:، وأظهر المفتاح التبديلي المضغوط عبر سطحه الخاص (انظر أدلة التصميم).

#عمودي

لوحة الأدوات عمود: axis="vertical" يرتب العناصر من الأعلى إلى الأسفل، ويتنقل Up وDown على امتدادها.

<Toolbar axis="vertical" accessibilityLabel="Tools" class="gap-1 self-start rounded-lg bg-muted p-1">
  <Each of={["select", "pen", "shape", "text"]}>
    {(t) => (
      <Toggle
        pressed={tool() === t()}
        accessibilityLabel={t()}
        onChange={({ pressed }) => pressed && tool.set(t())}
        class="size-8 items-center justify-center rounded-md aria-pressed:bg-accent"
      >
        <Text>{t()[0].toUpperCase()}</Text>
      </Toggle>
    )}
  </Each>
</Toolbar>

#معطَّل

disabled على شريط الأدوات يعطّل كل عنصر فيه، أي شريط يقفله التطبيق؛ وعلى مجموعة، يعطّل عناصر المجموعة. العنصر المعطَّل لا يقبل ضغطات، وتتجاوزه مفاتيح الأسهم، وتعلن عنه قارئات الشاشة على أنه باهت:

<Toolbar disabled={readonly()} accessibilityLabel="Review" class="flex-row gap-1">
  <Button onPress={approve} class="h-8 px-3 disabled:opacity-50">
    <Text>Approve</Text>
  </Button>
</Toolbar>

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

  • لـToolbar الدور "toolbar"، ويُسمّى بواسطة accessibilityLabel؛ وتبقى عناصره عناصر مستقلة. على iOS يكون شريط الأدوات مجموعة دلالية يدخلها VoiceOver.
  • لـToolbarGroup الدور "group"، ويُسمّى بواسطة accessibilityLabel، فتُقرأ سلسلة العناصر وحدة واحدة. وToggleGroup داخل شريط الأدوات مجموعة أيضًا.
  • ToolbarSeparator ليس عنصرًا بالنسبة لقارئات الشاشة.
  • يُعلَن عن disabled بالطريقة التي تعلن بها المنصة عنه (VoiceOver وTalkBack وUI Automation وشجرة إمكانية الوصول في المتصفح).

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

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

على macOS والويب، حيث يصل Tab إلى العروض التي تقبل الضغطات، يتبع Toolbar نمط شريط الأدوات في WAI-ARIA:

المفتاحالوظيفة
Tab، Shift+Tabيدخل شريط الأدوات عند العنصر الذي غادره التركيز آخر مرة (وفي البداية، عند أول عنصر غير معطَّل)، ويغادره بخطوة واحدة.
Right، Left (axis="horizontal")ينقل التركيز إلى العنصر التالي أو السابق، من الأخير إلى الأول والعكس.
Down، Up (axis="vertical")ينقل التركيز إلى العنصر التالي أو السابق، من الأخير إلى الأول والعكس.
Home، Endينقل التركيز إلى العنصر الأول أو الأخير.
Space، Returnيضغط العنصر المركَّز عليه.

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

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

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