Skip to content
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

Fix autocomplete trigger character detection #55301

Merged
merged 4 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
### Bug Fix

- `Modal`: fix closing when contained iframe is focused ([#51602](https://github.com/WordPress/gutenberg/pull/51602)).
- `Autocomplete`: Fix disappearing results issue when using multiple triggers inline ([#55301](https://github.com/WordPress/gutenberg/pull/55301))

### Internal

Expand Down
153 changes: 83 additions & 70 deletions packages/components/src/autocomplete/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,82 +218,95 @@ export function useAutocomplete( {
return;
}

const completer = completers?.find(
( { triggerPrefix, allowContext } ) => {
const index = textContent.lastIndexOf( triggerPrefix );
// Find the completer with the highest triggerPrefix index in the
// textContent.
const completer = completers.reduce< WPCompleter | null >(
( lastTrigger, currentCompleter ) => {
const triggerIndex = textContent.lastIndexOf(
currentCompleter.triggerPrefix
);
const lastTriggerIndex =
lastTrigger !== null
? textContent.lastIndexOf( lastTrigger.triggerPrefix )
: -1;

return triggerIndex > lastTriggerIndex
? currentCompleter
: lastTrigger;
},
null
);

if ( index === -1 ) {
return false;
}
if ( ! completer ) {
if ( autocompleter ) reset();
return;
}

const textWithoutTrigger = textContent.slice(
index + triggerPrefix.length
);
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 false;

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 &&
textWithoutTrigger.split( /\s/ ).length <= 3;

if (
mismatch &&
! ( matchingWhileBackspacing || hasOneTriggerWord )
) {
return false;
}

const textAfterSelection = getTextContent(
slice( record, undefined, getTextContent( record ).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;
}

if (
allowContext &&
! allowContext(
textContent.slice( 0, index ),
textAfterSelection
)
) {
return false;
}

if (
/^\s/.test( textWithoutTrigger ) ||
/\s\s+$/.test( textWithoutTrigger )
) {
return false;
}

return /[\u0000-\uFFFF]*$/.test( textWithoutTrigger );
}
const textAfterSelection = getTextContent(
slice( record, undefined, getTextContent( record ).length )
);

if ( ! completer ) {
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;
}
Expand Down
Loading