diff --git a/.ua/backlog.json b/.ua/backlog.json index 6a217b2..d04b993 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -1028,6 +1028,14 @@ "acceptance": "input 补 role=switch(选中态沿用原生 checked 映射);新增可选 ariaLabel,缺省取 label,纯图标开关也有可访问名;文档 props 与演示(无文案开关实例)同步;npm run check 全绿", "notes": "自补:无障碍;沿用原生 checkbox 的 Space 切换行为,不重造键盘交互", "status": "done" + }, + { + "id": "g84", + "title": "数据表定宽列支持拖拽调宽", + "motivation": "列宽只能在定义时写死,长内容把列撑开就再也调不回去;表格库缺这个基本操作,用户只能改代码重发版", + "acceptance": "width 为数字的列在表头右缘出现手柄:拖动实时改对应 宽度(下限 56px,拖出表头仍跟手),聚焦手柄后 ← → 每次 ±8px,双击复位到声明宽度;拖拽/双击不触发排序;拖拽期间全局禁选文本;样式走令牌且有 reduced-motion 兜底;dataTable ≤ 200 行;npm run check 全绿", + "notes": "自补:可用性;拖拽与键盘共用 apply 单一写宽入口,逻辑拆到 columnResize.ts 免撑大 dataTable", + "status": "open" } ] } diff --git a/apps/demo/main.ts b/apps/demo/main.ts index aa92c7d..0b033d6 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -1076,7 +1076,7 @@ const content = h( section( 'sec-table', '数据表格', - '粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、工具栏筛选、底部分页。', + '粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选、底部分页。', card({ title: '用户表' }, dataTable({ data: users, selectable: true, diff --git a/packages/skin/ui/src/columnResize.ts b/packages/skin/ui/src/columnResize.ts new file mode 100644 index 0000000..e2aebec --- /dev/null +++ b/packages/skin/ui/src/columnResize.ts @@ -0,0 +1,51 @@ +import { h } from '@ui-arch/frame'; + +const MIN = 56; // 列宽下限:小于这个值表头文字会被压成竖排 + +/*** 列宽手柄:右缘拖动改 宽度(双击复位),← → 每次微调 8px,两者共用 apply 收口 ***/ +export function attachResize(th: HTMLElement, col: HTMLElement, start: number, label: string): void { + // —— 状态集中声明 —— + let dragging = false; // 是否处于拖拽中 + let originX = 0; // 按下时的指针横坐标 + let originW = 0; // 按下时的实际列宽 + + /*** 唯一写宽入口:夹取下限后写回 ***/ + const apply = (w: number): void => { col.style.width = `${Math.max(MIN, Math.round(w))}px`; }; + + const grip = h('span', { + class: 'ua-col-grip', + role: 'separator', // 可调节分隔符语义:读屏可识别为「可调整」 + 'aria-orientation': 'vertical', + 'aria-label': `调整「${label}」列宽`, + tabindex: '0', + onpointerdown: (e: PointerEvent) => { + dragging = true; + originX = e.clientX; + originW = col.getBoundingClientRect().width; // 取实际渲染宽度,避免与 min-width 打架 + grip.setPointerCapture(e.pointerId); // 捕获指针:拖出表头也继续跟手 + e.preventDefault(); // 不触发列头排序 + e.stopPropagation(); + document.body.classList.add('ua-col-resizing'); // 拖拽期间全局禁用文本选中 + }, + onpointermove: (e: PointerEvent) => { if (dragging) apply(originW + e.clientX - originX); }, + onpointerup: stop, + onpointercancel: stop, + onclick: (e: MouseEvent) => e.stopPropagation(), // 点手柄不该触发排序 + ondblclick: (e: MouseEvent) => { e.stopPropagation(); apply(start); }, // 双击回到声明宽度 + onkeydown: (e: KeyboardEvent) => { + if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; // 卫语句:只接管方向键 + e.preventDefault(); + e.stopPropagation(); // 不冒泡到表格的行选择等外层快捷键 + apply(col.getBoundingClientRect().width + (e.key === 'ArrowRight' ? 8 : -8)); + }, + }) as HTMLElement; + + /*** 拖拽收尾:复位标志与全局样式 ***/ + function stop(): void { + if (!dragging) return; // 卫语句:不在拖拽中 + dragging = false; + document.body.classList.remove('ua-col-resizing'); + } + + th.append(grip); +} diff --git a/packages/skin/ui/src/dataTable.ts b/packages/skin/ui/src/dataTable.ts index c209037..3426f46 100644 --- a/packages/skin/ui/src/dataTable.ts +++ b/packages/skin/ui/src/dataTable.ts @@ -7,6 +7,7 @@ import { } from '@ui-arch/table-core'; import type { PageInfo, RowKey, TableInstance } from '@ui-arch/table-core'; import { computed, effect, h, renderList, signal } from '@ui-arch/frame'; +import { attachResize } from './columnResize.js'; import { button } from './button.js'; import { skeleton } from './skeleton.js'; import { textField } from './textField.js'; @@ -118,6 +119,7 @@ export function dataTable(props: DataTableProps): H col.header ?? col.key, col.sortable ? sortMark(col.key) : null, ); + if (typeof col.width === 'number') attachResize(el, cols[props.columns.indexOf(col)], col.width, String(col.header ?? col.key)); // 定宽列可拖拽 if (col.sortable) { effect(() => { const rule = state().sorting.find((r) => r.key === col.key); // 与视觉箭头同源 @@ -161,12 +163,8 @@ export function dataTable(props: DataTableProps): H }); // —— 列宽:数字落成 固定宽度,'flex' 或未设置交给浏览器自适应 —— - const colgroup = h( - 'colgroup', - null, - selectable ? h('col', { style: 'width:44px' }) : null, - ...props.columns.map((col) => (typeof col.width === 'number' ? h('col', { style: `width:${col.width}px` }) : h('col'))), - ); + const cols = props.columns.map((col) => (typeof col.width === 'number' ? h('col', { style: `width:${col.width}px` }) : h('col'))); // 每个数据列一个 ,拖拽直接改它 + const colgroup = h('colgroup', null, selectable ? h('col', { style: 'width:44px' }) : null, ...cols); const filterInput = textField({ placeholder: `筛选 ${filterKey}…`, diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 5f3d7a6..7fda853 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -128,6 +128,39 @@ font-size: var(--ua-font-size-sm); color: var(--ua-color-accent); } +/* 列宽手柄:右缘 7px 热区 + 1px 视觉线,拖拽/悬停/聚焦时显形 */ +.ua-col-grip { + position: absolute; + top: 0; + right: -4px; + width: 9px; + height: 100%; + cursor: col-resize; + touch-action: none; +} +.ua-col-grip::after { + content: ''; + position: absolute; + top: 6px; + bottom: 6px; + left: 4px; + width: 1px; + background: transparent; + transition: background 0.15s; +} +.ua-col-grip:hover::after, +.ua-col-grip:focus-visible::after { + background: var(--ua-color-accent); +} +body.ua-col-resizing { + user-select: none; + cursor: col-resize; +} +@media (prefers-reduced-motion: reduce) { + .ua-col-grip::after { + transition: none; + } +} /* 斑马纹 + 悬停高亮 + 选中行(低视觉比重:淡色底 + 弱化文字) */ .ua-table tbody tr { transition: background 0.12s;