From 1aa2e9063a8b99dd0267c97e28f96ecf14c22d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BB=D1=8F=D0=BA=D0=B8=D0=BD=20=D0=9A=D0=B8?= =?UTF-8?q?=D1=80=D0=B8=D0=BB=D0=BB?= Date: Wed, 9 Feb 2022 20:08:46 +0300 Subject: [PATCH 1/2] fix: Cannot read properties of undefined (reading 'focus') --- src/Autosuggest.js | 23 +++---- src/Autowhatever.js | 17 +++-- .../render-items-container/AutowhateverApp.js | 4 +- test/helpers.js | 63 ++++++++++--------- test/render-input-component/AutosuggestApp.js | 24 +++---- 5 files changed, 70 insertions(+), 61 deletions(-) diff --git a/src/Autosuggest.js b/src/Autosuggest.js index 6f3269da..a36044fc 100644 --- a/src/Autosuggest.js +++ b/src/Autosuggest.js @@ -6,8 +6,13 @@ import { defaultTheme, mapToAutowhateverTheme } from './theme'; const alwaysTrue = () => true; const defaultShouldRenderSuggestions = (value) => value.trim().length > 0; -const defaultRenderSuggestionsContainer = ({ containerProps, children }) => ( -
{children}
+const defaultRenderSuggestionsContainer = ({ + containerProps: { innerRef, ...otherContainerProps }, + children, +}) => ( +
+ {children} +
); const REASON_SUGGESTIONS_REVEALED = 'suggestions-revealed'; @@ -127,14 +132,16 @@ export default class Autosuggest extends Component { this.justMouseEntered = false; this.pressedSuggestion = null; + + this.autowhatever = React.createRef(); } componentDidMount() { document.addEventListener('mousedown', this.onDocumentMouseDown); document.addEventListener('mouseup', this.onDocumentMouseUp); - this.input = this.autowhatever.input; - this.suggestionsContainer = this.autowhatever.itemsContainer; + this.input = this.autowhatever.current.input; + this.suggestionsContainer = this.autowhatever.current.itemsContainer; } // eslint-disable-next-line camelcase, react/sort-comp @@ -364,12 +371,6 @@ export default class Autosuggest extends Component { return suggestions.length > 0 && shouldRenderSuggestions(value, reason); } - storeAutowhateverRef = (autowhatever) => { - if (autowhatever !== null) { - this.autowhatever = autowhatever; - } - }; - onSuggestionMouseEnter = (event, { sectionIndex, itemIndex }) => { this.updateHighlightedSuggestion(sectionIndex, itemIndex); @@ -814,7 +815,7 @@ export default class Autosuggest extends Component { itemProps={this.itemProps} theme={mapToAutowhateverTheme(theme)} id={id} - ref={this.storeAutowhateverRef} + ref={this.autowhatever} /> ); } diff --git a/src/Autowhatever.js b/src/Autowhatever.js index fe2ee8e7..b9400c68 100644 --- a/src/Autowhatever.js +++ b/src/Autowhatever.js @@ -6,9 +6,16 @@ import SectionTitle from './SectionTitle'; import ItemList from './ItemList'; const emptyObject = {}; -const defaultRenderInputComponent = (props) => ; -const defaultRenderItemsContainer = ({ containerProps, children }) => ( -
{children}
+const defaultRenderInputComponent = ({ innerRef, ...otherContainerProps }) => ( + +); +const defaultRenderItemsContainer = ({ + containerProps: { innerRef, ...otherContainerProps }, + children, +}) => ( +
+ {children} +
); const defaultTheme = { container: 'react-autowhatever__container', @@ -414,7 +421,7 @@ export default class Autowhatever extends Component { onFocus: this.onFocus, onBlur: this.onBlur, onKeyDown: this.props.inputProps.onKeyDown && this.onKeyDown, - ref: this.storeInputReference, + innerRef: this.storeInputReference, }); const itemsContainer = renderItemsContainer({ containerProps: { @@ -425,7 +432,7 @@ export default class Autowhatever extends Component { 'itemsContainer', isOpen && 'itemsContainerOpen' ), - ref: this.storeItemsContainerReference, + innerRef: this.storeItemsContainerReference, }, children: renderedItems, }); diff --git a/test/autowhatever/render-items-container/AutowhateverApp.js b/test/autowhatever/render-items-container/AutowhateverApp.js index 0dabf42d..ae4ea4c9 100644 --- a/test/autowhatever/render-items-container/AutowhateverApp.js +++ b/test/autowhatever/render-items-container/AutowhateverApp.js @@ -6,8 +6,8 @@ import items from './items'; export const renderItem = (item) => item.text; export const renderItemsContainer = sinon.spy( - ({ containerProps, children }) => ( -
+ ({ containerProps: { innerRef, ...otherContainerProps }, children }) => ( +
{children}
Footer
diff --git a/test/helpers.js b/test/helpers.js index a831a01d..b2f6ad73 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -17,7 +17,7 @@ export const clearEvents = () => { eventsArray = []; }; -export const addEvent = event => { +export const addEvent = (event) => { eventsArray.push(event); }; @@ -25,7 +25,7 @@ export const getEvents = () => { return eventsArray; }; -export const init = application => { +export const init = (application) => { app = application; container = TestUtils.findRenderedDOMComponentWithClass( app, @@ -43,7 +43,7 @@ export const init = application => { }; // Since react-dom doesn't export SyntheticEvent anymore -export const syntheticEventMatcher = sinon.match(value => { +export const syntheticEventMatcher = sinon.match((value) => { if (typeof value !== 'object' || value === null) { return false; } @@ -60,17 +60,18 @@ export const containerPropsMatcher = sinon.match({ id: sinon.match.string, key: sinon.match.string, className: sinon.match.string, - ref: sinon.match.func + innerRef: sinon.match.func, }); const reactAttributesRegex = / data-react[-\w]+="[^"]+"/g; // See: http://stackoverflow.com/q/28979533/247243 -const stripReactAttributes = html => html.replace(reactAttributesRegex, ''); +const stripReactAttributes = (html) => html.replace(reactAttributesRegex, ''); -export const getInnerHTML = element => stripReactAttributes(element.innerHTML); +export const getInnerHTML = (element) => + stripReactAttributes(element.innerHTML); -export const getElementWithClass = className => +export const getElementWithClass = (className) => TestUtils.findRenderedDOMComponentWithClass(app, className); export const expectContainerAttribute = (attributeName, expectedValue) => { @@ -81,10 +82,10 @@ export const expectInputAttribute = (attributeName, expectedValue) => { expect(input.getAttribute(attributeName)).to.equal(expectedValue); }; -export const getSuggestionsContainerAttribute = attributeName => +export const getSuggestionsContainerAttribute = (attributeName) => suggestionsContainer.getAttribute(attributeName); -export const expectInputValue = expectedValue => { +export const expectInputValue = (expectedValue) => { expect(input.value).to.equal(expectedValue); }; @@ -100,7 +101,7 @@ export const getSuggestions = () => 'react-autosuggest__suggestion' ); -export const getSuggestion = suggestionIndex => { +export const getSuggestion = (suggestionIndex) => { const suggestions = getSuggestions(); if (suggestionIndex >= suggestions.length) { @@ -137,7 +138,7 @@ export const getTitles = () => 'react-autosuggest__section-title' ); -export const getTitle = titleIndex => { +export const getTitle = (titleIndex) => { const titles = getTitles(); if (titleIndex >= titles.length) { @@ -151,15 +152,15 @@ export const expectInputReferenceToBeSet = () => { expect(app.input).to.equal(input); }; -export const expectSuggestions = expectedSuggestions => { +export const expectSuggestions = (expectedSuggestions) => { const suggestions = getSuggestions().map( - suggestion => suggestion.textContent + (suggestion) => suggestion.textContent ); expect(suggestions).to.deep.equal(expectedSuggestions); }; -export const expectHighlightedSuggestion = suggestion => { +export const expectHighlightedSuggestion = (suggestion) => { const highlightedSuggestions = TestUtils.scryRenderedDOMComponentsWithClass( app, 'react-autosuggest__suggestion--highlighted' @@ -191,30 +192,30 @@ export const expectDontLetBrowserHandleKeyDown = () => { expect(getEvents()).to.deep.include('onKeyDown-defaultPrevented'); }; -export const mouseEnterSuggestion = suggestionIndex => { +export const mouseEnterSuggestion = (suggestionIndex) => { Simulate.mouseEnter(getSuggestion(suggestionIndex)); }; -export const mouseLeaveSuggestion = suggestionIndex => { +export const mouseLeaveSuggestion = (suggestionIndex) => { Simulate.mouseLeave(getSuggestion(suggestionIndex)); }; -export const mouseDownSuggestion = suggestionIndex => { +export const mouseDownSuggestion = (suggestionIndex) => { Simulate.mouseDown(getSuggestion(suggestionIndex)); }; -const mouseDownDocument = target => { +const mouseDownDocument = (target) => { document.dispatchEvent( new window.CustomEvent('mousedown', { detail: { // must be 'detail' accoring to docs: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events#Adding_custom_data_–_CustomEvent() - target - } + target, + }, }) ); }; -export const mouseUpDocument = target => { +export const mouseUpDocument = (target) => { document.dispatchEvent( new window.CustomEvent( 'mouseup', @@ -222,25 +223,25 @@ export const mouseUpDocument = target => { ? { detail: { // must be 'detail' accoring to docs: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events#Adding_custom_data_–_CustomEvent() - target - } + target, + }, } : null ) ); }; -const touchStartSuggestion = suggestionIndex => { +const touchStartSuggestion = (suggestionIndex) => { Simulate.touchStart(getSuggestion(suggestionIndex)); }; -const touchMoveSuggestion = suggestionIndex => { +const touchMoveSuggestion = (suggestionIndex) => { Simulate.touchMove(getSuggestion(suggestionIndex)); }; // It doesn't feel right to emulate all the DOM events by copying the implementation. // Please show me a better way to emulate this. -export const clickSuggestion = suggestionIndex => { +export const clickSuggestion = (suggestionIndex) => { const suggestion = getSuggestion(suggestionIndex); mouseEnterSuggestion(suggestionIndex); @@ -254,7 +255,7 @@ export const clickSuggestion = suggestionIndex => { }; // Simulates only mouse events since on touch devices dragging considered as a scroll and is a different case. -export const dragSuggestionOut = suggestionIndex => { +export const dragSuggestionOut = (suggestionIndex) => { const suggestion = getSuggestion(suggestionIndex); mouseEnterSuggestion(suggestionIndex); @@ -264,7 +265,7 @@ export const dragSuggestionOut = suggestionIndex => { mouseUpDocument(); }; -export const dragSuggestionOutAndIn = suggestionIndex => { +export const dragSuggestionOutAndIn = (suggestionIndex) => { const suggestion = getSuggestion(suggestionIndex); mouseEnterSuggestion(suggestionIndex); @@ -281,7 +282,7 @@ export const dragSuggestionOutAndIn = suggestionIndex => { // Simulates mouse events as well as touch events since some browsers (chrome) mirror them and we should handle this. // Order of events is implemented according to docs: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent -export const dragSuggestionOutTouch = suggestionIndex => { +export const dragSuggestionOutTouch = (suggestionIndex) => { touchStartSuggestion(suggestionIndex); touchMoveSuggestion(suggestionIndex); mouseDownSuggestion(suggestionIndex); @@ -329,12 +330,12 @@ export const clickUp = (count = 1) => { } }; -export const setInputValue = value => { +export const setInputValue = (value) => { input.value = value; Simulate.change(input); }; -export const focusAndSetInputValue = value => { +export const focusAndSetInputValue = (value) => { focusInput(); setInputValue(value); }; diff --git a/test/render-input-component/AutosuggestApp.js b/test/render-input-component/AutosuggestApp.js index 60601871..e589449f 100644 --- a/test/render-input-component/AutosuggestApp.js +++ b/test/render-input-component/AutosuggestApp.js @@ -3,40 +3,40 @@ import Autosuggest from '../../src/Autosuggest'; import languages from '../plain-list/languages'; import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js'; -const getMatchingLanguages = value => { +const getMatchingLanguages = (value) => { const escapedValue = escapeRegexCharacters(value.trim()); const regex = new RegExp('^' + escapedValue, 'i'); - return languages.filter(language => regex.test(language.name)); + return languages.filter((language) => regex.test(language.name)); }; let app = null; const onChange = (event, { newValue }) => { app.setState({ - value: newValue + value: newValue, }); }; const onSuggestionsFetchRequested = ({ value }) => { app.setState({ - suggestions: getMatchingLanguages(value) + suggestions: getMatchingLanguages(value), }); }; const onSuggestionsClearRequested = () => { app.setState({ - suggestions: [] + suggestions: [], }); }; -const getSuggestionValue = suggestion => suggestion.name; +const getSuggestionValue = (suggestion) => suggestion.name; -const renderSuggestion = suggestion => suggestion.name; +const renderSuggestion = (suggestion) => suggestion.name; -const renderInputComponent = inputProps => ( +const renderInputComponent = ({ innerRef, ...otherInputProps }) => (
- +
); @@ -48,11 +48,11 @@ export default class AutosuggestApp extends Component { this.state = { value: '', - suggestions: [] + suggestions: [], }; } - storeAutosuggestReference = autosuggest => { + storeAutosuggestReference = (autosuggest) => { if (autosuggest !== null) { this.input = autosuggest.input; } @@ -62,7 +62,7 @@ export default class AutosuggestApp extends Component { const { value, suggestions } = this.state; const inputProps = { value, - onChange + onChange, }; return ( From d643c42a5249748945820448ddb9f8ce45f03776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BB=D1=8F=D0=BA=D0=B8=D0=BD=20=D0=9A=D0=B8?= =?UTF-8?q?=D1=80=D0=B8=D0=BB=D0=BB?= Date: Thu, 10 Feb 2022 09:31:37 +0300 Subject: [PATCH 2/2] fix: cannot find ref, if ref determine after componentDidMount --- package.json | 4 +-- src/Autosuggest.js | 25 +++++++++------ test/helpers.js | 2 +- test/plain-list/AutosuggestApp.js | 32 ++++++++++--------- test/render-input-component/AutosuggestApp.js | 12 +++---- 5 files changed, 41 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 92e5e85c..62ef24d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "react-autosuggest", - "version": "10.1.0", + "name": "@nfort/react-autosuggest", + "version": "11.0.1", "description": "WAI-ARIA compliant React autosuggest component", "main": "dist/index.js", "repository": { diff --git a/src/Autosuggest.js b/src/Autosuggest.js index a36044fc..815589a2 100644 --- a/src/Autosuggest.js +++ b/src/Autosuggest.js @@ -139,9 +139,6 @@ export default class Autosuggest extends Component { componentDidMount() { document.addEventListener('mousedown', this.onDocumentMouseDown); document.addEventListener('mouseup', this.onDocumentMouseUp); - - this.input = this.autowhatever.current.input; - this.suggestionsContainer = this.autowhatever.current.itemsContainer; } // eslint-disable-next-line camelcase, react/sort-comp @@ -212,6 +209,14 @@ export default class Autosuggest extends Component { document.removeEventListener('mouseup', this.onDocumentMouseUp); } + getInput() { + return this.autowhatever.current.input; + } + + getSuggestionsContainer() { + return this.autowhatever.current.itemsContainer; + } + updateHighlightedSuggestion(sectionIndex, suggestionIndex, prevValue) { this.setState((state) => { let { valueBeforeUpDown } = state; @@ -328,7 +333,7 @@ export default class Autosuggest extends Component { return; } - if (node === this.suggestionsContainer) { + if (node === this.getSuggestionsContainer()) { // Something else inside suggestions container was clicked this.justClickedOnSuggestionsContainer = true; return; @@ -391,7 +396,7 @@ export default class Autosuggest extends Component { onDocumentMouseUp = () => { if (this.pressedSuggestion && !this.justSelectedSuggestion) { - this.input.focus(); + this.getInput().focus(); } this.pressedSuggestion = null; }; @@ -464,7 +469,7 @@ export default class Autosuggest extends Component { } if (focusInputOnSuggestionClick === true) { - this.input.focus(); + this.getInput().focus(); } else { this.onBlur(); } @@ -512,7 +517,7 @@ export default class Autosuggest extends Component { onSuggestionTouchMove = () => { this.justSelectedSuggestion = false; this.pressedSuggestion = null; - this.input.focus(); + this.getInput().focus(); }; itemProps = ({ sectionIndex, itemIndex }) => { @@ -611,7 +616,7 @@ export default class Autosuggest extends Component { }, onBlur: (event) => { if (this.justClickedOnSuggestionsContainer) { - this.input.focus(); + this.getInput().focus(); return; } @@ -631,8 +636,8 @@ export default class Autosuggest extends Component { this.maybeCallOnChange(event, value, 'type'); - if (this.suggestionsContainer) { - this.suggestionsContainer.scrollTop = 0; + if (this.getSuggestionsContainer()) { + this.getSuggestionsContainer().scrollTop = 0; } this.setState({ diff --git a/test/helpers.js b/test/helpers.js index b2f6ad73..b87a0dcb 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -149,7 +149,7 @@ export const getTitle = (titleIndex) => { }; export const expectInputReferenceToBeSet = () => { - expect(app.input).to.equal(input); + expect(app.getInput()).to.equal(input); }; export const expectSuggestions = (expectedSuggestions) => { diff --git a/test/plain-list/AutosuggestApp.js b/test/plain-list/AutosuggestApp.js index e3238a81..32fb4a1f 100644 --- a/test/plain-list/AutosuggestApp.js +++ b/test/plain-list/AutosuggestApp.js @@ -7,16 +7,16 @@ import languages from './languages'; import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js'; import { addEvent, saveKeyDown } from '../helpers'; -const getMatchingLanguages = value => { +const getMatchingLanguages = (value) => { const escapedValue = escapeRegexCharacters(value.trim()); const regex = new RegExp('^' + escapedValue, 'i'); - return languages.filter(language => regex.test(language.name)); + return languages.filter((language) => regex.test(language.name)); }; let app = null; -export const getSuggestionValue = sinon.spy(suggestion => { +export const getSuggestionValue = sinon.spy((suggestion) => { return suggestion.name; }); @@ -37,7 +37,7 @@ export const onChange = sinon.spy((event, { newValue }) => { addEvent('onChange'); app.setState({ - value: newValue + value: newValue, }); }); @@ -47,17 +47,19 @@ export const onBlur = sinon.spy(); export const defaultShouldRenderSuggestionsStub = (value) => { return value.trim().length > 0 && value[0] !== ' '; }; -export const shouldRenderSuggestions = sinon.stub().callsFake(defaultShouldRenderSuggestionsStub); +export const shouldRenderSuggestions = sinon + .stub() + .callsFake(defaultShouldRenderSuggestionsStub); export const onSuggestionsFetchRequested = sinon.spy(({ value }) => { app.setState({ - suggestions: getMatchingLanguages(value) + suggestions: getMatchingLanguages(value), }); }); export const onSuggestionsClearRequested = sinon.spy(() => { app.setState({ - suggestions: [] + suggestions: [], }); }); @@ -77,15 +79,15 @@ export default class AutosuggestApp extends Component { this.state = { value: '', - suggestions: [] + suggestions: [], }; + + this.autosuggest = React.createRef(); } - storeAutosuggestReference = autosuggest => { - if (autosuggest !== null) { - this.input = autosuggest.input; - } - }; + getInput() { + return this.autosuggest.current.getInput(); + } render() { const { value, suggestions } = this.state; @@ -97,7 +99,7 @@ export default class AutosuggestApp extends Component { value, onChange, onFocus, - onBlur + onBlur, }; return ( @@ -111,7 +113,7 @@ export default class AutosuggestApp extends Component { renderSuggestion={renderSuggestion} inputProps={inputProps} shouldRenderSuggestions={shouldRenderSuggestions} - ref={this.storeAutosuggestReference} + ref={this.autosuggest} /> ); } diff --git a/test/render-input-component/AutosuggestApp.js b/test/render-input-component/AutosuggestApp.js index e589449f..a430990e 100644 --- a/test/render-input-component/AutosuggestApp.js +++ b/test/render-input-component/AutosuggestApp.js @@ -50,13 +50,13 @@ export default class AutosuggestApp extends Component { value: '', suggestions: [], }; + + this.autosuggest = React.createRef(); } - storeAutosuggestReference = (autosuggest) => { - if (autosuggest !== null) { - this.input = autosuggest.input; - } - }; + getInput() { + return this.autosuggest.current.getInput(); + } render() { const { value, suggestions } = this.state; @@ -74,7 +74,7 @@ export default class AutosuggestApp extends Component { renderSuggestion={renderSuggestion} renderInputComponent={renderInputComponent} inputProps={inputProps} - ref={this.storeAutosuggestReference} + ref={this.autosuggest} /> ); }