From 6eab991c42c409b900188d56c4985f67d43e2268 Mon Sep 17 00:00:00 2001 From: Sam Magura Date: Fri, 22 Jul 2022 10:29:12 -0400 Subject: [PATCH 1/2] Allow data function to return a promise --- src/MentionsInput.js | 53 +++++++++++++++++------------- src/MentionsInput.spec.js | 68 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 26 deletions(-) diff --git a/src/MentionsInput.js b/src/MentionsInput.js index 07576517..1815f671 100755 --- a/src/MentionsInput.js +++ b/src/MentionsInput.js @@ -278,7 +278,7 @@ class MentionsInput extends React.Component { scrollFocusedIntoView={this.state.scrollFocusedIntoView} containerRef={this.setSuggestionsElement} suggestions={this.state.suggestions} - customSuggestionsContainer ={this.props.customSuggestionsContainer} + customSuggestionsContainer={this.props.customSuggestionsContainer} onSelect={this.addMention} onMouseDown={this.handleSuggestionsMouseDown} onMouseEnter={this.handleSuggestionsMouseEnter} @@ -489,7 +489,7 @@ class MentionsInput extends React.Component { // Handle input element's change event handleChange = (ev) => { isComposing = false - if(isIE()){ + if (isIE()) { // if we are inside iframe, we need to find activeElement within its contentDocument const currentDocument = (document.activeElement && document.activeElement.contentDocument) || @@ -687,7 +687,11 @@ class MentionsInput extends React.Component { updateSuggestionsPosition = () => { let { caretPosition } = this.state - const { suggestionsPortalHost, allowSuggestionsAboveCursor, forceSuggestionsAboveCursor } = this.props + const { + suggestionsPortalHost, + allowSuggestionsAboveCursor, + forceSuggestionsAboveCursor, + } = this.props if (!caretPosition || !this.suggestionsElement) { return @@ -739,9 +743,9 @@ class MentionsInput extends React.Component { // is small enough to NOT cover up the caret if ( (allowSuggestionsAboveCursor && - top + suggestions.offsetHeight > viewportHeight && + top + suggestions.offsetHeight > viewportHeight && suggestions.offsetHeight < top - caretHeight) || - forceSuggestionsAboveCursor + forceSuggestionsAboveCursor ) { position.top = Math.max(0, top - suggestions.offsetHeight - caretHeight) } else { @@ -761,12 +765,12 @@ class MentionsInput extends React.Component { // is small enough to NOT cover up the caret if ( (allowSuggestionsAboveCursor && - viewportRelative.top - - highlighter.scrollTop + - suggestions.offsetHeight > - viewportHeight && - suggestions.offsetHeight < - caretOffsetParentRect.top - caretHeight - highlighter.scrollTop) || + viewportRelative.top - + highlighter.scrollTop + + suggestions.offsetHeight > + viewportHeight && + suggestions.offsetHeight < + caretOffsetParentRect.top - caretHeight - highlighter.scrollTop) || forceSuggestionsAboveCursor ) { position.top = top - suggestions.offsetHeight - caretHeight @@ -901,19 +905,20 @@ class MentionsInput extends React.Component { const { children, ignoreAccents } = this.props const mentionChild = Children.toArray(children)[childIndex] const provideData = getDataProvider(mentionChild.props.data, ignoreAccents) - const syncResult = provideData( + + const callback = this.updateSuggestions.bind( + null, + this._queryId, + childIndex, query, - this.updateSuggestions.bind( - null, - this._queryId, - childIndex, - query, - querySequenceStart, - querySequenceEnd, - plainTextValue - ) + querySequenceStart, + querySequenceEnd, + plainTextValue ) - if (syncResult instanceof Array) { + + const result = provideData(query, callback) + + if (result instanceof Array) { this.updateSuggestions( this._queryId, childIndex, @@ -921,8 +926,10 @@ class MentionsInput extends React.Component { querySequenceStart, querySequenceEnd, plainTextValue, - syncResult + result ) + } else if (result && typeof result.then === 'function') { + result.then(callback) } } diff --git a/src/MentionsInput.spec.js b/src/MentionsInput.spec.js index f192f1aa..af0a046f 100644 --- a/src/MentionsInput.spec.js +++ b/src/MentionsInput.spec.js @@ -51,7 +51,10 @@ describe('MentionsInput', () => { it.todo('should be possible to close the suggestions with esc.') it('should be able to handle sync responses from multiple mentions sources', () => { - const extraData = [{ id: 'a', value: 'A' }, { id: 'b', value: 'B' }] + const extraData = [ + { id: 'a', value: 'A' }, + { id: 'b', value: 'B' }, + ] const wrapper = mount( @@ -64,13 +67,69 @@ describe('MentionsInput', () => { wrapper.find('textarea').simulate('select', { target: { selectionStart: 1, selectionEnd: 1 }, }) - wrapper.find('textarea').getDOMNode().setSelectionRange(1, 1) + wrapper + .find('textarea') + .getDOMNode() + .setSelectionRange(1, 1) expect( wrapper.find('SuggestionsOverlay').find('Suggestion').length ).toEqual(data.length + extraData.length) }) + it('handles an async data function that executes the callback', () => { + function dataFunc(query, callback) { + callback(data) + } + + const wrapper = mount( + + + + ) + + wrapper.find('textarea').simulate('focus') + wrapper.find('textarea').simulate('select', { + target: { selectionStart: 1, selectionEnd: 1 }, + }) + wrapper + .find('textarea') + .getDOMNode() + .setSelectionRange(1, 1) + + expect( + wrapper.find('SuggestionsOverlay').find('Suggestion').length + ).toEqual(data.length) + }) + + it('handles an async data function that returns a promise', async () => { + function dataFunc() { + return Promise.resolve(data) + } + + const wrapper = mount( + + + + ) + + wrapper.find('textarea').simulate('focus') + wrapper.find('textarea').simulate('select', { + target: { selectionStart: 1, selectionEnd: 1 }, + }) + wrapper + .find('textarea') + .getDOMNode() + .setSelectionRange(1, 1) + + await new Promise((resolve) => setTimeout(resolve, 0)) + wrapper.update() + + expect( + wrapper.find('SuggestionsOverlay').find('Suggestion').length + ).toEqual(data.length) + }) + it('should scroll the highlighter in sync with the textarea', () => { const wrapper = mount( { }) it('should accept a custom regex attribute', () => { - const data = [{ id: 'aaaa', display: '@A' }, { id: 'bbbb', display: '@B' }] + const data = [ + { id: 'aaaa', display: '@A' }, + { id: 'bbbb', display: '@B' }, + ] const wrapper = mount( Date: Fri, 22 Jul 2022 10:39:59 -0400 Subject: [PATCH 2/2] Add changeset --- .changeset/afraid-rice-refuse.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/afraid-rice-refuse.md diff --git a/.changeset/afraid-rice-refuse.md b/.changeset/afraid-rice-refuse.md new file mode 100644 index 00000000..44df69d6 --- /dev/null +++ b/.changeset/afraid-rice-refuse.md @@ -0,0 +1,5 @@ +--- +"react-mentions": minor +--- + +Data functions can now return a promise. This is useful when fetching the suggestions asynchronously.