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