このコミットが含まれているのは:
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import CountsResizeHandle from '@/components/counts/CountsResizeHandle'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { CountSort } from '@/lib/counts'
|
||||
|
||||
|
||||
const CountsSortHeader = (
|
||||
{ field, label, sorts, change, structured = false }: {
|
||||
field: string
|
||||
label: { text: string; language: string }
|
||||
sorts: CountSort[]
|
||||
change: (key: string, additive: boolean) => void
|
||||
structured?: boolean },
|
||||
) => {
|
||||
const { message } = useUITranslation ('counts')
|
||||
const press = useRef<{ x: number; y: number
|
||||
timer: ReturnType<typeof setTimeout> } | null> (null)
|
||||
const suppressClick = useRef (false)
|
||||
const cancel = () => {
|
||||
if (press.current != null) clearTimeout (press.current.timer)
|
||||
press.current = null
|
||||
}
|
||||
useEffect (() => () => {
|
||||
if (press.current != null) clearTimeout (press.current.timer)
|
||||
}, [])
|
||||
const priority = sorts.findIndex (sort => sort.key === field)
|
||||
const direction = sorts[priority]?.direction
|
||||
const state = priority < 0 ? 'none' : (direction === 'asc' ? 'ascending' : 'descending')
|
||||
const title = message (structured ? 'sort.structured' : 'sort.action')
|
||||
const aria = message (priority < 0 ? 'sort.unsortedLabel'
|
||||
: (sorts.length === 1 ? 'sort.singleLabel' : 'sort.label'), {
|
||||
column: label.text, priority: priority + 1,
|
||||
direction: message (`sort.${state}`).text })
|
||||
|
||||
return (
|
||||
<th className="relative overflow-hidden whitespace-nowrap px-1 py-1"
|
||||
aria-sort={(sorts.length > 1 && priority >= 0) ? 'other' : state}>
|
||||
<Button type="button" variant="ghost" size="sm"
|
||||
className="h-auto w-full max-w-full justify-start select-none px-1 py-0 pr-3"
|
||||
lang={aria.language} aria-label={aria.text} title={title.text}
|
||||
onPointerDown={event => {
|
||||
if (!event.isPrimary)
|
||||
{
|
||||
cancel ()
|
||||
return
|
||||
}
|
||||
suppressClick.current = false
|
||||
if (event.pointerType !== 'touch' && event.pointerType !== 'pen')
|
||||
return
|
||||
cancel ()
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
press.current = { x, y, timer: setTimeout (() => {
|
||||
press.current = null
|
||||
suppressClick.current = true
|
||||
change (field, true)
|
||||
}, 500) }
|
||||
}}
|
||||
onPointerMove={event => {
|
||||
if (press.current != null && Math.hypot (
|
||||
event.clientX - press.current.x, event.clientY - press.current.y) > 8)
|
||||
cancel ()
|
||||
}}
|
||||
onPointerUp={cancel} onPointerCancel={cancel} onPointerLeave={cancel}
|
||||
onContextMenu={event => {
|
||||
if (press.current != null || suppressClick.current)
|
||||
event.preventDefault ()
|
||||
}}
|
||||
onKeyDown={event => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return
|
||||
event.preventDefault ()
|
||||
suppressClick.current = false
|
||||
change (field, event.ctrlKey || event.metaKey)
|
||||
}}
|
||||
onClick={event => {
|
||||
if (suppressClick.current)
|
||||
{
|
||||
suppressClick.current = false
|
||||
event.preventDefault ()
|
||||
return
|
||||
}
|
||||
change (field, event.ctrlKey || event.metaKey)
|
||||
}}>
|
||||
<span className="truncate" lang={label.language}>{label.text}</span>
|
||||
{priority >= 0 && <span aria-hidden>
|
||||
{direction === 'asc' ? '↑' : '↓'}{sorts.length > 1 && priority + 1}</span>}
|
||||
</Button>
|
||||
<CountsResizeHandle field={field} label={label}/>
|
||||
</th>)
|
||||
}
|
||||
|
||||
|
||||
export default CountsSortHeader
|
||||
新しいイシューから参照
ユーザーをブロックする