このコミットが含まれているのは:
@@ -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 ()
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする