From 4e0e05635a4b7d2ccdc39a4a0d6509830d88973d Mon Sep 17 00:00:00 2001 From: Tom Southall Date: Thu, 17 Feb 2022 22:39:33 +0000 Subject: [PATCH] Add useSelected hook --- src/lib/components/container.jsx | 29 ++++++--------- src/lib/components/hooks/containerEffects.js | 11 ++++++ .../components/hooks/containerEffects.test.js | 36 ++++++++++++++++++- src/lib/reducers/reducer.js | 11 +++--- 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/lib/components/container.jsx b/src/lib/components/container.jsx index 2de8869..20bb501 100644 --- a/src/lib/components/container.jsx +++ b/src/lib/components/container.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useContext } from 'react' +import React, { useState, useRef, useContext } from 'react' import { StateContext } from '../context/state' import Items from './items' import { useDebounce } from 'use-debounce' @@ -10,12 +10,12 @@ import { useItemsState, useAutoFocus, useQueryChange, - useHighlight + useHighlight, + useSelected } from './hooks/containerEffects' import { setQuery, setHighlighted, - clearHighlighted, highlightPrev, highlightNext, setSelected @@ -68,18 +68,20 @@ export default function Container(props) { const queryInput = useRef(null) const typeaheadInput = useRef(null) - const isImmutable = () => { + // Checks whether or not SWR data is to be treated as immutable + const isImmutable = (() => { return itemGroupsAreImmutable && !( defaultItemGroups && !defaultItemGroupsAreImmutable && debouncedQuery.length === 0 ) - } + })() + // Hook to retrieve data using SWR const swrData = useData( debouncedQuery.toLowerCase(), - isImmutable(), + isImmutable, itemGroups, defaultItemGroups, minQueryLength, @@ -102,14 +104,7 @@ export default function Container(props) { useHighlight(state.highlighted, hasFocus, queryInput, typeaheadInput) // When an item is selected alter the query to match and fire applicable events - useEffect(() => { - if (!isUndefined(state.selected)) { - typeaheadInput.current.value = '' - dispatch(setQuery(state.selected.text)) - queryInput.current.blur() - if (typeof onSelect === 'function') onSelect(state.selected.value) - } - }, [state.selected, onSelect]) + useSelected(state.selected, queryInput, typeaheadInput, onSelect) const onTabOrEnter = (keyPressed) => { // keyPressed must be 'enter' or 'tab' @@ -176,16 +171,12 @@ export default function Container(props) { setHasFocus(true) //TODO: make hasFocus part of global state? if (state.items && state.items.length > 0) { - dispatch(setHighlighted({ index: 0, text: state.items[0].text })) - } - else { - dispatch(clearHighlighted()) + dispatch(setHighlighted(0)) } } const handleBlur = () => { setHasFocus(false) - dispatch(clearHighlighted()) } return ( diff --git a/src/lib/components/hooks/containerEffects.js b/src/lib/components/hooks/containerEffects.js index be8ea20..3110e32 100644 --- a/src/lib/components/hooks/containerEffects.js +++ b/src/lib/components/hooks/containerEffects.js @@ -2,6 +2,7 @@ import { useEffect, useContext } from 'react' import setify from 'setify' // Sets input value without changing cursor position import { StateContext } from '../../context/state' import { setItems } from '../../actions/actions' +import isUndefined from '../../utils/isUndefined' import startsWithCaseInsensitive from '../../utils/startsWithCaseInsensitive' export const useItemsState = (swrData) => { @@ -52,6 +53,16 @@ export const useHighlight = (highlighted, hasFocus, queryInput, typeaheadInput) }, [highlighted, hasFocus]) } +export const useSelected = (selected, queryInput, typeaheadInput, onSelect) => { + useEffect(() => { + if (!isUndefined(selected)) { + typeaheadInput.current.value = '' + queryInput.current.blur() + if (typeof onSelect === 'function') onSelect(selected.value) + } + }, [selected, onSelect]) +} + ////////////////////////////// // Helper functions // ////////////////////////////// diff --git a/src/lib/components/hooks/containerEffects.test.js b/src/lib/components/hooks/containerEffects.test.js index 0e8c472..1883b0d 100644 --- a/src/lib/components/hooks/containerEffects.test.js +++ b/src/lib/components/hooks/containerEffects.test.js @@ -5,6 +5,7 @@ import { useAutoFocus, useQueryChange, useHighlight, + useSelected, formatQuery } from './containerEffects' @@ -12,6 +13,7 @@ let inputRef = (value = '') => ( //TODO: Put in a beforeEach when blogging { current: { focus: vi.fn(), + blur: vi.fn(), value } } @@ -80,7 +82,7 @@ describe('useQueryChange', () => { describe('useHighlight', () => { const queryValue = 'chi' - const highlighted = {index: 0, text: 'Chicago, Illinois, United States'} + const highlighted = { index: 0, text: 'Chicago, Illinois, United States' } test('Typeahead is set to highlighted text and query text changes to match case', () => { const queryRef = inputRef(queryValue) @@ -123,6 +125,38 @@ describe('useHighlight', () => { }) }) +describe('useSelected', () => { + const queryValue = 'Chicago, Illinois, United States' + const selected = { + text: queryValue, + value: { + name: queryValue, + coords: '41.882304590139135, -87.62327214400634' + } + } + + test('Side effects of item selection occur correctly', () => { + const queryRef = inputRef(queryValue) + const typeaheadRef = inputRef(queryValue) + const onSelect = vi.fn() + renderHook(() => useSelected(selected, queryRef, typeaheadRef, onSelect)) + expect(queryRef.current.value).toBe(queryValue) + expect(queryRef.current.blur).toHaveBeenCalledTimes(1) + expect(typeaheadRef.current.value).toBe('') + expect(onSelect).toHaveBeenCalledWith(selected.value) + }) + + test('Side effects do not occur if selected item is undefined', () => { + const queryRef = inputRef(queryValue) + const typeaheadRef = inputRef(queryValue) + const onSelect = vi.fn() + renderHook(() => useSelected(undef, queryRef, typeaheadRef, onSelect)) + expect(queryRef.current.blur).toHaveBeenCalledTimes(0) + expect(typeaheadRef.current.value).toBe(queryValue) + expect(onSelect).toHaveBeenCalledTimes(0) + }) +}) + ////////////////////////////// // Helper functions // ////////////////////////////// diff --git a/src/lib/reducers/reducer.js b/src/lib/reducers/reducer.js index 4dac77d..c10f5eb 100644 --- a/src/lib/reducers/reducer.js +++ b/src/lib/reducers/reducer.js @@ -6,7 +6,7 @@ const highlightedItem = (i, state) => { } const reducer = (state, action) => { - const newState = () => { + const newState = (() => { switch (action.type) { case types.SET_QUERY: return action.payload @@ -25,13 +25,16 @@ const reducer = (state, action) => { ? { highlighted: highlightedItem(state.highlighted.index + 1, state) } : {} case types.SET_SELECTED: - return { selected: state.items[action.index] } + return { + selected: state.items[action.index], + query: state.items[action.index].text, + } default: throw new Error('Invalid action type passed to reducer') } - } + })() - return {...state, ...newState()} + return {...state, ...newState} } export default reducer \ No newline at end of file