Элементы
Link
Ссылка ведет пользователя куда-либо: на веб-страницу, в другое приложение (mailto:, tel:) или на экран самого приложения. Она отвечает за поведение: нажатия, Return и Space, пока у нее есть фокус клавиатуры, отключенное состояние и роль ссылки для скринридеров. Внешний вид определяет приложение: у Link нет собственного цвета, подчеркивания или подписи, виден только его дочерний контент. href указывает, куда ведет ссылка; это данные для открытия, а не текст на экране.
import { mount, signal, Text, View } from "nsh";
import { Link } from "nsh/base";
import { desktop, focusRing, Icon, Log, Panel, size, Split, tint, ui } from "./palette";
// Links of the app's look: the accent with a quiet underline that fills in
// under the pointer, violet once visited (the app remembers what it
// opened), the palette's focus ring, dimmed while disabled. The color and
// the underline move with `transition-colors`, still with reduced motion.
const visited = signal(new Set<string>());
const events = signal<string[]>([]);
const route = signal("/docs/link");
const opened = ({ href }: { href: string }) => {
visited.set(new Set(visited()).add(href));
events.set([`onOpen ${href}`, ...events()].slice(0, 4));
};
function DocLink(props: { href: string; label: string; openWith?: "system" | "app"; disabled?: boolean; current?: boolean; icon?: boolean; inline?: boolean }) {
const seen = () => visited().has(props.href);
const color = () => (props.disabled ? ui().faint : props.current ? ui().text : seen() ? ui().violet : ui().accent);
return (
<Link
href={props.href}
openWith={props.openWith ?? "app"}
disabled={props.disabled}
current={props.current}
onOpen={(e: { href: string }) => {
opened(e);
if ((props.openWith ?? "app") === "app") route.set(e.href);
}}
class={`flex-row items-center gap-1 rounded-sm transition-colors motion-reduce:transition-none active:opacity-60 ${props.inline ? "align-text-bottom" : "self-start"}`}
focusStyle={focusRing(2)}
>
<Text
class="transition-colors motion-reduce:transition-none"
style={{
fontSize: props.inline ? 15 : size.label,
fontWeight: props.current ? "600" : "500",
color: color(),
textDecorationLine: props.current || props.disabled ? "none" : "underline",
textDecorationColor: tint(color(), 0.4),
}}
hoverStyle={props.disabled || props.current ? undefined : { textDecorationColor: color() }}
>
{props.label}
</Text>
{props.icon ? <Icon name="external" size={13} tint={color()} /> : null}
</Link>
);
}
function App() {
const crumb = (href: string, label: string) => <DocLink href={href} label={label} current={route() === href} />;
const slash = () => <Text style={{ color: ui().faint, fontSize: size.label }}>/</Text>;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>link</Text>
<Text style={{ color: ui().muted }}>{desktop ? "Click a link, or Tab to one and press Return" : "Tap a link"}</Text>
<Split
gap={14}
inspector={
<>
<Panel title="route · visited">
<Text style={{ fontSize: 13, color: ui().text }}>
{route()} · {visited().size} visited
</Text>
</Panel>
<Log title="onOpen({ href })" lines={events()} minHeight={110} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, padding: desktop ? 16 : 18, gap: 14 }}>
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 6 }}>
{crumb("/docs", "Docs")}
{slash()}
{crumb("/docs/elements", "Elements")}
{slash()}
{crumb("/docs/link", "Link")}
</View>
<Text style={{ fontSize: 15, lineHeight: 22, color: ui().text }}>
{"Read the "}
<DocLink href="/docs/release-notes" label="release notes" inline />
{" and the "}
<DocLink href="/docs/migration" label="migration guide" inline />
{" before you update."}
</Text>
<View style={{ height: 1, backgroundColor: ui().line }} />
<View style={{ gap: 10 }}>
<DocLink href="https://example.com/" label="example.com" openWith="system" icon />
<DocLink href="/docs/archive" label="Archived docs" disabled />
</View>
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{link, theme};
use nsh_core::prelude::*;
fn docs() -> impl IntoElement {
link()
.href("https://example.com/docs")
.text_color(move || theme().primary)
.underline()
.hover(|s| s.opacity(0.8))
.focus(|s| s.rounded_sm().outline(px(2.0)).outline_color(move || theme().ring))
.child(text("Read the docs"))
}import { mount, Text } from "nsh";
import { Link } from "nsh/base";
function App() {
return (
<Link
href="https://example.com/docs"
class="self-start text-primary underline hover:opacity-80 focus:rounded-sm focus:outline-2 focus:outline-ring"
>
<Text>Read the docs</Text>
</Link>
);
}
mount(App);Link берется из nsh/base, крейта nsh-base в Rust. В Rust link() создает ссылку с сеттером для каждого свойства (.href(..), .open_with(..)), слушателем для каждого события (.on_open(..), .on_press(..)), типизированными методами стиля для самой ссылки и .child(..) для ее содержимого.
#Открытие
Нажатие, клик или Return либо Space на ссылке в фокусе открывает ее href:
onOpen({ href })сообщает, куда ведет ссылка.- При
openWith="system"(по умолчанию) URL другого приложения или сайта, то есть URL со схемой (https:,mailto:,tel:, схема, зарегистрированная другим приложением), открывается за пределами приложения так же, как его открываетopenUrl: сначала его обрабатывает маршрутизатор встраивающего приложения, затем система. Любой другойhref, путь самого приложения (/orders/42) или#anchor, передается в событии и остается за приложением. onPress()сообщает о нажатии.
При openWith="app" ссылку открывает обработчик onOpen приложения: собственный маршрут, страница во встроенном веб-представлении или сначала подтверждение.
import { Link } from "nsh/base";
<Link href={`/orders/${order.id}`} openWith="app" onOpen={({ href }) => router.push(href)} class="text-primary">
<Text>Order #{order.id}</Text>
</Link>
Открытие URL за пределами приложения требует разрешения openUrl, если приложение объявляет разрешения в nsh.json ("openUrl": true или { "schemes": ["https", "mailto"] }).
#Свойства
| Свойство | Rust | Тип | По умолчанию | Описание |
|---|---|---|---|---|
href | .href(..) | string | нет | Куда ведет ссылка: URL или путь приложения. Передается в onOpen, никогда не отображается. |
openWith | .open_with(LinkOpenWith::App) | "system" | "app" (LinkOpenWith) | "system" | Кто открывает href: система открывает URL другого приложения или сайта; "app" оставляет это на усмотрение onOpen. |
disabled | .disabled(..) | boolean | false | Нет нажатий, отклика на нажатие и фокуса клавиатуры; стили disabled: применяются, а скринридеры сообщают, что элемент неактивен. |
current | .current(..) | boolean | false | Ссылка на страницу, на которой находится пользователь, в навигации или хлебных крошках: скринридеры сообщают, что это текущая страница. |
accessibilityLabel | .accessibility_label(..) | string | нет | Что произносят скринридеры, когда содержимое говорит слишком мало (иконка). |
hitSlop | .hit_slop(..) | number | Partial<Edges> | нет | На сколько за пределами границ нажатия все еще засчитываются, в пунктах: одно число для всех сторон или отдельно по сторонам. Как hitSlop у view. |
children | .child(..) | JSX.Element | нет | Содержимое: подпись, иконка, всё, что показывает приложение. |
class, style | методы стиля | string, StyleProp<ViewStyle> | нет | Внешний вид ссылки: цвет, подчеркивание, отступы; свойства текста передаются вложенным в нее текстам. |
activeStyle, hoverStyle, focusStyle, disabledStyle | .active(|s| ..), .hover(..), .focus(..), .disabled_style(..) | StyleProp<ViewStyle> | нет | Стиль ссылки при нажатии, наведении, фокусе или в отключенном состоянии, как классы <state>:. |
#События
| Событие | Rust | Описание |
|---|---|---|
onOpen | .on_open(|href: &SharedString| ..) | Ссылка открывает href: при openWith="app" ее открывает обработчик, при "system" он узнает, что именно система попросила открыть. Происходит до onPress. |
onPress | .on_press(|| ..) | Ссылку нажали: тап, клик или Return либо Space, пока у нее есть фокус клавиатуры. |
Отключенная ссылка ничего не сообщает, а нажатие на нее не доходит до нажимаемых view вокруг.
#Состояния
Приложение стилизует каждое состояние с помощью классов или свойств стиля:
<Link
href="https://example.com/changelog"
class="self-start text-primary underline decoration-primary/40 transition-colors hover:decoration-primary active:opacity-70 focus:outline-2 focus:outline-ring disabled:text-muted-foreground disabled:no-underline"
>
<Text>Changelog</Text>
</Link>
Посещенные ссылки запоминает приложение: оно хранит открытые href и стилизует такие ссылки по собственному состоянию.
const visited = signal(new Set<string>());
<Link
href={href}
onOpen={({ href }) => visited.set(new Set(visited()).add(href))}
class={visited().has(href) ? "text-violet-600 underline" : "text-primary underline"}
>
<Text>{label}</Text>
</Link>
#Внутри текста
Link внутри <Text> является строчным блоком абзаца, как и любое view внутри текста: он находится в строке, сохраняет собственные состояния, фокус и роль и остается на одной строке. align-text-bottom размещает ссылку с одной строкой текста на базовой линии абзаца:
<Text class="text-[15px] text-foreground">
By continuing you accept the{" "}
<Link href="https://example.com/terms" class="align-text-bottom">
<Text class="text-[15px] text-primary underline">terms of service</Text>
</Link>
.
</Text>
Ссылка, которая переносится вместе со строками абзаца, является вложенным <Text> с role="link" и onPress (см. Text); так TextView отображает ссылки документа.
#Маленькая цель и hitSlop
Ссылка может быть меньше пальца: иконка, короткое слово. hitSlop заставляет засчитываться нажатия, попавшие рядом с ней, в пунктах за каждой из сторон, не меняя ее раскладку и внешний вид: одно число для всех сторон или отдельно по сторонам ({ top: 8, bottom: 8 }), как их принимает hitSlop у view.
<Link href="https://example.com/help" hitSlop={12} accessibilityLabel="Help" class="size-5 items-center justify-center">
<Text>?</Text>
</Link>
#Доступность
- Ссылка для скринридеров является одним элементом с ролью
"link"; ее подпись — это текст дочерних элементов илиaccessibilityLabel.hrefостается данными: скринридеры произносят подпись. currentпроизносится как текущая страница (aria-current="page"из ARIA), аdisabled— так, как платформа произносит неактивный элемент.- В macOS и вебе ссылка является остановкой фокуса клавиатуры: Tab переходит на нее, Return и Space ее открывают, а стили
focus:показывают, где находится фокус.
Общие сведения о подписях, ролях и объявлениях см. в разделе Доступность.
#Примечания по платформам
- iOS, Android и Mac Catalyst: нажатие — это тап, который заканчивается на ссылке.
- Десктопы (macOS, Windows, Linux, GPUI) и веб: нажатие — это клик. В macOS и вебе Return и Space также открывают ссылку, у которой есть фокус клавиатуры.
- В вебе ссылка отображается как элемент с
role="link". - На каждой платформе отображается то, что задает стиль приложения.