import { Combobox } from '@headlessui/react' import { useSelector } from '@xstate/react' import Fuse from 'fuse.js' import { useCommandsContext } from 'hooks/useCommandsContext' import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes' import { useEffect, useMemo, useRef, useState } from 'react' import { AnyStateMachine, StateFrom } from 'xstate' const contextSelector = (snapshot: StateFrom | undefined) => snapshot?.context function CommandArgOptionInput({ arg, argName, stepBack, onSubmit, placeholder, }: { arg: CommandArgument & { inputType: 'options' } argName: string stepBack: () => void onSubmit: (data: unknown) => void placeholder?: string }) { const actorContext = useSelector(arg.machineActor, contextSelector) const { commandBarSend, commandBarState } = useCommandsContext() const resolvedOptions = useMemo( () => typeof arg.options === 'function' ? arg.options(commandBarState.context, actorContext) : arg.options, [argName, arg, commandBarState.context, actorContext] ) // The initial current option is either an already-input value or the configured default const currentOption = useMemo( () => resolvedOptions.find( (o) => o.value === commandBarState.context.argumentsToSubmit[argName] ) || resolvedOptions.find((o) => o.isCurrent), [commandBarState.context.argumentsToSubmit, argName, resolvedOptions] ) const inputRef = useRef(null) const formRef = useRef(null) const [shouldSubmitOnChange, setShouldSubmitOnChange] = useState(false) const [selectedOption, setSelectedOption] = useState< CommandArgumentOption >(currentOption || resolvedOptions[0]) const initialQuery = useMemo(() => '', [arg.options, argName]) const [query, setQuery] = useState(initialQuery) const [filteredOptions, setFilteredOptions] = useState() // Create a new Fuse instance when the options change const fuse = useMemo( () => new Fuse(resolvedOptions, { keys: ['name', 'description'], threshold: 0.3, }), [argName, resolvedOptions] ) // Reset the query and selected option when the argName changes useEffect(() => { setQuery(initialQuery) setSelectedOption(currentOption || resolvedOptions[0]) }, [argName]) // Auto focus and select the input when the component mounts useEffect(() => { inputRef.current?.focus() inputRef.current?.select() }, [inputRef]) useEffect(() => { // work around to make sure the user doesn't have to press the down arrow key to focus the first option // instead this makes it move from the first hit const downArrowEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', keyCode: 40, which: 40, bubbles: true, }) inputRef?.current?.dispatchEvent(downArrowEvent) }, []) // Filter the options based on the query, // resetting the query when the options change useEffect(() => { const results = fuse.search(query).map((result) => result.item) setFilteredOptions(query.length > 0 ? results : resolvedOptions) }, [query, resolvedOptions, fuse]) function handleSelectOption(option: CommandArgumentOption) { // We deal with the whole option object internally setSelectedOption(option) // But we only submit the value itself if (shouldSubmitOnChange) { onSubmit(option.value) } } function handleSubmit(e: React.FormEvent) { e.preventDefault() // We submit the value of the selected option, not the whole object onSubmit(selectedOption.value) } return (
{ if (e.key === 'Enter') { setShouldSubmitOnChange(true) } else { setShouldSubmitOnChange(false) } }} >
!event.target.disabled && setQuery(event.target.value) } className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none" onKeyDown={(event) => { if (event.metaKey && event.key === 'k') commandBarSend({ type: 'Close' }) if (event.key === 'Backspace' && !event.currentTarget.value) { stepBack() } if (event.key === 'Enter') { setShouldSubmitOnChange(true) } else { setShouldSubmitOnChange(false) } }} value={query} placeholder={ currentOption?.name || placeholder || argName || 'Select an option' } autoCapitalize="off" autoComplete="off" autoCorrect="off" spellCheck="false" autoFocus />
{ setShouldSubmitOnChange(true) }} > {filteredOptions?.map((option) => (

{option.name}

{option.value === currentOption?.value && ( current )}
))}
) } export default CommandArgOptionInput