diff --git a/src/components/FilterFieldCombobox.tsx b/src/components/FilterFieldCombobox.tsx index 1bbb0ee7..049a3fbc 100644 --- a/src/components/FilterFieldCombobox.tsx +++ b/src/components/FilterFieldCombobox.tsx @@ -1,7 +1,8 @@ import { CaretUpDown } from '@phosphor-icons/react' -import { useId, useMemo, useRef, useState } from 'react' +import { useEffect, useId, useMemo, useRef, useState, type ChangeEvent, type FocusEvent, type KeyboardEvent, type RefObject } from 'react' import { Input } from '@/components/ui/input' -import { cn } from '@/lib/utils' +import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover' +import { FilterFieldOptionsList } from './filter-builder/FilterFieldOptionsList' const CONTENT_FIELDS = new Set(['body']) @@ -47,8 +48,154 @@ function initialHighlightIndex(options: string[], currentValue: string): number return currentIndex >= 0 ? currentIndex : 0 } -function optionTestId(field: string): string { - return `filter-field-option-${field.replace(/[^a-z0-9_-]+/gi, '-')}` +function stepHighlightedIndex(current: number, optionCount: number, direction: 'next' | 'previous'): number { + if (current < 0) return direction === 'next' ? 0 : optionCount - 1 + if (direction === 'next') return (current + 1) % optionCount + return (current - 1 + optionCount) % optionCount +} + +function handleFilterFieldKeyDown({ + event, + open, + options, + highlightedIndex, + openCombobox, + setHighlightedIndex, + selectOption, + closeCombobox, +}: { + event: KeyboardEvent + open: boolean + options: string[] + highlightedIndex: number + openCombobox: () => void + setHighlightedIndex: (updater: number | ((current: number) => number)) => void + selectOption: (value: string) => void + closeCombobox: () => void +}) { + switch (event.key) { + case 'ArrowDown': + event.preventDefault() + if (!open) { + openCombobox() + return + } + if (options.length === 0) return + setHighlightedIndex((current) => stepHighlightedIndex(current, options.length, 'next')) + return + case 'ArrowUp': + event.preventDefault() + if (!open) { + openCombobox() + return + } + if (options.length === 0) return + setHighlightedIndex((current) => stepHighlightedIndex(current, options.length, 'previous')) + return + case 'Enter': + if (!open || highlightedIndex < 0 || options[highlightedIndex] === undefined) return + event.preventDefault() + selectOption(options[highlightedIndex]) + return + case 'Escape': + if (!open) return + event.preventDefault() + closeCombobox() + return + default: + return + } +} + +function FilterFieldInput({ + inputRef, + open, + query, + value, + listboxId, + highlightedIndex, + onFocus, + onChange, + onKeyDown, +}: { + inputRef: RefObject + open: boolean + query: string + value: string + listboxId: string + highlightedIndex: number + onFocus: () => void + onChange: (event: ChangeEvent) => void + onKeyDown: (event: KeyboardEvent) => void +}) { + return ( + <> + = 0 ? `${listboxId}-option-${highlightedIndex}` : undefined} + className="h-8 pr-7 text-sm" + data-testid="filter-field-combobox-input" + /> +