Skip to content

Commit cc4e2bc

Browse files
authored
Recalculate autocomplete suggestions if the input data changes while the menu is open (#3009)
* Add support for custom emoji suggestions * Allow declaratively setting suggestions as "loading" * Recalculate suggestions on update * Create .changeset/silly-plants-draw.md * Fix bug where page would be unresponsive if no suggestions provided
1 parent faa7667 commit cc4e2bc

File tree

5 files changed

+86
-32
lines changed

5 files changed

+86
-32
lines changed

.changeset/silly-plants-draw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": patch
3+
---
4+
5+
Recalculate autocomplete suggestions if the input data changes while the menu is open

src/drafts/MarkdownEditor/_MarkdownInput.tsx

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ interface MarkdownInputProps extends Omit<TextareaProps, 'onChange'> {
3030
visible: boolean
3131
}
3232

33+
const emptyArray: [] = [] // constant reference to avoid re-running effects
34+
3335
export const MarkdownInput = forwardRef<HTMLTextAreaElement, MarkdownInputProps>(
3436
(
3537
{
@@ -55,33 +57,62 @@ export const MarkdownInput = forwardRef<HTMLTextAreaElement, MarkdownInputProps>
5557
forwardedRef,
5658
) => {
5759
const [suggestions, setSuggestions] = useState<Suggestions | null>(null)
60+
const [event, setEvent] = useState<ShowSuggestionsEvent | null>(null)
5861

5962
const {trigger: emojiTrigger, calculateSuggestions: calculateEmojiSuggestions} = useEmojiSuggestions(
60-
emojiSuggestions ?? [],
63+
emojiSuggestions ?? emptyArray,
6164
)
6265
const {trigger: mentionsTrigger, calculateSuggestions: calculateMentionSuggestions} = useMentionSuggestions(
63-
mentionSuggestions ?? [],
66+
mentionSuggestions ?? emptyArray,
6467
)
6568
const {trigger: referencesTrigger, calculateSuggestions: calculateReferenceSuggestions} = useReferenceSuggestions(
66-
referenceSuggestions ?? [],
69+
referenceSuggestions ?? emptyArray,
6770
)
6871

6972
const triggers = useMemo(
7073
() => [mentionsTrigger, referencesTrigger, emojiTrigger],
7174
[mentionsTrigger, referencesTrigger, emojiTrigger],
7275
)
7376

74-
const onShowSuggestions = async (event: ShowSuggestionsEvent) => {
75-
setSuggestions('loading')
76-
if (event.trigger.triggerChar === emojiTrigger.triggerChar) {
77-
setSuggestions(await calculateEmojiSuggestions(event.query))
78-
} else if (event.trigger.triggerChar === mentionsTrigger.triggerChar) {
79-
setSuggestions(await calculateMentionSuggestions(event.query))
80-
} else if (event.trigger.triggerChar === referencesTrigger.triggerChar) {
81-
setSuggestions(await calculateReferenceSuggestions(event.query))
82-
}
77+
const lastEventRef = useRef<ShowSuggestionsEvent | null>(null)
78+
79+
const onHideSuggestions = () => {
80+
setEvent(null)
81+
setSuggestions(null) // the effect would do this anyway, but this allows React to batch the update
8382
}
8483

84+
// running the calculation in an effect (rather than in the onShowSuggestions handler) allows us
85+
// to automatically recalculate if the suggestions change while the menu is open
86+
useEffect(() => {
87+
if (!event) {
88+
setSuggestions(null)
89+
return
90+
}
91+
92+
// (prettier vs. eslint conflict)
93+
// eslint-disable-next-line @typescript-eslint/no-extra-semi
94+
;(async function () {
95+
lastEventRef.current = event
96+
setSuggestions('loading')
97+
if (event.trigger.triggerChar === emojiTrigger.triggerChar) {
98+
setSuggestions(await calculateEmojiSuggestions(event.query))
99+
} else if (event.trigger.triggerChar === mentionsTrigger.triggerChar) {
100+
setSuggestions(await calculateMentionSuggestions(event.query))
101+
} else if (event.trigger.triggerChar === referencesTrigger.triggerChar) {
102+
setSuggestions(await calculateReferenceSuggestions(event.query))
103+
}
104+
})()
105+
}, [
106+
event,
107+
calculateEmojiSuggestions,
108+
calculateMentionSuggestions,
109+
calculateReferenceSuggestions,
110+
// The triggers never actually change because they are statically defined
111+
emojiTrigger,
112+
mentionsTrigger,
113+
referencesTrigger,
114+
])
115+
85116
const ref = useRef<HTMLTextAreaElement>(null)
86117
useRefObjectAsForwardedRef(forwardedRef, ref)
87118

@@ -99,8 +130,8 @@ export const MarkdownInput = forwardRef<HTMLTextAreaElement, MarkdownInputProps>
99130
<InlineAutocomplete
100131
triggers={triggers}
101132
suggestions={suggestions}
102-
onShowSuggestions={e => onShowSuggestions(e)}
103-
onHideSuggestions={() => setSuggestions(null)}
133+
onShowSuggestions={setEvent}
134+
onHideSuggestions={onHideSuggestions}
104135
sx={{flex: 'auto'}}
105136
tabInsertsSuggestions
106137
>

src/drafts/MarkdownEditor/suggestions/_useEmojiSuggestions.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, {useMemo} from 'react'
22
import {suggestionsCalculator, UseSuggestionsHook} from '.'
33
import {ActionList} from '../../../ActionList'
44
import {Suggestion, Trigger} from '../../InlineAutocomplete'
@@ -56,7 +56,13 @@ const scoreSuggestion = (query: string, emoji: Emoji): number => {
5656
return score
5757
}
5858

59-
export const useEmojiSuggestions: UseSuggestionsHook<Emoji> = emojis => ({
60-
calculateSuggestions: suggestionsCalculator(emojis, scoreSuggestion, emojiToSugggestion),
61-
trigger,
62-
})
59+
export const useEmojiSuggestions: UseSuggestionsHook<Emoji> = emojis => {
60+
const calculateSuggestions = useMemo(
61+
() => suggestionsCalculator(emojis, scoreSuggestion, emojiToSugggestion),
62+
[emojis],
63+
)
64+
return {
65+
calculateSuggestions,
66+
trigger,
67+
}
68+
}

src/drafts/MarkdownEditor/suggestions/_useMentionSuggestions.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {score} from 'fzy.js'
2-
import React from 'react'
2+
import React, {useMemo} from 'react'
33
import {suggestionsCalculator, UseSuggestionsHook} from '.'
44
import {ActionList} from '../../../ActionList'
55
import {Suggestion, Trigger} from '../../InlineAutocomplete'
@@ -37,7 +37,13 @@ const scoreSuggestion = (query: string, mentionable: Mentionable): number => {
3737
return fzyScore
3838
}
3939

40-
export const useMentionSuggestions: UseSuggestionsHook<Mentionable> = mentionables => ({
41-
calculateSuggestions: suggestionsCalculator(mentionables, scoreSuggestion, mentionableToSuggestion),
42-
trigger,
43-
})
40+
export const useMentionSuggestions: UseSuggestionsHook<Mentionable> = mentionables => {
41+
const calculateSuggestions = useMemo(
42+
() => suggestionsCalculator(mentionables, scoreSuggestion, mentionableToSuggestion),
43+
[mentionables],
44+
)
45+
return {
46+
calculateSuggestions,
47+
trigger,
48+
}
49+
}

src/drafts/MarkdownEditor/suggestions/_useReferenceSuggestions.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, {useMemo} from 'react'
22
import {suggestionsCalculator, UseSuggestionsHook} from '.'
33
import {ActionList} from '../../../ActionList'
44
import {Suggestion, Trigger} from '../../InlineAutocomplete'
@@ -51,10 +51,16 @@ const scoreSuggestion = (query: string, reference: Reference): number => {
5151
return fzyScore === Infinity ? -Infinity : fzyScore
5252
}
5353

54-
export const useReferenceSuggestions: UseSuggestionsHook<Reference> = references => ({
55-
calculateSuggestions: async (query: string) => {
56-
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
57-
return suggestionsCalculator(references, scoreSuggestion, referenceToSuggestion)(query)
58-
},
59-
trigger,
60-
})
54+
export const useReferenceSuggestions: UseSuggestionsHook<Reference> = references => {
55+
const calculateSuggestions = useMemo(() => {
56+
const calculator = suggestionsCalculator(references, scoreSuggestion, referenceToSuggestion)
57+
return async (query: string) => {
58+
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
59+
return calculator(query)
60+
}
61+
}, [references])
62+
return {
63+
calculateSuggestions,
64+
trigger,
65+
}
66+
}

0 commit comments

Comments
 (0)