iter(g84): 数据表定宽列支持拖拽调宽
This commit is contained in:
@@ -1028,6 +1028,14 @@
|
|||||||
"acceptance": "input 补 role=switch(选中态沿用原生 checked 映射);新增可选 ariaLabel,缺省取 label,纯图标开关也有可访问名;文档 props 与演示(无文案开关实例)同步;npm run check 全绿",
|
"acceptance": "input 补 role=switch(选中态沿用原生 checked 映射);新增可选 ariaLabel,缺省取 label,纯图标开关也有可访问名;文档 props 与演示(无文案开关实例)同步;npm run check 全绿",
|
||||||
"notes": "自补:无障碍;沿用原生 checkbox 的 Space 切换行为,不重造键盘交互",
|
"notes": "自补:无障碍;沿用原生 checkbox 的 Space 切换行为,不重造键盘交互",
|
||||||
"status": "done"
|
"status": "done"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g84",
|
||||||
|
"title": "数据表定宽列支持拖拽调宽",
|
||||||
|
"motivation": "列宽只能在定义时写死,长内容把列撑开就再也调不回去;表格库缺这个基本操作,用户只能改代码重发版",
|
||||||
|
"acceptance": "width 为数字的列在表头右缘出现手柄:拖动实时改对应 <col> 宽度(下限 56px,拖出表头仍跟手),聚焦手柄后 ← → 每次 ±8px,双击复位到声明宽度;拖拽/双击不触发排序;拖拽期间全局禁选文本;样式走令牌且有 reduced-motion 兜底;dataTable ≤ 200 行;npm run check 全绿",
|
||||||
|
"notes": "自补:可用性;拖拽与键盘共用 apply 单一写宽入口,逻辑拆到 columnResize.ts 免撑大 dataTable",
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1076,7 +1076,7 @@ const content = h(
|
|||||||
section(
|
section(
|
||||||
'sec-table',
|
'sec-table',
|
||||||
'数据表格',
|
'数据表格',
|
||||||
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、工具栏筛选、底部分页。',
|
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选、底部分页。',
|
||||||
card({ title: '用户表' }, dataTable({
|
card({ title: '用户表' }, dataTable({
|
||||||
data: users,
|
data: users,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { h } from '@ui-arch/frame';
|
||||||
|
|
||||||
|
const MIN = 56; // 列宽下限:小于这个值表头文字会被压成竖排
|
||||||
|
|
||||||
|
/*** 列宽手柄:右缘拖动改 <col> 宽度(双击复位),← → 每次微调 8px,两者共用 apply 收口 ***/
|
||||||
|
export function attachResize(th: HTMLElement, col: HTMLElement, start: number, label: string): void {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
let dragging = false; // 是否处于拖拽中
|
||||||
|
let originX = 0; // 按下时的指针横坐标
|
||||||
|
let originW = 0; // 按下时的实际列宽
|
||||||
|
|
||||||
|
/*** 唯一写宽入口:夹取下限后写回 <col> ***/
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '@ui-arch/table-core';
|
} from '@ui-arch/table-core';
|
||||||
import type { PageInfo, RowKey, TableInstance } 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 { computed, effect, h, renderList, signal } from '@ui-arch/frame';
|
||||||
|
import { attachResize } from './columnResize.js';
|
||||||
import { button } from './button.js';
|
import { button } from './button.js';
|
||||||
import { skeleton } from './skeleton.js';
|
import { skeleton } from './skeleton.js';
|
||||||
import { textField } from './textField.js';
|
import { textField } from './textField.js';
|
||||||
@@ -118,6 +119,7 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
col.header ?? col.key,
|
col.header ?? col.key,
|
||||||
col.sortable ? sortMark(col.key) : null,
|
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) {
|
if (col.sortable) {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const rule = state().sorting.find((r) => r.key === col.key); // 与视觉箭头同源
|
const rule = state().sorting.find((r) => r.key === col.key); // 与视觉箭头同源
|
||||||
@@ -161,12 +163,8 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
});
|
});
|
||||||
|
|
||||||
// —— 列宽:数字落成 <col> 固定宽度,'flex' 或未设置交给浏览器自适应 ——
|
// —— 列宽:数字落成 <col> 固定宽度,'flex' 或未设置交给浏览器自适应 ——
|
||||||
const colgroup = h(
|
const cols = props.columns.map((col) => (typeof col.width === 'number' ? h('col', { style: `width:${col.width}px` }) : h('col'))); // 每个数据列一个 <col>,拖拽直接改它
|
||||||
'colgroup',
|
const colgroup = h('colgroup', null, selectable ? h('col', { style: 'width:44px' }) : null, ...cols);
|
||||||
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 filterInput = textField({
|
const filterInput = textField({
|
||||||
placeholder: `筛选 ${filterKey}…`,
|
placeholder: `筛选 ${filterKey}…`,
|
||||||
|
|||||||
@@ -128,6 +128,39 @@
|
|||||||
font-size: var(--ua-font-size-sm);
|
font-size: var(--ua-font-size-sm);
|
||||||
color: var(--ua-color-accent);
|
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 {
|
.ua-table tbody tr {
|
||||||
transition: background 0.12s;
|
transition: background 0.12s;
|
||||||
|
|||||||
Reference in New Issue
Block a user