-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
autocompleter-ui.tsx
127 lines (116 loc) · 3.21 KB
/
autocompleter-ui.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useLayoutEffect, useRef, useEffect } from '@wordpress/element';
// Error expected because `@wordpress/rich-text` is not yet fully typed.
// @ts-expect-error
import { useAnchor } from '@wordpress/rich-text';
/**
* Internal dependencies
*/
import getDefaultUseItems from './get-default-use-items';
import Button from '../button';
import Popover from '../popover';
import type { AutocompleterUIProps, WPCompleter } from './types';
export function getAutoCompleterUI( autocompleter: WPCompleter ) {
const useItems = autocompleter.useItems
? autocompleter.useItems
: getDefaultUseItems( autocompleter );
function AutocompleterUI( {
filterValue,
instanceId,
listBoxId,
className,
selectedIndex,
onChangeOptions,
onSelect,
onReset,
reset,
value,
contentRef,
}: AutocompleterUIProps ) {
const [ items ] = useItems( filterValue );
const popoverAnchor = useAnchor( {
editableContentElement: contentRef.current,
value,
} );
const popoverRef = useRef< HTMLElement >( null );
useOnClickOutside( popoverRef, reset );
useLayoutEffect( () => {
onChangeOptions( items );
// Temporarily disabling exhaustive-deps to avoid introducing unexpected side effecst.
// See https://github.com/WordPress/gutenberg/pull/41820
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ items ] );
if ( items.length === 0 ) {
return null;
}
return (
<Popover
focusOnMount={ false }
onClose={ onReset }
placement="top-start"
className="components-autocomplete__popover"
anchor={ popoverAnchor }
ref={ popoverRef }
>
<div
id={ listBoxId }
role="listbox"
className="components-autocomplete__results"
>
{ items.map( ( option, index ) => (
<Button
key={ option.key }
id={ `components-autocomplete-item-${ instanceId }-${ option.key }` }
role="option"
aria-selected={ index === selectedIndex }
disabled={ option.isDisabled }
className={ classnames(
'components-autocomplete__result',
className,
{
'is-selected': index === selectedIndex,
}
) }
onClick={ () => onSelect( option ) }
>
{ option.label }
</Button>
) ) }
</div>
</Popover>
);
}
return AutocompleterUI;
}
function useOnClickOutside(
ref: React.RefObject< HTMLElement >,
handler: AutocompleterUIProps[ 'reset' ]
) {
useEffect( () => {
const listener = ( event: MouseEvent | TouchEvent ) => {
// Do nothing if clicking ref's element or descendent elements, or if the ref is not referencing an element
if (
! ref.current ||
ref.current.contains( event.target as Node )
) {
return;
}
handler( event );
};
document.addEventListener( 'mousedown', listener );
document.addEventListener( 'touchstart', listener );
return () => {
document.removeEventListener( 'mousedown', listener );
document.removeEventListener( 'touchstart', listener );
};
// Disable reason: `ref` is a ref object and should not be included in a
// hook's dependency list.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ handler ] );
}