요소
Tooltip
A tooltip is a short hint next to what it is about: what an icon button does, the full text of a truncated label.
On the desktops and in desktop browsers it shows once the pointer has rested on its trigger for delay (500 ms), and
hides when the pointer leaves or presses the trigger. Phones and tablets show a tooltip only when the app opens it
(open). It never takes the pointer or presses, so the trigger works exactly as it does without it.
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 from nsh/base is given as the tooltip prop every element has
(Common props), as GPUI’s .tooltip(..): the element is what the pointer rests on and what it
is placed against. The tooltip itself is the bubble: style it with class / style, and its children are what it says.
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>
);
}It goes above its trigger by default (placement), on the other side when there is no room, and stays inside the safe
area. With open the app decides when it shows: it follows onOpenChange, which reports the pointer resting on the
trigger or leaving it, and can show it on any device.
It comes and goes with your transitions: startingClass is its look in its first frame and endingClass its look
once hidden, which it keeps while the transition-* classes of its class run; onClose follows.
<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>
#Props
| Prop | Rust | Type | Default | Description |
|---|---|---|---|---|
children | .child(..) | JSX.Element | none | What it says. |
class, style | .app_class(..), Styled methods | string, StyleProp<ViewStyle> | none | The look of the bubble: the tooltip itself. |
startingClass, startingStyle | .starting_class(..), .starting_style(..) | string, StyleProp<ViewStyle> | none | Its look in its first frame: its transitions run from it. |
endingClass, endingStyle | .ending_class(..), .ending_style(..) | string, StyleProp<ViewStyle> | none | Its look once hidden: it stays while its transitions run. |
open | .open(..) | boolean | none | Shown, controlled by the app. |
defaultOpen | .default_open(..) | boolean | false | Shown at first, without open. |
delay | .delay(..) | number | 500 | Milliseconds the pointer rests on the trigger before it shows; Infinity: never by itself, only when the app opens it. |
disabled | .disabled(..) | boolean | false | It never shows. |
placement | .placement(..) | "top" | "bottom" | "left" | "right" | "top" | The side of the trigger it goes on. |
align | .align(..) | "start" | "center" | "end" | "center" | How it lines up with the trigger along that side. |
offset | .offset(..) | number | 4 | The gap between the trigger and the tooltip. |
margin | .margin(..) | number | 4 | The distance it keeps from the edges of the safe area. |
#Events
| Event | Rust | Description |
|---|---|---|
onOpenChange | .on_open_change(|e| ..) | The pointer rested on the trigger, or left or pressed it (reason "hover"). |
onOpen | .on_open(|| ..) | It is shown. |
onClose | .on_close(|| ..) | It is gone, after its exit; also when the app hid it. |
#Accessibility
A tooltip repeats what its trigger says to screen readers: give the trigger an accessibilityLabel too, so the
hint is there for everyone.