Convert LanguageDropdownMenu to functional component (#33704)
		
	This commit is contained in:
		
					parent
					
						
							
								a1d9c3fb99
							
						
					
				
			
			
				commit
				
					
						9a0166c3f3
					
				
			
		
					 1 changed files with 144 additions and 178 deletions
				
			
		|  | @ -1,5 +1,5 @@ | |||
| import PropTypes from 'prop-types'; | ||||
| import { useCallback, useRef, useState, useEffect, PureComponent } from 'react'; | ||||
| import { useCallback, useRef, useState, useEffect, useMemo } from 'react'; | ||||
| 
 | ||||
| import { useIntl, defineMessages } from 'react-intl'; | ||||
| 
 | ||||
|  | @ -30,68 +30,142 @@ const messages = defineMessages({ | |||
| 
 | ||||
| const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; | ||||
| 
 | ||||
| class LanguageDropdownMenu extends PureComponent { | ||||
| const getFrequentlyUsedLanguages = createSelector([ | ||||
|   state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()), | ||||
| ], languageCounters => ( | ||||
|   languageCounters.keySeq() | ||||
|     .sort((a, b) => languageCounters.get(a) - languageCounters.get(b)) | ||||
|     .reverse() | ||||
|     .toArray() | ||||
| )); | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     value: PropTypes.string.isRequired, | ||||
|     guess: PropTypes.string, | ||||
|     frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired, | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     onChange: PropTypes.func.isRequired, | ||||
|     languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), | ||||
|     intl: PropTypes.object, | ||||
|   }; | ||||
| const LanguageDropdownMenu = ({ value, guess, onClose, onChange, languages = preloadedLanguages, intl }) => { | ||||
|   const [searchValue, setSearchValue] = useState(''); | ||||
|   const nodeRef = useRef(null); | ||||
|   const listNodeRef = useRef(null); | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|     languages: preloadedLanguages, | ||||
|   }; | ||||
|   const frequentlyUsedLanguages = useAppSelector(getFrequentlyUsedLanguages); | ||||
| 
 | ||||
|   state = { | ||||
|     searchValue: '', | ||||
|   }; | ||||
|   const handleSearchChange = useCallback(({ target }) => { | ||||
|     setSearchValue(target.value); | ||||
|   }, [setSearchValue]); | ||||
| 
 | ||||
|   handleDocumentClick = e => { | ||||
|     if (this.node && !this.node.contains(e.target)) { | ||||
|       this.props.onClose(); | ||||
|   const handleClick = useCallback((e) => { | ||||
|     const value = e.currentTarget.getAttribute('data-index'); | ||||
| 
 | ||||
|     e.preventDefault(); | ||||
| 
 | ||||
|     onClose(); | ||||
|     onChange(value); | ||||
|   }, [onClose, onChange]); | ||||
| 
 | ||||
|   const handleKeyDown = useCallback(e => { | ||||
|     const index = Array.from(listNodeRef.current.childNodes).findIndex(node => node === e.currentTarget); | ||||
| 
 | ||||
|     let element = null; | ||||
| 
 | ||||
|     switch(e.key) { | ||||
|     case 'Escape': | ||||
|       onClose(); | ||||
|       break; | ||||
|     case ' ': | ||||
|     case 'Enter': | ||||
|       handleClick(e); | ||||
|       break; | ||||
|     case 'ArrowDown': | ||||
|       element = listNodeRef.current.childNodes[index + 1] || listNodeRef.current.firstChild; | ||||
|       break; | ||||
|     case 'ArrowUp': | ||||
|       element = listNodeRef.current.childNodes[index - 1] || listNodeRef.current.lastChild; | ||||
|       break; | ||||
|     case 'Tab': | ||||
|       if (e.shiftKey) { | ||||
|         element = listNodeRef.current.childNodes[index - 1] || listNodeRef.current.lastChild; | ||||
|       } else { | ||||
|         element = listNodeRef.current.childNodes[index + 1] || listNodeRef.current.firstChild; | ||||
|       } | ||||
|       break; | ||||
|     case 'Home': | ||||
|       element = listNodeRef.current.firstChild; | ||||
|       break; | ||||
|     case 'End': | ||||
|       element = listNodeRef.current.lastChild; | ||||
|       break; | ||||
|     } | ||||
| 
 | ||||
|     if (element) { | ||||
|       element.focus(); | ||||
|       e.preventDefault(); | ||||
|       e.stopPropagation(); | ||||
|     } | ||||
|   }; | ||||
|   }, [onClose, handleClick]); | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     document.addEventListener('click', this.handleDocumentClick, { capture: true }); | ||||
|     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); | ||||
|   const handleSearchKeyDown = useCallback(e => { | ||||
|     let element = null; | ||||
| 
 | ||||
|     switch(e.key) { | ||||
|     case 'Tab': | ||||
|     case 'ArrowDown': | ||||
|       element = listNodeRef.current.firstChild; | ||||
| 
 | ||||
|       if (element) { | ||||
|         element.focus(); | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
|       } | ||||
| 
 | ||||
|       break; | ||||
|     case 'Enter': | ||||
|       element = listNodeRef.current.firstChild; | ||||
| 
 | ||||
|       if (element) { | ||||
|         onChange(element.getAttribute('data-index')); | ||||
|         onClose(); | ||||
|       } | ||||
|       break; | ||||
|     case 'Escape': | ||||
|       if (searchValue !== '') { | ||||
|         e.preventDefault(); | ||||
|         this.handleClear(); | ||||
|       } | ||||
| 
 | ||||
|       break; | ||||
|     } | ||||
|   }, [onChange, onClose, searchValue]); | ||||
| 
 | ||||
|   const handleClear = useCallback(() => { | ||||
|     setSearchValue(''); | ||||
|   }, [setSearchValue]); | ||||
| 
 | ||||
|   const isSearching = searchValue !== ''; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const handleDocumentClick = (e) => { | ||||
|       if (nodeRef.current && !nodeRef.current.contains(e.target)) { | ||||
|         onClose(); | ||||
|         e.stopPropagation(); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     document.addEventListener('click', handleDocumentClick, { capture: true }); | ||||
|     document.addEventListener('touchend', handleDocumentClick, listenerOptions); | ||||
| 
 | ||||
|     // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need | ||||
|     // to wait for a frame before focusing | ||||
|     requestAnimationFrame(() => { | ||||
|       if (this.node) { | ||||
|         const element = this.node.querySelector('input[type="search"]'); | ||||
|       if (nodeRef.current) { | ||||
|         const element = nodeRef.current.querySelector('input[type="search"]'); | ||||
|         if (element) element.focus(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|     document.removeEventListener('click', this.handleDocumentClick, { capture: true }); | ||||
|     document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); | ||||
|   } | ||||
| 
 | ||||
|   setRef = c => { | ||||
|     this.node = c; | ||||
|   }; | ||||
| 
 | ||||
|   setListRef = c => { | ||||
|     this.listNode = c; | ||||
|   }; | ||||
| 
 | ||||
|   handleSearchChange = ({ target }) => { | ||||
|     this.setState({ searchValue: target.value }); | ||||
|   }; | ||||
| 
 | ||||
|   search () { | ||||
|     const { languages, value, frequentlyUsedLanguages, guess } = this.props; | ||||
|     const { searchValue } = this.state; | ||||
|     return () => { | ||||
|       document.removeEventListener('click', handleDocumentClick, { capture: true }); | ||||
|       document.removeEventListener('touchend', handleDocumentClick, listenerOptions); | ||||
|     }; | ||||
|   }, [onClose]); | ||||
| 
 | ||||
|   const results = useMemo(() => { | ||||
|     if (searchValue === '') { | ||||
|       return [...languages].sort((a, b) => { | ||||
| 
 | ||||
|  | @ -119,139 +193,34 @@ class LanguageDropdownMenu extends PureComponent { | |||
|       limit: 5, | ||||
|       threshold: -10000, | ||||
|     }).map(result => result.obj); | ||||
|   } | ||||
|   }, [searchValue, languages, guess, frequentlyUsedLanguages, value]); | ||||
| 
 | ||||
|   handleClick = e => { | ||||
|     const value = e.currentTarget.getAttribute('data-index'); | ||||
| 
 | ||||
|     e.preventDefault(); | ||||
| 
 | ||||
|     this.props.onClose(); | ||||
|     this.props.onChange(value); | ||||
|   }; | ||||
| 
 | ||||
|   handleKeyDown = e => { | ||||
|     const { onClose } = this.props; | ||||
|     const index = Array.from(this.listNode.childNodes).findIndex(node => node === e.currentTarget); | ||||
| 
 | ||||
|     let element = null; | ||||
| 
 | ||||
|     switch(e.key) { | ||||
|     case 'Escape': | ||||
|       onClose(); | ||||
|       break; | ||||
|     case ' ': | ||||
|     case 'Enter': | ||||
|       this.handleClick(e); | ||||
|       break; | ||||
|     case 'ArrowDown': | ||||
|       element = this.listNode.childNodes[index + 1] || this.listNode.firstChild; | ||||
|       break; | ||||
|     case 'ArrowUp': | ||||
|       element = this.listNode.childNodes[index - 1] || this.listNode.lastChild; | ||||
|       break; | ||||
|     case 'Tab': | ||||
|       if (e.shiftKey) { | ||||
|         element = this.listNode.childNodes[index - 1] || this.listNode.lastChild; | ||||
|       } else { | ||||
|         element = this.listNode.childNodes[index + 1] || this.listNode.firstChild; | ||||
|       } | ||||
|       break; | ||||
|     case 'Home': | ||||
|       element = this.listNode.firstChild; | ||||
|       break; | ||||
|     case 'End': | ||||
|       element = this.listNode.lastChild; | ||||
|       break; | ||||
|     } | ||||
| 
 | ||||
|     if (element) { | ||||
|       element.focus(); | ||||
|       e.preventDefault(); | ||||
|       e.stopPropagation(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   handleSearchKeyDown = e => { | ||||
|     const { onChange, onClose } = this.props; | ||||
|     const { searchValue } = this.state; | ||||
| 
 | ||||
|     let element = null; | ||||
| 
 | ||||
|     switch(e.key) { | ||||
|     case 'Tab': | ||||
|     case 'ArrowDown': | ||||
|       element = this.listNode.firstChild; | ||||
| 
 | ||||
|       if (element) { | ||||
|         element.focus(); | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
|       } | ||||
| 
 | ||||
|       break; | ||||
|     case 'Enter': | ||||
|       element = this.listNode.firstChild; | ||||
| 
 | ||||
|       if (element) { | ||||
|         onChange(element.getAttribute('data-index')); | ||||
|         onClose(); | ||||
|       } | ||||
|       break; | ||||
|     case 'Escape': | ||||
|       if (searchValue !== '') { | ||||
|         e.preventDefault(); | ||||
|         this.handleClear(); | ||||
|       } | ||||
| 
 | ||||
|       break; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   handleClear = () => { | ||||
|     this.setState({ searchValue: '' }); | ||||
|   }; | ||||
| 
 | ||||
|   renderItem = lang => { | ||||
|     const { value } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <div key={lang[0]} role='option' tabIndex={0} data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}> | ||||
|         <span className='language-dropdown__dropdown__results__item__native-name' lang={lang[0]}>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span> | ||||
|   return ( | ||||
|     <div ref={nodeRef}> | ||||
|       <div className='emoji-mart-search'> | ||||
|         <input type='search' value={searchValue} onChange={handleSearchChange} onKeyDown={handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} /> | ||||
|         <button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={handleClear}><Icon icon={!isSearching ? SearchIcon : CancelIcon} /></button> | ||||
|       </div> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { intl } = this.props; | ||||
|     const { searchValue } = this.state; | ||||
|     const isSearching = searchValue !== ''; | ||||
|     const results = this.search(); | ||||
| 
 | ||||
|     return ( | ||||
|       <div ref={this.setRef}> | ||||
|         <div className='emoji-mart-search'> | ||||
|           <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} /> | ||||
|           <button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}><Icon icon={!isSearching ? SearchIcon : CancelIcon} /></button> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> | ||||
|           {results.map(this.renderItem)} | ||||
|         </div> | ||||
|       <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={listNodeRef}> | ||||
|         {results.map((lang) => ( | ||||
|           <div key={lang[0]} role='option' tabIndex={0} data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={handleClick} onKeyDown={handleKeyDown}> | ||||
|             <span className='language-dropdown__dropdown__results__item__native-name' lang={lang[0]}>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| const getFrequentlyUsedLanguages = createSelector([ | ||||
|   state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()), | ||||
| ], languageCounters => ( | ||||
|   languageCounters.keySeq() | ||||
|     .sort((a, b) => languageCounters.get(a) - languageCounters.get(b)) | ||||
|     .reverse() | ||||
|     .toArray() | ||||
| )); | ||||
| LanguageDropdownMenu.propTypes = { | ||||
|   value: PropTypes.string.isRequired, | ||||
|   guess: PropTypes.string, | ||||
|   onClose: PropTypes.func.isRequired, | ||||
|   onChange: PropTypes.func.isRequired, | ||||
|   languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), | ||||
|   intl: PropTypes.object, | ||||
| }; | ||||
| 
 | ||||
| export const LanguageDropdown = () => { | ||||
|   const [open, setOpen] = useState(false); | ||||
|  | @ -263,7 +232,6 @@ export const LanguageDropdown = () => { | |||
|   const intl = useIntl(); | ||||
| 
 | ||||
|   const dispatch = useAppDispatch(); | ||||
|   const frequentlyUsedLanguages = useAppSelector(getFrequentlyUsedLanguages); | ||||
|   const value = useAppSelector((state) => state.compose.get('language')); | ||||
|   const text = useAppSelector((state) => state.compose.get('text')); | ||||
| 
 | ||||
|  | @ -319,10 +287,8 @@ export const LanguageDropdown = () => { | |||
|               <LanguageDropdownMenu | ||||
|                 value={value} | ||||
|                 guess={guess} | ||||
|                 frequentlyUsedLanguages={frequentlyUsedLanguages} | ||||
|                 onClose={handleClose} | ||||
|                 onChange={handleChange} | ||||
|                 intl={intl} | ||||
|               /> | ||||
|             </div> | ||||
|           </div> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue