-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: (React Aria) Implement filtering on a per CollectionNode basis #8641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c9ee11d
ac323e9
290514a
2969511
d8a6f06
639acd0
6eb1753
d1efa8d
77936ca
7991977
e615835
d02197e
361286b
432a43c
3ec3fd6
4a69d50
73a1971
1ead59b
90c2056
3a8301e
45a39c1
9d65d5b
19b695e
6066c6c
d2b5e51
2c89783
739e93f
3c2e92c
35b627e
9408aa9
8e75339
57e57e0
4d5fde2
a2925e4
058f820
9b3aadf
8fffdf8
113b259
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,10 +10,10 @@ | |
* governing permissions and limitations under the License. | ||
*/ | ||
|
||
import {AriaLabelingProps, BaseEvent, DOMProps, RefObject} from '@react-types/shared'; | ||
import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, Node, RefObject} from '@react-types/shared'; | ||
import {AriaTextFieldProps} from '@react-aria/textfield'; | ||
import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete'; | ||
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isCtrlKeyPressed, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useObjectRef} from '@react-aria/utils'; | ||
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useLabels, useObjectRef, useSlotId} from '@react-aria/utils'; | ||
import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus'; | ||
import {getInteractionModality} from '@react-aria/interactions'; | ||
// @ts-ignore | ||
|
@@ -27,36 +27,44 @@ export interface CollectionOptions extends DOMProps, AriaLabelingProps { | |
/** Whether typeahead is disabled. */ | ||
disallowTypeAhead: boolean | ||
} | ||
export interface AriaAutocompleteProps extends AutocompleteProps { | ||
|
||
export interface AriaAutocompleteProps<T> extends AutocompleteProps { | ||
/** | ||
* An optional filter function used to determine if a option should be included in the autocomplete list. | ||
* Include this if the items you are providing to your wrapped collection aren't filtered by default. | ||
*/ | ||
filter?: (textValue: string, inputValue: string) => boolean, | ||
filter?: (textValue: string, inputValue: string, node: Node<T>) => boolean, | ||
|
||
/** | ||
* Whether or not to focus the first item in the collection after a filter is performed. Note this is only applicable | ||
* if virtual focus behavior is not turned off via `disableVirtualFocus`. | ||
* @default false | ||
*/ | ||
disableAutoFocusFirst?: boolean, | ||
|
||
/** | ||
* Whether or not to focus the first item in the collection after a filter is performed. | ||
* Whether the autocomplete should disable virtual focus, instead making the wrapped collection directly tabbable. | ||
* @default false | ||
*/ | ||
disableAutoFocusFirst?: boolean | ||
disableVirtualFocus?: boolean | ||
} | ||
|
||
export interface AriaAutocompleteOptions extends Omit<AriaAutocompleteProps, 'children'> { | ||
export interface AriaAutocompleteOptions<T> extends Omit<AriaAutocompleteProps<T>, 'children'> { | ||
/** The ref for the wrapped collection element. */ | ||
inputRef: RefObject<HTMLInputElement | null>, | ||
/** The ref for the wrapped collection element. */ | ||
collectionRef: RefObject<HTMLElement | null> | ||
} | ||
|
||
export interface AutocompleteAria { | ||
export interface AutocompleteAria<T> { | ||
/** Props for the autocomplete textfield/searchfield element. These should be passed to the textfield/searchfield aria hooks respectively. */ | ||
textFieldProps: AriaTextFieldProps, | ||
textFieldProps: AriaTextFieldProps<FocusableElement>, | ||
/** Props for the collection, to be passed to collection's respective aria hook (e.g. useMenu). */ | ||
collectionProps: CollectionOptions, | ||
/** Ref to attach to the wrapped collection. */ | ||
collectionRef: RefObject<HTMLElement | null>, | ||
/** A filter function that returns if the provided collection node should be filtered out of the collection. */ | ||
filter?: (nodeTextValue: string) => boolean | ||
filter?: (nodeTextValue: string, node: Node<T>) => boolean | ||
} | ||
|
||
/** | ||
|
@@ -65,24 +73,25 @@ export interface AutocompleteAria { | |
* @param props - Props for the autocomplete. | ||
* @param state - State for the autocomplete, as returned by `useAutocompleteState`. | ||
*/ | ||
export function useAutocomplete(props: AriaAutocompleteOptions, state: AutocompleteState): AutocompleteAria { | ||
export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: AutocompleteState): AutocompleteAria<T> { | ||
let { | ||
inputRef, | ||
collectionRef, | ||
filter, | ||
disableAutoFocusFirst = false | ||
disableAutoFocusFirst = false, | ||
disableVirtualFocus = false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's another issue here, what does virtual focus look like for grid collections? what if someone puts an editable field in it? Maybe it's better that it's not virtual by default? |
||
} = props; | ||
|
||
let collectionId = useId(); | ||
let collectionId = useSlotId(); | ||
let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); | ||
let delayNextActiveDescendant = useRef(false); | ||
let queuedActiveDescendant = useRef<string | null>(null); | ||
let lastCollectionNode = useRef<HTMLElement>(null); | ||
|
||
// For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually | ||
// moving focus back to the subtriggers | ||
let shouldUseVirtualFocus = getInteractionModality() !== 'virtual'; | ||
|
||
let isMobileScreenReader = getInteractionModality() === 'virtual' && (isIOS() || isAndroid()); | ||
let shouldUseVirtualFocus = !isMobileScreenReader && !disableVirtualFocus; | ||
useEffect(() => { | ||
return () => clearTimeout(timeout.current); | ||
}, []); | ||
|
@@ -252,15 +261,17 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl | |
} | ||
|
||
let shouldPerformDefaultAction = true; | ||
if (focusedNodeId == null) { | ||
shouldPerformDefaultAction = collectionRef.current?.dispatchEvent( | ||
new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) | ||
) || false; | ||
} else { | ||
let item = document.getElementById(focusedNodeId); | ||
shouldPerformDefaultAction = item?.dispatchEvent( | ||
new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) | ||
) || false; | ||
if (collectionRef.current !== null) { | ||
if (focusedNodeId == null) { | ||
shouldPerformDefaultAction = collectionRef.current?.dispatchEvent( | ||
new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) | ||
) || false; | ||
} else { | ||
let item = document.getElementById(focusedNodeId); | ||
shouldPerformDefaultAction = item?.dispatchEvent( | ||
new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) | ||
) || false; | ||
} | ||
} | ||
|
||
if (shouldPerformDefaultAction) { | ||
|
@@ -280,6 +291,9 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl | |
} | ||
break; | ||
} | ||
} else { | ||
// TODO: check if we can do this, want to stop textArea from using its default Enter behavior so items are properly triggered | ||
e.preventDefault(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. only do it for Enter? |
||
} | ||
}; | ||
|
||
|
@@ -316,9 +330,9 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl | |
'aria-label': stringFormatter.format('collectionLabel') | ||
}); | ||
|
||
let filterFn = useCallback((nodeTextValue: string) => { | ||
let filterFn = useCallback((nodeTextValue: string, node: Node<T>) => { | ||
if (filter) { | ||
return filter(nodeTextValue, state.inputValue); | ||
return filter(nodeTextValue, state.inputValue, node); | ||
} | ||
|
||
return true; | ||
|
@@ -352,25 +366,38 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl | |
} | ||
}; | ||
|
||
return { | ||
textFieldProps: { | ||
value: state.inputValue, | ||
onChange, | ||
onKeyDown, | ||
autoComplete: 'off', | ||
'aria-haspopup': 'listbox', | ||
// Only apply the autocomplete specific behaviors if the collection component wrapped by it is actually | ||
// being filtered/allows filtering by the Autocomplete. | ||
let textFieldProps = { | ||
value: state.inputValue, | ||
onChange | ||
} as AriaTextFieldProps<FocusableElement>; | ||
|
||
let virtualFocusProps = { | ||
onKeyDown, | ||
'aria-activedescendant': state.focusedNodeId ?? undefined, | ||
onBlur, | ||
onFocus | ||
}; | ||
|
||
if (collectionId) { | ||
textFieldProps = { | ||
...textFieldProps, | ||
...(shouldUseVirtualFocus && virtualFocusProps), | ||
enterKeyHint: 'go', | ||
'aria-controls': collectionId, | ||
// TODO: readd proper logic for completionMode = complete (aria-autocomplete: both) | ||
'aria-autocomplete': 'list', | ||
'aria-activedescendant': state.focusedNodeId ?? undefined, | ||
// This disable's iOS's autocorrect suggestions, since the autocomplete provides its own suggestions. | ||
autoCorrect: 'off', | ||
// This disable's the macOS Safari spell check auto corrections. | ||
spellCheck: 'false', | ||
enterKeyHint: 'go', | ||
onBlur, | ||
onFocus | ||
}, | ||
autoComplete: 'off' | ||
}; | ||
} | ||
|
||
return { | ||
textFieldProps, | ||
collectionProps: mergeProps(collectionProps, { | ||
shouldUseVirtualFocus, | ||
disallowTypeAhead: true | ||
|
Uh oh!
There was an error while loading. Please reload this page.