العناصر
Select
يختار Select قيمة واحدة من قائمة عناصر: إعداد، أو عامل تصفية، أو بلد، أو منطقة زمنية. يفتح المشغِّل (trigger) الخاص بالتطبيق القائمة، وتُعرض كما يتوقع كل جهاز:
import { type JSX, mount, signal, Text, View } from "nsh";
import { Button, type OpenChangeReason, Select, type SelectEntry, type SelectGroup, Switch } from "nsh/base";
import { button, buttonText, CheckMark, desktop, focusRing, hoverFill, Icon, Log, Panel, size, Split, ui } from "./palette";
// Two selects in a settings card: a plain one (typeahead jumps by letter)
// and a searchable one with sections, a disabled item and an empty state.
// The trigger is the shared button look (hover, pressed, focus ring,
// disabled) with the chosen title 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. The highlighted row (the keys or the pointer) is lit with
// the accent tint, the chosen one is bolder with a check in its own lane.
const LANGUAGES = [
{ value: "en", title: "English" },
{ value: "de", title: "Deutsch" },
{ value: "es", title: "Español" },
{ value: "fr", title: "Français" },
{ value: "it", title: "Italiano" },
{ value: "nl", title: "Nederlands" },
{ value: "pl", title: "Polski" },
{ value: "pt", title: "Português" },
{ value: "sv", title: "Svenska" },
{ value: "tr", title: "Türkçe" },
{ value: "ja", title: "日本語" },
{ value: "ko", title: "한국어" },
{ value: "zh", title: "中文" },
];
const REGIONS = [
{ value: "us-east-1", title: "US East (Virginia)", group: "Americas" },
{ value: "us-west-2", title: "US West (Oregon)", group: "Americas" },
{ value: "ca-central-1", title: "Canada (Montréal)", group: "Americas" },
{ value: "sa-east-1", title: "South America (São Paulo)", group: "Americas" },
{ value: "eu-west-1", title: "Europe (Ireland)", group: "Europe" },
{ value: "eu-central-1", title: "Europe (Frankfurt)", group: "Europe" },
{ value: "eu-north-1", title: "Europe (Stockholm)", group: "Europe" },
{ value: "ap-northeast-1", title: "Asia Pacific (Tokyo)", group: "Asia Pacific" },
{ value: "ap-northeast-2", title: "Asia Pacific (Seoul)", group: "Asia Pacific", disabled: true },
{ value: "ap-southeast-1", title: "Asia Pacific (Singapore)", group: "Asia Pacific" },
{ value: "ap-east-1", title: "Asia Pacific (Hong Kong)", group: "Asia Pacific" },
{ value: "ap-southeast-2", title: "Asia Pacific (Sydney)", group: "Asia Pacific" },
];
const titleOf = (items: { value: string; title: string }[], value: string | null) => items.find((i) => i.value === value)?.title;
/** 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 trigger: the shared button's look, the chosen title at the start and a chevron in a lane as wide as it. */
function Trigger(props: { title?: string; placeholder: string; open: boolean; disabled?: boolean; clearable?: boolean }) {
const look = button("secondary", {
style: () => ({
flexDirection: "row",
justifyContent: "space-between",
gap: 8,
...(desktop ? { width: 260 } : {}),
// Open: the border stays lit while the list is up; disabled, dimmed.
...(props.open ? { borderColor: ui().faint } : {}),
...(props.disabled ? { opacity: 0.4 } : {}),
}),
});
return (
// No hover while disabled.
<View {...look} hoverStyle={props.disabled ? undefined : look.hoverStyle}>
<Text style={{ numberOfLines: 1, flex: 1, fontSize: controlText, color: props.title ? ui().text : ui().muted }}>
{props.title ?? props.placeholder}
</Text>
{/* With a clear button over the end (`cleanable`), an empty lane as wide as the button in place of the chevron. */}
<View
class="transition-transform duration-150 motion-reduce:transition-none"
style={{
width: props.clearable ? 20 : 10,
height: 20,
alignItems: "center",
justifyContent: "center",
opacity: props.clearable ? 0 : 1,
transform: [{ rotate: props.open ? "270deg" : "90deg" }],
}}
>
<Text style={{ fontSize: 18, color: ui().muted }}>›</Text>
</View>
</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 (bolder while chosen), a quieter detail at the
* end, the check in its own lane (`group-selected:`).
*/
function Row(props: { item: SelectEntry; detail?: string }) {
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 class="font-normal group-selected:font-semibold" style={{ numberOfLines: 1, flex: 1, fontSize: controlText, color: ui().text }}>
{props.item.title}
</Text>
<If when={props.detail}>
<Text style={{ fontSize: 12, color: ui().faint }}>{props.detail}</Text>
</If>
<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 language = signal<string | null>("en");
const region = signal<string | null>(null);
const languageOpen = signal(false);
const regionOpen = signal(false);
const locked = signal(false);
const events = signal<string[]>([]);
const log = (line: string) => events.set([line, ...events()].slice(0, 8));
const opened = (name: string, open: (next: boolean) => void) => ({ open: next, reason }: { open: boolean; reason: OpenChangeReason }) => {
open(next);
log(`${name} ${next ? "opened" : "closed"} · ${reason}`);
};
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>select</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Click a select, or Tab to it and press Down; type a letter to jump" : "Tap a select to choose from a sheet"}
</Text>
<Split
gap={14}
inspector={
<>
<Panel title="value">
<Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{`language ${language() ?? "null"}`}</Text>
<Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{`region ${region() ?? "null"}`}</Text>
<Button
disabled={region() === null}
onPress={() => region.set(null)}
{...button("secondary", { size: "sm", style: { marginTop: 6 } })}
>
<Text style={buttonText("secondary", { size: "sm" })}>Clear region</Text>
</Button>
<Switch
checked={locked()}
onChange={({ checked }) => locked.set(checked)}
class={`-mx-2 mt-1 flex-row-reverse items-center justify-between gap-3 rounded-lg px-2 ${desktop ? "py-1" : "py-2"} active:opacity-70`}
hoverStyle={hoverFill()}
focusStyle={focusRing()}
trackClass={`${desktop ? "w-9" : "w-12"} rounded-full p-0.5 transition-colors motion-reduce:transition-none`}
trackStyle={locked() ? ui().accentFill : ui().inset}
thumbClass={desktop ? "size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4" : "size-5.5 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-5"}
thumbStyle={locked() ? { backgroundColor: ui().onAccent } : ui().raised}
>
<Text style={{ flex: 1, fontSize: size.label, color: ui().text }}>Region disabled</Text>
</Switch>
</Panel>
<Log title="onChange · onOpenChange" lines={events()} minHeight={120} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, padding: 16, gap: 20 }}>
<Field label="Language" detail={desktop ? "Typing a letter on the trigger chooses by title" : "Thirteen languages in a sheet"}>
<Select
items={LANGUAGES}
value={language()}
onChange={({ value: next }) => {
language.set(next);
log(`Language → ${titleOf(LANGUAGES, next)}`);
}}
onOpenChange={opened("Language", (next) => languageOpen.set(next))}
accessibilityLabel="Language"
offset={4}
margin={8}
popupStyle={surface()}
backdropStyle={backdrop}
itemClass={rowClass}
trigger={<Trigger title={titleOf(LANGUAGES, language())} placeholder="Choose a language" open={languageOpen()} />}
renderItem={(item) => <Row item={item} />}
/>
</Field>
<Field label="Region" detail="Search by name; one region is at capacity">
<Select
items={REGIONS}
value={region()}
onChange={({ value: next }) => {
region.set(next);
log(next === null ? "Region cleared" : `Region → ${titleOf(REGIONS, next)}`);
}}
onOpenChange={opened("Region", (next) => regionOpen.set(next))}
disabled={locked()}
accessibilityLabel="Region"
searchable
searchPlaceholder="Search regions"
searchAccessibilityLabel="Search regions"
searchClass={`${desktop ? "text-sm" : "text-base"} placeholder:text-[${ui().faint}] text-[${ui().text}]`}
searchStyle={{ ...ui().inset, borderRadius: 8, height: size.control, paddingHorizontal: 8, marginBottom: 4 }}
empty={<Text style={{ paddingVertical: 20, textAlign: "center", fontSize: 13, color: ui().muted }}>No region matches</Text>}
offset={4}
margin={8}
// Wider than the trigger on the desktops: the titles and their codes side by side.
popupStyle={desktop ? { ...surface(), width: 340 } : surface()}
backdropStyle={backdrop}
itemClass={rowClass}
// As wide as the trigger, so the clear button lands over its end.
style={desktop ? { width: 260 } : undefined}
cleanable
clearButtonAccessibilityLabel="Clear the region"
clearButtonClass="absolute items-center justify-center rounded-full enabled:hover:opacity-70"
clearButtonStyle={{ top: (size.control - 20) / 2, right: 10, width: 20, height: 20 }}
clearButton={<Icon name="close" size={14} tint={ui().muted} />}
trigger={<Trigger title={titleOf(REGIONS, region())} placeholder="Choose a region" open={regionOpen()} disabled={locked()} clearable={region() !== null} />}
renderItem={(item) => <Row item={item} detail={item.disabled ? "At capacity" : item.value} />}
renderGroup={(group) => <Header group={group} />}
/>
</Field>
</View>
</Split>
</View>
);
}
mount(App);- على أجهزة سطح المكتب وفي متصفحاتها: نافذة منبثقة فوق المشغِّل يقع فيها الصف المحدد فوق المشغِّل تمامًا، فيبقى العنوان المختار في مكانه (موضع
item-alignedفي shadcn/ui)، وبعرض المشغِّل (ويحددpopupStyle/popupClassفي التطبيق عرضًا آخر، كما فيmenu_widthالخاص بـgpui-component) وبارتفاع صفوفها بحدّ المساحة المتاحة على الشاشة؛ وعند عدم تحديد أي عنصر تُفتح أسفل المشغِّل. تتنقل لوحة المفاتيح بين العناصر وتختار أحدها. - على الهواتف والأجهزة اللوحية: ورقة (sheet) في أسفل الشاشة فوق خلفية معتمة (backdrop).
يتحكم Select في السلوك: القيمة (قيمة التطبيق أو قيمته الخاصة)، وحالة الفتح، ولوحة المفاتيح، والبحث بالكتابة (typeahead)، والبحث، والأقسام، والصف المميَّز، وما تنطق به قارئات الشاشة. أما المظهر فللتطبيق: فهو ينسّق المشغِّل والنافذة المنبثقة والقائمة والصفوف وترويسات الأقسام وحقل البحث، أو يرسم الصفوف بنفسه.
use nsh_base::{SelectEntry, SelectItem, select, theme};
use nsh_core::prelude::*;
const FRUITS: [(&str, &str); 5] = [
("apple", "Apple"),
("banana", "Banana"),
("blueberry", "Blueberry"),
("grapes", "Grapes"),
("pineapple", "Pineapple"),
];
fn fruit_select() -> impl IntoElement {
let fruit = signal(None::<SharedString>);
let title = move || {
let chosen = fruit.get();
FRUITS
.iter()
.find(|(value, _)| Some(*value) == chosen.as_deref())
.map_or("Pick a fruit", |(_, title)| *title)
.to_string()
};
select()
.items(
FRUITS
.iter()
.map(|&(value, title)| SelectItem::new(value, title).disabled(value == "grapes"))
.collect::<Vec<_>>(),
)
.value(move || fruit.get())
.on_change(move |next: &Option<SharedString>| fruit.set(next.clone()))
.accessibility_label("Fruit")
.offset(px(4.0))
.popup_style(|s| {
s.p_1()
.rounded_lg()
.border_1()
.border_color(move || theme().border)
.bg(move || theme().popover)
.text_color(move || theme().popover_foreground)
.shadow_md()
})
.item_class("rounded-md focus:bg-accent selected:font-medium disabled:opacity-50")
.trigger(
view()
.h_9()
.w_48()
.flex_row()
.items_center()
.justify_between()
.px_3()
.rounded_md()
.border_1()
.border_color(move || theme().input)
.hover(|s| s.bg(move || theme().accent))
.child(text(title))
.child(text("▾").text_color(move || theme().muted_foreground)),
)
.render_item(|item: &SelectEntry| {
view()
.flex_row()
.items_center()
.px_2()
.py_1p5()
.child(text(item.title.clone()).text_sm())
})
}import { mount, signal, Text, View } from "nsh";
import { Select } from "nsh/base";
const FRUITS = [
{ value: "apple", title: "Apple" },
{ value: "banana", title: "Banana" },
{ value: "blueberry", title: "Blueberry" },
{ value: "grapes", title: "Grapes", disabled: true },
{ value: "pineapple", title: "Pineapple" },
];
function App() {
const fruit = signal<string | null>(null);
const title = () => FRUITS.find((f) => f.value === fruit())?.title;
return (
<Select
items={FRUITS}
value={fruit()}
onChange={({ value }) => fruit.set(value)}
accessibilityLabel="Fruit"
offset={4}
popupClass="rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-md"
backdropClass="bg-black/40"
itemClass="rounded-md px-2 py-1.5 text-sm focus:bg-accent focus:text-accent-foreground disabled:opacity-50"
trigger={
<View class="h-9 w-48 flex-row items-center justify-between rounded-md border border-input px-3 hover:bg-accent active:bg-accent focus-visible:ring-2 focus-visible:ring-ring">
<Text class={fruit() ? "text-foreground" : "text-muted-foreground"}>{title() ?? "Pick a fruit"}</Text>
<Text class="text-muted-foreground">▾</Text>
</View>
}
/>
);
}
mount(App);يأتي Select من nsh/base، وهي حزمة (crate) nsh-base في Rust. في Rust ينشئ select() واحدًا منه بدالة ضبط لكل خاصية (.items(..)، .value(..)، .item_style(..)) ومستمع لكل حدث (.on_change(..))؛ وينشئ SelectItem::new(value, title) عنصرًا، مع .disabled(..) و.group(..).
#العناصر
items هي القائمة بوصفها بيانات: لكل عنصر value (فريدة بين العناصر: وهي ما يُضبط عند اختياره)، وtitle (وهو ما يعرضه صفه، وما يطابقه البحث والكتابة السريعة، وما تنطق به قارئات الشاشة)، واختياريًا disabled وgroup. العناصر المتجاورة التي لها group نفسه تشكّل قسمًا تحت ترويسة بذلك العنوان:
<Select
items={[
{ value: "lisbon", title: "Lisbon", group: "Europe" },
{ value: "london", title: "London", group: "Europe" },
{ value: "tokyo", title: "Tokyo", group: "Asia" },
{ value: "seoul", title: "Seoul", group: "Asia", disabled: true },
]}
groupClass="px-2 pb-1 pt-2 text-xs text-muted-foreground"
trigger={trigger}
/>
الصفوف قائمة افتراضية (virtualized) داخل عرض التمرير في النافذة المنبثقة: فالقائمة التي تضم آلاف العناصر لا تنشئ إلا الصفوف الظاهرة، وتتمرر النافذة المنبثقة عندما تزيد صفوفها طولًا على المساحة المتاحة لها.
#القيمة
مع value يملك التطبيق العنصر المختار: يعرض Select العنصر ذا هذه القيمة ويبلّغ عن الاختيار عبر onChange({ value })؛ ويضبط التطبيق value ليتبعه، أو يحتفظ بقيمته الخاصة. value={null} تعني عدم اختيار أي شيء. وإذا حُذفت، يحتفظ Select بقيمته الخاصة، بدءًا من defaultValue. يبلّغ onChange عن التغييرات: واختيار العنصر المختار نفسه مرة أخرى يغلق القائمة.
يضيف cleanable زر مسح ما دام عنصر مختارًا: يمسح الضغط عليه القيمة ويُبلَّغ عنه عبر onChange({ value: null })، وتبقى القائمة مغلقة. يأتي الزر بعد المشغِّل في عرض Select، لا داخله، فلا يفتح الضغط عليه القائمة أبدًا؛ ويضعه التطبيق (فوق نهاية المشغِّل باستخدام absolute، حيث يكون السهم عادةً) ويملؤه:
<Select
items={fonts}
value={font()}
onChange={({ value }) => font.set(value)}
cleanable
clearButtonAccessibilityLabel="Clear the font"
clearButtonClass="absolute right-2 top-2 size-5 items-center justify-center rounded-full hover:bg-accent"
clearButton={<Text class="text-xs text-muted-foreground">✕</Text>}
trigger={trigger}
/>
المشغِّل عنصر يخص التطبيق، فيعرض ما يشاؤه التطبيق للعنصر المختار: عنوانه، أو أيقونة، أو نصًا بديلًا ما دام لا شيء مختارًا. في TSX هو العنصر الذي تمرره: فهو الـ combobox ويتولى بنفسه الضغطات، وتركيز لوحة المفاتيح، وحالة التوسيع، وdisabled، فتتبع فئات active: وfocus: وdisabled: الخاصة به حالة Select، ويعمل onPress الخاص به أيضًا، وتوضع القائمة قبالته وبعرضه.
#حالة الفتح
بدون open يحتفظ Select بحالة الفتح الخاصة به (بدءًا من defaultOpen). ومع open يملكها التطبيق ويغيّرها في onOpenChange({ open, reason }): "triggerPress" (ضغطة على المشغِّل، أو أحد مفاتيحه)، "select" (اختيار)، "pressOutside"، "backdropPress" أو "cancel" (Escape أو الرجوع). يخبر onOpen وonClose بوقت ظهور القائمة وزوالها (في الورقة: بعد أن دخلت وبعد أن خرجت)، ويخبر onCancel بأن المستخدم أغلقها دون اختيار.
#البحث
يضع searchable حقل بحث فوق العناصر: تصفّيها الكتابة بحسب العنوان دون مراعاة حالة الأحرف، ويختفي القسم الذي لا يطابق شيء منه مع ترويسته. ينتقل التمييز إلى أول نتيجة مطابقة، وتواصل المفاتيح تحريكه ما دام الحقل يملك لوحة المفاتيح، ويختارها Return. empty هو ما تعرضه القائمة عندما لا يطابق شيء:
<Select
items={zones}
searchable
searchPlaceholder="Search time zones"
searchAccessibilityLabel="Search time zones"
searchClass="mx-1 mb-1 rounded-md border border-input px-2 py-1.5 text-sm"
empty={<Text class="px-2 py-6 text-center text-sm text-muted-foreground">No time zone found</Text>}
trigger={trigger}
/>
على أجهزة سطح المكتب يستحوذ حقل البحث على لوحة المفاتيح عند فتح القائمة؛ وعلى الشاشات التي تعمل باللمس تظهر لوحة المفاتيح عند الضغط على الحقل. يبدأ كل فتح ببحث فارغ.
#الصفوف التي يرسمها التطبيق
ينسّق itemClass صف كل عنصر، الذي يعرض عنوان العنصر إلى أن يرسمه renderItem: يُطبَّق focus: على الصف المميَّز (حيث المفاتيح أو المؤشر)، وselected: على الصف المختار، وdisabled: على الصف المعطَّل. لرسم صف، مرّر renderItem: يتلقى ما هو العنصر (SelectEntry) ويعيد محتوى الصف، للصفوف الظاهرة. ولا يتلقى حالة الصف أبدًا: فمع group في itemClass يتبع المحتوى حالات الصف بفئات group-focus: وgroup-selected: وgroup-disabled:، فيُنجَز الرسم مرة واحدة لكل عنصر، ولا ينتج عن انتقال التمييز أو الاختيار أي رسم جديد. يرسم renderGroup ترويسة القسم بالطريقة نفسها:
<Select
items={fonts}
itemClass="group rounded-md focus:bg-accent disabled:opacity-50"
renderItem={(item) => (
<View class="flex-row items-center gap-2 px-2 py-1.5">
<Text class="flex-1 text-sm" style={{ fontFamily: item.value }}>{item.title}</Text>
<Text class="w-4 text-right text-sm opacity-0 group-selected:opacity-100">✓</Text>
</View>
)}
renderGroup={(group) => <Text class="px-2 pt-2 text-xs font-medium text-muted-foreground">{group.title}</Text>}
trigger={trigger}
/>
#الخصائص
| الخاصية | Rust | النوع | الافتراضي | الوصف |
|---|---|---|---|---|
items | .items(..) | SelectItem[] | [] | العناصر، بالترتيب. |
value | .value(..) | string | null | لا شيء | value العنصر المختار، ويملكها التطبيق؛ وnull تعني عدم اختيار شيء. وإذا حُذفت، يحتفظ Select بقيمته الخاصة. |
defaultValue | .default_value(..) | string | لا شيء | القيمة في البداية، بدون value. |
open | .open(..) | boolean | لا شيء | حالة الفتح، ويملكها التطبيق. |
defaultOpen | .default_open(..) | boolean | false | مفتوحة في البداية، بدون open. |
searchable | .searchable(..) | boolean | false | حقل بحث فوق العناصر يصفّيها بحسب العنوان. |
searchPlaceholder | .search_placeholder(..) | string | لا شيء | النص البديل لحقل البحث. |
searchAccessibilityLabel | .search_accessibility_label(..) | string | لا شيء | ما تسمّي به قارئات الشاشة حقل البحث: ما الذي يبحث فيه. |
cleanable | .cleanable(..) | boolean | false | زر مسح ما دام عنصر مختارًا؛ يمسح الضغط عليه القيمة (onChange({ value: null })). |
disabled | .disabled(..) | boolean | false | تبقى القائمة مغلقة: لا يقبل المشغِّل ضغطات ولا تركيز لوحة المفاتيح، وتُطبَّق عليه أنماط disabled:. |
accessibilityLabel | .accessibility_label(..) | string | لا شيء | ما تسمّي به قارئات الشاشة عنصر Select: موضوع الاختيار. |
placement | .placement(..) | "top" | "bottom" | "left" | "right" | "bottom" | أجهزة سطح المكتب: جهة المشغِّل التي توضع عليها القائمة، والجهة الأخرى إذا كانت وحدها تتسع لها. |
align | .align(..) | "start" | "center" | "end" | "start" | أجهزة سطح المكتب: كيف تُحاذى مع المشغِّل. |
offset | .offset(..) | number | 0 | أجهزة سطح المكتب: الفجوة بين المشغِّل والقائمة. |
margin | .margin(..) | number | 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> | لا شيء | ترويسة كل قسم. |
searchClass، searchStyle | .search_class(..)، .search_style(..) | string، StyleProp<InputStyle> | لا شيء | حقل البحث: صندوقه ونصه (color، fontSize، placeholderColor). |
clearButtonClass، clearButtonStyle | .clear_button_class(..)، .clear_button_style(..) | string، StyleProp<ViewStyle> | لا شيء | زر المسح (cleanable)؛ disabled: مع Select. |
clearButton | .clear_button(..) | JSX.Element | لا شيء | محتوى زر المسح: علامة ×، أو أيقونة. |
clearButtonAccessibilityLabel | .clear_button_accessibility_label(..) | string | لا شيء | ما تسمّي به قارئات الشاشة زر المسح. |
trigger | .trigger(..) | JSX.Element | لا شيء | ما يفتح القائمة عند الضغط عليه؛ وتوضع القائمة قبالته. |
empty | .empty(..) | JSX.Element | لا شيء | ما تعرضه القائمة عندما لا يظهر أي عنصر (بحث بلا نتائج). |
renderItem | .render_item(..) | (item: SelectEntry) => JSX.Element | لا شيء | صف العنصر، يرسمه التطبيق مما هو العنصر (وحالاته هي فئات الصف)؛ وإلا فعنوان العنصر. |
renderGroup | .render_group(..) | (group: SelectGroup) => JSX.Element | لا شيء | ترويسة القسم، يرسمها التطبيق؛ وإلا فعنوانه. |
class، style | دوال التنسيق | string، StyleProp<ViewStyle> | لا شيء | العرض المحيط بالمشغِّل. |
#الأحداث
| الحدث | Rust | الوصف |
|---|---|---|
onChange | .on_change(|value| ..) | اختار المستخدم عنصرًا آخر (ضغطة، أو Return أو Space، أو الكتابة السريعة على المشغِّل): value الخاصة به؛ أو null عندما مسحه زر المسح. اضبط value لتتبعه. |
onOpenChange | .on_open_change(|e| ..) | فُتحت القائمة أو أُغلقت، مع السبب. اضبط open لتتبعه. |
onOpen | .on_open(|| ..) | القائمة ظاهرة؛ وفي حالة الورقة: بعد أن دخلت. |
onClose | .on_close(|| ..) | زالت القائمة؛ وفي حالة الورقة: بعد أن خرجت. وكذلك عندما أغلقها التطبيق. |
onCancel | .on_cancel(|| ..) | أغلق المستخدم القائمة دون اختيار (Escape أو الرجوع، أو ضغطة خارجها أو على الخلفية المعتمة). |
تأتي بهذا الترتيب: onChange عند الاختيار، أو onCancel عند الإغلاق دون اختيار، ثم onOpenChange، ثم onOpen أو onClose بعد أن تظهر القائمة أو تزول.
#الأنواع
| النوع | الأعضاء | الوصف |
|---|---|---|
SelectItem | value: string، title: string، disabled?: boolean، group?: string | عنصر في items. |
SelectEntry | value، title، disabled، group? (الخاصة بالعنصر)، index: number | ما يتلقاه renderItem: العنصر وفهرسه بين items، وليس حالته أبدًا (focus: وselected: وdisabled: الخاصة بالصف). |
SelectGroup | title: string | ما يتلقاه renderGroup: group الخاص بالقسم. |
في Rust هي nsh_base::SelectItem وSelectEntry وSelectGroup؛ وvalue هو SelectValue (Controlled(Some(..)) للقيمة، Controlled(None) لـ null)، ويصنعه .value(..) من Option<SharedString> أو من سلسلة نصية.
#الأجزاء
| الجزء | الموضع | يُنسَّق بواسطة |
|---|---|---|
| المشغِّل | عرض Select | عنصر التطبيق نفسه (trigger) |
| زر المسح | عرض Select، بعد المشغِّل (cleanable، ما دام عنصر مختارًا) | clearButtonClass، clearButtonStyle؛ ومحتواه clearButton |
| النافذة المنبثقة أو الورقة | فوق كل شيء، عند المشغِّل (أجهزة سطح المكتب) أو في الأسفل (الشاشات اللمسية) | popupClass، popupStyle |
| الخلفية المعتمة | خلف الورقة (الشاشات اللمسية) | backdropClass، backdropStyle |
| حقل البحث | النافذة المنبثقة، أولًا (searchable) | searchClass، searchStyle، searchPlaceholder، searchAccessibilityLabel |
| القائمة | النافذة المنبثقة، بعد حقل البحث | listClass، listStyle |
| ترويسة القسم | القائمة، قبل عناصره | groupClass، groupStyle، renderGroup |
| صف العنصر | القائمة | itemClass، itemStyle، renderItem (group-focus:، group-selected:، group-disabled: في الداخل، تحت group في itemClass) |
| الحالة الفارغة | النافذة المنبثقة، ما دام لا عنصر ظاهرًا | عنصرها الخاص (empty) |
النافذة المنبثقة عمود: يتبع ارتفاعها حقل البحث والصفوف، فيحدّه max-h-* على popupClass وتتمرر القائمة داخله.
#لوحة المفاتيح
على أجهزة سطح المكتب يصل Tab إلى مشغِّل عنصر Select غير disabled. والقائمة مغلقة، تفتحها Down وUp وReturn وSpace عند العنصر المختار (وإن لم يكن شيء مختارًا، تفتحها Up عند آخر عنصر مفعّل وتفتحها الباقية عند الأول)، وتختار الأحرف والأرقام العنصر الذي يبدأ عنوانه بما كُتب، كما يفعل عنصر select الأصلي. والقائمة مفتوحة:
| المفتاح | الإجراء |
|---|---|
| Down، Up | العنصر المفعّل التالي أو السابق، مع الالتفاف عند الطرفين. |
| Home، End | أول عنصر مفعّل أو آخره (وتبقى لحقل البحث وظيفتهما الخاصة ما دام يملك لوحة المفاتيح). |
| Page Down، Page Up | التقدم صفحة من الصفوف أو الرجوع صفحة. |
| Return | يختار العنصر المميَّز ويغلق القائمة. |
| Space | يختار العنصر المميَّز (بدون حقل بحث). |
| الأحرف، الأرقام | العنصر التالي الذي يبدأ عنوانه بما كُتب في الثانية الأخيرة (بدون حقل بحث). |
| Escape | يغلق القائمة دون اختيار. |
عند إغلاق القائمة تعود لوحة المفاتيح إلى المشغِّل، ما لم تنقلها ضغطة خارجية إلى مكان آخر. يميّز المؤشر الصف الذي يمر فوقه؛ ويُكشف الصف المميَّز عندما تنتقل إليه المفاتيح.
#على كل جهاز
| أجهزة سطح المكتب ومتصفحاتها | الهواتف والأجهزة اللوحية | |
|---|---|---|
| القائمة | نافذة منبثقة فوق المشغِّل والصف المختار فوقه (أسفله إن لم يكن شيء مختارًا)، بعرض المشغِّل ما لم يحدد popupStyle عرضًا، وبارتفاع صفوفها بحدّ المساحة المتاحة على الشاشة | ورقة في الأسفل فوق خلفية معتمة، بارتفاع صفوفها حتى شريط الحالة |
| إغلاقها | اختيار، أو Escape، أو ضغطة خارجها (تمتد إلى ما تحت المؤشر) | اختيار، أو الخلفية المعتمة، أو زر الرجوع في Android |
| البحث | يستحوذ الحقل على لوحة المفاتيح عند فتح القائمة | تظهر لوحة المفاتيح عند الضغط على الحقل |
| الاختيار | الضغطة النازلة تختار، فيحتفظ حقل البحث بلوحة المفاتيح | نقرة |
ينسّق popupClass النافذة المنبثقة على أجهزة سطح المكتب والورقة على الهواتف؛ ويمكن لنمط يختلف بينهما أن يقارن platform.idiom (nsh/device) بـ "desktop".
#إمكانية الوصول
- يُعرَّف المحفّز (trigger) لقارئات الشاشة على أنه مربّع تحرير وسرد (combobox): تسميته
accessibilityLabel، وقيمته عنوان العنصر المختار، ويُعلن عمّا إذا كانت القائمة موسّعة. ويؤديdisabledإلى تعتيمه. - زر المسح هو زر يحمل الاسم
clearButtonAccessibilityLabel؛ ولا يُعدّ إحدى محطات مفتاح Tab. - حقل البحث هو حقل نصي يحمل الاسم
searchAccessibilityLabel؛ ونصه البديل (placeholder) تلميح يظهر ما دام الحقل فارغًا. - القائمة هي listbox وكل صف فيها خيار (option) مسمّى بعنوانه؛ ويكون الخيار المختار محدَّدًا، ويُعتَّم الخيار المعطَّل. أما ترويسات الأقسام فهي ترويسات (headers).
- أثناء تحريك المفاتيح للتمييز (highlight)، ينتقل تركيز قارئ الشاشة إلى الصف المميَّز؛ وما دام حقل البحث يستقبل لوحة المفاتيح، يُعلن عنوان العنصر المميَّز بدلًا من ذلك، فيستمر الكتابة دون انقطاع.
راجع إمكانية الوصول لمعرفة التسميات والأدوار والإعلانات بوجه عام.