跳到主要内容

偏好设置

Preferences 把用户偏好写到 data-* 属性上供 CSS 使用,可持久化、可作用于整页,并在首帧前应用已保存的值。

Preferences(偏好设置)把偏好值放到 data-* 属性上并保持更新,让你的 CSS 直接跟着用户的选择走:列表还是网格、紧凑还是宽松、浅色还是深色。它不渲染任何控件,切换按钮由你自己写;它负责应用属性、保存值,以及在服务端渲染时提前生效。

用法

Preferences 只有一个根,以 display: contents 渲染,不产生自己的盒子。偏好实例来自 @ui-sdk/core 的 createPreference 或 createColorScheme:

Preferences
└── 你的内容(属性写在 Preferences 元素上,global 时写在 html 上)

先定义偏好,指定可选值、默认值与要写出的属性:

preferences.tsts
import { createPreference } from "@ui-sdk/core";

const view = createPreference({
  values: ["list", "grid"],
  defaultValue: "list",
  attribute: "data-view",
});

export { view };

把它交给 Preferences,然后按属性写样式:

file-list.tsxtsx
import { Preferences } from "@ui-sdk/react/preferences";
import { view } from "./preferences";

function FileList() {
  return (
    <Preferences preferences={[view]}>
      <ul data-slot="files">
        <li>三季度路线图.pdf</li>
        <li>发布检查清单.md</li>
      </ul>
    </Preferences>
  );
}
[data-view="grid"] [data-slot="files"] {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}

初始是 data-view="list"。调用 view.set("grid") 后属性更新,列表变成网格。

持久化保存

加上 storage,选择在两次访问之间保留:

import { createLocalStorage, createPreference } from "@ui-sdk/core";

const view = createPreference({
  values: ["list", "grid"],
  defaultValue: "list",
  attribute: "data-view",
  storage: createLocalStorage("app-preferences-view"),
});

改动写入本地存储,刷新后恢复。也可以用 createSessionStorage 或 createCookieStorage;服务端渲染时 cookie 存储要按请求创建,详见服务端渲染。保存的值不在 values 里时会被忽略,回到 defaultValue。

读取与切换

值会影响渲染内容时用 usePreference,它在变化时更新组件,也可以用在 Preferences 之外。同一个偏好实例在哪里需要就传到哪里:

import { Button } from "@ui-sdk/react/button";
import { usePreference } from "@ui-sdk/react/preferences";
import { view } from "./preferences";

function ViewSwitch() {
  const { value } = usePreference(view);

  return (
    <div role="group" aria-label="视图">
      <Button
        aria-pressed={value === "list"}
        onClick={(event) =>
          view.set("list", { reason: "press", event: event.nativeEvent })
        }
      >
        列表
      </Button>
      <Button
        aria-pressed={value === "grid"}
        onClick={(event) =>
          view.set("grid", { reason: "press", event: event.nativeEvent })
        }
      >
        网格
      </Button>
    </div>
  );
}

set(next, { reason, event }) 返回是否真的改变,reason 默认 "imperative"。偏好自己的 onValueChange 在变化前触发,details.cancel() 可以拒绝这次变更,参见状态与事件。

整页与多个偏好

定义时设 global: true,属性就写到 <html> 上,整页样式都能用;仍然要把它传给 Preferences,一个挂载的 Preferences 就够:

const density = createPreference({
  values: ["comfortable", "compact"],
  defaultValue: "comfortable",
  attribute: "data-density",
  global: true,
  storage: createLocalStorage("app-preferences-density"),
});

一次可以应用多个偏好,各自用自己的属性与 global 设置:

import { createColorScheme, createLocalStorage } from "@ui-sdk/core";

const colorScheme = createColorScheme({
  storage: createLocalStorage("app-color-scheme"),
});

<Preferences preferences={[view, density, colorScheme]}>
  <App />
</Preferences>;

共享同一个实例的多个包装器跟随同一个值;要互相独立的选择就用不同实例与不同存储键。createColorScheme 的可选值是 "system"、"light"、"dark",写出 data-color-scheme,并额外解析浏览器的偏好。

服务端渲染

服务端渲染时 Preferences 会输出一段脚本,在页面绘制前应用已保存的值,CSS 可以立刻使用这些属性。usePreference 先用服务端的值,hydration 之后再更新。

内容安全策略要求 nonce 时,把响应里的 nonce 传进来:

<Preferences preferences={[view]} nonce={nonce}>
  <App />
</Preferences>

基于 cookie 的偏好要按请求创建,避免把一位访客的选择带给另一位。完整做法参见服务端渲染。

样式与状态

Preferences 没有自己的 data-slot 状态属性,它写出的就是你在 attribute 里指定的那个属性,值来自 values。样式全部围绕这些属性展开:

[data-view="grid"] [data-slot="files"] {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 12px;
}
[data-density="compact"] [data-slot="files"] li {
  padding-block: 4px;
}
[data-color-scheme="dark"] {
  color-scheme: dark;
}

global 偏好写在 <html> 上,因此选择器可以从根开始匹配整页;非 global 偏好只影响 Preferences 内部的内容。详见样式与动画。

API

Preferences:

属性 类型 说明
preferences readonly PreferenceLike[] 要应用的偏好,来自 createPreference 或 createColorScheme。
nonce string 内容安全策略给内联脚本的 nonce。

usePreference(preference) 返回偏好当前状态并在变化时更新组件:普通偏好是 { value },配色方案还包含 system(浏览器偏好)与 resolved(实际生效的方案)。

createPreference 的选项:values、defaultValue、attribute(必须以 data- 开头)、global、storage、onValueChange。实例提供 get()、set(next, change?)、subscribe(listener)、initialState 与 follow(store, key)。

键盘与无障碍

Preferences 本身不渲染可聚焦元素,没有键盘交互,也不播报变化。无障碍表现取决于你写的控件:

  • 一组互斥的视图按钮用 role="group" 加 aria-label 命名,每个按钮用 aria-pressed 表达当前选择;也可以改用分段控件或切换。
  • 属性变化不会自动通知屏幕阅读器。选择会改变内容含义时,自己补一条状态文本。
  • 深浅色切换要说明当前是「跟随系统」还是显式选择,不要只靠图标颜色。

相关组件

  • 分段控件:少量互斥选项的切换控件。
  • 切换:开关式的单个偏好。
  • 按钮:带 aria-pressed 的切换按钮。
  • 服务端渲染:首帧前应用偏好与按请求隔离存储。

输入关键词搜索…

↑↓ 移动↵ 选择Esc 关闭