({ field, value, editing, onStartEdit, onEndEdit, onCommit }: CellProps)
| 809 | const SELECT_PANEL_WIDTH = 224 |
| 810 | |
| 811 | function SelectCell({ field, value, editing, onStartEdit, onEndEdit, onCommit }: CellProps): JSX.Element { |
| 812 | const multi = field.type === 'multiSelect' |
| 813 | const selected = multi ? splitMultiSelect(value) : value ? [value] : [] |
| 814 | const triggerRef = useRef<HTMLButtonElement | null>(null) |
| 815 | const panelRef = useRef<HTMLDivElement | null>(null) |
| 816 | const [rect, setRect] = useState<DOMRect | null>(null) |
| 817 | const [draft, setDraft] = useState('') |
| 818 | |
| 819 | useEffect(() => { |
| 820 | setRect(editing && triggerRef.current ? triggerRef.current.getBoundingClientRect() : null) |
| 821 | }, [editing]) |
| 822 | |
| 823 | useEffect(() => { |
| 824 | if (!editing) return |
| 825 | const onDown = (e: MouseEvent): void => { |
| 826 | const t = e.target as Node |
| 827 | if (triggerRef.current?.contains(t) || panelRef.current?.contains(t)) return |
| 828 | onEndEdit() |
| 829 | } |
| 830 | window.addEventListener('mousedown', onDown, true) |
| 831 | return () => window.removeEventListener('mousedown', onDown, true) |
| 832 | }, [editing, onEndEdit]) |
| 833 | |
| 834 | const toggle = (optValue: string): void => { |
| 835 | if (multi) { |
| 836 | const next = selected.includes(optValue) |
| 837 | ? selected.filter((v) => v !== optValue) |
| 838 | : [...selected, optValue] |
| 839 | onCommit(next.join(', ')) |
| 840 | } else { |
| 841 | onCommit(selected[0] === optValue ? '' : optValue) |
| 842 | onEndEdit() |
| 843 | } |
| 844 | } |
| 845 | |
| 846 | const chips = ( |
| 847 | <div className="flex flex-wrap gap-1"> |
| 848 | {selected.length === 0 ? ( |
| 849 | <span className="text-ink-400">—</span> |
| 850 | ) : ( |
| 851 | selected.map((v) => ( |
| 852 | <span |
| 853 | key={v} |
| 854 | className="rounded-full bg-accent/15 px-2 py-0.5 text-2xs font-medium text-accent ring-1 ring-accent/30" |
| 855 | > |
| 856 | {optionLabel(field, v)} |
| 857 | </span> |
| 858 | )) |
| 859 | )} |
| 860 | </div> |
| 861 | ) |
| 862 | |
| 863 | // Portal the option list to the body so the table's overflow doesn't clip it; |
| 864 | // flip above the cell when there isn't room below. |
| 865 | const placeAbove = !!rect && rect.bottom > window.innerHeight - 280 |
| 866 | const panelStyle: CSSProperties = rect |
| 867 | ? { |
| 868 | position: 'fixed', |
nothing calls this directly
no test coverage detected