四、集合组件
React Aria 提供了一套强大的集合组件系统,用于处理列表、表格、树等结构化数据。
4.1 useListBox - 列表框
useListBox 用于创建可访问的列表框,支持单选/多选、键盘导航等。通常与 useListBoxSection 和 useOption 结合使用。
import { useListBox, useOption } from 'react-aria';
import { useListState } from 'react-stately';
function ListBox(props) {
let state = useListState(props);
let ref = useRef(null);
let { listBoxProps } = useListBox(props, state, ref);
return (
<ul {...listBoxProps} ref={ref} style={
{
margin: 0,
padding: '8px 0',
listStyle: 'none',
border: '1px solid #ccc',
borderRadius: '4px',
background: 'white',
width: '200px'
}}>
{[...state.collection].map(item => (
<Option key={item.key} item={item} state={state} />
))}
</ul>
);
}
function Option({ item, state }) {
let ref = useRef(null);
let { optionProps, isSelected, isFocused } = useOption({ key: item.key }, state, ref);
return (
<li {...optionProps} ref={ref} style={
{
padding: '8px 12px',
background: isSelected ? '#007bff' : isFocused ? '#f0f0f0' : 'transparent',
color: isSelected ? 'white' : 'black',
cursor: 'pointer'
}}>
{item.rendered}
</li>
);
}
4.2 useTable - 表格组件
http://htnus.cn
useTable 系列 Hooks(useTable、useTableColumnHeader、useTableRow、useTableCell)用于创建可访问的数据表格,支持列排序、选择、键盘导航等高级功能。
import { useTable, useTableColumnHeader, useTableRow, useTableCell } from 'react-aria';
import { useTableState } from 'react-stately';
function Table(props) {
let state = useTableState(props);
let ref = useRef(null);
let { gridProps } = useTable(props, state, ref);
return (
<div style={
{ overflow: 'auto' }}>
<table {...gridProps} ref={ref} style={
{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
<tr>
{[...state.collection.headerRows].map(headerRow => (
<TableRow key={headerRow.key} item={headerRow} state={state} isHeader />
))}
</tr>
</thead>
<tbody>
{[...state.collection.body].map(row => (
<TableRow key={row.key} item={row} state={state} />
))}
</tbody>
</table>
</div>
);
}
function TableRow({ item, state, isHeader }) {
let ref = useRef(null);
let { rowProps } = useTableRow({ node: item }, state, ref);
return (
<tr {...rowProps} ref={ref}>
{[...item.childNodes].map(column => (
isHeader
? <TableHeaderColumn key={column.key} column={column} state={state} />
: <TableCell key={column.key} column={column} state={state} />
))}
</tr>
);
}
function TableHeaderColumn({ column, state }) {
let ref = useRef(null);
let { columnHeaderProps } = useTableColumnHeader({ node: column }, state, ref);
return (
<th {...columnHeaderProps} ref={ref} style={
{ borderBottom: '2px solid #ccc', padding: '12px', textAlign: 'left' }}>
{column.rendered}
</th>
);
}
function TableCell({ column, state }) {
let ref = useRef(null);
let { gridCellProps } = useTableCell({ node: column }, state, ref);
return (
<td {...gridCellProps} ref={ref} style={
{ borderBottom: '1px solid #eee', padding: '12px' }}>
{column.rendered}
</td>
);
}
4.3 useVirtualizer - 虚拟滚动
在处理大量数据时,useVirtualizer 可以显著提升性能,只渲染视口内的内容。
import { useTable, useVirtualizer } from 'react-aria';
import { useTableState } from 'react-stately';
function VirtualTable(props) {
let state = useTableState(props);
let ref = useRef(null);
let { gridProps } = useTable(props, state, ref);
// 计算行高和可见范围
let rowHeight = 48;
let rows = [...state.collection.body];
let totalHeight = rows.length * rowHeight;
// 根据滚动位置计算可见行
let isScrolling = useVirtualizer();
let [scrollTop, setScrollTop] = useState(0);
let startIndex = Math.floor(scrollTop / rowHeight);
let endIndex = Math.min(startIndex + Math.ceil(ref.current?.clientHeight / rowHeight) + 5, rows.length);
let visibleRows = rows.slice(startIndex, endIndex);
let offsetY = startIndex * rowHeight;
const handleScroll = (e) => {
setScrollTop(e.currentTarget.scrollTop);
};
return (
<div ref={ref} onScroll={handleScroll} style={
{ height: 400, overflow: 'auto' }}>
<div {...gridProps} style={
{ position: 'relative', height: totalHeight }}>
<div style={
{ position: 'absolute', top: offsetY, left: 0, right: 0 }}>
<tr>
{visibleRows.map(row => (
<TableRow key={row.key} item={row} state={state} />
))}
</tbody>
</div>
</div>
</div>
);
}
五、拖拽与排序
React Aria 提供了完整的拖拽支持,包括拖拽源、放置目标、拖拽预览等。useDrag 和 useDrop 是核心的基础 Hooks。
5.1 useDrag - 拖拽源
import { useDrag } from 'react-aria';
function DraggableItem({ item }) {
let ref = useRef(null);
let { dragProps, isDragging } = useDrag({
getItems: () => [{ 'text/plain': item.name }],
onDragStart: () => console.log('拖拽开始'),
onDragEnd: () => console.log('拖拽结束')
});
return (
<div ref={ref} {...dragProps} style={
{
padding: '8px 12px',
margin: '4px',
background: isDragging ? '#e0e0e0' : '#f5f5f5',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'grab',
opacity: isDragging ? 0.5 : 1
}}>
{item.name}
</div>
);
}
5.2 useDrop - 放置目标
import { useDrop } from 'react-aria';
function DropZone({ onDrop }) {
let ref = useRef(null);
let { dropProps, isDropTarget } = useDrop({
onDrop: async (e) => {
let items = await Promise.all(
e.items
.filter(item => item.kind === 'string' && item.types.has('text/plain'))
.map(item => item.getText('text/plain'))
);
onDrop(items);
}
});
return (
<div ref={ref} {...dropProps} style={
{
padding: '100px',
border: `2px dashed ${isDropTarget ? '#007bff' : '#ccc'}`,
borderRadius: '8px',
textAlign: 'center',
background: isDropTarget ? '#f0f8ff' : 'white'
}}>
拖拽文件或元素到这里
</div>
);
}