iter(g301): 新增拖拽排序列表 sortableList
This commit is contained in:
@@ -3095,6 +3095,15 @@
|
||||
"motivation": "toast.ts 里写了 pointerenter 暂停倒计时,但样式只在 .ua-toast.is-closable / .is-action 时才给 pointer-events: auto,默认 toast 全程穿透——最常见的「toast.success(msg)」根本收不到 hover,暂停逻辑成了死代码;键盘用户 Tab 进关闭按钮时也照样被倒计时收走。",
|
||||
"acceptance": "① .ua-toast 自身接收指针,删掉 is-closable / is-action 两处补丁式 pointer-events(宿主容器仍穿透,只有卡片本体可命中);② 暂停 / 恢复抽成 pause/resume,剩余时长不低于 400ms 且重复进入不二次扣减;③ focusin / focusout 与悬停同口径暂停,焦点在卡片内部移动不恢复;④ manifest 的 closable 说明与手把手步骤同步,npm run check 全绿。",
|
||||
"notes": "pointer-events: auto 落到 .ua-toast 本体,删掉 is-closable / is-action 两处补丁规则;暂停恢复抽成 pause/resume(剩余不低于 400ms、重复进入不二次扣减),focusin/focusout 与悬停同口径、焦点在卡片内部移动不恢复;manifest 的 closable 说明与手把手步骤同步"
|
||||
},
|
||||
{
|
||||
"id": "g301",
|
||||
"domain": "new",
|
||||
"status": "open",
|
||||
"title": "新增拖拽排序列表 sortableList(88 个组件)",
|
||||
"motivation": "库里有 list 展示列表、virtualList 长列表,但缺「可调整顺序」的交互件:任务排优先级、字段顺序、看板列这类场景每次都得自己写一遍拖放,dragstart/dragover 的边界(不拦 dragover 就没 drop、拖拽中重建节点会中断手势)反复踩。",
|
||||
"acceptance": "① 整行可拖,指针越过邻行中线即换位,dragend 落定;② 键盘等价 Alt+↑↓ 上下搬一格、Alt+Home/End 搬首尾,裸 ↑↓ 留给页面滚动;③ 顺序写回 value 信号并触发 onChange,拖拽期间跳过外部重排避免中断手势;④ 句柄 { el, move, order },下标越界与禁用时静默忽略;⑤ 样式只消费 --ua-* 令牌,禁止新增硬编码阴影/时长/字号;⑥ manifest/docs/README/demo/llms-full 同步,npm run check 全绿。",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+47
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "ui-arch-registry-index/0.1",
|
||||
"generatedAt": "2026-10-26T16:25:36.184Z",
|
||||
"generatedAt": "2026-10-26T16:28:37.124Z",
|
||||
"repoRoot": ".",
|
||||
"packages": [
|
||||
{
|
||||
@@ -230,7 +230,8 @@
|
||||
"sparkline(props):迷你趋势线,把数值序列压成折线(可填充 + 末点标记),无坐标轴无交互",
|
||||
"heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细",
|
||||
"progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘",
|
||||
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报"
|
||||
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报",
|
||||
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄"
|
||||
],
|
||||
"keyTypes": [
|
||||
"ButtonProps",
|
||||
@@ -4650,6 +4651,50 @@
|
||||
"跑马灯在减少动效偏好下自动停成静态首屏——完整原文始终在 title 里,不必为可访问性另写一份",
|
||||
"点 × 是先淡出再隐藏(200ms,和 alert 同一档),回调 onClose 立刻触发:想同步清理外部状态就在回调里做,别等隐藏"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sortableList",
|
||||
"title": "拖拽排序列表 SortableList",
|
||||
"demo": "sec-sortable",
|
||||
"props": [
|
||||
{
|
||||
"n": "items",
|
||||
"t": "string[]",
|
||||
"r": 1,
|
||||
"d": "全部条目文案;文案即排序键,要求唯一(重名会让顺序回读撞车)"
|
||||
},
|
||||
{
|
||||
"n": "value",
|
||||
"t": "Signal<string[]>",
|
||||
"d": "受控顺序信号;传了则内部改动写回,外部改动即重排(拖拽中跳过)"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(next: string[]) => void",
|
||||
"k": 1,
|
||||
"d": "顺序变化回调,拖放落定 / 键盘搬位 / move 调用后各触发一次"
|
||||
},
|
||||
{
|
||||
"n": "disabled",
|
||||
"t": "boolean | Signal<boolean>",
|
||||
"d": "禁用拖拽与键盘搬位;缺省 false",
|
||||
"df": "false"
|
||||
},
|
||||
{
|
||||
"n": "emptyText",
|
||||
"t": "string",
|
||||
"d": "无条目占位文案;缺省「暂无可排序项」"
|
||||
}
|
||||
],
|
||||
"example": "const sort = sortableList({ items: ['需求评审', '接口联调', '灰度发布'], onChange: (next) => console.log(next) });\ncontainer.append(sort.el);",
|
||||
"steps": [
|
||||
"items 传全部条目文案、顺序即当前顺序:文案同时是排序键,所以必须唯一(重名会分不清谁被搬动)",
|
||||
"鼠标 / 触摸按住任意一行拖动,指针越过邻行中线就换位;松手后写回 value 信号并触发 onChange,拖拽期间不重排避免手势中断",
|
||||
"键盘等价:Tab 聚焦某行后按 Alt+↑ / Alt+↓ 上下搬一格、Alt+Home / Alt+End 搬到首尾(裸 ↑↓ 留给页面滚动)",
|
||||
"被拖的行以虚线边框 + 淡色虚影标示,落点由指针位置决定,不会出现「拖了三格只动一格」的错觉",
|
||||
"外部要按代码调整顺序用句柄 move(from, to),下标越界或禁用时静默忽略;order() 以 DOM 为唯一真相回读当前顺序",
|
||||
"什么时候别用:条目超过一屏(拖到边缘不会自动滚动,改用筛选或上下按钮)、需要跨列表搬运(用两个实例并自己管状态)"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
|
||||
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
|
||||
- `packages/skin/ui`(@ui-arch/ui):87 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
|
||||
- `packages/skin/ui`(@ui-arch/ui):88 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
|
||||
- `packages/design/tokens`(@ui-arch/tokens):设计令牌
|
||||
- `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private)
|
||||
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ ui-arch-mvp/
|
||||
│ │ ├── table-core/ # 表格状态内核
|
||||
│ │ └── frame/ # 信号 + hyperscript 微框架
|
||||
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
|
||||
│ │ └── ui/ # 87 个组件 + ui.css
|
||||
│ │ └── ui/ # 88 个组件 + ui.css
|
||||
│ ├── design/ # 资产层(可发布)
|
||||
│ │ └── tokens/ # --ua-* 设计令牌
|
||||
│ └── tools/ # 工程层(private,不发布)
|
||||
|
||||
@@ -12,13 +12,13 @@ npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全
|
||||
npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移)
|
||||
```
|
||||
|
||||
## 组件(共 87 个)
|
||||
## 组件(共 88 个)
|
||||
|
||||
<!-- ua:components -->
|
||||
accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · codeBlock · colorPicker · commandPalette · copyButton · countdown · dataTable · datePicker · descriptions · diffView · divider · drawer · empty · floatButton · form · grid · heatmap · highlight · image · inputNumber · inputTag · jsonViewer · kbd · layout · link · list · loading · menu · modal · mentions · multiSelect · noticeBar · notification · pagination · pinInput · popover · popconfirm · progress · progressRing · radioGroup · rate · relativeTime · result · segmented · select · signaturePad · skeleton · slider · space · sparkline · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · timePicker · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · virtualList · watermark
|
||||
accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · codeBlock · colorPicker · commandPalette · copyButton · countdown · dataTable · datePicker · descriptions · diffView · divider · drawer · empty · floatButton · form · grid · heatmap · highlight · image · inputNumber · inputTag · jsonViewer · kbd · layout · link · list · loading · menu · modal · mentions · multiSelect · noticeBar · notification · pagination · pinInput · popover · popconfirm · progress · progressRing · radioGroup · rate · relativeTime · result · segmented · select · signaturePad · skeleton · slider · sortableList · space · sparkline · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · timePicker · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · virtualList · watermark
|
||||
<!-- /ua:components -->
|
||||
|
||||
分组:表单(button / copyButton / signaturePad / textField / inputNumber / inputTag / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / timePicker / pinInput / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / loading / toast / notification / noticeBar / result / spinner / progress / progressRing / skeleton / badge / empty / statistic / countdown)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / jsonViewer / diffView / codeBlock / relativeTime / sparkline / heatmap / descriptions / card / list / kbd / highlight / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / layout / link / space / divider / splitter / backTop / affix / tour / watermark)。
|
||||
分组:表单(button / copyButton / signaturePad / textField / inputNumber / inputTag / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / timePicker / pinInput / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / loading / toast / notification / noticeBar / result / spinner / progress / progressRing / skeleton / badge / empty / statistic / countdown)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / jsonViewer / diffView / codeBlock / relativeTime / sparkline / heatmap / descriptions / card / list / sortableList / kbd / highlight / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / layout / link / space / divider / splitter / backTop / affix / tour / watermark)。
|
||||
|
||||
## 可访问性约定
|
||||
|
||||
@@ -34,7 +34,7 @@ accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup
|
||||
|---|---|
|
||||
| `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) |
|
||||
| `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) |
|
||||
| `packages/skin/ui` | @ui-arch/ui:87 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
|
||||
| `packages/skin/ui` | @ui-arch/ui:88 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
|
||||
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
|
||||
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
|
||||
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
|
||||
|
||||
@@ -79,6 +79,7 @@ import {
|
||||
showToast,
|
||||
signaturePad,
|
||||
skeleton,
|
||||
sortableList,
|
||||
splitter,
|
||||
slider,
|
||||
sparkline,
|
||||
@@ -329,6 +330,10 @@ effect(() => {
|
||||
localStorage.setItem(THEME_KEY, mode); // 记住选择,刷新不闪
|
||||
});
|
||||
|
||||
// —— 拖拽排序演示:顺序写回信号,下方摘要随之刷新 ——
|
||||
const sortableOrder = signal(['需求评审', '接口联调', '灰度发布', '线上回归']);
|
||||
const sortableDemo = sortableList({ items: ['需求评审', '接口联调', '灰度发布', '线上回归'], value: sortableOrder });
|
||||
|
||||
const content = h(
|
||||
'div',
|
||||
{ class: 'page' },
|
||||
@@ -1914,6 +1919,17 @@ content.append(
|
||||
noticeBar({ text: '连接已断开,正在重试第 3 次…', variant: 'danger', icon: '⚠' }),
|
||||
),
|
||||
),
|
||||
section(
|
||||
'sec-sortable',
|
||||
'拖拽排序',
|
||||
'sortableList:整行可拖(越过邻行中线即换位),Alt+↑↓ 上下搬一格、Alt+Home/End 搬首尾;松手写回 value 信号,摘要行跟着刷新。',
|
||||
h(
|
||||
'div',
|
||||
{ class: 'col' },
|
||||
sortableDemo.el,
|
||||
h('p', { class: 'desc' }, () => `当前顺序:${sortableOrder().join(' → ')}`),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 悬浮按钮:fixed 定位,挂 body 不参与页面布局
|
||||
|
||||
+30
-1
@@ -1,6 +1,6 @@
|
||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||
|
||||
> 自动生成于 2026-10-26T16:25:36.184Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 自动生成于 2026-10-26T16:28:37.124Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||
|
||||
## 快速上手
|
||||
@@ -2495,6 +2495,34 @@ noticeBar({ text: '系统将于今晚 23:00 维护 10 分钟', scroll: true })
|
||||
|
||||
现场演示:apps/docs/demo.html#sec-notice
|
||||
|
||||
### sortableList — 拖拽排序列表 SortableList
|
||||
|
||||
参数:
|
||||
|
||||
- items(必填):string[] —— 全部条目文案;文案即排序键,要求唯一(重名会让顺序回读撞车)
|
||||
- value:Signal<string[]> —— 受控顺序信号;传了则内部改动写回,外部改动即重排(拖拽中跳过)
|
||||
- onChange(回调):(next: string[]) => void —— 顺序变化回调,拖放落定 / 键盘搬位 / move 调用后各触发一次
|
||||
- disabled(默认 false):boolean | Signal<boolean> —— 禁用拖拽与键盘搬位;缺省 false
|
||||
- emptyText:string —— 无条目占位文案;缺省「暂无可排序项」
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
const sort = sortableList({ items: ['需求评审', '接口联调', '灰度发布'], onChange: (next) => console.log(next) });
|
||||
container.append(sort.el);
|
||||
```
|
||||
|
||||
手把手:
|
||||
|
||||
1. items 传全部条目文案、顺序即当前顺序:文案同时是排序键,所以必须唯一(重名会分不清谁被搬动)
|
||||
2. 鼠标 / 触摸按住任意一行拖动,指针越过邻行中线就换位;松手后写回 value 信号并触发 onChange,拖拽期间不重排避免手势中断
|
||||
3. 键盘等价:Tab 聚焦某行后按 Alt+↑ / Alt+↓ 上下搬一格、Alt+Home / Alt+End 搬到首尾(裸 ↑↓ 留给页面滚动)
|
||||
4. 被拖的行以虚线边框 + 淡色虚影标示,落点由指针位置决定,不会出现「拖了三格只动一格」的错觉
|
||||
5. 外部要按代码调整顺序用句柄 move(from, to),下标越界或禁用时静默忽略;order() 以 DOM 为唯一真相回读当前顺序
|
||||
6. 什么时候别用:条目超过一屏(拖到边缘不会自动滚动,改用筛选或上下按钮)、需要跨列表搬运(用两个实例并自己管状态)
|
||||
|
||||
现场演示:apps/docs/demo.html#sec-sortable
|
||||
|
||||
API 速览:
|
||||
|
||||
- button(props):按钮,variant 三档视觉强度,disabled / loading 支持布尔或信号动态同步(loading 转圈并禁点),onClick 点击回调
|
||||
@@ -2586,6 +2614,7 @@ API 速览:
|
||||
- heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细
|
||||
- progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘
|
||||
- noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报
|
||||
- sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄
|
||||
|
||||
一行用法:
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
- heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细
|
||||
- progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘
|
||||
- noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报
|
||||
- sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄
|
||||
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
|
||||
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
|
||||
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
|
||||
|
||||
@@ -87,3 +87,4 @@ export { sparkline } from './sparkline.js';
|
||||
export { heatmap } from './heatmap.js';
|
||||
export { progressRing } from './progressRing.js';
|
||||
export { noticeBar } from './noticeBar.js';
|
||||
export { sortableList } from './sortableList.js';
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { effect, h, signal } from '@ui-arch/frame';
|
||||
import { empty } from './empty.js';
|
||||
import type { SortableListHandle, SortableListProps } from './types.js';
|
||||
|
||||
/*** 拖拽排序列表:整行可拖(HTML5 拖放)+ Alt+↑↓/Home/End 键盘搬位,顺序变化写回 value 信号并触发 onChange ***/
|
||||
export function sortableList(props: SortableListProps): SortableListHandle {
|
||||
// —— 状态集中声明 ——
|
||||
const value = props.value ?? signal(props.items); // 当前顺序:受控信号优先,否则内部持有
|
||||
const disabled = (): boolean => (typeof props.disabled === 'function' ? props.disabled() : props.disabled === true); // 禁用读法:布尔直读、信号保持响应性
|
||||
const handle = (el: HTMLElement): string => el.dataset.label ?? ''; // 行 → 条目文案(文案即排序键)
|
||||
let dragging: HTMLElement | null = null; // 正在被拖的行:dragover 据此搬节点
|
||||
const rows = new Map(props.items.map((text) => [text, h('div', {
|
||||
class: 'ua-sortable-item', role: 'listitem', tabindex: '0', draggable: 'true', 'data-label': text,
|
||||
}, h('span', { class: 'ua-sortable-grip', 'aria-hidden': 'true' }, '⠿'), h('span', { class: 'ua-sortable-label' }, text))])); // 行只建一次,排序靠搬节点
|
||||
|
||||
// —— 空列表提前返回:句柄三件套给齐,调用方无需判空 ——
|
||||
if (!props.items.length) {
|
||||
const el = h('div', { class: 'ua-sortable' }, empty({ title: props.emptyText ?? '暂无可排序项' })) as HTMLElement;
|
||||
return { el, move: (): void => undefined, order: (): string[] => [] };
|
||||
}
|
||||
|
||||
/*** 同步无障碍位次:role=list 的 posinset 随顺序变 ***/
|
||||
const syncAria = (): void => { Array.from(root.children).forEach((el, i) => el.setAttribute('aria-posinset', String(i + 1))); };
|
||||
/*** 按给定顺序重排 DOM:只搬已有节点,不重建元素(拖拽中重建会中断手势) ***/
|
||||
const place = (next: string[]): void => {
|
||||
next.forEach((text) => { const row = rows.get(text); if (row) root.append(row); }); // 依次 append 即按序落位
|
||||
syncAria();
|
||||
};
|
||||
/*** 提交当前 DOM 顺序:顺序没变就不打扰外部 ***/
|
||||
const commit = (): void => {
|
||||
const next = Array.from(root.children).map((el) => handle(el as HTMLElement));
|
||||
if (next.join('\u0000') === value().join('\u0000')) return;
|
||||
value(next);
|
||||
props.onChange?.(next);
|
||||
};
|
||||
|
||||
const root = h(
|
||||
'div',
|
||||
{ class: 'ua-sortable', role: 'list' },
|
||||
...(props.value ? props.value() : props.items).map((text) => rows.get(text)).filter((row): row is HTMLElement => !!row),
|
||||
) as HTMLElement;
|
||||
syncAria();
|
||||
|
||||
// —— 鼠标 / 触摸拖放:dragstart 记源行,dragover 就地搬节点,dragend 落定提交 ——
|
||||
root.addEventListener('dragstart', (e) => {
|
||||
const row = (e.target as HTMLElement).closest('.ua-sortable-item') as HTMLElement | null;
|
||||
if (!row || disabled()) { e.preventDefault(); return; } // 禁用时不许拖起
|
||||
dragging = row;
|
||||
row.classList.add('is-dragging');
|
||||
e.dataTransfer?.setData('text/plain', handle(row)); // Firefox 不设数据就不派发 dragover
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
root.addEventListener('dragover', (e) => {
|
||||
if (!dragging) return;
|
||||
e.preventDefault(); // 不拦截就拿不到 drop 与后续事件
|
||||
const over = (e.target as HTMLElement).closest('.ua-sortable-item') as HTMLElement | null;
|
||||
if (!over || over === dragging) return;
|
||||
const box = over.getBoundingClientRect();
|
||||
const after = e.clientY > box.top + box.height / 2; // 过中线即插到其后,与指针位置一致
|
||||
over.parentElement?.insertBefore(dragging, after ? over.nextSibling : over);
|
||||
});
|
||||
root.addEventListener('dragend', () => {
|
||||
if (!dragging) return;
|
||||
dragging.classList.remove('is-dragging');
|
||||
dragging = null;
|
||||
syncAria();
|
||||
commit();
|
||||
});
|
||||
|
||||
// —— 键盘等价:Alt 组合才搬位,裸 ↑↓ 留给页面滚动 ——
|
||||
root.addEventListener('keydown', (e) => {
|
||||
if (!e.altKey || disabled()) return;
|
||||
const row = (e.target as HTMLElement).closest('.ua-sortable-item') as HTMLElement | null;
|
||||
if (!row) return;
|
||||
const order = Array.from(root.children) as HTMLElement[];
|
||||
const at = order.indexOf(row);
|
||||
const to = e.key === 'Home' ? 0 : e.key === 'End' ? order.length - 1
|
||||
: e.key === 'ArrowUp' ? at - 1 : e.key === 'ArrowDown' ? at + 1 : at;
|
||||
if (to === at) return; // 已在顶 / 底:静默忽略
|
||||
e.preventDefault();
|
||||
root.insertBefore(row, to < at ? order[to] : order[to].nextSibling); // 向下搬要插到目标之后
|
||||
row.focus(); // 节点搬动后显式收回焦点,读屏与连续搬位都不断线
|
||||
syncAria();
|
||||
commit();
|
||||
});
|
||||
|
||||
// —— 受控回写:外部改顺序信号即重排,拖拽中跳过以免打断手势 ——
|
||||
effect(() => {
|
||||
const next = value();
|
||||
if (!root.children.length || dragging) return; // 首帧尚未挂载 / 正在拖
|
||||
const now = Array.from(root.children).map((el) => handle(el as HTMLElement));
|
||||
if (next.join('\u0000') === now.join('\u0000')) return;
|
||||
place(next);
|
||||
});
|
||||
|
||||
return {
|
||||
el: root,
|
||||
/*** 程序化搬位:下标越界或禁用时静默忽略 ***/
|
||||
move: (from: number, to: number): void => {
|
||||
const order = Array.from(root.children) as HTMLElement[];
|
||||
if (disabled() || from === to || !order[from] || to < 0 || to >= order.length) return;
|
||||
const row = order[from];
|
||||
root.insertBefore(row, to < from ? order[to] : order[to].nextSibling);
|
||||
syncAria();
|
||||
commit();
|
||||
},
|
||||
order: (): string[] => Array.from(root.children).map((el) => handle(el as HTMLElement)),
|
||||
};
|
||||
}
|
||||
@@ -1130,3 +1130,19 @@ export interface NoticeBarProps {
|
||||
closable?: boolean; // 是否显示关闭按钮;缺省 true
|
||||
onClose?: () => void; // 关闭回调
|
||||
}
|
||||
|
||||
/*** 拖拽排序列表入参 ***/
|
||||
export interface SortableListProps {
|
||||
items: string[]; // 全部条目文案,文案即排序键(要求唯一)
|
||||
value?: Signal<string[]>; // 受控顺序信号;传了则内部改动写回,外部改动即重排
|
||||
onChange?: (next: string[]) => void; // 顺序变化回调(拖放落定 / 键盘搬位 / move 调用后各一次)
|
||||
disabled?: boolean | Signal<boolean>; // 禁用拖拽与键盘搬位;缺省 false
|
||||
emptyText?: string; // 无条目占位文案;缺省「暂无可排序项」
|
||||
}
|
||||
|
||||
/*** 拖拽排序列表句柄 ***/
|
||||
export interface SortableListHandle {
|
||||
el: HTMLElement; // 列表根节点
|
||||
move: (from: number, to: number) => void; // 程序化搬位:下标越界或禁用时静默忽略
|
||||
order: () => string[]; // 读取当前顺序(以 DOM 为唯一真相)
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"sparkline(props):迷你趋势线,把数值序列压成折线(可填充 + 末点标记),无坐标轴无交互",
|
||||
"heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细",
|
||||
"progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘",
|
||||
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报"
|
||||
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报",
|
||||
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄"
|
||||
],
|
||||
"keyTypes": [
|
||||
"ButtonProps",
|
||||
@@ -4521,6 +4522,50 @@
|
||||
"跑马灯在减少动效偏好下自动停成静态首屏——完整原文始终在 title 里,不必为可访问性另写一份",
|
||||
"点 × 是先淡出再隐藏(200ms,和 alert 同一档),回调 onClose 立刻触发:想同步清理外部状态就在回调里做,别等隐藏"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sortableList",
|
||||
"title": "拖拽排序列表 SortableList",
|
||||
"demo": "sec-sortable",
|
||||
"props": [
|
||||
{
|
||||
"n": "items",
|
||||
"t": "string[]",
|
||||
"r": 1,
|
||||
"d": "全部条目文案;文案即排序键,要求唯一(重名会让顺序回读撞车)"
|
||||
},
|
||||
{
|
||||
"n": "value",
|
||||
"t": "Signal<string[]>",
|
||||
"d": "受控顺序信号;传了则内部改动写回,外部改动即重排(拖拽中跳过)"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(next: string[]) => void",
|
||||
"k": 1,
|
||||
"d": "顺序变化回调,拖放落定 / 键盘搬位 / move 调用后各触发一次"
|
||||
},
|
||||
{
|
||||
"n": "disabled",
|
||||
"t": "boolean | Signal<boolean>",
|
||||
"d": "禁用拖拽与键盘搬位;缺省 false",
|
||||
"df": "false"
|
||||
},
|
||||
{
|
||||
"n": "emptyText",
|
||||
"t": "string",
|
||||
"d": "无条目占位文案;缺省「暂无可排序项」"
|
||||
}
|
||||
],
|
||||
"example": "const sort = sortableList({ items: ['需求评审', '接口联调', '灰度发布'], onChange: (next) => console.log(next) });\ncontainer.append(sort.el);",
|
||||
"steps": [
|
||||
"items 传全部条目文案、顺序即当前顺序:文案同时是排序键,所以必须唯一(重名会分不清谁被搬动)",
|
||||
"鼠标 / 触摸按住任意一行拖动,指针越过邻行中线就换位;松手后写回 value 信号并触发 onChange,拖拽期间不重排避免手势中断",
|
||||
"键盘等价:Tab 聚焦某行后按 Alt+↑ / Alt+↓ 上下搬一格、Alt+Home / Alt+End 搬到首尾(裸 ↑↓ 留给页面滚动)",
|
||||
"被拖的行以虚线边框 + 淡色虚影标示,落点由指针位置决定,不会出现「拖了三格只动一格」的错觉",
|
||||
"外部要按代码调整顺序用句柄 move(from, to),下标越界或禁用时静默忽略;order() 以 DOM 为唯一真相回读当前顺序",
|
||||
"什么时候别用:条目超过一屏(拖到边缘不会自动滚动,改用筛选或上下按钮)、需要跨列表搬运(用两个实例并自己管状态)"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2699,6 +2699,49 @@ button.ua-step-dot:hover {
|
||||
}
|
||||
}
|
||||
|
||||
/* —— 拖拽排序列表:整行可拖,Alt+↑↓ 键盘搬位 —— */
|
||||
.ua-sortable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ua-space-1);
|
||||
}
|
||||
.ua-sortable-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ua-space-2);
|
||||
padding: var(--ua-space-2) var(--ua-space-3);
|
||||
border: 1px solid var(--ua-color-border);
|
||||
border-radius: var(--ua-radius-md);
|
||||
background: var(--ua-color-surface);
|
||||
color: var(--ua-color-text);
|
||||
font-size: var(--ua-font-size-md);
|
||||
cursor: grab;
|
||||
transition: background var(--ua-duration-fast) var(--ua-ease), border-color var(--ua-duration-fast) var(--ua-ease);
|
||||
}
|
||||
.ua-sortable-item:hover {
|
||||
background: var(--ua-color-tint-accent-weak); /* 低视觉比重的悬停底色 */
|
||||
}
|
||||
.ua-sortable-item:active {
|
||||
background: var(--ua-color-surface-3); /* 拿起时按下加深一档,与悬停区分 */
|
||||
cursor: grabbing;
|
||||
}
|
||||
.ua-sortable-item:focus-visible {
|
||||
outline: var(--ua-focus-outline);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.ua-sortable-item.is-dragging {
|
||||
border-style: dashed;
|
||||
border-color: var(--ua-color-accent);
|
||||
background: var(--ua-color-tint-accent);
|
||||
opacity: 0.7; /* 拖拽中的虚影:本体留在原地随指针走,稍淡以示「正在被搬」 */
|
||||
}
|
||||
.ua-sortable-grip {
|
||||
flex: none;
|
||||
color: var(--ua-color-text-faint);
|
||||
font-size: var(--ua-font-size-sm);
|
||||
line-height: var(--ua-leading-none);
|
||||
}
|
||||
|
||||
/* —— 轮播 —— */
|
||||
.ua-carousel {
|
||||
position: relative;
|
||||
|
||||
Reference in New Issue
Block a user