@wordpress/components 32.5.0 → 32.5.2-next.v.202604091042.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +20 -0
- package/README.md +18 -4
- package/build/autocomplete/autocompleter-ui.cjs +75 -79
- package/build/autocomplete/autocompleter-ui.cjs.map +2 -2
- package/build/autocomplete/get-autocomplete-match.cjs +91 -0
- package/build/autocomplete/get-autocomplete-match.cjs.map +7 -0
- package/build/autocomplete/index.cjs +104 -107
- package/build/autocomplete/index.cjs.map +3 -3
- package/build/box-control/index.cjs +0 -8
- package/build/box-control/index.cjs.map +2 -2
- package/build/box-control/utils.cjs +1 -10
- package/build/box-control/utils.cjs.map +2 -2
- package/build/calendar/utils/use-localization-props.cjs +3 -2
- package/build/calendar/utils/use-localization-props.cjs.map +2 -2
- package/build/custom-select-control/index.cjs.map +3 -3
- package/build/custom-select-control-v2/custom-select.cjs +2 -2
- package/build/custom-select-control-v2/custom-select.cjs.map +2 -2
- package/build/custom-select-control-v2/index.cjs.map +3 -3
- package/build/sandbox/index.cjs +2 -2
- package/build/sandbox/index.cjs.map +2 -2
- package/build/validated-form-controls/control-with-error.cjs +12 -8
- package/build/validated-form-controls/control-with-error.cjs.map +2 -2
- package/build-module/autocomplete/autocompleter-ui.mjs +74 -78
- package/build-module/autocomplete/autocompleter-ui.mjs.map +2 -2
- package/build-module/autocomplete/get-autocomplete-match.mjs +56 -0
- package/build-module/autocomplete/get-autocomplete-match.mjs.map +7 -0
- package/build-module/autocomplete/index.mjs +103 -107
- package/build-module/autocomplete/index.mjs.map +3 -3
- package/build-module/box-control/index.mjs +1 -9
- package/build-module/box-control/index.mjs.map +2 -2
- package/build-module/box-control/utils.mjs +1 -9
- package/build-module/box-control/utils.mjs.map +2 -2
- package/build-module/calendar/utils/use-localization-props.mjs +3 -2
- package/build-module/calendar/utils/use-localization-props.mjs.map +2 -2
- package/build-module/custom-select-control/index.mjs +2 -2
- package/build-module/custom-select-control/index.mjs.map +2 -2
- package/build-module/custom-select-control-v2/custom-select.mjs +2 -2
- package/build-module/custom-select-control-v2/custom-select.mjs.map +2 -2
- package/build-module/custom-select-control-v2/index.mjs +2 -2
- package/build-module/custom-select-control-v2/index.mjs.map +2 -2
- package/build-module/sandbox/index.mjs +2 -2
- package/build-module/sandbox/index.mjs.map +2 -2
- package/build-module/validated-form-controls/control-with-error.mjs +12 -8
- package/build-module/validated-form-controls/control-with-error.mjs.map +2 -2
- package/build-style/style-rtl.css +0 -3
- package/build-style/style.css +0 -3
- package/build-types/autocomplete/autocompleter-ui.d.ts +2 -2
- package/build-types/autocomplete/autocompleter-ui.d.ts.map +1 -1
- package/build-types/autocomplete/get-autocomplete-match.d.ts +11 -0
- package/build-types/autocomplete/get-autocomplete-match.d.ts.map +1 -0
- package/build-types/autocomplete/index.d.ts +8 -0
- package/build-types/autocomplete/index.d.ts.map +1 -1
- package/build-types/autocomplete/test/get-autocomplete-match.d.ts +2 -0
- package/build-types/autocomplete/test/get-autocomplete-match.d.ts.map +1 -0
- package/build-types/autocomplete/types.d.ts +23 -9
- package/build-types/autocomplete/types.d.ts.map +1 -1
- package/build-types/box-control/index.d.ts.map +1 -1
- package/build-types/box-control/utils.d.ts +7 -16
- package/build-types/box-control/utils.d.ts.map +1 -1
- package/build-types/button/stories/index.story.d.ts +0 -1
- package/build-types/button/stories/index.story.d.ts.map +1 -1
- package/build-types/calendar/utils/use-localization-props.d.ts +3 -3
- package/build-types/calendar/utils/use-localization-props.d.ts.map +1 -1
- package/build-types/custom-gradient-picker/constants.d.ts +2 -2
- package/build-types/custom-select-control-v2/custom-select.d.ts +3 -3
- package/build-types/custom-select-control-v2/custom-select.d.ts.map +1 -1
- package/build-types/custom-select-control-v2/types.d.ts +1 -1
- package/build-types/custom-select-control-v2/types.d.ts.map +1 -1
- package/build-types/font-size-picker/constants.d.ts +2 -2
- package/build-types/font-size-picker/constants.d.ts.map +1 -1
- package/build-types/palette-edit/index.d.ts +1 -1
- package/build-types/validated-form-controls/control-with-error.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/alignment-matrix-control/README.md +1 -1
- package/src/angle-picker-control/style.module.scss +1 -0
- package/src/autocomplete/README.md +2 -2
- package/src/autocomplete/autocompleter-ui.native.js +166 -173
- package/src/autocomplete/autocompleter-ui.tsx +114 -116
- package/src/autocomplete/get-autocomplete-match.ts +115 -0
- package/src/autocomplete/index.tsx +129 -208
- package/src/autocomplete/test/get-autocomplete-match.ts +338 -0
- package/src/autocomplete/test/index.tsx +112 -4
- package/src/autocomplete/types.ts +17 -10
- package/src/box-control/index.tsx +1 -19
- package/src/box-control/utils.ts +1 -19
- package/src/button/README.md +1 -1
- package/src/button/stories/index.story.tsx +0 -1
- package/src/button/style.scss +0 -6
- package/src/calendar/utils/use-localization-props.ts +3 -4
- package/src/custom-select-control/index.tsx +3 -3
- package/src/custom-select-control-v2/custom-select.tsx +4 -4
- package/src/custom-select-control-v2/index.tsx +2 -2
- package/src/custom-select-control-v2/types.ts +1 -1
- package/src/divider/README.md +5 -6
- package/src/flex/stories/index.story.tsx +1 -1
- package/src/form-file-upload/README.md +3 -3
- package/src/gradient-picker/README.md +2 -2
- package/src/h-stack/README.md +10 -15
- package/src/h-stack/stories/index.story.tsx +2 -2
- package/src/heading/stories/index.story.tsx +1 -1
- package/src/higher-order/with-focus-outside/index.native.js +21 -20
- package/src/icon/README.md +1 -1
- package/src/menu/README.md +2 -2
- package/src/mobile/utils/get-px-from-css-unit.native.js +1 -1
- package/src/sandbox/index.native.js +2 -2
- package/src/sandbox/index.tsx +2 -2
- package/src/tabs/README.md +6 -6
- package/src/text/stories/index.story.tsx +1 -1
- package/src/toolbar/toolbar-button/toolbar-button-container.native.js +3 -1
- package/src/tree-select/README.md +1 -1
- package/src/v-stack/README.md +10 -15
- package/src/v-stack/stories/index.story.tsx +2 -2
- package/src/validated-form-controls/control-with-error.tsx +17 -12
- package/src/validated-form-controls/test/control-with-error.tsx +28 -1
- package/src/view/README.md +2 -5
- package/src/button/stories/style.css +0 -8
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import removeAccents from 'remove-accents';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import type { WPCompleter } from './types';
|
|
10
|
+
|
|
11
|
+
type AutocompleteMatch = {
|
|
12
|
+
completer: WPCompleter;
|
|
13
|
+
filterValue: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function getAutocompleteMatch(
|
|
17
|
+
textContent: string,
|
|
18
|
+
completers: WPCompleter[],
|
|
19
|
+
filteredOptionsLength: number,
|
|
20
|
+
isBackspacing: boolean,
|
|
21
|
+
getTextAfterSelection: () => string
|
|
22
|
+
): AutocompleteMatch | null {
|
|
23
|
+
if ( ! textContent ) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Find the completer whose trigger prefix ends closest to the cursor
|
|
28
|
+
// (rightmost end position). Comparing end positions instead of start
|
|
29
|
+
// positions correctly resolves overlapping prefixes like "@" and "@@".
|
|
30
|
+
let completer: WPCompleter | null = null;
|
|
31
|
+
let triggerIndex = -1;
|
|
32
|
+
let matchedEndIndex = -1;
|
|
33
|
+
let matchedPrefixLength = 0;
|
|
34
|
+
|
|
35
|
+
for ( const currentCompleter of completers ) {
|
|
36
|
+
const currentIndex = textContent.lastIndexOf(
|
|
37
|
+
currentCompleter.triggerPrefix
|
|
38
|
+
);
|
|
39
|
+
if ( currentIndex < 0 ) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const currentEndIndex =
|
|
43
|
+
currentIndex + currentCompleter.triggerPrefix.length;
|
|
44
|
+
if (
|
|
45
|
+
currentEndIndex > matchedEndIndex ||
|
|
46
|
+
( currentEndIndex === matchedEndIndex &&
|
|
47
|
+
currentCompleter.triggerPrefix.length > matchedPrefixLength )
|
|
48
|
+
) {
|
|
49
|
+
completer = currentCompleter;
|
|
50
|
+
triggerIndex = currentIndex;
|
|
51
|
+
matchedEndIndex = currentEndIndex;
|
|
52
|
+
matchedPrefixLength = currentCompleter.triggerPrefix.length;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if ( ! completer ) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { allowContext, triggerPrefix } = completer;
|
|
61
|
+
const textWithoutTrigger = textContent.slice(
|
|
62
|
+
triggerIndex + triggerPrefix.length
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Prevent matching with an extremely long string, which causes
|
|
66
|
+
// the editor to slow-down significantly. This could happen, for
|
|
67
|
+
// example, if `matchingWhileBackspacing` is true and one of the
|
|
68
|
+
// "words" ends up being too long. Returning null here intentionally
|
|
69
|
+
// resets the autocompleter state in the caller.
|
|
70
|
+
if ( textWithoutTrigger.length > 50 ) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const mismatch = filteredOptionsLength === 0;
|
|
75
|
+
const wordsFromTrigger = textWithoutTrigger.split( /\s/ );
|
|
76
|
+
|
|
77
|
+
// Allow matching when typing a trigger + the match string or when
|
|
78
|
+
// clicking in an existing trigger word on the page.
|
|
79
|
+
// E.g. "Some text @a" — "@a" is detected as a trigger word.
|
|
80
|
+
const hasOneTriggerWord = wordsFromTrigger.length === 1;
|
|
81
|
+
|
|
82
|
+
// Allow matching when backspacing near a trigger word (up to 3
|
|
83
|
+
// words from the trigger character). This lets us recover from a
|
|
84
|
+
// mismatch when backspacing while still imposing sane limits.
|
|
85
|
+
// E.g. "Some text @marcelo sekkkk" — backspacing "kkkk" re-shows
|
|
86
|
+
// the popup once the text matches again.
|
|
87
|
+
const matchingWhileBackspacing =
|
|
88
|
+
isBackspacing && wordsFromTrigger.length <= 3;
|
|
89
|
+
|
|
90
|
+
if ( mismatch && ! ( matchingWhileBackspacing || hasOneTriggerWord ) ) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
allowContext &&
|
|
96
|
+
! allowContext(
|
|
97
|
+
textContent.slice( 0, triggerIndex ),
|
|
98
|
+
getTextAfterSelection()
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
/^\s/.test( textWithoutTrigger ) ||
|
|
106
|
+
/\s\s+$/.test( textWithoutTrigger )
|
|
107
|
+
) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
completer,
|
|
113
|
+
filterValue: removeAccents( textWithoutTrigger ),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* External dependencies
|
|
3
|
-
*/
|
|
4
|
-
import removeAccents from 'remove-accents';
|
|
5
|
-
|
|
6
1
|
/**
|
|
7
2
|
* WordPress dependencies
|
|
8
3
|
*/
|
|
9
4
|
import {
|
|
10
5
|
renderToString,
|
|
11
6
|
useEffect,
|
|
12
|
-
useState,
|
|
13
|
-
useRef,
|
|
14
7
|
useMemo,
|
|
8
|
+
useReducer,
|
|
9
|
+
useRef,
|
|
15
10
|
} from '@wordpress/element';
|
|
16
11
|
import { useInstanceId, useMergeRefs, useRefEffect } from '@wordpress/compose';
|
|
17
12
|
import {
|
|
@@ -27,18 +22,18 @@ import { isAppleOS } from '@wordpress/keycodes';
|
|
|
27
22
|
/**
|
|
28
23
|
* Internal dependencies
|
|
29
24
|
*/
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
25
|
+
import { AutocompleterUI } from './autocompleter-ui';
|
|
26
|
+
import { getAutocompleteMatch } from './get-autocomplete-match';
|
|
32
27
|
import { withIgnoreIMEEvents } from '../utils/with-ignore-ime-events';
|
|
33
28
|
import type {
|
|
29
|
+
AutocompleteAction,
|
|
34
30
|
AutocompleteProps,
|
|
35
|
-
|
|
31
|
+
AutocompleteState,
|
|
36
32
|
InsertOption,
|
|
37
33
|
KeyedOption,
|
|
38
34
|
OptionCompletion,
|
|
39
35
|
ReplaceOption,
|
|
40
36
|
UseAutocompleteProps,
|
|
41
|
-
WPCompleter,
|
|
42
37
|
} from './types';
|
|
43
38
|
import getNodeText from '../utils/get-node-text';
|
|
44
39
|
|
|
@@ -47,6 +42,59 @@ const EMPTY_FILTERED_OPTIONS: KeyedOption[] = [];
|
|
|
47
42
|
// Used for generating the instance ID
|
|
48
43
|
const AUTOCOMPLETE_HOOK_REFERENCE = {};
|
|
49
44
|
|
|
45
|
+
function getCompletionObject(
|
|
46
|
+
completion: OptionCompletion
|
|
47
|
+
): InsertOption | ReplaceOption {
|
|
48
|
+
if (
|
|
49
|
+
completion !== null &&
|
|
50
|
+
typeof completion === 'object' &&
|
|
51
|
+
'action' in completion &&
|
|
52
|
+
completion.action !== undefined &&
|
|
53
|
+
'value' in completion &&
|
|
54
|
+
completion.value !== undefined
|
|
55
|
+
) {
|
|
56
|
+
return completion;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
action: 'insert-at-caret',
|
|
60
|
+
value: completion as React.ReactNode,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const initialState: AutocompleteState = {
|
|
65
|
+
selectedIndex: 0,
|
|
66
|
+
filteredOptions: EMPTY_FILTERED_OPTIONS,
|
|
67
|
+
filterValue: '',
|
|
68
|
+
autocompleter: null,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function autocompleteReducer(
|
|
72
|
+
state: AutocompleteState,
|
|
73
|
+
action: AutocompleteAction
|
|
74
|
+
): AutocompleteState {
|
|
75
|
+
switch ( action.type ) {
|
|
76
|
+
case 'RESET':
|
|
77
|
+
return initialState;
|
|
78
|
+
case 'SELECT':
|
|
79
|
+
return { ...state, selectedIndex: action.index };
|
|
80
|
+
case 'OPTIONS':
|
|
81
|
+
return {
|
|
82
|
+
...state,
|
|
83
|
+
filteredOptions: action.options,
|
|
84
|
+
selectedIndex:
|
|
85
|
+
action.options.length === state.filteredOptions.length
|
|
86
|
+
? state.selectedIndex
|
|
87
|
+
: 0,
|
|
88
|
+
};
|
|
89
|
+
case 'MATCH':
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
autocompleter: action.completer,
|
|
93
|
+
filterValue: action.query,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
50
98
|
export function useAutocomplete( {
|
|
51
99
|
record,
|
|
52
100
|
onChange,
|
|
@@ -55,19 +103,9 @@ export function useAutocomplete( {
|
|
|
55
103
|
contentRef,
|
|
56
104
|
}: UseAutocompleteProps ) {
|
|
57
105
|
const instanceId = useInstanceId( AUTOCOMPLETE_HOOK_REFERENCE );
|
|
58
|
-
const [
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Array< KeyedOption >
|
|
62
|
-
>( EMPTY_FILTERED_OPTIONS );
|
|
63
|
-
const [ filterValue, setFilterValue ] =
|
|
64
|
-
useState< AutocompleterUIProps[ 'filterValue' ] >( '' );
|
|
65
|
-
const [ autocompleter, setAutocompleter ] = useState< WPCompleter | null >(
|
|
66
|
-
null
|
|
67
|
-
);
|
|
68
|
-
const [ AutocompleterUI, setAutocompleterUI ] = useState<
|
|
69
|
-
( ( props: AutocompleterUIProps ) => React.JSX.Element | null ) | null
|
|
70
|
-
>( null );
|
|
106
|
+
const [ state, dispatch ] = useReducer( autocompleteReducer, initialState );
|
|
107
|
+
const { selectedIndex, filteredOptions, filterValue, autocompleter } =
|
|
108
|
+
state;
|
|
71
109
|
|
|
72
110
|
const backspacingRef = useRef( false );
|
|
73
111
|
|
|
@@ -91,27 +129,9 @@ export function useAutocomplete( {
|
|
|
91
129
|
}
|
|
92
130
|
|
|
93
131
|
if ( getOptionCompletion ) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
obj: OptionCompletion
|
|
98
|
-
): obj is InsertOption | ReplaceOption => {
|
|
99
|
-
return (
|
|
100
|
-
obj !== null &&
|
|
101
|
-
typeof obj === 'object' &&
|
|
102
|
-
'action' in obj &&
|
|
103
|
-
obj.action !== undefined &&
|
|
104
|
-
'value' in obj &&
|
|
105
|
-
obj.value !== undefined
|
|
106
|
-
);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const completionObject = isCompletionObject( completion )
|
|
110
|
-
? completion
|
|
111
|
-
: ( {
|
|
112
|
-
action: 'insert-at-caret',
|
|
113
|
-
value: completion,
|
|
114
|
-
} as InsertOption );
|
|
132
|
+
const completionObject = getCompletionObject(
|
|
133
|
+
getOptionCompletion( option.value, filterValue )
|
|
134
|
+
);
|
|
115
135
|
|
|
116
136
|
if ( 'replace' === completionObject.action ) {
|
|
117
137
|
onReplace( [ completionObject.value ] );
|
|
@@ -125,31 +145,15 @@ export function useAutocomplete( {
|
|
|
125
145
|
|
|
126
146
|
// Reset autocomplete state after insertion rather than before
|
|
127
147
|
// so insertion events don't cause the completion menu to redisplay.
|
|
128
|
-
|
|
148
|
+
dispatch( { type: 'RESET' } );
|
|
129
149
|
|
|
130
150
|
// Make sure that the content remains focused after making a selection
|
|
131
151
|
// and that the text cursor position is not lost.
|
|
132
152
|
contentRef.current?.focus();
|
|
133
153
|
}
|
|
134
154
|
|
|
135
|
-
function reset() {
|
|
136
|
-
setSelectedIndex( 0 );
|
|
137
|
-
setFilteredOptions( EMPTY_FILTERED_OPTIONS );
|
|
138
|
-
setFilterValue( '' );
|
|
139
|
-
setAutocompleter( null );
|
|
140
|
-
setAutocompleterUI( null );
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Load options for an autocompleter.
|
|
145
|
-
*
|
|
146
|
-
* @param {Array} options
|
|
147
|
-
*/
|
|
148
155
|
function onChangeOptions( options: Array< KeyedOption > ) {
|
|
149
|
-
|
|
150
|
-
options.length === filteredOptions.length ? selectedIndex : 0
|
|
151
|
-
);
|
|
152
|
-
setFilteredOptions( options );
|
|
156
|
+
dispatch( { type: 'OPTIONS', options } );
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
function handleKeyDown( event: KeyboardEvent ) {
|
|
@@ -167,12 +171,13 @@ export function useAutocomplete( {
|
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
switch ( event.key ) {
|
|
170
|
-
case 'ArrowUp':
|
|
174
|
+
case 'ArrowUp':
|
|
175
|
+
case 'ArrowDown': {
|
|
176
|
+
const offset = event.key === 'ArrowUp' ? -1 : 1;
|
|
171
177
|
const newIndex =
|
|
172
|
-
( selectedIndex
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
setSelectedIndex( newIndex );
|
|
178
|
+
( selectedIndex + offset + filteredOptions.length ) %
|
|
179
|
+
filteredOptions.length;
|
|
180
|
+
dispatch( { type: 'SELECT', index: newIndex } );
|
|
176
181
|
// See the related PR as to why this is necessary: https://github.com/WordPress/gutenberg/pull/54902.
|
|
177
182
|
if ( isAppleOS() ) {
|
|
178
183
|
speak(
|
|
@@ -183,21 +188,8 @@ export function useAutocomplete( {
|
|
|
183
188
|
break;
|
|
184
189
|
}
|
|
185
190
|
|
|
186
|
-
case 'ArrowDown': {
|
|
187
|
-
const newIndex = ( selectedIndex + 1 ) % filteredOptions.length;
|
|
188
|
-
setSelectedIndex( newIndex );
|
|
189
|
-
if ( isAppleOS() ) {
|
|
190
|
-
speak(
|
|
191
|
-
getNodeText( filteredOptions[ newIndex ].label ),
|
|
192
|
-
'assertive'
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
191
|
case 'Escape':
|
|
199
|
-
|
|
200
|
-
setAutocompleterUI( null );
|
|
192
|
+
dispatch( { type: 'RESET' } );
|
|
201
193
|
event.preventDefault();
|
|
202
194
|
break;
|
|
203
195
|
|
|
@@ -207,7 +199,7 @@ export function useAutocomplete( {
|
|
|
207
199
|
|
|
208
200
|
case 'ArrowLeft':
|
|
209
201
|
case 'ArrowRight':
|
|
210
|
-
|
|
202
|
+
dispatch( { type: 'RESET' } );
|
|
211
203
|
return;
|
|
212
204
|
|
|
213
205
|
default:
|
|
@@ -230,132 +222,36 @@ export function useAutocomplete( {
|
|
|
230
222
|
}, [ record ] );
|
|
231
223
|
|
|
232
224
|
useEffect( () => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
( lastTrigger, currentCompleter ) => {
|
|
244
|
-
const triggerIndex = textContent.lastIndexOf(
|
|
245
|
-
currentCompleter.triggerPrefix
|
|
246
|
-
);
|
|
247
|
-
const lastTriggerIndex =
|
|
248
|
-
lastTrigger !== null
|
|
249
|
-
? textContent.lastIndexOf( lastTrigger.triggerPrefix )
|
|
250
|
-
: -1;
|
|
251
|
-
|
|
252
|
-
return triggerIndex > lastTriggerIndex
|
|
253
|
-
? currentCompleter
|
|
254
|
-
: lastTrigger;
|
|
255
|
-
},
|
|
256
|
-
null
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
if ( ! completer ) {
|
|
260
|
-
if ( autocompleter ) {
|
|
261
|
-
reset();
|
|
262
|
-
}
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const { allowContext, triggerPrefix } = completer;
|
|
267
|
-
const triggerIndex = textContent.lastIndexOf( triggerPrefix );
|
|
268
|
-
const textWithoutTrigger = textContent.slice(
|
|
269
|
-
triggerIndex + triggerPrefix.length
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
const tooDistantFromTrigger = textWithoutTrigger.length > 50; // 50 chars seems to be a good limit.
|
|
273
|
-
// This is a final barrier to prevent the effect from completing with
|
|
274
|
-
// an extremely long string, which causes the editor to slow-down
|
|
275
|
-
// significantly. This could happen, for example, if `matchingWhileBackspacing`
|
|
276
|
-
// is true and one of the "words" end up being too long. If that's the case,
|
|
277
|
-
// it will be caught by this guard.
|
|
278
|
-
if ( tooDistantFromTrigger ) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const mismatch = filteredOptions.length === 0;
|
|
283
|
-
const wordsFromTrigger = textWithoutTrigger.split( /\s/ );
|
|
284
|
-
// We need to allow the effect to run when not backspacing and if there
|
|
285
|
-
// was a mismatch. i.e when typing a trigger + the match string or when
|
|
286
|
-
// clicking in an existing trigger word on the page. We do that if we
|
|
287
|
-
// detect that we have one word from trigger in the current textual context.
|
|
288
|
-
//
|
|
289
|
-
// Ex.: "Some text @a" <-- "@a" will be detected as the trigger word and
|
|
290
|
-
// allow the effect to run. It will run until there's a mismatch.
|
|
291
|
-
const hasOneTriggerWord = wordsFromTrigger.length === 1;
|
|
292
|
-
// This is used to allow the effect to run when backspacing and if
|
|
293
|
-
// "touching" a word that "belongs" to a trigger. We consider a "trigger
|
|
294
|
-
// word" any word up to the limit of 3 from the trigger character.
|
|
295
|
-
// Anything beyond that is ignored if there's a mismatch. This allows
|
|
296
|
-
// us to "escape" a mismatch when backspacing, but still imposing some
|
|
297
|
-
// sane limits.
|
|
298
|
-
//
|
|
299
|
-
// Ex: "Some text @marcelo sekkkk" <--- "kkkk" caused a mismatch, but
|
|
300
|
-
// if the user presses backspace here, it will show the completion popup again.
|
|
301
|
-
const matchingWhileBackspacing =
|
|
302
|
-
backspacingRef.current && wordsFromTrigger.length <= 3;
|
|
303
|
-
|
|
304
|
-
if ( mismatch && ! ( matchingWhileBackspacing || hasOneTriggerWord ) ) {
|
|
305
|
-
if ( autocompleter ) {
|
|
306
|
-
reset();
|
|
307
|
-
}
|
|
308
|
-
return;
|
|
225
|
+
function getTextAfterSelection() {
|
|
226
|
+
return textContent
|
|
227
|
+
? getTextContent(
|
|
228
|
+
slice(
|
|
229
|
+
record,
|
|
230
|
+
undefined,
|
|
231
|
+
getTextContent( record ).length
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
: '';
|
|
309
235
|
}
|
|
310
236
|
|
|
311
|
-
const
|
|
312
|
-
|
|
237
|
+
const match = getAutocompleteMatch(
|
|
238
|
+
textContent,
|
|
239
|
+
completers,
|
|
240
|
+
filteredOptions.length,
|
|
241
|
+
backspacingRef.current,
|
|
242
|
+
getTextAfterSelection
|
|
313
243
|
);
|
|
314
244
|
|
|
315
|
-
if (
|
|
316
|
-
allowContext &&
|
|
317
|
-
! allowContext(
|
|
318
|
-
textContent.slice( 0, triggerIndex ),
|
|
319
|
-
textAfterSelection
|
|
320
|
-
)
|
|
321
|
-
) {
|
|
245
|
+
if ( ! match ) {
|
|
322
246
|
if ( autocompleter ) {
|
|
323
|
-
|
|
247
|
+
dispatch( { type: 'RESET' } );
|
|
324
248
|
}
|
|
325
249
|
return;
|
|
326
250
|
}
|
|
327
251
|
|
|
328
|
-
|
|
329
|
-
/^\s/.test( textWithoutTrigger ) ||
|
|
330
|
-
/\s\s+$/.test( textWithoutTrigger )
|
|
331
|
-
) {
|
|
332
|
-
if ( autocompleter ) {
|
|
333
|
-
reset();
|
|
334
|
-
}
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
252
|
+
const { completer, filterValue: query } = match;
|
|
337
253
|
|
|
338
|
-
|
|
339
|
-
if ( autocompleter ) {
|
|
340
|
-
reset();
|
|
341
|
-
}
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const safeTrigger = escapeRegExp( completer.triggerPrefix );
|
|
346
|
-
const text = removeAccents( textContent );
|
|
347
|
-
const match = text
|
|
348
|
-
.slice( text.lastIndexOf( completer.triggerPrefix ) )
|
|
349
|
-
.match( new RegExp( `${ safeTrigger }([\u0000-\uFFFF]*)$` ) );
|
|
350
|
-
const query = match && match[ 1 ];
|
|
351
|
-
|
|
352
|
-
setAutocompleter( completer );
|
|
353
|
-
setAutocompleterUI( () =>
|
|
354
|
-
completer !== autocompleter
|
|
355
|
-
? getAutoCompleterUI( completer )
|
|
356
|
-
: AutocompleterUI
|
|
357
|
-
);
|
|
358
|
-
setFilterValue( query === null ? '' : query );
|
|
254
|
+
dispatch( { type: 'MATCH', completer, query } );
|
|
359
255
|
// We want to avoid introducing unexpected side effects.
|
|
360
256
|
// See https://github.com/WordPress/gutenberg/pull/41820
|
|
361
257
|
}, [ textContent ] );
|
|
@@ -370,7 +266,7 @@ export function useAutocomplete( {
|
|
|
370
266
|
? `components-autocomplete-item-${ instanceId }-${ selectedKey }`
|
|
371
267
|
: null;
|
|
372
268
|
const hasSelection = record.start !== undefined;
|
|
373
|
-
const showPopover = !! textContent && hasSelection && !!
|
|
269
|
+
const showPopover = !! textContent && hasSelection && !! autocompleter;
|
|
374
270
|
|
|
375
271
|
return {
|
|
376
272
|
listBoxId,
|
|
@@ -378,6 +274,8 @@ export function useAutocomplete( {
|
|
|
378
274
|
onKeyDown: withIgnoreIMEEvents( handleKeyDown ),
|
|
379
275
|
popover: showPopover && (
|
|
380
276
|
<AutocompleterUI
|
|
277
|
+
key={ autocompleter.name + autocompleter.triggerPrefix }
|
|
278
|
+
autocompleter={ autocompleter }
|
|
381
279
|
className={ className }
|
|
382
280
|
filterValue={ filterValue }
|
|
383
281
|
instanceId={ instanceId }
|
|
@@ -385,25 +283,48 @@ export function useAutocomplete( {
|
|
|
385
283
|
selectedIndex={ selectedIndex }
|
|
386
284
|
onChangeOptions={ onChangeOptions }
|
|
387
285
|
onSelect={ select }
|
|
388
|
-
value={ record }
|
|
389
286
|
contentRef={ contentRef }
|
|
390
|
-
reset={
|
|
287
|
+
reset={ () => dispatch( { type: 'RESET' } ) }
|
|
391
288
|
/>
|
|
392
289
|
),
|
|
393
290
|
};
|
|
394
291
|
}
|
|
395
292
|
|
|
396
|
-
|
|
397
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Checks whether two records represent the same user-visible state
|
|
295
|
+
* (same text content and cursor position).
|
|
296
|
+
*/
|
|
297
|
+
function recordValuesMatch(
|
|
298
|
+
a: UseAutocompleteProps[ 'record' ],
|
|
299
|
+
b: UseAutocompleteProps[ 'record' ]
|
|
300
|
+
) {
|
|
301
|
+
return a.text === b.text && a.start === b.start && a.end === b.end;
|
|
302
|
+
}
|
|
398
303
|
|
|
399
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Tracks the last record whose value differed from the current one.
|
|
306
|
+
* Used to determine whether the user has actually typed something
|
|
307
|
+
*/
|
|
308
|
+
export function useLastDifferentValue(
|
|
309
|
+
value: UseAutocompleteProps[ 'record' ]
|
|
310
|
+
) {
|
|
311
|
+
const history = useRef< Array< typeof value > >( [] );
|
|
312
|
+
|
|
313
|
+
const lastEntry = history.current[ history.current.length - 1 ];
|
|
314
|
+
|
|
315
|
+
// Only add to history if the value is meaningfully different from
|
|
316
|
+
// the most recent entry (analogous to Set.add being a no-op for
|
|
317
|
+
// duplicate references in the original implementation).
|
|
318
|
+
if ( ! lastEntry || ! recordValuesMatch( value, lastEntry ) ) {
|
|
319
|
+
history.current.push( value );
|
|
320
|
+
}
|
|
400
321
|
|
|
401
322
|
// Keep the history size to 2.
|
|
402
|
-
if ( history.current.
|
|
403
|
-
history.current.
|
|
323
|
+
if ( history.current.length > 2 ) {
|
|
324
|
+
history.current.shift();
|
|
404
325
|
}
|
|
405
326
|
|
|
406
|
-
return
|
|
327
|
+
return history.current[ 0 ];
|
|
407
328
|
}
|
|
408
329
|
|
|
409
330
|
export function useAutocompleteProps( options: UseAutocompleteProps ) {
|