العناصر
Toolbar
شريط الأدوات هو شريط من عناصر التحكم: أدوات التنسيق في محرر النصوص، وأزرار التحكم في مشغّل الوسائط، ولوحة أدوات عمودية.
يتولى Toolbar أمر لوحة المفاتيح ودلالات الشريط: تعلن قارئات الشاشة عن شريط الأدوات (باسمه المحدد بواسطة
accessibilityLabel)، وتصل إليه لوحة المفاتيح بوقفة واحدة وتتنقل بين عناصره بمفاتيح الأسهم الخاصة بـ
axis. عناصره هي Button وToggle وLink الموجودة بداخله، على أي عمق. أما ToolbarGroup فهي سلسلة مسماة من
العناصر المترابطة (التراجع والإعادة، المحاذاة)، وToolbarSeparator خط يرسمه التطبيق بين هذه السلاسل. المظهر من شأن
التطبيق: ليس لشريط الأدوات ألوان أو حشوة أو فراغات خاصة به، ويحتفظ كل عنصر تحكم بحالته الخاصة.
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")),
)
}import { mount, signal, Text } from "nsh";
import { Button, Toggle, Toolbar, ToolbarGroup, ToolbarSeparator } from "nsh/base";
function App() {
const bold = signal(false);
return (
<Toolbar accessibilityLabel="Formatting" class="items-center gap-1 self-start rounded-lg bg-muted p-1">
<ToolbarGroup accessibilityLabel="History">
<Button accessibilityLabel="Undo" class="size-8 items-center justify-center rounded-md hover:bg-accent">
<Text>↶</Text>
</Button>
<Button accessibilityLabel="Redo" class="size-8 items-center justify-center rounded-md hover:bg-accent">
<Text>↷</Text>
</Button>
</ToolbarGroup>
<ToolbarSeparator class="h-5 w-px bg-border" />
<Toggle
pressed={bold()}
accessibilityLabel="Bold"
onChange={({ pressed }) => bold.set(pressed)}
class="size-8 items-center justify-center rounded-md aria-pressed:bg-accent"
>
<Text>B</Text>
</Toggle>
</Toolbar>
);
}
mount(App);تأتي 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(..) | boolean | false | كل عنصر في شريط الأدوات معطَّل، مهما كانت قيمة disabled الخاصة به: لا ضغطات، ولا وقفة لتركيز لوحة المفاتيح، ويظهر باهتًا لقارئات الشاشة. |
accessibilityLabel | .accessibility_label(..) | string | لا شيء | الاسم الذي تنطق به قارئات الشاشة لشريط الأدوات (“Formatting”)، للصفحة التي تحتوي على أكثر من شريط. |
children | .child(..) | JSX.Element | لا شيء | العناصر ومجموعاتها وفواصلها، وأي شيء آخر يرتبه التطبيق بينها (عنوان، أو فراغ مرن، أو حقل إدخال). |
class، style | دوال التنسيق | string، StyleProp<ViewStyle> | لا شيء | تخطيط الشريط ومظهره: المحاذاة والفراغات والحشوة وسطحه. |
#ToolbarGroup
| الخاصية | Rust | النوع | الافتراضي | الوصف |
|---|---|---|---|---|
disabled | .disabled(..) | boolean | false | كل عنصر في المجموعة معطَّل، مهما كانت قيمة 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 بين عناصره (لوحة المفاتيح).
- ترسم كل منصة ما ينسّقه التطبيق: الشريط ومجموعاته وفواصله وعناصره.