このコミットが含まれているのは:
@@ -8,13 +8,13 @@ import type { ComponentPropsWithoutRef, FC, FocusEvent } from 'react'
|
||||
const pad = (n: number): string => n.toString ().padStart (2, '0')
|
||||
|
||||
|
||||
const toDateTimeLocalValue = (value: Date) => {
|
||||
const toDateTimeLocalValue = (value: Date, seconds = false) => {
|
||||
const y = value.getFullYear ()
|
||||
const m = pad (value.getMonth () + 1)
|
||||
const day = pad (value.getDate ())
|
||||
const h = pad (value.getHours ())
|
||||
const min = pad (value.getMinutes ())
|
||||
return `${ y }-${ m }-${ day }T${ h }:${ min }`
|
||||
return `${ y }-${ m }-${ day }T${ h }:${ min }${seconds ? `:${pad (value.getSeconds ())}` : ''}`
|
||||
}
|
||||
|
||||
|
||||
@@ -37,15 +37,17 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'onChange'> & {
|
||||
onChange?: (isoUTC: string | null) => void
|
||||
className?: string
|
||||
onBlur?: (ev: FocusEvent<HTMLInputElement>) => void
|
||||
invalid?: boolean }
|
||||
invalid?: boolean
|
||||
precision?: 'minute' | 'second' }
|
||||
|
||||
|
||||
const DateTimeField: FC<Props> = ({ value, onChange, className, onBlur, invalid, ...rest }) => {
|
||||
const DateTimeField: FC<Props> = props => {
|
||||
const { value, onChange, className, onBlur, invalid, precision = 'minute', ...rest } = props
|
||||
const [local, setLocal] = useState ('')
|
||||
|
||||
useEffect (() => {
|
||||
setLocal (value ? toDateTimeLocalValue (new Date (value)) : '')
|
||||
}, [value])
|
||||
setLocal (value ? toDateTimeLocalValue (new Date (value), precision === 'second') : '')
|
||||
}, [value, precision])
|
||||
|
||||
return (
|
||||
<input
|
||||
@@ -61,13 +63,15 @@ const DateTimeField: FC<Props> = ({ value, onChange, className, onBlur, invalid,
|
||||
'focus:ring-2 focus:ring-blue-200']),
|
||||
className)}
|
||||
type="datetime-local"
|
||||
step={60}
|
||||
step={precision === 'second' ? 1 : 60}
|
||||
value={local}
|
||||
aria-invalid={invalid}
|
||||
onChange={ev => {
|
||||
const v = ev.target.value
|
||||
setLocal (v)
|
||||
onChange?.(v ? toMinutePrecisionIsoUtc (v) : null)
|
||||
const iso = v ? (precision === 'second'
|
||||
? new Date (v).toISOString () : toMinutePrecisionIsoUtc (v)) : null
|
||||
onChange?.(iso)
|
||||
}}
|
||||
onBlur={onBlur}/>)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useId, useRef } from 'react'
|
||||
import { CircleHelp } from 'lucide-react'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
|
||||
const HelpPopover = ({ label, children }: {
|
||||
label: { text: string; language: string }; children: ReactNode },
|
||||
) => {
|
||||
const id = useId ()
|
||||
const trigger = useRef<HTMLButtonElement> (null)
|
||||
const panel = useRef<HTMLDivElement> (null)
|
||||
return <>
|
||||
<button ref={trigger} type="button" popoverTarget={id} aria-label={label.text}
|
||||
lang={label.language} className="inline-flex align-middle"
|
||||
onClick={() => {
|
||||
const rect = trigger.current?.getBoundingClientRect ()
|
||||
if (panel.current != null && rect != null)
|
||||
{
|
||||
panel.current.style.top = `${rect.bottom + 4}px`
|
||||
panel.current.style.left = `${Math.min (rect.left, window.innerWidth - 288)}px`
|
||||
}
|
||||
}}><CircleHelp size={14}/></button>
|
||||
<div ref={panel} id={id} popover="auto" tabIndex={-1}
|
||||
role="dialog" aria-label={label.text} lang={label.language}
|
||||
onToggle={event => {
|
||||
if (event.newState === 'open') panel.current?.focus ()
|
||||
}} className="fixed m-0 w-72 max-w-[90vw] rounded border bg-white p-3
|
||||
text-sm text-slate-900 shadow-lg dark:border-slate-600 dark:bg-slate-900
|
||||
dark:text-slate-100">{children}</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
export default HelpPopover
|
||||
@@ -12,6 +12,7 @@ import type { Tag } from '@/types'
|
||||
|
||||
type Props = {
|
||||
includeNico?: boolean
|
||||
includeUnused?: boolean
|
||||
describedBy?: string
|
||||
invalid?: boolean
|
||||
value: string
|
||||
@@ -20,6 +21,7 @@ type Props = {
|
||||
|
||||
const TagInput: FC<Props> = ({
|
||||
includeNico = false,
|
||||
includeUnused = false,
|
||||
describedBy,
|
||||
invalid,
|
||||
value,
|
||||
@@ -57,11 +59,11 @@ const TagInput: FC<Props> = ({
|
||||
{
|
||||
const tags = await apiGet<Tag[]> ('/tags/autocomplete', {
|
||||
locale, signal: controller.signal,
|
||||
params: { q: request.query, nico: includeNico ? '1' : '0' } })
|
||||
params: { q: request.query.replace (/^not:/i, ''), nico: includeNico ? '1' : '0' } })
|
||||
if (active)
|
||||
{
|
||||
setResult ({ locale, includeNico, request,
|
||||
tags: tags.filter (tag => tag.postCount > 0) })
|
||||
tags: includeUnused ? tags : tags.filter (tag => tag.postCount > 0) })
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -74,7 +76,7 @@ const TagInput: FC<Props> = ({
|
||||
active = false
|
||||
controller.abort ()
|
||||
}
|
||||
}, [request, locale, includeNico])
|
||||
}, [request, locale, includeNico, includeUnused])
|
||||
|
||||
useEffect (() => {
|
||||
setActiveIndex (-1)
|
||||
@@ -85,7 +87,8 @@ const TagInput: FC<Props> = ({
|
||||
// TODO: TagSearch からのコピペのため,共通化を考へる.
|
||||
const handleTagSelect = (tag: Tag) => {
|
||||
const parts = value?.split (' ')
|
||||
parts[parts.length - 1] = tag.name
|
||||
const negative = /^not:/i.test (parts[parts.length - 1])
|
||||
parts[parts.length - 1] = `${negative ? 'not:' : ''}${tag.name}`
|
||||
setValue (parts.join (' ') + ' ')
|
||||
setRequest ({ query: '' })
|
||||
setActiveIndex (-1)
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする