このコミットが含まれているのは:
2026-10-26 22:57:39 +09:00
コミット 5493673996
60個のファイルの変更、1202行の追加、273行の削除
+57 -20
ファイルの表示
@@ -16,8 +16,29 @@ type Selection = { identity: string; anchor: Point; end: Point }
const interactive = 'a,button,input,select,textarea,summary,[contenteditable],[data-counts-resize]'
const cellGrid = (table: HTMLTableElement): HTMLTableCellElement[][] => {
const grid: HTMLTableCellElement[][] = []
for (const [row, tr] of Array.from (table.tBodies[0]?.rows ?? []).entries ())
{
grid[row] ??= []
let column = 0
for (const cell of Array.from (tr.cells))
{
while (grid[row][column] != null) ++column
for (let y = 0; y < cell.rowSpan; ++y)
{
grid[row + y] ??= []
for (let x = 0; x < cell.colSpan; ++x) grid[row + y][column + x] = cell
}
column += cell.colSpan
}
}
return grid
}
const cellAt = (table: HTMLTableElement, point: Point) =>
table.tBodies[0]?.rows[point.row]?.cells[point.column]
cellGrid (table)[point.row]?.[point.column]
const CountsSelectableTable = (
@@ -66,31 +87,48 @@ const CountsSelectableTable = (
}
}, [])
useLayoutEffect (() => {
const rows = root.current?.tBodies[0]?.rows
if (rows == null) return
for (const [row, tr] of Array.from (rows).entries ())
if (root.current == null) return
const grid = cellGrid (root.current)
const assigned = new Set<HTMLTableCellElement> ()
for (const [row, cells] of grid.entries ())
{
for (const [column, cell] of Array.from (tr.cells).entries ())
for (const [column, cell] of cells.entries ())
{
const selected = bounds != null && row >= bounds.top && row <= bounds.bottom
&& column >= bounds.left && column <= bounds.right
cell.id = `${id}-${row}-${column}`
cell.setAttribute ('role', 'gridcell')
cell.setAttribute ('aria-selected', String (selected))
if (selected) cell.dataset.countsSelected = ''
else delete cell.dataset.countsSelected
if (assigned.has (cell)) continue
assigned.add (cell)
const selected = bounds != null && row <= bounds.bottom
&& row + cell.rowSpan - 1 >= bounds.top && column <= bounds.right
&& column + cell.colSpan - 1 >= bounds.left
cell.id = `${id}-${row}-${column}`
if (cell.getAttribute ('role') !== 'rowheader') cell.setAttribute ('role', 'gridcell')
cell.setAttribute ('aria-selected', String (selected))
if (selected) cell.dataset.countsSelected = ''
else delete cell.dataset.countsSelected
}
}
if (bounds != null)
root.current.setAttribute ('aria-activedescendant', grid[bounds.bottom]?.[bounds.right]?.id ?? '')
else root.current.removeAttribute ('aria-activedescendant')
}, [bounds, children, id])
const pointFor = (element: Element | null): Point | null => {
const pointFor = (element: Element | null, y?: number): Point | null => {
const table = root.current
if (table == null || element == null || !table.contains (element)) return null
let cell = element.closest ('td')
while (cell != null && cell.closest ('table') !== table)
cell = cell.closest ('table')?.parentElement?.closest ('td') ?? null
if (cell == null || cell.parentElement?.parentElement !== table.tBodies[0]) return null
return { row: (cell.parentElement as HTMLTableRowElement).sectionRowIndex,
column: (cell as HTMLTableCellElement).cellIndex }
const grid = cellGrid (table)
const hitRow = y == null ? -1 : Array.from (table.tBodies[0].rows).findIndex (row => {
const rect = row.getBoundingClientRect ()
return y >= rect.top && y < rect.bottom
})
for (const [row, cells] of grid.entries ())
{
if (hitRow >= 0 && row !== hitRow) continue
const column = cells.indexOf (cell as HTMLTableCellElement)
if (column >= 0) return { row, column }
}
return null
}
const closeMenu = useCallback (() => {
setMenu (null)
@@ -181,8 +219,7 @@ const CountsSelectableTable = (
<table ref={root} role="grid" tabIndex={0} aria-multiselectable="true"
aria-label={name?.text ?? message ('selection.table').text}
aria-describedby={`${id}-hint`}
aria-activedescendant={selection == null ? undefined
: `${id}-${selection.end.row}-${selection.end.column}`}
style={{ tableLayout: 'fixed', width: totalWidth }}
className={cn ('counts-grid select-none border-separate border-spacing-0 outline-none',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500',
@@ -213,7 +250,7 @@ const CountsSelectableTable = (
if (event.clientX >= rect.left + element.clientLeft + element.clientWidth
|| event.clientY >= rect.top + element.clientTop + element.clientHeight) return
}
const point = pointFor (element)
const point = pointFor (element, event.clientY)
if (point == null) return
event.stopPropagation ()
setMenu (null)
@@ -246,7 +283,7 @@ const CountsSelectableTable = (
event.clientX - touchPress.current.x, event.clientY - touchPress.current.y) > 8)
finish ()
if (!dragging.current || event.buttons === 0) return
const point = pointFor (document.elementFromPoint (event.clientX, event.clientY))
const point = pointFor (document.elementFromPoint (event.clientX, event.clientY), event.clientY)
if (point == null) return
if (touchArmed.current && touchPress.current != null
&& Math.hypot (event.clientX - touchPress.current.x,
@@ -263,7 +300,7 @@ const CountsSelectableTable = (
onContextMenu={event => {
const element = event.target as Element
if (element.closest ('table') !== event.currentTarget) return
const point = pointFor (element)
const point = pointFor (element, event.clientY)
if (point == null || element.closest ('[data-counts-resize]') != null) return
event.preventDefault ()
event.stopPropagation ()