ファイル
btrc-hub/frontend/src/components/counts/CountsCellMenu.tsx
T
2026-10-25 16:47:05 +09:00

89 行
3.3 KiB
TypeScript

import { useEffect, useLayoutEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { useUITranslation } from '@/lib/useUITranslation'
export type CountsMenuAction = 'copy' | 'copyHeaders' | 'row' | 'column' | 'all'
const CountsCellMenu = (
{ x, y, act, close }: {
x: number
y: number
act: (action: CountsMenuAction) => void
close: () => void },
) => {
const root = useRef<HTMLDivElement> (null)
const { label, message } = useUITranslation ('counts')
useLayoutEffect (() => {
const menu = root.current
if (menu == null) return
const rect = menu.getBoundingClientRect ()
menu.style.left = `${Math.max (4, Math.min (x, window.innerWidth - rect.width - 4))}px`
menu.style.top = `${Math.max (4, Math.min (y, window.innerHeight - rect.height - 4))}px`
menu.querySelector ('button')?.focus ({ preventScroll: true })
}, [x, y])
useEffect (() => {
const outside = (event: Event) => {
if (!root.current?.contains (event.target as Node)) close ()
}
const escape = (event: KeyboardEvent) => {
if (event.key === 'Escape')
{
event.preventDefault ()
event.stopPropagation ()
close ()
}
}
document.addEventListener ('pointerdown', outside, true)
document.addEventListener ('keydown', escape, true)
window.addEventListener ('resize', close)
document.addEventListener ('scroll', outside, true)
return () => {
document.removeEventListener ('pointerdown', outside, true)
document.removeEventListener ('keydown', escape, true)
window.removeEventListener ('resize', close)
document.removeEventListener ('scroll', outside, true)
}
}, [close])
const actions: CountsMenuAction[] = ['copy', 'copyHeaders', 'row', 'column', 'all']
return createPortal (
<div ref={root} role="menu" aria-label={message ('selection.table').text}
className="fixed z-50 min-w-40 rounded border border-slate-400 bg-white
py-1 text-sm text-slate-900 shadow-lg dark:border-slate-600
dark:bg-slate-800 dark:text-slate-100"
style={{ left: x, top: y }} onContextMenu={event => event.preventDefault ()}
onPointerDown={event => event.stopPropagation ()}
onKeyDown={event => {
event.stopPropagation ()
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase () === 'c')
{
event.preventDefault ()
act ('copy')
return
}
const buttons = Array.from (root.current?.querySelectorAll ('button') ?? [])
const current = buttons.indexOf (document.activeElement as HTMLButtonElement)
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes (event.key))
{
event.preventDefault ()
const next = event.key === 'Home' ? 0 : (event.key === 'End' ? buttons.length - 1
: (current + (event.key === 'ArrowDown' ? 1 : -1) + buttons.length)
% buttons.length)
buttons[next]?.focus ()
}
if (event.key === 'Tab') close ()
}}>
{actions.map (action => (
<button key={action} type="button" role="menuitem"
className="block w-full px-3 py-1 text-left hover:bg-slate-200
hover:text-slate-900 focus:bg-slate-200 focus:text-slate-900
dark:hover:bg-slate-700 dark:hover:text-slate-100
dark:focus:bg-slate-700 dark:focus:text-slate-100"
onClick={() => act (action)}>{label (`selection.${action}`)}</button>))}
</div>, document.body)
}
export default CountsCellMenu