89 行
3.3 KiB
TypeScript
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
|