fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,131 @@
import { createElement, Fragment } from "react";
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useLayoutEffect, useRef, useEffect, useState } from '@wordpress/element';
import { useAnchor } from '@wordpress/rich-text';
import { useDebounce, useMergeRefs, useRefEffect } from '@wordpress/compose';
import { speak } from '@wordpress/a11y';
import { __, _n, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import getDefaultUseItems from './get-default-use-items';
import Button from '../button';
import Popover from '../popover';
import { VisuallyHidden } from '../visually-hidden';
import { createPortal } from 'react-dom';
export function getAutoCompleterUI(autocompleter) {
const useItems = autocompleter.useItems ? autocompleter.useItems : getDefaultUseItems(autocompleter);
function AutocompleterUI({
filterValue,
instanceId,
listBoxId,
className,
selectedIndex,
onChangeOptions,
onSelect,
onReset,
reset,
contentRef
}) {
const [items] = useItems(filterValue);
const popoverAnchor = useAnchor({
editableContentElement: contentRef.current
});
const [needsA11yCompat, setNeedsA11yCompat] = useState(false);
const popoverRef = useRef(null);
const popoverRefs = useMergeRefs([popoverRef, useRefEffect(node => {
if (!contentRef.current) return;
// If the popover is rendered in a different document than
// the content, we need to duplicate the options list in the
// content document so that it's available to the screen
// readers, which check the DOM ID based aira-* attributes.
setNeedsA11yCompat(node.ownerDocument !== contentRef.current.ownerDocument);
}, [contentRef])]);
useOnClickOutside(popoverRef, reset);
const debouncedSpeak = useDebounce(speak, 500);
function announce(options) {
if (!debouncedSpeak) {
return;
}
if (!!options.length) {
if (filterValue) {
debouncedSpeak(sprintf( /* translators: %d: number of results. */
_n('%d result found, use up and down arrow keys to navigate.', '%d results found, use up and down arrow keys to navigate.', options.length), options.length), 'assertive');
} else {
debouncedSpeak(sprintf( /* translators: %d: number of results. */
_n('Initial %d result loaded. Type to filter all available results. Use up and down arrow keys to navigate.', 'Initial %d results loaded. Type to filter all available results. Use up and down arrow keys to navigate.', options.length), options.length), 'assertive');
}
} else {
debouncedSpeak(__('No results.'), 'assertive');
}
}
useLayoutEffect(() => {
onChangeOptions(items);
announce(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;
}
const ListBox = ({
Component = 'div'
}) => createElement(Component, {
id: listBoxId,
role: "listbox",
className: "components-autocomplete__results"
}, items.map((option, index) => createElement(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)));
return createElement(Fragment, null, createElement(Popover, {
focusOnMount: false,
onClose: onReset,
placement: "top-start",
className: "components-autocomplete__popover",
anchor: popoverAnchor,
ref: popoverRefs
}, createElement(ListBox, null)), contentRef.current && needsA11yCompat && createPortal(createElement(ListBox, {
Component: VisuallyHidden
}), contentRef.current.ownerDocument.body));
}
return AutocompleterUI;
}
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = event => {
// 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)) {
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]);
}
//# sourceMappingURL=autocompleter-ui.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,138 @@
import { createElement } from "react";
/**
* External dependencies
*/
import { View, Animated, StyleSheet, Text, TouchableOpacity, ScrollView } from 'react-native';
/**
* WordPress dependencies
*/
import { useLayoutEffect, useEffect, useRef, useState, useCallback } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { Icon, __unstableAutocompletionItemsFill as AutocompletionItemsFill } from '@wordpress/components';
import { usePreferredColorSchemeStyle } from '@wordpress/compose';
/**
* Internal dependencies
*/
import BackgroundView from './background-view';
import getDefaultUseItems from './get-default-use-items';
import styles from './style.scss';
const {
compose: stylesCompose
} = StyleSheet;
export function getAutoCompleterUI(autocompleter) {
const useItems = autocompleter.useItems ? autocompleter.useItems : getDefaultUseItems(autocompleter);
function AutocompleterUI({
filterValue,
selectedIndex,
onChangeOptions,
onSelect,
value,
reset
}) {
const [items] = useItems(filterValue);
const filteredItems = items.filter(item => !item.isDisabled);
const scrollViewRef = useRef();
const animationValue = useRef(new Animated.Value(0)).current;
const [isVisible, setIsVisible] = useState(false);
const {
text
} = value;
useEffect(() => {
if (!isVisible && text.length > 0) {
setIsVisible(true);
}
}, [isVisible, text]);
useLayoutEffect(() => {
onChangeOptions(items);
scrollViewRef.current?.scrollTo({
x: 0,
animated: false
});
if (isVisible && text.length > 0) {
startAnimation(true);
} else if (isVisible && text.length === 0) {
startAnimation(false);
}
// 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, isVisible, text]);
const activeItemStyles = usePreferredColorSchemeStyle(styles['components-autocomplete__item-active'], styles['components-autocomplete__item-active-dark']);
const iconStyles = usePreferredColorSchemeStyle(styles['components-autocomplete__icon'], styles['components-autocomplete__icon-active-dark']);
const activeIconStyles = usePreferredColorSchemeStyle(styles['components-autocomplete__icon-active '], styles['components-autocomplete__icon-active-dark']);
const textStyles = usePreferredColorSchemeStyle(styles['components-autocomplete__text'], styles['components-autocomplete__text-dark']);
const activeTextStyles = usePreferredColorSchemeStyle(styles['components-autocomplete__text-active'], styles['components-autocomplete__text-active-dark']);
const startAnimation = useCallback(show => {
Animated.timing(animationValue, {
toValue: show ? 1 : 0,
duration: show ? 200 : 100,
useNativeDriver: true
}).start(({
finished
}) => {
if (finished && !show && isVisible) {
setIsVisible(false);
reset();
}
});
},
// 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
[isVisible]);
const contentStyles = {
transform: [{
translateY: animationValue.interpolate({
inputRange: [0, 1],
outputRange: [styles['components-autocomplete'].height, 0]
})
}]
};
if (!filteredItems.length > 0 || !isVisible) {
return null;
}
return createElement(AutocompletionItemsFill, null, createElement(View, {
style: styles['components-autocomplete']
}, createElement(Animated.View, {
style: contentStyles
}, createElement(BackgroundView, null, createElement(ScrollView, {
testID: "autocompleter",
ref: scrollViewRef,
horizontal: true,
contentContainerStyle: styles['components-autocomplete__content'],
showsHorizontalScrollIndicator: false,
keyboardShouldPersistTaps: "always",
accessibilityLabel:
// translators: Slash inserter autocomplete results
__('Slash inserter results')
}, filteredItems.map((option, index) => {
const isActive = index === selectedIndex;
const itemStyle = stylesCompose(styles['components-autocomplete__item'], isActive && activeItemStyles);
const textStyle = stylesCompose(textStyles, isActive && activeTextStyles);
const iconStyle = stylesCompose(iconStyles, isActive && activeIconStyles);
const iconSource = option?.value?.icon?.src || option?.value?.icon;
return createElement(TouchableOpacity, {
activeOpacity: 0.5,
style: itemStyle,
key: index,
onPress: () => onSelect(option),
accessibilityLabel: sprintf(
// translators: %s: Block name e.g. "Image block"
__('%s block'), option?.value?.title)
}, createElement(View, {
style: styles['components-autocomplete__icon']
}, createElement(Icon, {
icon: iconSource,
size: 24,
style: iconStyle
})), createElement(Text, {
style: textStyle
}, option?.value?.title));
}))))));
}
return AutocompleterUI;
}
export default getAutoCompleterUI;
//# sourceMappingURL=autocompleter-ui.native.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
import { createElement } from "react";
/**
* External dependencies
*/
import { View } from 'react-native';
/**
* WordPress dependencies
*/
import { usePreferredColorSchemeStyle } from '@wordpress/compose';
/**
* Internal dependencies
*/
import styles from './style.scss';
const BackgroundView = ({
children
}) => {
const backgroundStyles = usePreferredColorSchemeStyle(styles['components-autocomplete__background'], styles['components-autocomplete__background-dark']);
return createElement(View, {
style: backgroundStyles
}, children);
};
export default BackgroundView;
//# sourceMappingURL=background-view.android.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["View","usePreferredColorSchemeStyle","styles","BackgroundView","children","backgroundStyles","createElement","style"],"sources":["@wordpress/components/src/autocomplete/background-view.android.js"],"sourcesContent":["/**\n * External dependencies\n */\nimport { View } from 'react-native';\n\n/**\n * WordPress dependencies\n */\nimport { usePreferredColorSchemeStyle } from '@wordpress/compose';\n\n/**\n * Internal dependencies\n */\nimport styles from './style.scss';\n\nconst BackgroundView = ( { children } ) => {\n\tconst backgroundStyles = usePreferredColorSchemeStyle(\n\t\tstyles[ 'components-autocomplete__background' ],\n\t\tstyles[ 'components-autocomplete__background-dark' ]\n\t);\n\n\treturn <View style={ backgroundStyles }>{ children }</View>;\n};\n\nexport default BackgroundView;\n"],"mappings":";AAAA;AACA;AACA;AACA,SAASA,IAAI,QAAQ,cAAc;;AAEnC;AACA;AACA;AACA,SAASC,4BAA4B,QAAQ,oBAAoB;;AAEjE;AACA;AACA;AACA,OAAOC,MAAM,MAAM,cAAc;AAEjC,MAAMC,cAAc,GAAGA,CAAE;EAAEC;AAAS,CAAC,KAAM;EAC1C,MAAMC,gBAAgB,GAAGJ,4BAA4B,CACpDC,MAAM,CAAE,qCAAqC,CAAE,EAC/CA,MAAM,CAAE,0CAA0C,CACnD,CAAC;EAED,OAAOI,aAAA,CAACN,IAAI;IAACO,KAAK,EAAGF;EAAkB,GAAGD,QAAgB,CAAC;AAC5D,CAAC;AAED,eAAeD,cAAc"}

View File

@@ -0,0 +1,21 @@
import { createElement } from "react";
/**
* External dependencies
*/
import { BlurView } from '@react-native-community/blur';
/**
* Internal dependencies
*/
import styles from './style.scss';
const BackgroundView = ({
children
}) => {
return createElement(BlurView, {
style: styles['components-autocomplete__background-blur'],
blurType: "prominent",
blurAmount: 10
}, children);
};
export default BackgroundView;
//# sourceMappingURL=background-view.ios.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["BlurView","styles","BackgroundView","children","createElement","style","blurType","blurAmount"],"sources":["@wordpress/components/src/autocomplete/background-view.ios.js"],"sourcesContent":["/**\n * External dependencies\n */\nimport { BlurView } from '@react-native-community/blur';\n\n/**\n * Internal dependencies\n */\nimport styles from './style.scss';\n\nconst BackgroundView = ( { children } ) => {\n\treturn (\n\t\t<BlurView\n\t\t\tstyle={ styles[ 'components-autocomplete__background-blur' ] }\n\t\t\tblurType=\"prominent\"\n\t\t\tblurAmount={ 10 }\n\t\t>\n\t\t\t{ children }\n\t\t</BlurView>\n\t);\n};\n\nexport default BackgroundView;\n"],"mappings":";AAAA;AACA;AACA;AACA,SAASA,QAAQ,QAAQ,8BAA8B;;AAEvD;AACA;AACA;AACA,OAAOC,MAAM,MAAM,cAAc;AAEjC,MAAMC,cAAc,GAAGA,CAAE;EAAEC;AAAS,CAAC,KAAM;EAC1C,OACCC,aAAA,CAACJ,QAAQ;IACRK,KAAK,EAAGJ,MAAM,CAAE,0CAA0C,CAAI;IAC9DK,QAAQ,EAAC,WAAW;IACpBC,UAAU,EAAG;EAAI,GAEfJ,QACO,CAAC;AAEb,CAAC;AAED,eAAeD,cAAc"}

View File

@@ -0,0 +1,90 @@
/**
* External dependencies
*/
import removeAccents from 'remove-accents';
/**
* WordPress dependencies
*/
import { debounce } from '@wordpress/compose';
import { useLayoutEffect, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { escapeRegExp } from '../utils/strings';
function filterOptions(search, options = [], maxResults = 10) {
const filtered = [];
for (let i = 0; i < options.length; i++) {
const option = options[i];
// Merge label into keywords.
let {
keywords = []
} = option;
if ('string' === typeof option.label) {
keywords = [...keywords, option.label];
}
const isMatch = keywords.some(keyword => search.test(removeAccents(keyword)));
if (!isMatch) {
continue;
}
filtered.push(option);
// Abort early if max reached.
if (filtered.length === maxResults) {
break;
}
}
return filtered;
}
export default function getDefaultUseItems(autocompleter) {
return filterValue => {
const [items, setItems] = useState([]);
/*
* We support both synchronous and asynchronous retrieval of completer options
* but internally treat all as async so we maintain a single, consistent code path.
*
* Because networks can be slow, and the internet is wonderfully unpredictable,
* we don't want two promises updating the state at once. This ensures that only
* the most recent promise will act on `optionsData`. This doesn't use the state
* because `setState` is batched, and so there's no guarantee that setting
* `activePromise` in the state would result in it actually being in `this.state`
* before the promise resolves and we check to see if this is the active promise or not.
*/
useLayoutEffect(() => {
const {
options,
isDebounced
} = autocompleter;
const loadOptions = debounce(() => {
const promise = Promise.resolve(typeof options === 'function' ? options(filterValue) : options).then(optionsData => {
if (promise.canceled) {
return;
}
const keyedOptions = optionsData.map((optionData, optionIndex) => ({
key: `${autocompleter.name}-${optionIndex}`,
value: optionData,
label: autocompleter.getOptionLabel(optionData),
keywords: autocompleter.getOptionKeywords ? autocompleter.getOptionKeywords(optionData) : [],
isDisabled: autocompleter.isOptionDisabled ? autocompleter.isOptionDisabled(optionData) : false
}));
// Create a regular expression to filter the options.
const search = new RegExp('(?:\\b|\\s|^)' + escapeRegExp(filterValue), 'i');
setItems(filterOptions(search, keyedOptions));
});
return promise;
}, isDebounced ? 250 : 0);
const promise = loadOptions();
return () => {
loadOptions.cancel();
if (promise) {
promise.canceled = true;
}
};
}, [filterValue]);
return [items];
};
}
//# sourceMappingURL=get-default-use-items.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,357 @@
import { createElement, Fragment } from "react";
/**
* External dependencies
*/
import removeAccents from 'remove-accents';
/**
* WordPress dependencies
*/
import { renderToString, useEffect, useState, useRef, useMemo } from '@wordpress/element';
import { useInstanceId, useMergeRefs, useRefEffect } from '@wordpress/compose';
import { create, slice, insert, isCollapsed, getTextContent } from '@wordpress/rich-text';
import { speak } from '@wordpress/a11y';
import { isAppleOS } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
import { getAutoCompleterUI } from './autocompleter-ui';
import { escapeRegExp } from '../utils/strings';
const getNodeText = node => {
if (node === null) {
return '';
}
switch (typeof node) {
case 'string':
case 'number':
return node.toString();
break;
case 'boolean':
return '';
break;
case 'object':
{
if (node instanceof Array) {
return node.map(getNodeText).join('');
}
if ('props' in node) {
return getNodeText(node.props.children);
}
break;
}
default:
return '';
}
return '';
};
const EMPTY_FILTERED_OPTIONS = [];
export function useAutocomplete({
record,
onChange,
onReplace,
completers,
contentRef
}) {
const instanceId = useInstanceId(useAutocomplete);
const [selectedIndex, setSelectedIndex] = useState(0);
const [filteredOptions, setFilteredOptions] = useState(EMPTY_FILTERED_OPTIONS);
const [filterValue, setFilterValue] = useState('');
const [autocompleter, setAutocompleter] = useState(null);
const [AutocompleterUI, setAutocompleterUI] = useState(null);
const backspacing = useRef(false);
function insertCompletion(replacement) {
if (autocompleter === null) {
return;
}
const end = record.start;
const start = end - autocompleter.triggerPrefix.length - filterValue.length;
const toInsert = create({
html: renderToString(replacement)
});
onChange(insert(record, toInsert, start, end));
}
function select(option) {
const {
getOptionCompletion
} = autocompleter || {};
if (option.isDisabled) {
return;
}
if (getOptionCompletion) {
const completion = getOptionCompletion(option.value, filterValue);
const isCompletionObject = obj => {
return obj !== null && typeof obj === 'object' && 'action' in obj && obj.action !== undefined && 'value' in obj && obj.value !== undefined;
};
const completionObject = isCompletionObject(completion) ? completion : {
action: 'insert-at-caret',
value: completion
};
if ('replace' === completionObject.action) {
onReplace([completionObject.value]);
// When replacing, the component will unmount, so don't reset
// state (below) on an unmounted component.
return;
} else if ('insert-at-caret' === completionObject.action) {
insertCompletion(completionObject.value);
}
}
// Reset autocomplete state after insertion rather than before
// so insertion events don't cause the completion menu to redisplay.
reset();
}
function reset() {
setSelectedIndex(0);
setFilteredOptions(EMPTY_FILTERED_OPTIONS);
setFilterValue('');
setAutocompleter(null);
setAutocompleterUI(null);
}
/**
* Load options for an autocompleter.
*
* @param {Array} options
*/
function onChangeOptions(options) {
setSelectedIndex(options.length === filteredOptions.length ? selectedIndex : 0);
setFilteredOptions(options);
}
function handleKeyDown(event) {
backspacing.current = event.key === 'Backspace';
if (!autocompleter) {
return;
}
if (filteredOptions.length === 0) {
return;
}
if (event.defaultPrevented ||
// Ignore keydowns from IMEs
event.isComposing ||
// Workaround for Mac Safari where the final Enter/Backspace of an IME composition
// is `isComposing=false`, even though it's technically still part of the composition.
// These can only be detected by keyCode.
event.keyCode === 229) {
return;
}
switch (event.key) {
case 'ArrowUp':
{
const newIndex = (selectedIndex === 0 ? filteredOptions.length : selectedIndex) - 1;
setSelectedIndex(newIndex);
// See the related PR as to why this is necessary: https://github.com/WordPress/gutenberg/pull/54902.
if (isAppleOS()) {
speak(getNodeText(filteredOptions[newIndex].label), 'assertive');
}
break;
}
case 'ArrowDown':
{
const newIndex = (selectedIndex + 1) % filteredOptions.length;
setSelectedIndex(newIndex);
if (isAppleOS()) {
speak(getNodeText(filteredOptions[newIndex].label), 'assertive');
}
break;
}
case 'Escape':
setAutocompleter(null);
setAutocompleterUI(null);
event.preventDefault();
break;
case 'Enter':
select(filteredOptions[selectedIndex]);
break;
case 'ArrowLeft':
case 'ArrowRight':
reset();
return;
default:
return;
}
// Any handled key should prevent original behavior. This relies on
// the early return in the default case.
event.preventDefault();
}
// textContent is a primitive (string), memoizing is not strictly necessary
// but this is a preemptive performance improvement, since the autocompleter
// is a potential bottleneck for the editor type metric.
const textContent = useMemo(() => {
if (isCollapsed(record)) {
return getTextContent(slice(record, 0));
}
return '';
}, [record]);
useEffect(() => {
if (!textContent) {
if (autocompleter) reset();
return;
}
// Find the completer with the highest triggerPrefix index in the
// textContent.
const completer = completers.reduce((lastTrigger, currentCompleter) => {
const triggerIndex = textContent.lastIndexOf(currentCompleter.triggerPrefix);
const lastTriggerIndex = lastTrigger !== null ? textContent.lastIndexOf(lastTrigger.triggerPrefix) : -1;
return triggerIndex > lastTriggerIndex ? currentCompleter : lastTrigger;
}, null);
if (!completer) {
if (autocompleter) reset();
return;
}
const {
allowContext,
triggerPrefix
} = completer;
const triggerIndex = textContent.lastIndexOf(triggerPrefix);
const textWithoutTrigger = textContent.slice(triggerIndex + triggerPrefix.length);
const tooDistantFromTrigger = textWithoutTrigger.length > 50; // 50 chars seems to be a good limit.
// This is a final barrier to prevent the effect from completing with
// an extremely long string, which causes the editor to slow-down
// significantly. This could happen, for example, if `matchingWhileBackspacing`
// is true and one of the "words" end up being too long. If that's the case,
// it will be caught by this guard.
if (tooDistantFromTrigger) return;
const mismatch = filteredOptions.length === 0;
const wordsFromTrigger = textWithoutTrigger.split(/\s/);
// We need to allow the effect to run when not backspacing and if there
// was a mismatch. i.e when typing a trigger + the match string or when
// clicking in an existing trigger word on the page. We do that if we
// detect that we have one word from trigger in the current textual context.
//
// Ex.: "Some text @a" <-- "@a" will be detected as the trigger word and
// allow the effect to run. It will run until there's a mismatch.
const hasOneTriggerWord = wordsFromTrigger.length === 1;
// This is used to allow the effect to run when backspacing and if
// "touching" a word that "belongs" to a trigger. We consider a "trigger
// word" any word up to the limit of 3 from the trigger character.
// Anything beyond that is ignored if there's a mismatch. This allows
// us to "escape" a mismatch when backspacing, but still imposing some
// sane limits.
//
// Ex: "Some text @marcelo sekkkk" <--- "kkkk" caused a mismatch, but
// if the user presses backspace here, it will show the completion popup again.
const matchingWhileBackspacing = backspacing.current && wordsFromTrigger.length <= 3;
if (mismatch && !(matchingWhileBackspacing || hasOneTriggerWord)) {
if (autocompleter) reset();
return;
}
const textAfterSelection = getTextContent(slice(record, undefined, getTextContent(record).length));
if (allowContext && !allowContext(textContent.slice(0, triggerIndex), textAfterSelection)) {
if (autocompleter) reset();
return;
}
if (/^\s/.test(textWithoutTrigger) || /\s\s+$/.test(textWithoutTrigger)) {
if (autocompleter) reset();
return;
}
if (!/[\u0000-\uFFFF]*$/.test(textWithoutTrigger)) {
if (autocompleter) reset();
return;
}
const safeTrigger = escapeRegExp(completer.triggerPrefix);
const text = removeAccents(textContent);
const match = text.slice(text.lastIndexOf(completer.triggerPrefix)).match(new RegExp(`${safeTrigger}([\u0000-\uFFFF]*)$`));
const query = match && match[1];
setAutocompleter(completer);
setAutocompleterUI(() => completer !== autocompleter ? getAutoCompleterUI(completer) : AutocompleterUI);
setFilterValue(query === null ? '' : query);
// 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
}, [textContent]);
const {
key: selectedKey = ''
} = filteredOptions[selectedIndex] || {};
const {
className
} = autocompleter || {};
const isExpanded = !!autocompleter && filteredOptions.length > 0;
const listBoxId = isExpanded ? `components-autocomplete-listbox-${instanceId}` : undefined;
const activeId = isExpanded ? `components-autocomplete-item-${instanceId}-${selectedKey}` : null;
const hasSelection = record.start !== undefined;
return {
listBoxId,
activeId,
onKeyDown: handleKeyDown,
popover: hasSelection && AutocompleterUI && createElement(AutocompleterUI, {
className: className,
filterValue: filterValue,
instanceId: instanceId,
listBoxId: listBoxId,
selectedIndex: selectedIndex,
onChangeOptions: onChangeOptions,
onSelect: select,
value: record,
contentRef: contentRef,
reset: reset
})
};
}
function useLastDifferentValue(value) {
const history = useRef(new Set());
history.current.add(value);
// Keep the history size to 2.
if (history.current.size > 2) {
history.current.delete(Array.from(history.current)[0]);
}
return Array.from(history.current)[0];
}
export function useAutocompleteProps(options) {
const ref = useRef(null);
const onKeyDownRef = useRef();
const {
record
} = options;
const previousRecord = useLastDifferentValue(record);
const {
popover,
listBoxId,
activeId,
onKeyDown
} = useAutocomplete({
...options,
contentRef: ref
});
onKeyDownRef.current = onKeyDown;
const mergedRefs = useMergeRefs([ref, useRefEffect(element => {
function _onKeyDown(event) {
onKeyDownRef.current?.(event);
}
element.addEventListener('keydown', _onKeyDown);
return () => {
element.removeEventListener('keydown', _onKeyDown);
};
}, [])]);
// We only want to show the popover if the user has typed something.
const didUserInput = record.text !== previousRecord?.text;
if (!didUserInput) {
return {
ref: mergedRefs
};
}
return {
ref: mergedRefs,
children: popover,
'aria-autocomplete': listBoxId ? 'list' : undefined,
'aria-owns': listBoxId,
'aria-activedescendant': activeId
};
}
export default function Autocomplete({
children,
isSelected,
...options
}) {
const {
popover,
...props
} = useAutocomplete(options);
return createElement(Fragment, null, children(props), isSelected && popover);
}
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map

File diff suppressed because one or more lines are too long