shinkan-jinkendo/frontend/src/components/SkillTreeSelect.jsx

85 lines
2.4 KiB
React
Raw Normal View History

import React, { useEffect, useMemo, useRef, useState } from 'react'
import { skillCatalogPathLabel } from '../utils/skillCatalogTree'
import SkillTreePickerPanel from './SkillTreePickerPanel'
/**
* Einzelauswahl einer Fähigkeit als hierarchische Treeview.
*/
export default function SkillTreeSelect({
value = '',
onChange,
skills = [],
excludeIds,
placeholder = 'Fähigkeit wählen…',
disabled = false,
className = '',
searchPlaceholder = 'Fähigkeit suchen…',
}) {
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const rootRef = useRef(null)
const selected = useMemo(() => {
const id = value ? Number(value) : NaN
if (!Number.isFinite(id)) return null
return skills.find((s) => Number(s.id) === id) || null
}, [value, skills])
const displayLabel = selected ? skillCatalogPathLabel(selected) : ''
useEffect(() => {
const onDoc = (e) => {
if (!rootRef.current?.contains(e.target)) {
setOpen(false)
setQuery('')
}
}
document.addEventListener('mousedown', onDoc)
return () => document.removeEventListener('mousedown', onDoc)
}, [])
const pick = (skillId) => {
onChange(String(skillId))
setOpen(false)
setQuery('')
}
return (
<div className={`skill-tree-select ${className}`.trim()} ref={rootRef}>
<button
type="button"
className="form-input skill-tree-select__trigger"
disabled={disabled}
aria-haspopup="listbox"
aria-expanded={open}
onClick={() => !disabled && setOpen((o) => !o)}
>
<span className={displayLabel ? '' : 'skill-tree-select__placeholder'}>
{displayLabel || placeholder}
</span>
</button>
{open ? (
<div className="skill-tree-select__panel">
<input
type="search"
className="form-input skill-tree-select__search"
placeholder={searchPlaceholder}
value={query}
onChange={(e) => setQuery(e.target.value)}
autoComplete="off"
/>
<div className="skill-tree-select__tree-wrap">
<SkillTreePickerPanel
skills={skills}
excludeIds={excludeIds}
searchQuery={query}
onPickSkill={pick}
pickMode="single"
/>
</div>
</div>
) : null}
</div>
)
}