Autocomplete

PHOTO EMBED

Thu May 27 2021 15:31:56 GMT+0000 (Coordinated Universal Time)

Saved by @georeyff #react.js

import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { TextField, List, ListItem, CircularProgress, InputAdornment, createMuiTheme, makeStyles } from '@material-ui/core'
import NodeSuggestions from 'components/Autocomplete/NodeSuggestions'
import AddressSuggestions from 'components/Autocomplete/AddressSuggestions'
import UserSuggestions from 'components/Autocomplete/UserSuggestions'
import GeocodedSuggestion from 'components/Autocomplete/GeocodedSuggestion'
import {map} from 'lodash'
import { ARROW_UP, ARROW_DOWN,
	enter, onInputChange, RESET_AUTOCOMPLETE, contextToData } from 'containers/Autocomplete/actions'
import PickUp from 'components/Icons/PickUp/PickUp.jsx'
import {useIntl} from 'react-intl'


const suggestionProviders = {
	'geocodes': GeocodedSuggestion,
	'nodes': NodeSuggestions,
	'addresses': AddressSuggestions,
	'users': UserSuggestions
}

const useStyles = makeStyles((theme) => ({
	root: {
		'& .MuiTextField-root': {
			margin: '20px',
		}
	}
}))

function Autocomplete(props){
	const classes = useStyles()

	/********  props from parent
    * text : the text in the field, controlled by parent with:
		* onChange
    * context : the view where is the autocomplete => which data it displays ?
    * onSelect : when clicking on suggestion
    */
	const { onChange, onInputChange, context, onSelect, loading, highlightedIndex } = props
	const { text, helperText, label} = _.get(props, 'inputProps', {})
	/******* props generated from inside
		*/
	const { arrowUp, arrowDown, enter, resetAutocomplete } = props

	/******** internal state that is not needed above the component
    */
	const [isFocused, setFocus] = useState(false)
	// conditional opening of autocomplete
	const open = isFocused && text &&	text.length >= 3

	// highlighted index (for keyboard navigation) is managed via redux to answer the case of
	// switching from node to address suggestion

	const handleKey = (e) => {
		e.stopPropagation()
		switch (e.keyCode) {
		// second thought : a "selected" element with arrows is also displayed and entered in form
		// context to know which datatypes are displayed and to guess the highlightedIndex element
		case 38:// arrowUp key
			arrowUp(context)
			enter(context, onSelect)
			break
		case 40:// arrowDown key
			arrowDown(context)
			enter(context, onSelect)

			break
		case 13:// enter key
			enter(context, onSelect)
			setFocus(false)
			resetAutocomplete()
			break
		}
	}

	// below is cleaning the "highlightedIndex" in an edge case
	useEffect(()=>{
		if(isFocused && text &&	text.length < 3){
			resetAutocomplete()
		}
	},[text])

	const intl = useIntl()

	return(
		<div className="fullWidth" style={{position:'relative'}}
			//ACCESSIBILITY
			role="combobox"
			aria-expanded={open}
		>
			<label htmlFor={label} style={{color:'#6c6c6c'}}>{label}</label>
			<TextField
				autoComplete="off" // not from the browser
				id={label}
				fullWidth
				value={text}
				onChange={e =>{
					// update search.formData and textfield
					onChange(e)
					// update autoComplete only when user types a letter
					onInputChange(e.target.value, context)
					setFocus(true)
				}}
				onFocus={()=>{
					// loads suggestions also when clicking
					if (text && text.length >= 3){
						onInputChange(text, context)
					}
					setFocus(true)
				}}
				onBlur={()=>{
					setFocus(false)
					resetAutocomplete()
				}}
				onKeyDown={handleKey}
				// label={label}
				// hiddenLabel
				helperText={helperText}
				error={Boolean(helperText)}
				title={label}
				InputProps={{
					disableUnderline: true
				}}
				InputLabelProps={{ shrink: true }}

				// ACCESSIBILITY
				inputProps={{
					autoComplete:'off',
					'aria-autocomplete':open ? 'list' : 'none',
					'aria-controls':open ? 'autocomplete_results' : '',
					'aria-activedescendant': open ? 'suggestion-'+highlightedIndex : '',
					role:'textbox'
				}}
				placeholder={intl.formatMessage({ id: 'help.address' })}
				style={{height: '2.5rem', marginTop:'0.5rem', backgroundColor:'#f1f0f0', borderRadius:'0 5px 5px 0'}}
			/>

			{ open &&
			<List
				tabIndex={-1}
				disablePadding
				className="suggestionsContainer"
				// ACCESSIBILITY
				id="autocomplete_results"
				role="listbox"
			>
        	{ loading ? <div className="row centered fullWidth padded"><CircularProgress size={20} /></div>
					:
						 map(contextToData[context],(dataType,i)=>{
        		const SuggestionProvider = suggestionProviders[dataType]
        		return(
        			<SuggestionProvider
        				key={i}
        				onSelect={onSelect}
        			/>
        		)
        	})}
			</List>
			}
		</div>
	)
}
const mapStateToProps = state => ({
	loading: state.autocomplete.loading,
	highlightedIndex: state.autocomplete.highlightedIndex
})

const mapDispatchToProps = dispatch => ({
	arrowUp : (context) => dispatch({ type : ARROW_UP, context }),
	arrowDown : (context) => dispatch({ type : ARROW_DOWN, context }),
	enter : (context, onSelect) => dispatch(enter(context, onSelect)),
	onInputChange : (newText, context) => dispatch(onInputChange(newText, context)),
	resetAutocomplete: () => dispatch({type: RESET_AUTOCOMPLETE})
})
export default connect(mapStateToProps, mapDispatchToProps)(Autocomplete)
content_copyCOPY