refactoring to functional component, and added onIput to type def

This commit is contained in:
andrelandgraf 2020-04-28 08:55:57 +02:00
parent e0a4cade03
commit 48bf055334
4 changed files with 429 additions and 488 deletions

View File

@ -9,18 +9,6 @@
"react" "react"
], ],
"rules": { "rules": {
"indent": ["error", 4], "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
"react/jsx-indent": ["error", 4],
"react/jsx-indent-props": ["error", 4],
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"space-in-parens": [ 2, "always" ],
"template-curly-spacing": [ 2, "always" ],
"array-bracket-spacing": [ 2, "always" ],
"object-curly-spacing": [ 2, "always" ],
"computed-property-spacing": [ 2, "always" ],
"space-infix-ops": [2, {"int32Hint": false}],
"react/prop-types": [2],
"react/require-default-props": [2],
"no-underscore-dangle": [ "error", { "allow": [ "_id" ] } ]
} }
} }

View File

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import csvFile from './data.csv'; import csvFile from './data.csv';
@ -6,96 +7,96 @@ import DataListInput from './DataListInput';
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const data = [ const data = [
{ {
key: '0', key: '0',
label: 'Apple', label: 'Apple',
}, },
{ {
key: '1', key: '1',
label: 'Mango', label: 'Mango',
}, },
{ {
key: '2', key: '2',
label: 'Potatoe', label: 'Potatoe',
}, },
]; ];
async function getAndParseData( filename ) { async function getAndParseData(filename) {
const response = await fetch( filename ); const response = await fetch(filename);
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder( 'utf-8' ); const decoder = new TextDecoder('utf-8');
const result = await reader.read(); const result = await reader.read();
const allText = decoder.decode( result.value ); const allText = decoder.decode(result.value);
const allTextLines = allText.split( /\r\n|\n/ ); const allTextLines = allText.split(/\r\n|\n/);
const headers = allTextLines[ 0 ].split( ',' ); const headers = allTextLines[0].split(',');
const lines = []; const lines = [];
for ( let i = 1; i < allTextLines.length; i += 1 ) { for (let i = 1; i < allTextLines.length; i += 1) {
const set = allTextLines[ i ].split( ',' ); const set = allTextLines[i].split(',');
if ( set.length === headers.length ) { if (set.length === headers.length) {
const tarr = {}; const tarr = {};
for ( let j = 0; j < headers.length; j += 1 ) { for (let j = 0; j < headers.length; j += 1) {
tarr[ headers[ j ] ] = set[ j ]; tarr[headers[j]] = set[j];
} }
lines.push( tarr ); lines.push(tarr);
}
} }
return lines; }
return lines;
} }
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
function annoyinglySlowMatchingAlg( currentInput, item ) { function annoyinglySlowMatchingAlg(currentInput, item) {
for ( let i = 0; i < 100000; i += 1 ) { for (let i = 0; i < 100000; i += 1) {
i += 1; i += 1;
i -= 1; i -= 1;
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
( currentInput.length + item.label.length ) % 2; (currentInput.length + item.label.length) % 2;
} }
return item.label.substr( 0, currentInput.length ).toUpperCase() === currentInput.toUpperCase(); return item.label.substr(0, currentInput.length).toUpperCase() === currentInput.toUpperCase();
} }
function App() { function App() {
const [ item, setItem ] = useState(); const [item, setItem] = useState();
const [ items, setItems ] = useState( [] ); const [items, setItems] = useState(data);
useEffect( () => { // useEffect(() => {
getAndParseData( csvFile ).then( obj => setItems( obj // getAndParseData(csvFile).then(obj => setItems(obj
.concat( obj ) // .concat(obj)
.map( ( row, i ) => ( // .map((row, i) => (
{ // {
...row, // ...row,
label: row.vorname, // label: row.vorname,
key: i, // key: i,
} // }
) ) ) ); // ))));
}, [] ); // }, []);
return ( return (
<div className="App"> <div className="App">
<div className="content"> <div className="content">
{ {
item && ( item && (
<div> <div>
{ `Current Item: ${ item.label }` } { `Current Item: ${item.label}` }
</div> </div>
) )
} }
<div className="wrapper"> <div className="wrapper">
<DataListInput <DataListInput
items={items} items={items}
onSelect={i => setItem( i )} onSelect={i => setItem(i)}
placeholder="Select a ingredient" placeholder="Select a ingredient"
clearInputOnSelect={false} clearInputOnSelect
suppressReselect={false} suppressReselect={false}
initialValue={item ? item.label : ''} initialValue={item ? item.label : ''}
debounceTime={1000} // debounceTime={1000}
debounceLoader={<>Hello</>} // debounceLoader={<>Hello</>}
/> />
</div>
</div>
</div> </div>
); </div>
</div>
);
} }
export default App; export default App;

View File

@ -1,434 +1,386 @@
import React from 'react'; import React, {
useState, useRef, useEffect, useCallback, useMemo,
} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './DataListInput.css'; import './DataListInput.css';
const windowExists = () => typeof window !== 'undefined'; /**
* default function for matching the current input value (needle)
* and the values of the items array
* @param currentInput
* @param item
* @returns {boolean}
*/
const labelMatch = (currentInput, item) => item
.label.substr(0, currentInput.length).toUpperCase() === currentInput.toUpperCase();
class DataListInput extends React.Component { /**
constructor( props ) { * function for getting the index of the currentValue inside a value of the values array
super( props ); * @param currentInput
* @param item
* @returns {number}
*/
const indexOfMatch = (currentInput, item) => item
.label.toUpperCase().indexOf(currentInput.toUpperCase());
const { initialValue } = this.props; /**
* index of item in items
* @param {*} item
* @param {*} items
*/
const indexOfItem = (item, items) => items
.indexOf(items.find(i => i.key === item.key));
this.state = { const DataListInput = ({
/* last valid item that was selected from the drop down menu */ activeItemClassName,
lastValidItem: undefined, clearInputOnSelect,
/* current input text */ debounceLoader,
currentInput: initialValue, debounceTime,
/* current set of matching items */ dropdownClassName,
matchingItems: [], dropDownLength,
/* visibility property of the drop down menu */ initialValue,
visible: false, inputClassName,
/* index of the currently focused item in the drop down menu */ itemClassName,
focusIndex: 0, match,
/* cleaner click events, click interaction within dropdown menu */ onDropdownClose,
interactionHappened: false, onDropdownOpen,
/* show loader if still matching in debounced mode */ onInput,
isMatchingDebounced: false, onSelect,
}; placeholder,
requiredInputLength,
suppressReselect,
items,
}) => {
/* last valid item that was selected from the drop down menu */
const [lastValidItem, setLastValidItem] = useState();
/* current input text */
const [currentInput, setCurrentInput] = useState(initialValue);
/* current set of matching items */
const [matchingItems, setMatchingItems] = useState([]);
/* visibility property of the drop down menu */
const [visible, setVisible] = useState(false);
/* index of the currently focused item in the drop down menu */
const [focusIndex, setFocusIndex] = useState(0);
/* cleaner click events, click interaction within dropdown menu */
const [interactionHappened, setInteractionHappened] = useState(false);
/* show loader if still matching in debounced mode */
const [isMatchingDebounced, setIsMatchingDebounced] = useState(false);
/* to manage debouncing of matching, typing input into the input field */ /* to manage debouncing of matching, typing input into the input field */
this.inputHappenedTimeout = undefined; const inputHappenedTimeout = useRef();
const menu = useRef();
const inputField = useRef();
const onClickCloseMenu = useCallback((event) => {
if (!menu.current) return;
// if rerender, items inside might change, allow one click without further checking
if (interactionHappened) {
setInteractionHappened(false);
return;
}
// do not do anything if input is clicked, as we have a dedicated func for that
if (!inputField.current) return;
const targetIsInput = event.target === inputField.current;
const targetInInput = inputField.current.contains(event.target);
if (targetIsInput || targetInInput) return;
// do not close menu if user clicked inside
const targetInMenu = menu.current.contains(event.target);
const targetIsMenu = event.target === menu.current;
if (targetInMenu || targetIsMenu) return;
if (visible) {
setVisible(false);
setFocusIndex(-1);
onDropdownClose();
}
}, [interactionHappened, onDropdownClose]);
useEffect(() => {
window.addEventListener('click', onClickCloseMenu, false);
return () => {
window.removeEventListener('click', onClickCloseMenu);
};
}, []);
useEffect(() => {
// if we have an initialValue, we want to reset it everytime we update and are empty
// also setting a new initialValue will trigger this
if (!currentInput && initialValue && !visible && !isMatchingDebounced) {
setCurrentInput(initialValue);
}
}, [currentInput, visible, isMatchingDebounced, initialValue]);
/**
* matching process to find matching entries in items array
* @returns {Array}
*/
const computeMatchingItems = useCallback(() => items.filter((item) => {
if (typeof (match) === typeof (Function)) { return match(currentInput, item); }
return labelMatch(currentInput, item);
}), [items, match, currentInput]);
/**
* runs the matching process of the current input
* and handles debouncing the different callback calls to reduce lag time
* for bigger datasets or heavier matching algorithms
* @param nextInput
*/
const debouncedMatchingUpdateStep = useCallback((nextInput) => {
// cleanup waiting update step
if (inputHappenedTimeout.current) {
clearTimeout(inputHappenedTimeout.current);
inputHappenedTimeout.current = null;
} }
componentDidMount = () => { // set nextInput into input field and show loading if debounced mode is on
if ( windowExists() ) { const reachedRequiredLength = nextInput.length >= requiredInputLength;
window.addEventListener( 'click', this.onClickCloseMenu, false ); const showMatchingStillLoading = debounceTime >= 0 && reachedRequiredLength;
setCurrentInput(nextInput);
setIsMatchingDebounced(showMatchingStillLoading);
// no matching if we do not reach required input length
if (!reachedRequiredLength) return;
const updateMatchingItems = () => {
const updatedMatchingItems = computeMatchingItems();
const displayableItems = updatedMatchingItems.slice(0, dropDownLength);
const showDragIndex = lastValidItem && !clearInputOnSelect;
const index = showDragIndex ? indexOfItem(lastValidItem, displayableItems) : 0;
if (displayableItems.length) {
setMatchingItems(displayableItems);
setFocusIndex(index > 0 ? index : 0);
setIsMatchingDebounced(false);
setVisible(true);
onDropdownOpen();
} else {
if (visible) {
setVisible(false);
onDropdownClose();
} }
} setMatchingItems(displayableItems);
setFocusIndex(-1);
componentDidUpdate = () => { setIsMatchingDebounced(false);
const { currentInput, visible, isMatchingDebounced } = this.state; }
const { initialValue } = this.props;
// if we have an initialValue, we want to reset it everytime we update and are empty
// also setting a new initialValue will trigger this
if ( !currentInput && initialValue && !visible && !isMatchingDebounced ) {
this.setState( { currentInput: initialValue } );
}
}
componentWillUnmount = () => {
if ( windowExists() ) {
window.removeEventListener( 'click', this.onClickCloseMenu );
}
}
onClickCloseMenu = ( event ) => {
const menu = document.getElementsByClassName( 'datalist-items' );
if ( !menu || !menu.length ) return;
// if rerender, items inside might change, allow one click without further checking
const { interactionHappened } = this.state;
if ( interactionHappened ) {
this.setState( { interactionHappened: false } );
return;
}
// do not do anything if input is clicked, as we have a dedicated func for that
const input = document.getElementsByClassName( 'autocomplete-input' );
if ( !input ) return;
for ( let i = 0; i < input.length; i += 1 ) {
const targetIsInput = event.target === input[ i ];
const targetInInput = input[ i ].contains( event.target );
if ( targetIsInput || targetInInput ) return;
}
// do not close menu if user clicked inside
for ( let i = 0; i < menu.length; i += 1 ) {
const targetInMenu = menu[ i ].contains( event.target );
const targetIsMenu = event.target === menu[ i ];
if ( targetInMenu || targetIsMenu ) return;
}
const { visible } = this.state;
const { onDropdownClose } = this.props;
if ( visible ) {
this.setState( { visible: false, focusIndex: -1 }, onDropdownClose );
}
}
/**
* default function for matching the current input value (needle)
* and the values of the items array
* @param currentInput
* @param item
* @returns {boolean}
*/
match = ( currentInput, item ) => item
.label.substr( 0, currentInput.length ).toUpperCase() === currentInput.toUpperCase();
/**
* matching process to find matching entries in items array
* @param currentInput
* @param item
* @param match
* @returns {Array}
*/
matching = ( currentInput, items, match ) => items.filter( ( item ) => {
if ( typeof ( match ) === typeof ( Function ) ) { return match( currentInput, item ); }
return this.match( currentInput, item );
} );
/**
* function for getting the index of the currentValue inside a value of the values array
* @param currentInput
* @param item
* @returns {number}
*/
indexOfMatch = ( currentInput, item ) => item
.label.toUpperCase().indexOf( currentInput.toUpperCase() );
indexOfItem = ( item, items ) => items.indexOf( items.find( i => i.key === item.key ) )
/**
* runs the matching process of the current input
* and handles debouncing the different callback calls to reduce lag time
* for bigger datasets or heavier matching algorithms
* @param currentInput
*/
debouncedMatchingUpdateStep = ( currentInput ) => {
const { lastValidItem } = this.state;
const {
items, match, debounceTime, dropDownLength, requiredInputLength,
clearInputOnSelect, onDropdownOpen, onDropdownClose,
} = this.props;
// cleanup waiting update step
if ( this.inputHappenedTimeout ) {
clearTimeout( this.inputHappenedTimeout );
}
// set currentInput into input field and show loading if debounced mode is on
const reachedRequiredLength = currentInput.length >= requiredInputLength;
const showMatchingStillLoading = debounceTime >= 0 && reachedRequiredLength;
this.setState( { currentInput, isMatchingDebounced: showMatchingStillLoading } );
// no matching if we do not reach required input length
if ( !reachedRequiredLength ) return;
const updateMatchingItems = () => {
const matchingItems = this.matching( currentInput, items, match );
const displayableItems = matchingItems.slice( 0, dropDownLength );
const showDragIndex = lastValidItem && !clearInputOnSelect;
const index = showDragIndex ? this.indexOfItem( lastValidItem, displayableItems ) : 0;
if ( matchingItems.length > 0 ) {
this.setState( {
matchingItems: displayableItems,
focusIndex: index > 0 ? index : 0,
visible: true,
isMatchingDebounced: false,
}, onDropdownOpen );
} else {
this.setState( {
matchingItems: displayableItems,
visible: false,
focusIndex: -1,
isMatchingDebounced: false,
}, onDropdownClose );
}
};
if ( debounceTime <= 0 ) {
updateMatchingItems();
} else {
this.inputHappenedTimeout = setTimeout( updateMatchingItems, debounceTime );
}
}
/**
* gets called when someone starts to write in the input field
* @param value
*/
onHandleInput = ( event ) => {
const { onInput } = this.props;
const currentInput = event.target.value;
this.debouncedMatchingUpdateStep( currentInput );
onInput( currentInput );
}; };
onClickInput = () => { if (debounceTime <= 0) {
const { visible } = this.state; updateMatchingItems();
let { currentInput } = this.state; } else {
const { requiredInputLength, initialValue } = this.props; inputHappenedTimeout.current = setTimeout(updateMatchingItems, debounceTime);
}
}, [requiredInputLength, debounceTime, computeMatchingItems,
dropDownLength, lastValidItem, clearInputOnSelect,
onDropdownOpen, onDropdownClose, visible]);
// if user clicks on input field with initialValue, /**
// the user most likely wants to clear the input field * gets called when someone starts to write in the input field
if ( initialValue && currentInput === initialValue ) { * @param value
this.setState( { currentInput: '' } ); */
currentInput = ''; const onHandleInput = useCallback((event) => {
} const { value } = event.target;
debouncedMatchingUpdateStep(value);
onInput(value);
}, [debouncedMatchingUpdateStep, onInput]);
const reachedRequiredLength = currentInput.length >= requiredInputLength; const onClickInput = useCallback(() => {
if ( reachedRequiredLength && !visible ) { let value = currentInput;
this.debouncedMatchingUpdateStep( currentInput ); // if user clicks on input field with initialValue,
} // the user most likely wants to clear the input field
if (initialValue && currentInput === initialValue) {
value = '';
} }
/** const reachedRequiredLength = value.length >= requiredInputLength;
* handle key events if (reachedRequiredLength && !visible) {
* @param event debouncedMatchingUpdateStep(value);
*/ }
onHandleKeydown = ( event ) => { }, [visible, currentInput, requiredInputLength, initialValue, debouncedMatchingUpdateStep]);
const { visible, focusIndex, matchingItems } = this.state;
// only do something if drop-down div is visible
if ( !visible ) return;
let currentFocusIndex = focusIndex;
if ( event.keyCode === 40 || event.keyCode === 9 ) {
// If the arrow DOWN key or tab is pressed increase the currentFocus variable:
currentFocusIndex += 1;
if ( currentFocusIndex >= matchingItems.length ) currentFocusIndex = 0;
this.setState( {
focusIndex: currentFocusIndex,
} );
// prevent tab to jump to the next input field if drop down is still open
event.preventDefault();
} else if ( event.keyCode === 38 ) {
// If the arrow UP key is pressed, decrease the currentFocus variable:
currentFocusIndex -= 1;
if ( currentFocusIndex <= -1 ) currentFocusIndex = matchingItems.length - 1;
this.setState( {
focusIndex: currentFocusIndex,
} );
} else if ( event.keyCode === 13 ) {
// Enter pressed, similar to onClickItem
if ( focusIndex > -1 ) {
// Simulate a click on the "active" item:
const selectedItem = matchingItems[ currentFocusIndex ];
this.onSelect( selectedItem );
}
}
};
/** /**
* onClickItem gets called when onClick happens on one of the list elements * handleSelect is called onClickItem and onEnter upon an option of the drop down menu
* @param event * does nothing if the key has not changed since the last onSelect event
*/ * @param selectedItem
onClickItem = ( event ) => { */
const { matchingItems } = this.state; const onHandleSelect = useCallback((selectedItem) => {
// update the input value and close the dropdown again // block select call until last matching went through
const elements = event.currentTarget.children; if (isMatchingDebounced) return;
let selectedKey;
for ( let i = 0; i < elements.length; i += 1 ) {
if ( elements[ i ].tagName === 'INPUT' ) {
selectedKey = elements[ i ].value;
break;
}
}
// key can either be number or string
// eslint-disable-next-line eqeqeq
const selectedItem = matchingItems.find( item => item.key == selectedKey );
this.onSelect( selectedItem );
};
/** setCurrentInput(clearInputOnSelect ? '' : selectedItem.label);
* onSelect is called onClickItem and onEnter upon an option of the drop down menu setVisible(false);
* does nothing if the key has not changed since the last onSelect event setFocusIndex(-1);
* @param selectedItem setInteractionHappened(true);
*/ onDropdownClose();
onSelect = ( selectedItem ) => {
const { suppressReselect, clearInputOnSelect, onDropdownClose } = this.props;
const { lastValidItem, isMatchingDebounced } = this.state;
// block select call until last matching went through
if ( isMatchingDebounced ) return;
if ( suppressReselect && lastValidItem && selectedItem.key === lastValidItem.key ) {
// do not trigger the callback function
// but still change state to fit new selection
this.setState( {
currentInput: clearInputOnSelect ? '' : selectedItem.label,
visible: false,
focusIndex: -1,
interactionHappened: true,
}, onDropdownClose );
return;
}
// change state to fit new selection
this.setState( {
currentInput: clearInputOnSelect ? '' : selectedItem.label,
lastValidItem: selectedItem,
visible: false,
focusIndex: -1,
interactionHappened: true,
}, onDropdownClose );
// callback function onSelect
const { onSelect } = this.props;
onSelect( selectedItem );
};
renderMatchingLabel = ( currentInput, item, indexOfMatch ) => ( if (suppressReselect && lastValidItem && selectedItem.key === lastValidItem.key) {
<React.Fragment> // do not trigger the callback function
{item.label.substr( 0, indexOfMatch ) } // but still change state to fit new selection
<strong> return;
{ item.label.substr( indexOfMatch, currentInput.length ) } }
</strong> // change state to fit new selection
{ item.label.substr( indexOfMatch + currentInput.length, item.label.length ) } setLastValidItem(selectedItem);
</React.Fragment> // callback function onSelect
onSelect(selectedItem);
}, [suppressReselect, clearInputOnSelect, onDropdownClose,
lastValidItem, isMatchingDebounced, onSelect]);
/**
* handle key events
* @param event
*/
const onHandleKeydown = useCallback((event) => {
// only do something if drop-down div is visible
if (!visible) return;
let currentFocusIndex = focusIndex;
if (event.keyCode === 40 || event.keyCode === 9) {
// If the arrow DOWN key or tab is pressed increase the currentFocus variable:
currentFocusIndex += 1;
if (currentFocusIndex >= matchingItems.length) currentFocusIndex = 0;
setFocusIndex(currentFocusIndex);
// prevent tab to jump to the next input field if drop down is still open
event.preventDefault();
} else if (event.keyCode === 38) {
// If the arrow UP key is pressed, decrease the currentFocus variable:
currentFocusIndex -= 1;
if (currentFocusIndex <= -1) currentFocusIndex = matchingItems.length - 1;
setFocusIndex(currentFocusIndex);
} else if (event.keyCode === 13) {
// Enter pressed, similar to onClickItem
if (focusIndex > -1) {
// Simulate a click on the "active" item:
const selectedItem = matchingItems[currentFocusIndex];
onHandleSelect(selectedItem);
}
}
}, [visible, focusIndex, matchingItems, onHandleSelect]);
const renderItemLabel = useCallback((item) => {
const index = indexOfMatch(currentInput, item);
const inputLength = currentInput.length;
console.log('renderLabelAgain');
return (
<>
{ index >= 0 && inputLength
// renders label with matching search string marked
? (
<>
{item.label.substr(0, index) }
<strong>
{ item.label.substr(index, inputLength) }
</strong>
{ item.label.substr(index + inputLength, item.label.length) }
</>
)
: item.label }
</>
); );
}, [currentInput]);
renderItemLabel = ( currentInput, item, indexOfMatch ) => ( const renderItems = useCallback(() => (
<React.Fragment> <div ref={menu} className={`datalist-items ${dropdownClassName || 'default-datalist-items'}`}>
{ indexOfMatch >= 0 && currentInput.length {items.map((item, i) => {
? this.renderMatchingLabel( currentInput, item, indexOfMatch ) const isActive = focusIndex === i;
: item.label } const itemActiveClasses = isActive
</React.Fragment> ? `datalist-active-item ${activeItemClassName || 'datalist-active-item-default'}` : '';
) const itemClasses = `${itemClassName} ${itemActiveClasses}`;
renderItems = (
currentInput, items, focusIndex, activeItemClassName, itemClassName, dropdownClassName,
) => (
<div className={`datalist-items ${ dropdownClassName || 'default-datalist-items' }`}>
{items.map( ( item, i ) => {
const isActive = focusIndex === i;
const itemActiveClasses = isActive
? `datalist-active-item ${ activeItemClassName || 'datalist-active-item-default' }` : '';
const itemClasses = `${ itemClassName } ${ itemActiveClasses }`;
return (
<div
onClick={this.onClickItem}
className={itemClasses}
key={item.key}
tabIndex={0}
role="button"
onKeyUp={event => event.preventDefault()}
>
{this.renderItemLabel(
currentInput, item, this.indexOfMatch( currentInput, item ),
)}
<input type="hidden" value={item.key} readOnly />
</div>
);
} )}
</div>
);
renderLoader = ( debounceLoader, dropdownClassName, itemClassName ) => (
<div className={`datalist-items ${ dropdownClassName || 'default-datalist-items' }`}>
<div className={itemClassName}>{debounceLoader || 'loading...'}</div>
</div>
)
renderInputField = ( placeholder, currentInput, inputClassName ) => (
<input
onChange={this.onHandleInput}
onClick={this.onClickInput}
onKeyDown={this.onHandleKeydown}
type="text"
className={`autocomplete-input ${ inputClassName }`}
placeholder={placeholder}
value={currentInput}
/>
)
render() {
const {
currentInput, matchingItems, focusIndex, visible, isMatchingDebounced,
} = this.state;
const {
placeholder, inputClassName, activeItemClassName, itemClassName,
requiredInputLength, dropdownClassName, debounceLoader,
} = this.props;
const reachedRequiredLength = currentInput.length >= requiredInputLength;
let renderedResults;
if ( reachedRequiredLength && isMatchingDebounced ) {
renderedResults = this.renderLoader( debounceLoader, itemClassName, dropdownClassName );
} else if ( reachedRequiredLength && visible ) {
renderedResults = this.renderItems( currentInput, matchingItems, focusIndex,
activeItemClassName, itemClassName, dropdownClassName );
}
return ( return (
<div className="datalist-input"> <div
{ this.renderInputField( placeholder, currentInput, inputClassName ) } onClick={() => onHandleSelect(item)}
{ renderedResults } className={itemClasses}
</div> key={item.key}
tabIndex={0}
role="button"
onKeyUp={event => event.preventDefault()}
>
{renderItemLabel(item)}
</div>
); );
})}
</div>
), [dropdownClassName, items, focusIndex,
activeItemClassName, itemClassName, onHandleSelect, renderItemLabel]);
const renderLoader = useCallback(() => (
<div ref={menu} className={`datalist-items ${dropdownClassName || 'default-datalist-items'}`}>
<div className={itemClassName}>{debounceLoader || 'loading...'}</div>
</div>
), [dropdownClassName, itemClassName, debounceLoader]);
const dropDown = useMemo(() => {
const reachedRequiredLength = currentInput.length >= requiredInputLength;
if (reachedRequiredLength && isMatchingDebounced) {
return renderLoader();
} }
} if (reachedRequiredLength && visible) {
return renderItems();
}
return undefined;
}, [currentInput, requiredInputLength, isMatchingDebounced, renderItems, renderLoader, visible]);
return (
<div className="datalist-input">
<input
ref={inputField}
onChange={onHandleInput}
onClick={onClickInput}
onKeyDown={onHandleKeydown}
type="text"
className={`autocomplete-input ${inputClassName}`}
placeholder={placeholder}
value={currentInput}
/>
{ dropDown }
</div>
);
};
DataListInput.propTypes = { DataListInput.propTypes = {
items: PropTypes.arrayOf( items: PropTypes.arrayOf(
PropTypes.shape( { PropTypes.shape({
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
key: PropTypes.oneOfType( [ key: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.number, PropTypes.number,
] ).isRequired, ]).isRequired,
} ), }),
).isRequired, ).isRequired,
placeholder: PropTypes.string, placeholder: PropTypes.string,
onSelect: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired,
onDropdownOpen: PropTypes.func, onDropdownOpen: PropTypes.func,
onDropdownClose: PropTypes.func, onDropdownClose: PropTypes.func,
match: PropTypes.func, match: PropTypes.func,
inputClassName: PropTypes.string, inputClassName: PropTypes.string,
dropdownClassName: PropTypes.string, dropdownClassName: PropTypes.string,
itemClassName: PropTypes.string, itemClassName: PropTypes.string,
activeItemClassName: PropTypes.string, activeItemClassName: PropTypes.string,
requiredInputLength: PropTypes.number, requiredInputLength: PropTypes.number,
clearInputOnSelect: PropTypes.bool, clearInputOnSelect: PropTypes.bool,
suppressReselect: PropTypes.bool, suppressReselect: PropTypes.bool,
dropDownLength: PropTypes.number, dropDownLength: PropTypes.number,
initialValue: PropTypes.string, initialValue: PropTypes.string,
debounceTime: PropTypes.number, debounceTime: PropTypes.number,
debounceLoader: PropTypes.node, debounceLoader: PropTypes.node,
onInput: PropTypes.func, onInput: PropTypes.func,
}; };
DataListInput.defaultProps = { DataListInput.defaultProps = {
placeholder: '', placeholder: '',
match: undefined, match: undefined,
inputClassName: '', inputClassName: '',
dropdownClassName: '', dropdownClassName: '',
itemClassName: '', itemClassName: '',
activeItemClassName: '', activeItemClassName: '',
requiredInputLength: 0, requiredInputLength: 0,
clearInputOnSelect: false, clearInputOnSelect: false,
suppressReselect: true, suppressReselect: true,
dropDownLength: Infinity, dropDownLength: Infinity,
initialValue: '', initialValue: '',
debounceTime: 0, debounceTime: 0,
debounceLoader: undefined, debounceLoader: undefined,
onDropdownOpen: () => {}, onDropdownOpen: () => {},
onDropdownClose: () => {}, onDropdownClose: () => {},
onInput: () => {}, onInput: () => {},
}; };
export default DataListInput; export default DataListInput;

View File

@ -3,4 +3,4 @@ import ReactDOM from 'react-dom';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
ReactDOM.render( <App />, document.getElementById( 'root' ) ); ReactDOM.render(<App />, document.getElementById('root'));