From cc7eab9dcff5815c43d352f649cbd8e40512e49e Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Tue, 10 Feb 2026 08:48:07 -0700 Subject: [PATCH] feat: updated highlight status when using the new filter bar (#42623) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? The experience is now based on the keyboard - Selecting a filter can be done by pressing left or backspace on the keyboard - Pressing enter allows you to select and edit that filter - Pressing backspace again on a filters value deletes it - Left or right arrows does selection, enter to edit ## Demo ![filters_new_keyboard](https://github.com/user-attachments/assets/3bc6f348-5a03-448f-93b8-a48ac3068c52) ## Summary by CodeRabbit ## Release Notes * **New Features** * Filter conditions now display visual highlighting with a distinct ring outline when navigated via keyboard * Enhanced keyboard navigation behavior for more intuitive filter management * Filter condition highlights can be cleared using the Escape key * Improved navigation flow when moving through filter groups and conditions --- .../src/FilterBar/FilterBarContext.tsx | 30 +- .../src/FilterBar/FilterCondition.tsx | 5 +- .../ui-patterns/src/FilterBar/FilterGroup.tsx | 14 +- packages/ui-patterns/src/FilterBar/hooks.ts | 42 ++- .../ui-patterns/src/FilterBar/menuItems.ts | 7 +- packages/ui-patterns/src/FilterBar/types.ts | 25 +- .../ui-patterns/src/FilterBar/useAIFilter.ts | 5 +- .../src/FilterBar/useCommandHandling.ts | 7 +- .../src/FilterBar/useCommandMenu.ts | 11 +- .../src/FilterBar/useKeyboardNavigation.ts | 266 +++++++++--------- 10 files changed, 246 insertions(+), 166 deletions(-) diff --git a/packages/ui-patterns/src/FilterBar/FilterBarContext.tsx b/packages/ui-patterns/src/FilterBar/FilterBarContext.tsx index a9bf5c3fdbc..25aaae94f29 100644 --- a/packages/ui-patterns/src/FilterBar/FilterBarContext.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterBarContext.tsx @@ -2,9 +2,15 @@ import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react' -import { ActiveInput, useFilterBarState, useOptionsCache } from './hooks' +import { useFilterBarState, useOptionsCache } from './hooks' import { MenuItem } from './menuItems' -import { FilterBarAction, FilterGroup, FilterOptionObject, FilterProperty } from './types' +import { + ActiveInputState, + FilterBarAction, + FilterGroup, + FilterOptionObject, + FilterProperty, +} from './types' import { useCommandHandling } from './useCommandHandling' import { useKeyboardNavigation } from './useKeyboardNavigation' import { @@ -20,15 +26,16 @@ export type FilterBarContextValue = { // Core state filters: FilterGroup filterProperties: FilterProperty[] - activeInput: ActiveInput + activeInput: ActiveInputState freeformText: string isLoading: boolean error: string | null + highlightedConditionPath: number[] | null // Handlers onFilterChange: (filters: FilterGroup) => void onFreeformTextChange: (text: string) => void - setActiveInput: (input: ActiveInput) => void + setActiveInput: (input: ActiveInputState) => void handleInputChange: (path: number[], value: string) => void handleOperatorChange: (path: number[], value: string) => void handleRemoveCondition: (path: number[]) => void @@ -110,6 +117,8 @@ export function FilterBarRoot({ setActiveInput, newPathRef, setIsCommandMenuVisible, + highlightedConditionPath, + setHighlightedConditionPath, } = useFilterBarState() const { loadingOptions, propertyOptionsCache, loadPropertyOptions, optionsError } = @@ -163,6 +172,8 @@ export function FilterBarRoot({ setActiveInput, activeFilters: filters, onFilterChange, + highlightedConditionPath, + setHighlightedConditionPath, }) const handleInputFocus = useCallback( @@ -229,14 +240,20 @@ export function FilterBarRoot({ } setIsCommandMenuVisible(false) setActiveInput(null) + // Clear highlight when clicking outside + setHighlightedConditionPath(null) }, 0) - }, [setIsCommandMenuVisible, setActiveInput, hideTimeoutRef]) + }, [setIsCommandMenuVisible, setActiveInput, hideTimeoutRef, setHighlightedConditionPath]) const handleGroupFreeformChange = useCallback( (_path: number[], value: string) => { + // Clear highlight when user types + if (highlightedConditionPath) { + setHighlightedConditionPath(null) + } onFreeformTextChange(value) }, - [onFreeformTextChange] + [onFreeformTextChange, highlightedConditionPath, setHighlightedConditionPath] ) const handleLabelClick = useCallback( @@ -282,6 +299,7 @@ export function FilterBarRoot({ freeformText, isLoading: loading, error, + highlightedConditionPath, // Handlers onFilterChange, diff --git a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx index ae0f5cd5afc..dc75b55b5b1 100644 --- a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx @@ -22,6 +22,7 @@ export type FilterConditionProps = { path: number[] isActive: boolean isOperatorActive: boolean + isHighlighted: boolean } export function FilterCondition({ @@ -29,6 +30,7 @@ export function FilterCondition({ path, isActive, isOperatorActive, + isHighlighted, }: FilterConditionProps) { const { filters: rootFilters, @@ -210,7 +212,8 @@ export function FilterCondition({ ref={wrapperRef} className={cn( 'flex items-stretch px-0 bg-muted group shrink-0', - variant === 'pill' ? 'rounded border' : 'border-r' + variant === 'pill' ? 'rounded border' : 'border-r', + isHighlighted && 'ring-2 ring-primary' )} > { + if (!highlightedConditionPath) return false + return pathsEqual(conditionPath, highlightedConditionPath) + } + const items = useMemo( () => buildPropertyItems({ @@ -106,7 +112,8 @@ export function FilterGroup({ group, path }: FilterGroupProps) { (index) => { if (items[index]) handleSelectMenuItem(items[index]) }, - handleKeyDown + handleKeyDown, + { skipEnterWhenFilterHighlighted: highlightedConditionPath !== null } ) useEffect(() => { @@ -156,12 +163,15 @@ export function FilterGroup({ group, path }: FilterGroupProps) { path={currentPath} isActive={isConditionActive(currentPath)} isOperatorActive={isOperatorActive(currentPath)} + isHighlighted={isConditionHighlighted(currentPath)} /> )} ) })} - 0}> + 0 && !highlightedConditionPath} + > {isRootGroup ? ( (null) const [isCommandMenuVisible, setIsCommandMenuVisible] = useState(false) const hideTimeoutRef = useRef(null) - const [activeInput, setActiveInput] = useState(null) - const newPathRef = useRef([]) + const [activeInput, setActiveInput] = useState(null) + const newPathRef = useRef([]) + const [highlightedConditionPath, setHighlightedConditionPath] = useState( + null + ) return { isLoading, @@ -30,6 +37,8 @@ export function useFilterBarState() { activeInput, setActiveInput, newPathRef, + highlightedConditionPath, + setHighlightedConditionPath, } } @@ -122,7 +131,8 @@ export function useDeferredBlur(wrapperRef: React.RefObject, onBlur export function useHighlightNavigation( itemsLength: number, onEnter: (index: number) => void, - fallbackKeyDown?: (e: React.KeyboardEvent) => void + fallbackKeyDown?: (e: React.KeyboardEvent) => void, + options?: HighlightNavigationOptions ) { const [highlightedIndex, setHighlightedIndex] = useState(0) @@ -145,7 +155,11 @@ export function useHighlightNavigation( return } if (e.key === 'Enter' || e.key === 'Tab') { - // Only select dropdown item if there are items available + // Edge case: when a filter is highlighted, skip dropdown selection and let fallback handle it + if (options?.skipEnterWhenFilterHighlighted) { + if (fallbackKeyDown) fallbackKeyDown(e) + return + } if (itemsLength > 0) { e.preventDefault() onEnter(highlightedIndex) @@ -160,7 +174,13 @@ export function useHighlightNavigation( } if (fallbackKeyDown) fallbackKeyDown(e) }, - [itemsLength, highlightedIndex, onEnter, fallbackKeyDown] + [ + itemsLength, + highlightedIndex, + onEnter, + fallbackKeyDown, + options?.skipEnterWhenFilterHighlighted, + ] ) const reset = useCallback(() => setHighlightedIndex(0), []) diff --git a/packages/ui-patterns/src/FilterBar/menuItems.ts b/packages/ui-patterns/src/FilterBar/menuItems.ts index dffec3f65da..f545b3d0729 100644 --- a/packages/ui-patterns/src/FilterBar/menuItems.ts +++ b/packages/ui-patterns/src/FilterBar/menuItems.ts @@ -1,5 +1,4 @@ -import { ActiveInput } from './hooks' -import { FilterBarAction, FilterGroup, FilterProperty } from './types' +import { ActiveInputState, FilterBarAction, FilterGroup, FilterProperty } from './types' import { findConditionByPath, isCustomOptionObject, @@ -19,7 +18,7 @@ export type MenuItem = { } export function buildOperatorItems( - activeInput: Extract | null, + activeInput: Extract | null, activeFilters: FilterGroup, filterProperties: FilterProperty[], hasTypedSinceFocus: boolean = true @@ -89,7 +88,7 @@ export function buildPropertyItems(params: { } export function buildValueItems( - activeInput: Extract | null, + activeInput: Extract | null, activeFilters: FilterGroup, filterProperties: FilterProperty[], propertyOptionsCache: Record, diff --git a/packages/ui-patterns/src/FilterBar/types.ts b/packages/ui-patterns/src/FilterBar/types.ts index 73d2b58e9db..6259d44e08b 100644 --- a/packages/ui-patterns/src/FilterBar/types.ts +++ b/packages/ui-patterns/src/FilterBar/types.ts @@ -51,13 +51,15 @@ export function isGroup(condition: FilterCondition | FilterGroup): condition is return 'logicalOperator' in condition } +export type ConditionPath = number[] + export type FilterBarAction = { value: string label: string icon?: React.ReactNode onSelect: ( inputValue: string, - context: { path: number[]; activeFilters: FilterGroup } + context: { path: ConditionPath; activeFilters: FilterGroup } ) => void | Promise } @@ -71,5 +73,24 @@ export type SerializableFilterProperty = Pick< export type AIFilterRequestPayload = { prompt: string filterProperties: SerializableFilterProperty[] - currentPath: number[] + currentPath: ConditionPath +} + +export type NavigationDirection = 'prev' | 'next' + +export type HighlightNavigationResult = ConditionPath | 'clear' | null + +export type ActiveInputState = + | { type: 'value'; path: ConditionPath } + | { type: 'operator'; path: ConditionPath } + | { type: 'group'; path: ConditionPath } + | null + +export type KeyboardNavigationConfig = { + activeInput: ActiveInputState + setActiveInput: (input: ActiveInputState) => void + activeFilters: FilterGroup + onFilterChange: (filters: FilterGroup) => void + highlightedConditionPath: ConditionPath | null + setHighlightedConditionPath: (path: ConditionPath | null) => void } diff --git a/packages/ui-patterns/src/FilterBar/useAIFilter.ts b/packages/ui-patterns/src/FilterBar/useAIFilter.ts index 99e216a8ae6..9e7f05c7e0e 100644 --- a/packages/ui-patterns/src/FilterBar/useAIFilter.ts +++ b/packages/ui-patterns/src/FilterBar/useAIFilter.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react' -import { ActiveInput } from './hooks' -import { FilterGroup, FilterProperty, isGroup } from './types' +import { ActiveInputState, FilterGroup, FilterProperty, isGroup } from './types' import { updateGroupAtPath } from './utils' export function useAIFilter({ @@ -16,7 +15,7 @@ export function useAIFilter({ setError, setIsCommandMenuVisible, }: { - activeInput: ActiveInput + activeInput: ActiveInputState aiApiUrl?: string freeformText: string filterProperties: FilterProperty[] diff --git a/packages/ui-patterns/src/FilterBar/useCommandHandling.ts b/packages/ui-patterns/src/FilterBar/useCommandHandling.ts index 3855cb1df6a..60df348c5d4 100644 --- a/packages/ui-patterns/src/FilterBar/useCommandHandling.ts +++ b/packages/ui-patterns/src/FilterBar/useCommandHandling.ts @@ -1,8 +1,7 @@ import { useCallback } from 'react' -import { ActiveInput } from './hooks' import { MenuItem } from './menuItems' -import { FilterGroup, FilterProperty } from './types' +import { ActiveInputState, FilterGroup, FilterProperty } from './types' import { addFilterToGroup, addGroupToGroup, findGroupByPath, isCustomOptionObject } from './utils' export function useCommandHandling({ @@ -18,8 +17,8 @@ export function useCommandHandling({ newPathRef, setIsCommandMenuVisible, }: { - activeInput: ActiveInput - setActiveInput: (input: ActiveInput) => void + activeInput: ActiveInputState + setActiveInput: (input: ActiveInputState) => void activeFilters: FilterGroup onFilterChange: (filters: FilterGroup) => void filterProperties: FilterProperty[] diff --git a/packages/ui-patterns/src/FilterBar/useCommandMenu.ts b/packages/ui-patterns/src/FilterBar/useCommandMenu.ts index be575d1b3ae..7330c86905b 100644 --- a/packages/ui-patterns/src/FilterBar/useCommandMenu.ts +++ b/packages/ui-patterns/src/FilterBar/useCommandMenu.ts @@ -2,8 +2,7 @@ import { Sparkles } from 'lucide-react' import { useMemo } from 'react' import * as React from 'react' -import { ActiveInput } from './hooks' -import { FilterGroup, FilterProperty } from './types' +import { ActiveInputState, FilterGroup, FilterProperty } from './types' import { findConditionByPath, isCustomOptionObject, @@ -31,7 +30,7 @@ export function useCommandMenu({ aiApiUrl, supportsOperators, }: { - activeInput: ActiveInput + activeInput: ActiveInputState freeformText: string activeFilters: FilterGroup filterProperties: FilterProperty[] @@ -94,7 +93,7 @@ export function useCommandMenu({ } function getOperatorItems( - activeInput: Extract, + activeInput: Extract, activeFilters: FilterGroup, filterProperties: FilterProperty[] ): CommandItem[] { @@ -117,7 +116,7 @@ function getOperatorItems( } function getInputValue( - activeInput: ActiveInput, + activeInput: ActiveInputState, freeformText: string, activeFilters: FilterGroup ): string { @@ -138,7 +137,7 @@ function getPropertyItems(filterProperties: FilterProperty[], inputValue: string } function getValueItems( - activeInput: Extract, + activeInput: Extract, activeFilters: FilterGroup, filterProperties: FilterProperty[], propertyOptionsCache: Record, diff --git a/packages/ui-patterns/src/FilterBar/useKeyboardNavigation.ts b/packages/ui-patterns/src/FilterBar/useKeyboardNavigation.ts index ca495b358f4..d30c7a69983 100644 --- a/packages/ui-patterns/src/FilterBar/useKeyboardNavigation.ts +++ b/packages/ui-patterns/src/FilterBar/useKeyboardNavigation.ts @@ -1,7 +1,11 @@ import { KeyboardEvent, useCallback } from 'react' -import { ActiveInput } from './hooks' -import { FilterGroup } from './types' +import { + ConditionPath, + HighlightNavigationResult, + KeyboardNavigationConfig, + NavigationDirection, +} from './types' import { findConditionByPath, findGroupByPath, removeFromGroup } from './utils' export function useKeyboardNavigation({ @@ -9,14 +13,11 @@ export function useKeyboardNavigation({ setActiveInput, activeFilters, onFilterChange, -}: { - activeInput: ActiveInput - setActiveInput: (input: ActiveInput) => void - activeFilters: FilterGroup - onFilterChange: (filters: FilterGroup) => void -}) { + highlightedConditionPath, + setHighlightedConditionPath, +}: KeyboardNavigationConfig) { const removeByPath = useCallback( - (path: number[]) => { + (path: ConditionPath) => { const updatedFilters = removeFromGroup(activeFilters, path) onFilterChange(updatedFilters) }, @@ -24,7 +25,7 @@ export function useKeyboardNavigation({ ) const findFirstConditionInGroup = useCallback( - (groupPath: number[]): number[] | null => { + (groupPath: ConditionPath): ConditionPath | null => { const group = findGroupByPath(activeFilters, groupPath) if (!group || group.conditions.length === 0) return null @@ -38,7 +39,7 @@ export function useKeyboardNavigation({ ) const findLastConditionInGroup = useCallback( - (groupPath: number[]): number[] | null => { + (groupPath: ConditionPath): ConditionPath | null => { const group = findGroupByPath(activeFilters, groupPath) if (!group || group.conditions.length === 0) return null @@ -53,7 +54,7 @@ export function useKeyboardNavigation({ ) const findPreviousCondition = useCallback( - (currentPath: number[]): number[] | null => { + (currentPath: ConditionPath): ConditionPath | null => { const groupPath = currentPath.slice(0, -1) const conditionIndex = currentPath[currentPath.length - 1] @@ -78,82 +79,47 @@ export function useKeyboardNavigation({ [activeFilters, findLastConditionInGroup] ) - const findNextCondition = useCallback( - (currentPath: number[]): number[] | null => { - const groupPath = currentPath.slice(0, -1) - const conditionIndex = currentPath[currentPath.length - 1] - const group = findGroupByPath(activeFilters, groupPath) + const getHighlightNavigationPath = useCallback( + (direction: NavigationDirection): HighlightNavigationResult => { + if (activeInput?.type !== 'group') return null - if (group && conditionIndex < group.conditions.length - 1) { - const nextPath = [...groupPath, conditionIndex + 1] - const nextCondition = group.conditions[conditionIndex + 1] - if (!('logicalOperator' in nextCondition)) { - return nextPath + const group = findGroupByPath(activeFilters, activeInput.path) + if (!group || group.conditions.length === 0) return null + + if (highlightedConditionPath) { + const currentIndex = highlightedConditionPath[highlightedConditionPath.length - 1] + + if (direction === 'prev') { + if (currentIndex > 0) { + return [...activeInput.path, currentIndex - 1] + } + return 'clear' + } else { + if (currentIndex < group.conditions.length - 1) { + return [...activeInput.path, currentIndex + 1] + } + return 'clear' } - return findFirstConditionInGroup(nextPath) + } else { + if (direction === 'prev') { + return [...activeInput.path, group.conditions.length - 1] + } + // 'next' with no highlight does nothing - user is already at the rightmost position + return null } - - if (groupPath.length > 0) { - return findNextCondition(groupPath) - } - - return null }, - [activeFilters, findFirstConditionInGroup] + [activeInput, activeFilters, highlightedConditionPath] ) - const findPreviousConditionFromGroup = useCallback( - (groupPath: number[]): number[] | null => { - const group = findGroupByPath(activeFilters, groupPath) - if (group && group.conditions.length > 0) { - return findLastConditionInGroup(groupPath) + const applyHighlightNavigation = useCallback( + (result: HighlightNavigationResult) => { + if (result === 'clear') { + setHighlightedConditionPath(null) + } else if (result) { + setHighlightedConditionPath(result) } - - if (groupPath.length > 0) { - const parentPath = groupPath.slice(0, -1) - const groupIndex = groupPath[groupPath.length - 1] - if (groupIndex > 0) { - const prevSiblingPath = [...parentPath, groupIndex - 1] - const parentGroup = findGroupByPath(activeFilters, parentPath) - const prevSibling = parentGroup?.conditions[groupIndex - 1] - if (prevSibling) { - if ('logicalOperator' in prevSibling) { - return findLastConditionInGroup(prevSiblingPath) - } else { - return prevSiblingPath - } - } - } - return findPreviousConditionFromGroup(parentPath) - } - - return null }, - [activeFilters, findLastConditionInGroup] - ) - - const findNextConditionFromGroup = useCallback( - (groupPath: number[]): number[] | null => { - if (groupPath.length > 0) { - const parentPath = groupPath.slice(0, -1) - const groupIndex = groupPath[groupPath.length - 1] - const parentGroup = findGroupByPath(activeFilters, parentPath) - - if (parentGroup && groupIndex < parentGroup.conditions.length - 1) { - const nextSiblingPath = [...parentPath, groupIndex + 1] - const nextSibling = parentGroup.conditions[groupIndex + 1] - if ('logicalOperator' in nextSibling) { - return findFirstConditionInGroup(nextSiblingPath) - } else { - return nextSiblingPath - } - } - return findNextConditionFromGroup(parentPath) - } - - return null - }, - [activeFilters, findFirstConditionInGroup] + [setHighlightedConditionPath] ) const handleBackspace = useCallback( @@ -165,14 +131,19 @@ export function useKeyboardNavigation({ if (activeInput?.type === 'group' && isEmpty) { e.preventDefault() - const group = findGroupByPath(activeFilters, activeInput.path) - if (group && group.conditions.length > 0) { - const lastConditionPath = [...activeInput.path, group.conditions.length - 1] - removeByPath(lastConditionPath) - setActiveInput({ type: 'group', path: activeInput.path }) - } else if (group && group.conditions.length === 0 && activeInput.path.length > 0) { - // Only remove nested empty groups, not the root group + if (highlightedConditionPath) { + removeByPath(highlightedConditionPath) + setHighlightedConditionPath(null) + } else { + const result = getHighlightNavigationPath('prev') + applyHighlightNavigation(result) + } + } else if (activeInput?.type === 'group' && activeInput.path.length > 0) { + // Edge case: remove nested empty groups, but not the root group + const group = findGroupByPath(activeFilters, activeInput.path) + if (group && group.conditions.length === 0) { + e.preventDefault() removeByPath(activeInput.path) setActiveInput({ type: 'group', @@ -187,66 +158,90 @@ export function useKeyboardNavigation({ } } }, - [activeInput, activeFilters, removeByPath, setActiveInput] + [ + activeInput, + activeFilters, + removeByPath, + setActiveInput, + highlightedConditionPath, + setHighlightedConditionPath, + getHighlightNavigationPath, + applyHighlightNavigation, + ] ) const handleArrowLeft = useCallback( (e: KeyboardEvent) => { const inputElement = e.target as HTMLInputElement - if (inputElement.selectionStart === 0) { + const isEmpty = inputElement.value === '' + + if (activeInput?.type === 'group' && isEmpty) { e.preventDefault() - if (activeInput?.type === 'value') { - const prevPath = findPreviousCondition(activeInput.path) - if (prevPath) { - setActiveInput({ type: 'value', path: prevPath }) - } - } else if (activeInput?.type === 'group') { - const prevPath = findPreviousConditionFromGroup(activeInput.path) - if (prevPath) { - setActiveInput({ type: 'value', path: prevPath }) - } + const result = getHighlightNavigationPath('prev') + applyHighlightNavigation(result) + return + } + + if (activeInput?.type === 'value' && inputElement.selectionStart === 0) { + e.preventDefault() + const prevPath = findPreviousCondition(activeInput.path) + if (prevPath) { + setActiveInput({ type: 'value', path: prevPath }) } } }, - [activeInput, findPreviousCondition, findPreviousConditionFromGroup, setActiveInput] + [ + activeInput, + getHighlightNavigationPath, + applyHighlightNavigation, + findPreviousCondition, + setActiveInput, + ] ) const handleArrowRight = useCallback( (e: KeyboardEvent) => { const inputElement = e.target as HTMLInputElement - if (inputElement.selectionStart === inputElement.value.length) { - e.preventDefault() - if (activeInput?.type === 'value') { - const groupPath = activeInput.path.slice(0, -1) - const conditionIndex = activeInput.path[activeInput.path.length - 1] - const group = findGroupByPath(activeFilters, groupPath) + const isEmpty = inputElement.value === '' - if (group && conditionIndex < group.conditions.length - 1) { - const nextCondition = group.conditions[conditionIndex + 1] - if ('logicalOperator' in nextCondition) { - const nextPath = findFirstConditionInGroup([...groupPath, conditionIndex + 1]) - if (nextPath) { - setActiveInput({ type: 'value', path: nextPath }) - } - } else { - setActiveInput({ type: 'value', path: [...groupPath, conditionIndex + 1] }) + if (activeInput?.type === 'group' && isEmpty && highlightedConditionPath) { + e.preventDefault() + const result = getHighlightNavigationPath('next') + applyHighlightNavigation(result) + return + } + + if ( + activeInput?.type === 'value' && + inputElement.selectionStart === inputElement.value.length + ) { + e.preventDefault() + const groupPath = activeInput.path.slice(0, -1) + const conditionIndex = activeInput.path[activeInput.path.length - 1] + const group = findGroupByPath(activeFilters, groupPath) + + if (group && conditionIndex < group.conditions.length - 1) { + const nextCondition = group.conditions[conditionIndex + 1] + if ('logicalOperator' in nextCondition) { + const nextPath = findFirstConditionInGroup([...groupPath, conditionIndex + 1]) + if (nextPath) { + setActiveInput({ type: 'value', path: nextPath }) } } else { - setActiveInput({ type: 'group', path: groupPath }) - } - } else if (activeInput?.type === 'group') { - const nextPath = findNextConditionFromGroup(activeInput.path) - if (nextPath) { - setActiveInput({ type: 'value', path: nextPath }) + setActiveInput({ type: 'value', path: [...groupPath, conditionIndex + 1] }) } + } else { + setActiveInput({ type: 'group', path: groupPath }) } } }, [ activeInput, activeFilters, + highlightedConditionPath, + getHighlightNavigationPath, + applyHighlightNavigation, findFirstConditionInGroup, - findNextConditionFromGroup, setActiveInput, ] ) @@ -260,12 +255,21 @@ export function useKeyboardNavigation({ } else if (e.key === 'ArrowRight') { handleArrowRight(e) } else if (e.key === 'Escape') { - const activeElement = document.activeElement as HTMLElement | null - if (activeElement && activeElement.blur) { - activeElement.blur() + if (highlightedConditionPath) { + e.preventDefault() + setHighlightedConditionPath(null) + } else { + const activeElement = document.activeElement as HTMLElement | null + if (activeElement && activeElement.blur) { + activeElement.blur() + } } } else if (e.key === 'Enter') { - if (activeInput?.type === 'value') { + if (highlightedConditionPath) { + e.preventDefault() + setActiveInput({ type: 'value', path: highlightedConditionPath }) + setHighlightedConditionPath(null) + } else if (activeInput?.type === 'value') { e.preventDefault() setActiveInput({ type: 'group', path: activeInput.path.slice(0, -1) }) } else if (activeInput?.type === 'operator') { @@ -275,7 +279,15 @@ export function useKeyboardNavigation({ } } }, - [activeInput, handleBackspace, handleArrowLeft, handleArrowRight, setActiveInput] + [ + activeInput, + handleBackspace, + handleArrowLeft, + handleArrowRight, + setActiveInput, + highlightedConditionPath, + setHighlightedConditionPath, + ] ) return {