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"
],
"rules": {
"indent": ["error", 4],
"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" ] } ]
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
}
}

View File

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import React, { useState, useEffect } from 'react';
import csvFile from './data.csv';
@ -6,96 +7,96 @@ import DataListInput from './DataListInput';
// eslint-disable-next-line no-unused-vars
const data = [
{
key: '0',
label: 'Apple',
},
{
key: '1',
label: 'Mango',
},
{
key: '2',
label: 'Potatoe',
},
{
key: '0',
label: 'Apple',
},
{
key: '1',
label: 'Mango',
},
{
key: '2',
label: 'Potatoe',
},
];
async function getAndParseData( filename ) {
const response = await fetch( filename );
const reader = response.body.getReader();
const decoder = new TextDecoder( 'utf-8' );
const result = await reader.read();
const allText = decoder.decode( result.value );
async function getAndParseData(filename) {
const response = await fetch(filename);
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
const result = await reader.read();
const allText = decoder.decode(result.value);
const allTextLines = allText.split( /\r\n|\n/ );
const headers = allTextLines[ 0 ].split( ',' );
const lines = [];
const allTextLines = allText.split(/\r\n|\n/);
const headers = allTextLines[0].split(',');
const lines = [];
for ( let i = 1; i < allTextLines.length; i += 1 ) {
const set = allTextLines[ i ].split( ',' );
if ( set.length === headers.length ) {
const tarr = {};
for ( let j = 0; j < headers.length; j += 1 ) {
tarr[ headers[ j ] ] = set[ j ];
}
lines.push( tarr );
}
for (let i = 1; i < allTextLines.length; i += 1) {
const set = allTextLines[i].split(',');
if (set.length === headers.length) {
const tarr = {};
for (let j = 0; j < headers.length; j += 1) {
tarr[headers[j]] = set[j];
}
lines.push(tarr);
}
return lines;
}
return lines;
}
// eslint-disable-next-line no-unused-vars
function annoyinglySlowMatchingAlg( currentInput, item ) {
for ( let i = 0; i < 100000; i += 1 ) {
i += 1;
i -= 1;
// eslint-disable-next-line no-unused-expressions
( currentInput.length + item.label.length ) % 2;
}
return item.label.substr( 0, currentInput.length ).toUpperCase() === currentInput.toUpperCase();
function annoyinglySlowMatchingAlg(currentInput, item) {
for (let i = 0; i < 100000; i += 1) {
i += 1;
i -= 1;
// eslint-disable-next-line no-unused-expressions
(currentInput.length + item.label.length) % 2;
}
return item.label.substr(0, currentInput.length).toUpperCase() === currentInput.toUpperCase();
}
function App() {
const [ item, setItem ] = useState();
const [ items, setItems ] = useState( [] );
const [item, setItem] = useState();
const [items, setItems] = useState(data);
useEffect( () => {
getAndParseData( csvFile ).then( obj => setItems( obj
.concat( obj )
.map( ( row, i ) => (
{
...row,
label: row.vorname,
key: i,
}
) ) ) );
}, [] );
// useEffect(() => {
// getAndParseData(csvFile).then(obj => setItems(obj
// .concat(obj)
// .map((row, i) => (
// {
// ...row,
// label: row.vorname,
// key: i,
// }
// ))));
// }, []);
return (
<div className="App">
<div className="content">
{
return (
<div className="App">
<div className="content">
{
item && (
<div>
{ `Current Item: ${ item.label }` }
</div>
<div>
{ `Current Item: ${item.label}` }
</div>
)
}
<div className="wrapper">
<DataListInput
items={items}
onSelect={i => setItem( i )}
placeholder="Select a ingredient"
clearInputOnSelect={false}
suppressReselect={false}
initialValue={item ? item.label : ''}
debounceTime={1000}
debounceLoader={<>Hello</>}
/>
</div>
</div>
<div className="wrapper">
<DataListInput
items={items}
onSelect={i => setItem(i)}
placeholder="Select a ingredient"
clearInputOnSelect
suppressReselect={false}
initialValue={item ? item.label : ''}
// debounceTime={1000}
// debounceLoader={<>Hello</>}
/>
</div>
);
</div>
</div>
);
}
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 './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 ) {
super( props );
/**
* function for getting the index of the currentValue inside a value of the values array
* @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 = {
/* last valid item that was selected from the drop down menu */
lastValidItem: undefined,
/* current input text */
currentInput: initialValue,
/* current set of matching items */
matchingItems: [],
/* visibility property of the drop down menu */
visible: false,
/* index of the currently focused item in the drop down menu */
focusIndex: 0,
/* cleaner click events, click interaction within dropdown menu */
interactionHappened: false,
/* show loader if still matching in debounced mode */
isMatchingDebounced: false,
};
const DataListInput = ({
activeItemClassName,
clearInputOnSelect,
debounceLoader,
debounceTime,
dropdownClassName,
dropDownLength,
initialValue,
inputClassName,
itemClassName,
match,
onDropdownClose,
onDropdownOpen,
onInput,
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 */
this.inputHappenedTimeout = undefined;
/* to manage debouncing of matching, typing input into the input field */
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 = () => {
if ( windowExists() ) {
window.addEventListener( 'click', this.onClickCloseMenu, false );
// set nextInput into input field and show loading if debounced mode is on
const reachedRequiredLength = nextInput.length >= requiredInputLength;
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();
}
}
componentDidUpdate = () => {
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 );
setMatchingItems(displayableItems);
setFocusIndex(-1);
setIsMatchingDebounced(false);
}
};
onClickInput = () => {
const { visible } = this.state;
let { currentInput } = this.state;
const { requiredInputLength, initialValue } = this.props;
if (debounceTime <= 0) {
updateMatchingItems();
} else {
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
if ( initialValue && currentInput === initialValue ) {
this.setState( { currentInput: '' } );
currentInput = '';
}
/**
* gets called when someone starts to write in the input field
* @param value
*/
const onHandleInput = useCallback((event) => {
const { value } = event.target;
debouncedMatchingUpdateStep(value);
onInput(value);
}, [debouncedMatchingUpdateStep, onInput]);
const reachedRequiredLength = currentInput.length >= requiredInputLength;
if ( reachedRequiredLength && !visible ) {
this.debouncedMatchingUpdateStep( currentInput );
}
const onClickInput = useCallback(() => {
let value = currentInput;
// if user clicks on input field with initialValue,
// the user most likely wants to clear the input field
if (initialValue && currentInput === initialValue) {
value = '';
}
/**
* handle key events
* @param event
*/
onHandleKeydown = ( event ) => {
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 );
}
}
};
const reachedRequiredLength = value.length >= requiredInputLength;
if (reachedRequiredLength && !visible) {
debouncedMatchingUpdateStep(value);
}
}, [visible, currentInput, requiredInputLength, initialValue, debouncedMatchingUpdateStep]);
/**
* onClickItem gets called when onClick happens on one of the list elements
* @param event
*/
onClickItem = ( event ) => {
const { matchingItems } = this.state;
// update the input value and close the dropdown again
const elements = event.currentTarget.children;
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 );
};
/**
* handleSelect is called onClickItem and onEnter upon an option of the drop down menu
* does nothing if the key has not changed since the last onSelect event
* @param selectedItem
*/
const onHandleSelect = useCallback((selectedItem) => {
// block select call until last matching went through
if (isMatchingDebounced) return;
/**
* onSelect is called onClickItem and onEnter upon an option of the drop down menu
* does nothing if the key has not changed since the last onSelect event
* @param selectedItem
*/
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 );
};
setCurrentInput(clearInputOnSelect ? '' : selectedItem.label);
setVisible(false);
setFocusIndex(-1);
setInteractionHappened(true);
onDropdownClose();
renderMatchingLabel = ( currentInput, item, indexOfMatch ) => (
<React.Fragment>
{item.label.substr( 0, indexOfMatch ) }
<strong>
{ item.label.substr( indexOfMatch, currentInput.length ) }
</strong>
{ item.label.substr( indexOfMatch + currentInput.length, item.label.length ) }
</React.Fragment>
if (suppressReselect && lastValidItem && selectedItem.key === lastValidItem.key) {
// do not trigger the callback function
// but still change state to fit new selection
return;
}
// change state to fit new selection
setLastValidItem(selectedItem);
// 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 ) => (
<React.Fragment>
{ indexOfMatch >= 0 && currentInput.length
? this.renderMatchingLabel( currentInput, item, indexOfMatch )
: item.label }
</React.Fragment>
)
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 );
}
const renderItems = useCallback(() => (
<div ref={menu} 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 className="datalist-input">
{ this.renderInputField( placeholder, currentInput, inputClassName ) }
{ renderedResults }
</div>
<div
onClick={() => onHandleSelect(item)}
className={itemClasses}
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 = {
items: PropTypes.arrayOf(
PropTypes.shape( {
label: PropTypes.string.isRequired,
key: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.number,
] ).isRequired,
} ),
).isRequired,
placeholder: PropTypes.string,
onSelect: PropTypes.func.isRequired,
onDropdownOpen: PropTypes.func,
onDropdownClose: PropTypes.func,
match: PropTypes.func,
inputClassName: PropTypes.string,
dropdownClassName: PropTypes.string,
itemClassName: PropTypes.string,
activeItemClassName: PropTypes.string,
requiredInputLength: PropTypes.number,
clearInputOnSelect: PropTypes.bool,
suppressReselect: PropTypes.bool,
dropDownLength: PropTypes.number,
initialValue: PropTypes.string,
debounceTime: PropTypes.number,
debounceLoader: PropTypes.node,
onInput: PropTypes.func,
items: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
key: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
}),
).isRequired,
placeholder: PropTypes.string,
onSelect: PropTypes.func.isRequired,
onDropdownOpen: PropTypes.func,
onDropdownClose: PropTypes.func,
match: PropTypes.func,
inputClassName: PropTypes.string,
dropdownClassName: PropTypes.string,
itemClassName: PropTypes.string,
activeItemClassName: PropTypes.string,
requiredInputLength: PropTypes.number,
clearInputOnSelect: PropTypes.bool,
suppressReselect: PropTypes.bool,
dropDownLength: PropTypes.number,
initialValue: PropTypes.string,
debounceTime: PropTypes.number,
debounceLoader: PropTypes.node,
onInput: PropTypes.func,
};
DataListInput.defaultProps = {
placeholder: '',
match: undefined,
inputClassName: '',
dropdownClassName: '',
itemClassName: '',
activeItemClassName: '',
requiredInputLength: 0,
clearInputOnSelect: false,
suppressReselect: true,
dropDownLength: Infinity,
initialValue: '',
debounceTime: 0,
debounceLoader: undefined,
onDropdownOpen: () => {},
onDropdownClose: () => {},
onInput: () => {},
placeholder: '',
match: undefined,
inputClassName: '',
dropdownClassName: '',
itemClassName: '',
activeItemClassName: '',
requiredInputLength: 0,
clearInputOnSelect: false,
suppressReselect: true,
dropDownLength: Infinity,
initialValue: '',
debounceTime: 0,
debounceLoader: undefined,
onDropdownOpen: () => {},
onDropdownClose: () => {},
onInput: () => {},
};
export default DataListInput;

View File

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