العناصر
Tooltip
التلميح (Tooltip) إشارة قصيرة بجوار العنصر الذي تتعلق به: ماذا يفعل زر أيقونة، أو النص الكامل لتسمية مقتطعة.
على أجهزة سطح المكتب ومتصفحاتها يظهر بعد أن يستقر المؤشر على العنصر المُحفِّز لمدة delay (500 مللي ثانية)، ويختفي
عندما يغادر المؤشر العنصر أو يضغط عليه. على الهواتف والأجهزة اللوحية لا يظهر التلميح إلا عندما يفتحه التطبيق
(open). لا يستحوذ أبدًا على المؤشر أو الضغطات، لذا يعمل العنصر المُحفِّز تمامًا كما يعمل بدونه.
import { mount, signal, Text, View } from "nsh";
import { type OpenChangeReason, Tooltip } from "nsh/base";
import { button, buttonText, createPresence, desktop, Inspector, Log, Panel, Stage, ui, wide } from "./palette";
/** An icon button with a hint of the app's look, above or below it. */
function Tool(props: {
icon: string;
label: string;
hint: string;
placement: "top" | "bottom";
pinned: boolean;
onPress: () => void;
onOpenChange: (e: { open: boolean; reason: OpenChangeReason }) => void;
}) {
const hovered = signal(false);
// The bubble stays made between showings: it comes in from its side as it shows.
const presence = createPresence(props.placement);
return (
<View style={{ alignItems: "center", gap: 6 }}>
<View
onPress={props.onPress}
accessibilityLabel={props.label}
{...button("secondary", { align: "center", style: { width: desktop ? 36 : 52, height: desktop ? 36 : 52, paddingHorizontal: 0 } })}
tooltip={
<Tooltip
open={props.pinned || hovered()}
placement={props.placement}
offset={8}
margin={8}
delay={400}
onOpenChange={(e) => {
hovered.set(e.open);
props.onOpenChange(e);
}}
onOpen={presence.show}
onClose={presence.hide}
>
<View style={{ backgroundColor: ui().text, borderRadius: 7, paddingHorizontal: 10, paddingVertical: 6, ...presence.style }}>
<Text style={{ fontSize: 13, color: ui().page }}>{props.hint}</Text>
</View>
</Tooltip>
}
>
<Text style={{ fontSize: desktop ? 17 : 22, color: ui().text }}>{props.icon}</Text>
</View>
<Text style={{ fontSize: 11, color: ui().faint }}>{props.placement}</Text>
</View>
);
}
function App() {
const events = signal<string[]>([]);
const pressed = signal("—");
const pinned = signal(false);
const log = (name: string) => ({ open, reason }: { open: boolean; reason: OpenChangeReason }) =>
events.set([`${name} ${open ? "shown" : "hidden"} · ${reason}`, ...events()].slice(0, 6));
const tools = () => (
<>
<Tool icon="★" label="Star" hint="Add to favorites" placement="top" pinned={pinned()} onPress={() => pressed.set("Star")} onOpenChange={log("Star")} />
<Tool icon="↗" label="Share" hint="Share with your team" placement="bottom" pinned={pinned()} onPress={() => pressed.set("Share")} onOpenChange={log("Share")} />
<Tool icon="✕" label="Delete" hint="Move to the trash" placement="top" pinned={pinned()} onPress={() => pressed.set("Delete")} onOpenChange={log("Delete")} />
</>
);
// The `tooltip` prop of any element: a `Tooltip` with the app's look.
const pin = () => (
<View
tooltip={
<Tooltip style={{ backgroundColor: ui().text, borderRadius: 7, paddingHorizontal: 10, paddingVertical: 6 }}>
<Text style={{ fontSize: 13, color: ui().page }}>Shows every tooltip without a pointer</Text>
</Tooltip>
}
onPress={() => pinned.set(!pinned())}
{...button("secondary", { align: "center", style: wide() ? {} : { flex: 1 } })}
>
<Text style={buttonText()}>
{wide() ? (pinned() ? "Unpin the tooltips" : "Pin the tooltips") : pinned() ? "Unpin" : "Pin"}
</Text>
</View>
);
const onPress = () => (
<Panel title="onPress">
<Text style={{ fontSize: 13, color: ui().text }}>{pressed()}</Text>
</Panel>
);
const changes = () => <Log title="onOpenChange({ open, reason })" lines={events()} minHeight={130} />;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>tooltip</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Rest the pointer on a button; a click still presses it" : "Tooltips follow a pointer: pin them to see them here"}
</Text>
{() =>
wide() ? (
<View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
<Stage gap={40}>
<View style={{ flexDirection: "row", justifyContent: "center", gap: 64 }}>{tools()}</View>
{pin()}
</Stage>
<Inspector>
{onPress()}
{changes()}
</Inspector>
</View>
) : (
<>
<View style={{ flex: 1, minHeight: 200, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 28 }}>{tools()}</View>
<View style={{ flexDirection: "row", gap: 8 }}>{pin()}</View>
{onPress()}
{changes()}
</>
)
}
</View>
);
}
mount(App);Tooltip من nsh/base يُمرَّر عبر الخاصية tooltip التي يملكها كل عنصر
(الخصائص المشتركة)، مثل .tooltip(..) في GPUI: العنصر هو ما يستقر عليه المؤشر وما يُوضَع التلميح بالنسبة إليه.
أما التلميح نفسه فهو الفقاعة: نسّقها باستخدام class / style، وأبناؤها هم ما تقوله.
use nsh_base::tooltip;
use nsh_core::prelude::*;
fn save_button(on_save: impl Fn() + 'static) -> impl IntoElement {
view()
.on_press(on_save)
.accessibility_label("Save")
.p_2()
.child(text("💾"))
.tooltip(
tooltip()
.placement(Placement::Bottom)
.rounded_md()
.bg(rgb(0x111827))
.px_2()
.py_1()
.child(text("Save the document").text_color(white())),
)
}import { Text, View } from "nsh";
import { Tooltip } from "nsh/base";
function SaveButton(props: { onSave: () => void }) {
return (
<View
onPress={props.onSave}
accessibilityLabel="Save"
class="p-2"
tooltip={
<Tooltip placement="bottom" class="rounded-md bg-gray-900 px-2 py-1">
<Text class="text-white">Save the document</Text>
</Tooltip>
}
>
<Text>💾</Text>
</View>
);
}يظهر فوق العنصر المُحفِّز افتراضيًا (placement)، وعلى الجانب الآخر عند عدم وجود مساحة، ويبقى داخل المنطقة الآمنة.
مع open يقرر التطبيق متى يظهر: فهو يتبع onOpenChange، الذي يُبلِّغ عن استقرار المؤشر على العنصر المُحفِّز أو مغادرته له،
ويمكنه إظهاره على أي جهاز.
يظهر ويختفي وفق انتقالاتك: startingClass هو مظهره في إطاره الأول وendingClass مظهره بعد الإخفاء، ويحتفظ به أثناء تشغيل
فئات transition-* في class؛ ثم يليه onClose.
<Tooltip
class="rounded-md bg-gray-900 px-2 py-1 transition-opacity duration-100"
startingClass="opacity-0"
endingClass="opacity-0"
>
<Text class="text-white">Save the document</Text>
</Tooltip>
#الخصائص
| الخاصية | Rust | النوع | الافتراضي | الوصف |
|---|---|---|---|---|
children | .child(..) | JSX.Element | لا شيء | ما يقوله. |
class، style | .app_class(..)، طرائق Styled | string، StyleProp<ViewStyle> | لا شيء | مظهر الفقاعة، أي التلميح نفسه. |
startingClass، startingStyle | .starting_class(..)، .starting_style(..) | string، StyleProp<ViewStyle> | لا شيء | مظهره في إطاره الأول: تبدأ انتقالاته منه. |
endingClass، endingStyle | .ending_class(..)، .ending_style(..) | string، StyleProp<ViewStyle> | لا شيء | مظهره بعد الإخفاء: يبقى أثناء تشغيل انتقالاته. |
open | .open(..) | boolean | لا شيء | ظاهر، ويتحكم فيه التطبيق. |
defaultOpen | .default_open(..) | boolean | false | ظاهر في البداية، دون open. |
delay | .delay(..) | number | 500 | عدد المللي ثانية التي يستقر فيها المؤشر على العنصر المُحفِّز قبل ظهوره؛ Infinity: لا يظهر من تلقاء نفسه أبدًا، بل فقط عندما يفتحه التطبيق. |
disabled | .disabled(..) | boolean | false | لا يظهر أبدًا. |
placement | .placement(..) | "top" | "bottom" | "left" | "right" | "top" | جانب العنصر المُحفِّز الذي يوضع عليه. |
align | .align(..) | "start" | "center" | "end" | "center" | كيفية محاذاته للعنصر المُحفِّز على امتداد ذلك الجانب. |
offset | .offset(..) | number | 4 | المسافة بين العنصر المُحفِّز والتلميح. |
margin | .margin(..) | number | 4 | المسافة التي يحافظ عليها من حواف المنطقة الآمنة. |
#الأحداث
| الحدث | Rust | الوصف |
|---|---|---|
onOpenChange | .on_open_change(|e| ..) | استقر المؤشر على العنصر المُحفِّز، أو غادره أو ضغط عليه (reason "hover"). |
onOpen | .on_open(|| ..) | ظهر التلميح. |
onClose | .on_close(|| ..) | اختفى التلميح بعد انتهاء خروجه؛ وكذلك عندما يخفيه التطبيق. |
#إمكانية الوصول
يكرر التلميح ما يقوله العنصر المُحفِّز لقارئات الشاشة: امنح العنصر المُحفِّز accessibilityLabel أيضًا، ليكون
التلميح متاحًا للجميع.