diff --git a/web/calendar/thread-picker.react.js b/web/calendar/thread-picker.react.js --- a/web/calendar/thread-picker.react.js +++ b/web/calendar/thread-picker.react.js @@ -6,7 +6,6 @@ import { threadSearchIndex } from 'lib/selectors/nav-selectors'; import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors'; -import SearchIndex from 'lib/shared/search-index'; import type { ThreadInfo } from 'lib/types/thread-types'; import { useSelector } from '../redux/redux-utils'; @@ -35,126 +34,111 @@ ); } -type BaseProps = { +type Props = { +createNewEntry: (threadID: string) => void, +closePicker: () => void, }; -type Props = { - ...BaseProps, - +onScreenThreadInfos: $ReadOnlyArray, - +searchIndex: SearchIndex, -}; -type State = { - +searchText: string, - +searchResults: Set, -}; -type PropsAndState = { ...Props, ...State }; - -class ThreadPicker extends React.PureComponent { - pickerDiv: ?HTMLDivElement; - - constructor(props: Props) { - super(props); - this.state = { - searchText: '', - searchResults: new Set(), - }; - invariant( - props.onScreenThreadInfos.length > 0, - "ThreadPicker can't be open when onScreenThreadInfos is empty", - ); - } - - componentDidMount() { - invariant(this.pickerDiv, 'pickerDiv ref unset'); - this.pickerDiv.focus(); - } - - listDataSelector = createSelector( - (propsAndState: PropsAndState) => propsAndState.onScreenThreadInfos, - (propsAndState: PropsAndState) => propsAndState.searchText, - (propsAndState: PropsAndState) => propsAndState.searchResults, + +function ThreadPicker(props: Props): React.Node { + const { closePicker, createNewEntry } = props; + + const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos); + const searchIndex = useSelector(state => threadSearchIndex(state)); + + invariant( + onScreenThreadInfos.length > 0, + "ThreadPicker can't be open when onScreenThreadInfos is empty", + ); + + const pickerDivRef = React.useRef(null); + + React.useLayoutEffect(() => { + invariant(pickerDivRef, 'pickerDivRef must be set'); + const { current } = pickerDivRef; + current?.focus(); + }, []); + + const [searchText, setSearchText] = React.useState(''); + const [searchResults, setSearchResults] = React.useState>( + new Set(), + ); + + const onPickerKeyDown = React.useCallback( + (event: SyntheticKeyboardEvent) => { + if (event.keyCode === 27) { + // esc + closePicker(); + } + }, + [closePicker], + ); + + const onMouseDown = React.useCallback( + (event: SyntheticEvent) => { + const target = htmlTargetFromEvent(event); + invariant(pickerDivRef, 'pickerDivRef must be set'); + if (pickerDivRef.current?.contains(target)) { + // This prevents onBlur from firing + event.preventDefault(); + } + }, + [], + ); + + // eslint-disable-next-line no-unused-vars + const onChangeSearchText = React.useCallback( + (text: string) => { + const results = searchIndex.getSearchResults(text); + setSearchText(text); + setSearchResults(new Set(results)); + }, + [searchIndex], + ); + + const listDataSelector = createSelector( + state => state.onScreenThreadInfos, + state => state.searchText, + state => state.searchResults, ( threadInfos: $ReadOnlyArray, text: string, - searchResults: Set, + results: Set, ) => text - ? threadInfos.filter(threadInfo => searchResults.has(threadInfo.id)) + ? threadInfos.filter(threadInfo => results.has(threadInfo.id)) : [...threadInfos], ); - get getListData() { - return this.listDataSelector({ ...this.props, ...this.state }); - } - - render() { - const length = this.props.onScreenThreadInfos.length; - invariant( - length > 0, - "ThreadPicker can't be open when onScreenThreadInfos is empty", - ); + const threads = useSelector(() => + listDataSelector({ + onScreenThreadInfos, + searchText, + searchResults, + }), + ); - const options = this.getListData.map(threadInfo => ( + const options = React.useMemo(() => { + return threads.map(threadInfo => ( )); + }, [threads, createNewEntry]); - return ( -
- {options} -
- ); - } - - pickerDivRef = (pickerDiv: ?HTMLDivElement) => { - this.pickerDiv = pickerDiv; - }; - - onPickerKeyDown = (event: SyntheticKeyboardEvent) => { - if (event.keyCode === 27) { - // Esc - this.props.closePicker(); - } - }; - - onMouseDown = (event: SyntheticEvent) => { - const target = htmlTargetFromEvent(event); - invariant(this.pickerDiv, 'pickerDiv ref not set'); - if (this.pickerDiv.contains(target)) { - // This prevents onBlur from firing - event.preventDefault(); - } - }; - - onChangeSearchText = (searchText: string) => { - const results = this.props.searchIndex.getSearchResults(searchText); - this.setState({ searchText, searchResults: new Set(results) }); - }; + return ( +
+ {options} +
+ ); } -const ConnectedThreadPicker: React.ComponentType = React.memo( - function ConnectedThreadPicker(props) { - const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos); - const index = useSelector(state => threadSearchIndex(state)); - return ( - - ); - }, -); - -export default ConnectedThreadPicker; +export default ThreadPicker;