Skip to content

Commit

Permalink
Autocomplete: Fix multiple trigger character detection (#55301)
Browse files Browse the repository at this point in the history
  • Loading branch information
WunderBart committed Oct 19, 2023
1 parent b52b7cd commit fba216c
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 70 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

- Render a "mouse event trap" when using a `ColorPicker` inside a `Popover` to prevent issues when rendering on top of `iframes` ([#55149](https://github.com/WordPress/gutenberg/pull/55149)).
- `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

0 comments on commit fba216c

Please sign in to comment.