diff --git a/.changeset/silly-plants-draw.md b/.changeset/silly-plants-draw.md new file mode 100644 index 00000000000..5231b92bad0 --- /dev/null +++ b/.changeset/silly-plants-draw.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +Recalculate autocomplete suggestions if the input data changes while the menu is open diff --git a/src/drafts/MarkdownEditor/_MarkdownInput.tsx b/src/drafts/MarkdownEditor/_MarkdownInput.tsx index 71897ea389f..6b78f2e5892 100644 --- a/src/drafts/MarkdownEditor/_MarkdownInput.tsx +++ b/src/drafts/MarkdownEditor/_MarkdownInput.tsx @@ -30,6 +30,8 @@ interface MarkdownInputProps extends Omit { visible: boolean } +const emptyArray: [] = [] // constant reference to avoid re-running effects + export const MarkdownInput = forwardRef( ( { @@ -55,15 +57,16 @@ export const MarkdownInput = forwardRef forwardedRef, ) => { const [suggestions, setSuggestions] = useState(null) + const [event, setEvent] = useState(null) const {trigger: emojiTrigger, calculateSuggestions: calculateEmojiSuggestions} = useEmojiSuggestions( - emojiSuggestions ?? [], + emojiSuggestions ?? emptyArray, ) const {trigger: mentionsTrigger, calculateSuggestions: calculateMentionSuggestions} = useMentionSuggestions( - mentionSuggestions ?? [], + mentionSuggestions ?? emptyArray, ) const {trigger: referencesTrigger, calculateSuggestions: calculateReferenceSuggestions} = useReferenceSuggestions( - referenceSuggestions ?? [], + referenceSuggestions ?? emptyArray, ) const triggers = useMemo( @@ -71,17 +74,45 @@ export const MarkdownInput = forwardRef [mentionsTrigger, referencesTrigger, emojiTrigger], ) - const onShowSuggestions = async (event: ShowSuggestionsEvent) => { - setSuggestions('loading') - if (event.trigger.triggerChar === emojiTrigger.triggerChar) { - setSuggestions(await calculateEmojiSuggestions(event.query)) - } else if (event.trigger.triggerChar === mentionsTrigger.triggerChar) { - setSuggestions(await calculateMentionSuggestions(event.query)) - } else if (event.trigger.triggerChar === referencesTrigger.triggerChar) { - setSuggestions(await calculateReferenceSuggestions(event.query)) - } + const lastEventRef = useRef(null) + + const onHideSuggestions = () => { + setEvent(null) + setSuggestions(null) // the effect would do this anyway, but this allows React to batch the update } + // running the calculation in an effect (rather than in the onShowSuggestions handler) allows us + // to automatically recalculate if the suggestions change while the menu is open + useEffect(() => { + if (!event) { + setSuggestions(null) + return + } + + // (prettier vs. eslint conflict) + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;(async function () { + lastEventRef.current = event + setSuggestions('loading') + if (event.trigger.triggerChar === emojiTrigger.triggerChar) { + setSuggestions(await calculateEmojiSuggestions(event.query)) + } else if (event.trigger.triggerChar === mentionsTrigger.triggerChar) { + setSuggestions(await calculateMentionSuggestions(event.query)) + } else if (event.trigger.triggerChar === referencesTrigger.triggerChar) { + setSuggestions(await calculateReferenceSuggestions(event.query)) + } + })() + }, [ + event, + calculateEmojiSuggestions, + calculateMentionSuggestions, + calculateReferenceSuggestions, + // The triggers never actually change because they are statically defined + emojiTrigger, + mentionsTrigger, + referencesTrigger, + ]) + const ref = useRef(null) useRefObjectAsForwardedRef(forwardedRef, ref) @@ -99,8 +130,8 @@ export const MarkdownInput = forwardRef onShowSuggestions(e)} - onHideSuggestions={() => setSuggestions(null)} + onShowSuggestions={setEvent} + onHideSuggestions={onHideSuggestions} sx={{flex: 'auto'}} tabInsertsSuggestions > diff --git a/src/drafts/MarkdownEditor/suggestions/_useEmojiSuggestions.tsx b/src/drafts/MarkdownEditor/suggestions/_useEmojiSuggestions.tsx index ec3108f1387..7c40dd82230 100644 --- a/src/drafts/MarkdownEditor/suggestions/_useEmojiSuggestions.tsx +++ b/src/drafts/MarkdownEditor/suggestions/_useEmojiSuggestions.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useMemo} from 'react' import {suggestionsCalculator, UseSuggestionsHook} from '.' import {ActionList} from '../../../ActionList' import {Suggestion, Trigger} from '../../InlineAutocomplete' @@ -56,7 +56,13 @@ const scoreSuggestion = (query: string, emoji: Emoji): number => { return score } -export const useEmojiSuggestions: UseSuggestionsHook = emojis => ({ - calculateSuggestions: suggestionsCalculator(emojis, scoreSuggestion, emojiToSugggestion), - trigger, -}) +export const useEmojiSuggestions: UseSuggestionsHook = emojis => { + const calculateSuggestions = useMemo( + () => suggestionsCalculator(emojis, scoreSuggestion, emojiToSugggestion), + [emojis], + ) + return { + calculateSuggestions, + trigger, + } +} diff --git a/src/drafts/MarkdownEditor/suggestions/_useMentionSuggestions.tsx b/src/drafts/MarkdownEditor/suggestions/_useMentionSuggestions.tsx index b3ecb41ea4a..386b4e4165e 100644 --- a/src/drafts/MarkdownEditor/suggestions/_useMentionSuggestions.tsx +++ b/src/drafts/MarkdownEditor/suggestions/_useMentionSuggestions.tsx @@ -1,5 +1,5 @@ import {score} from 'fzy.js' -import React from 'react' +import React, {useMemo} from 'react' import {suggestionsCalculator, UseSuggestionsHook} from '.' import {ActionList} from '../../../ActionList' import {Suggestion, Trigger} from '../../InlineAutocomplete' @@ -37,7 +37,13 @@ const scoreSuggestion = (query: string, mentionable: Mentionable): number => { return fzyScore } -export const useMentionSuggestions: UseSuggestionsHook = mentionables => ({ - calculateSuggestions: suggestionsCalculator(mentionables, scoreSuggestion, mentionableToSuggestion), - trigger, -}) +export const useMentionSuggestions: UseSuggestionsHook = mentionables => { + const calculateSuggestions = useMemo( + () => suggestionsCalculator(mentionables, scoreSuggestion, mentionableToSuggestion), + [mentionables], + ) + return { + calculateSuggestions, + trigger, + } +} diff --git a/src/drafts/MarkdownEditor/suggestions/_useReferenceSuggestions.tsx b/src/drafts/MarkdownEditor/suggestions/_useReferenceSuggestions.tsx index 46f89269f52..efd97c4f582 100644 --- a/src/drafts/MarkdownEditor/suggestions/_useReferenceSuggestions.tsx +++ b/src/drafts/MarkdownEditor/suggestions/_useReferenceSuggestions.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useMemo} from 'react' import {suggestionsCalculator, UseSuggestionsHook} from '.' import {ActionList} from '../../../ActionList' import {Suggestion, Trigger} from '../../InlineAutocomplete' @@ -51,10 +51,16 @@ const scoreSuggestion = (query: string, reference: Reference): number => { return fzyScore === Infinity ? -Infinity : fzyScore } -export const useReferenceSuggestions: UseSuggestionsHook = references => ({ - calculateSuggestions: async (query: string) => { - if (/^\d+\s/.test(query)) return [] // don't return anything if the query is in the form #123 ..., assuming they already have the number they want - return suggestionsCalculator(references, scoreSuggestion, referenceToSuggestion)(query) - }, - trigger, -}) +export const useReferenceSuggestions: UseSuggestionsHook = references => { + const calculateSuggestions = useMemo(() => { + const calculator = suggestionsCalculator(references, scoreSuggestion, referenceToSuggestion) + return async (query: string) => { + if (/^\d+\s/.test(query)) return [] // don't return anything if the query is in the form #123 ..., assuming they already have the number they want + return calculator(query) + } + }, [references]) + return { + calculateSuggestions, + trigger, + } +}