From ba803e200c7159f83151b78681e086308c6d48c2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 4 Jun 2022 18:21:16 -0500 Subject: [PATCH] AutosuggestInput: convert to TSX --- .../components/autosuggest_account_input.tsx | 2 +- ...suggest_input.js => autosuggest_input.tsx} | 124 ++++++++++-------- 2 files changed, 67 insertions(+), 59 deletions(-) rename app/soapbox/components/{autosuggest_input.js => autosuggest_input.tsx} (71%) diff --git a/app/soapbox/components/autosuggest_account_input.tsx b/app/soapbox/components/autosuggest_account_input.tsx index d437e79fc..f58e37623 100644 --- a/app/soapbox/components/autosuggest_account_input.tsx +++ b/app/soapbox/components/autosuggest_account_input.tsx @@ -59,7 +59,7 @@ const AutosuggestAccountInput: React.FC = ({ onChange(e); }; - const handleSelected = (_tokenStart: string, _lastToken: string, accountId: string) => { + const handleSelected = (_tokenStart: number, _lastToken: string | null, accountId: string) => { onSelected(accountId); }; diff --git a/app/soapbox/components/autosuggest_input.js b/app/soapbox/components/autosuggest_input.tsx similarity index 71% rename from app/soapbox/components/autosuggest_input.js rename to app/soapbox/components/autosuggest_input.tsx index a252eb537..493b541a6 100644 --- a/app/soapbox/components/autosuggest_input.js +++ b/app/soapbox/components/autosuggest_input.tsx @@ -1,22 +1,25 @@ import classNames from 'classnames'; import { List as ImmutableList } from 'immutable'; -import PropTypes from 'prop-types'; import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import AutosuggestEmoji from 'soapbox/components/autosuggest_emoji'; import Icon from 'soapbox/components/icon'; +import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest_account'; +import { isRtl } from 'soapbox/rtl'; -import AutosuggestAccount from '../features/compose/components/autosuggest_account'; -import { isRtl } from '../rtl'; +import type { Menu, MenuItem } from 'soapbox/components/dropdown_menu'; -import AutosuggestEmoji from './autosuggest_emoji'; +type CursorMatch = [ + tokenStart: number | null, + token: string | null, +]; -const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { - let word; +const textAtCursorMatchesToken = (str: string, caretPosition: number, searchTokens: string[]): CursorMatch => { + let word: string; - const left = str.slice(0, caretPosition).search(/\S+$/); - const right = str.slice(caretPosition).search(/\s/); + const left: number = str.slice(0, caretPosition).search(/\S+$/); + const right: number = str.slice(caretPosition).search(/\s/); if (right < 0) { word = str.slice(left); @@ -37,28 +40,25 @@ const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { } }; -export default class AutosuggestInput extends ImmutablePureComponent { +interface IAutosuggestInput extends Pick, 'onChange' | 'onKeyUp' | 'onKeyDown'> { + value: string, + suggestions: ImmutableList, + disabled?: boolean, + placeholder?: string, + onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: any) => void, + onSuggestionsClearRequested: () => void, + onSuggestionsFetchRequested: (token: string) => void, + autoFocus: boolean, + autoSelect: boolean, + className?: string, + id?: string, + searchTokens: string[], + maxLength?: number, + menu?: Menu, + resultsPosition: string, +} - static propTypes = { - value: PropTypes.string, - suggestions: ImmutablePropTypes.list, - disabled: PropTypes.bool, - placeholder: PropTypes.string, - onSuggestionSelected: PropTypes.func.isRequired, - onSuggestionsClearRequested: PropTypes.func.isRequired, - onSuggestionsFetchRequested: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onKeyUp: PropTypes.func, - onKeyDown: PropTypes.func, - autoFocus: PropTypes.bool, - autoSelect: PropTypes.bool, - className: PropTypes.string, - id: PropTypes.string, - searchTokens: PropTypes.arrayOf(PropTypes.string), - maxLength: PropTypes.number, - menu: PropTypes.arrayOf(PropTypes.object), - resultsPosition: PropTypes.string, - }; +export default class AutosuggestInput extends ImmutablePureComponent { static defaultProps = { autoFocus: false, @@ -79,8 +79,10 @@ export default class AutosuggestInput extends ImmutablePureComponent { tokenStart: 0, }; - onChange = (e) => { - const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens); + input: HTMLInputElement | null = null; + + onChange: React.ChangeEventHandler = (e) => { + const [tokenStart, token] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart || 0, this.props.searchTokens); if (token !== null && this.state.lastToken !== token) { this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); @@ -90,10 +92,12 @@ export default class AutosuggestInput extends ImmutablePureComponent { this.props.onSuggestionsClearRequested(); } - this.props.onChange(e); + if (this.props.onChange) { + this.props.onChange(e); + } } - onKeyDown = (e) => { + onKeyDown: React.KeyboardEventHandler = (e) => { const { suggestions, menu, disabled } = this.props; const { selectedSuggestion, suggestionsHidden } = this.state; const firstIndex = this.getFirstIndex(); @@ -104,7 +108,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { return; } - if (e.which === 229 || e.isComposing) { + if (e.which === 229) { // Ignore key events during text composition // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) return; @@ -113,7 +117,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { switch (e.key) { case 'Escape': if (suggestions.size === 0 || suggestionsHidden) { - document.querySelector('.ui').parentElement.focus(); + document.querySelector('.ui')?.parentElement?.focus(); } else { e.preventDefault(); this.setState({ suggestionsHidden: true }); @@ -136,7 +140,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { break; case 'Enter': case 'Tab': - // Select suggestion + // Select suggestion if (!suggestionsHidden && selectedSuggestion > -1 && (suggestions.size > 0 || menu)) { e.preventDefault(); e.stopPropagation(); @@ -144,9 +148,9 @@ export default class AutosuggestInput extends ImmutablePureComponent { if (selectedSuggestion < suggestions.size) { this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); - } else { + } else if (menu) { const item = menu[selectedSuggestion - suggestions.size]; - this.handleMenuItemAction(item); + this.handleMenuItemAction(item, e); } } @@ -157,7 +161,9 @@ export default class AutosuggestInput extends ImmutablePureComponent { return; } - this.props.onKeyDown(e); + if (this.props.onKeyDown) { + this.props.onKeyDown(e); + } } onBlur = () => { @@ -168,25 +174,26 @@ export default class AutosuggestInput extends ImmutablePureComponent { this.setState({ focused: true }); } - onSuggestionClick = (e) => { - const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); - e.preventDefault(); + onSuggestionClick: React.MouseEventHandler = (e) => { + const index = Number(e.currentTarget?.getAttribute('data-index')); + const suggestion = this.props.suggestions.get(index); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); - this.input.focus(); + this.input?.focus(); + e.preventDefault(); } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: IAutosuggestInput, prevState: any) { const { suggestions } = this.props; if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) { this.setState({ suggestionsHidden: false }); } } - setInput = (c) => { + setInput = (c: HTMLInputElement) => { this.input = c; } - renderSuggestion = (suggestion, i) => { + renderSuggestion = (suggestion: any, i: number) => { const { selectedSuggestion } = this.state; let inner, key; @@ -204,7 +211,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { return (
{ + handleMenuItemAction = (item: MenuItem | null, e: React.MouseEvent | React.KeyboardEvent) => { this.onBlur(); - item.action(); + if (item?.action) { + item.action(e); + } } - handleMenuItemClick = item => { + handleMenuItemClick = (item: MenuItem | null): React.MouseEventHandler => { return e => { e.preventDefault(); - this.handleMenuItemAction(item); + this.handleMenuItemAction(item, e); }; } @@ -243,15 +252,15 @@ export default class AutosuggestInput extends ImmutablePureComponent { className={classNames('flex items-center space-x-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700', { selected: suggestions.size - selectedSuggestion === i })} href='#' role='button' - tabIndex='0' + tabIndex={0} onMouseDown={this.handleMenuItemClick(item)} key={i} > - {item.icon && ( + {item?.icon && ( )} - {item.text} + {item?.text} )); }; @@ -259,7 +268,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { render() { const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, resultsPosition } = this.props; const { suggestionsHidden } = this.state; - const style = { direction: 'ltr' }; + const style: React.CSSProperties = { direction: 'ltr' }; const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value)); @@ -275,8 +284,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { type='text' className={classNames({ 'block w-full sm:text-sm dark:bg-slate-800 dark:text-white dark:placeholder:text-gray-500 focus:ring-indigo-500 focus:border-indigo-500': true, - [className]: typeof className !== 'undefined', - })} + }, className)} ref={this.setInput} disabled={disabled} placeholder={placeholder}