diff --git a/src/lib/components/container.jsx b/src/lib/components/container.jsx index 1ff56f5..369b75e 100644 --- a/src/lib/components/container.jsx +++ b/src/lib/components/container.jsx @@ -12,6 +12,7 @@ import { useItemsState, useAutoFocus, useQueryChange, + useHighlight } from './hooks/containerEffects' import { setQuery, @@ -95,25 +96,12 @@ export default function Container(props) { useAutoFocus(queryInput, autoFocus) // As soon as the query state changes (ignoring debounce) update the - // typeahead value and the query value and fire onChnage + // typeahead value and the query value and fire onChange useQueryChange(state.query, queryInput, typeaheadInput, onChange) // When the highlighted item changes, make sure the typeahead matches and format // the query text to match the case of the typeahead text - useEffect(() => { - const typeAheadValue = - state.highlighted && - hasFocus && - queryInput.current.value.length > 0 && - startsWithCaseInsensitive(state.highlighted.text, queryInput.current.value) - ? state.highlighted.text - : '' - const queryValue = formatQuery(queryInput.current.value, typeAheadValue) - - typeaheadInput.current.value = typeAheadValue - - setify(queryInput.current, queryValue) - }, [state.highlighted, hasFocus]) + useHighlight(state.highlighted, hasFocus, queryInput, typeaheadInput) // When an item is selected alter the query to match and fire applicable events useEffect(() => { @@ -125,15 +113,6 @@ export default function Container(props) { } }, [state.selected, onSelect]) - const formatQuery = (query, typeahead) => { - const formattedQuery = typeahead.substring(0, query.length) - return formattedQuery.length > 0 && - query.toLowerCase() === formattedQuery.toLowerCase() && - query !== formattedQuery - ? formattedQuery - : query - } - const onTabOrEnter = (keyPressed) => { // keyPressed must be 'enter' or 'tab' const highlightedIndex = state.highlighted && state.highlighted.index diff --git a/src/lib/components/hooks/containerEffects.js b/src/lib/components/hooks/containerEffects.js index d9060ea..be8ea20 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 startsWithCaseInsensitive from '../../utils/startsWithCaseInsensitive' export const useItemsState = (swrData) => { const { dispatch } = useContext(StateContext) @@ -23,7 +24,6 @@ export const useQueryChange = (query, queryInput, typeaheadInput, onChange) => { const value = (() => { const currentValue = typeaheadInput.current.value if (!query) return '' - //if (!queryMatchesTypeahead(query, currentValue, true)) return '' if (!currentValue.startsWith(query)) return '' return currentValue })() @@ -33,4 +33,34 @@ export const useQueryChange = (query, queryInput, typeaheadInput, onChange) => { setify(queryInput.current, query) if (typeof onChange === 'function') onChange(query) }, [query, onChange]) +} + +export const useHighlight = (highlighted, hasFocus, queryInput, typeaheadInput) => { + useEffect(() => { + const typeAheadValue = + highlighted && + hasFocus && + queryInput.current.value.length > 0 && + startsWithCaseInsensitive(highlighted.text, queryInput.current.value) + ? highlighted.text + : '' + const queryValue = formatQuery(queryInput.current.value, typeAheadValue) + + typeaheadInput.current.value = typeAheadValue + + setify(queryInput.current, queryValue) + }, [highlighted, hasFocus]) +} + +////////////////////////////// +// Helper functions // +////////////////////////////// + +export const formatQuery = (query, typeahead) => { + const formattedQuery = typeahead.substring(0, query.length) + return formattedQuery.length > 0 && + query.toLowerCase() === formattedQuery.toLowerCase() && + query !== formattedQuery + ? formattedQuery + : query } \ No newline at end of file diff --git a/src/lib/components/hooks/containerEffects.test.js b/src/lib/components/hooks/containerEffects.test.js index 1905171..0e8c472 100644 --- a/src/lib/components/hooks/containerEffects.test.js +++ b/src/lib/components/hooks/containerEffects.test.js @@ -1,8 +1,11 @@ import { vi, describe, test, expect } from 'vitest' import { renderHook } from '@testing-library/react-hooks' +import undef from '../../utils/undef' import { useAutoFocus, - useQueryChange + useQueryChange, + useHighlight, + formatQuery } from './containerEffects' let inputRef = (value = '') => ( //TODO: Put in a beforeEach when blogging @@ -73,4 +76,80 @@ describe('useQueryChange', () => { renderHook(() => useQueryChange(queryValue, queryRef, typeaheadRef, onChange)) expect(onChange).toHaveBeenCalledWith(queryValue) }) +}) + +describe('useHighlight', () => { + const queryValue = 'chi' + 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) + const typeaheadRef = inputRef() + renderHook(() => useHighlight(highlighted, true, queryRef, typeaheadRef)) + expect(queryRef.current.value).toBe('Chi') + expect(typeaheadRef.current.value).toBe(highlighted.text) + }) + + test('If there is no highlighted item, no change occurs', () => { + const queryRef = inputRef(queryValue) + const typeaheadRef = inputRef() + renderHook(() => useHighlight(undef, true, queryRef, typeaheadRef)) + expect(queryRef.current.value).toBe(queryValue) + expect(typeaheadRef.current.value).toBe('') + }) + + test('If focus is not set, no change occurs', () => { + const queryRef = inputRef(queryValue) + const typeaheadRef = inputRef() + renderHook(() => useHighlight(highlighted, false, queryRef, typeaheadRef)) + expect(queryRef.current.value).toBe(queryValue) + expect(typeaheadRef.current.value).toBe('') + }) + + test('If there is no query, no change occurs', () => { + const queryRef = inputRef() + const typeaheadRef = inputRef() + renderHook(() => useHighlight(highlighted, true, queryRef, typeaheadRef)) + expect(queryRef.current.value).toBe('') + expect(typeaheadRef.current.value).toBe('') + }) + + test('If selected text does not match typed text, no change occurs', () => { + const queryRef = inputRef('Foo') + const typeaheadRef = inputRef() + renderHook(() => useHighlight(highlighted, true, queryRef, typeaheadRef)) + expect(queryRef.current.value).toBe('Foo') + expect(typeaheadRef.current.value).toBe('') + }) +}) + +////////////////////////////// +// Helper functions // +////////////////////////////// + +describe('formatQuery', () => { + test('If query does not match typeahead, change the case', () => { + const formattedQuery = formatQuery('chi', 'Chicago') + expect(formattedQuery).toBe('Chi') + }) + + test('If there is no typeahead, return original query', () => { + const formattedQuery = formatQuery('chi', '') + expect(formattedQuery).toBe('chi') + }) + + test('If query and typeahead do not match, return original query', () => { + const formattedQuery = formatQuery('chi', 'New York') + expect(formattedQuery).toBe('chi') + }) + + test('If query is blank, return original query', () => { + const formattedQuery = formatQuery('', 'Chicago') + expect(formattedQuery).toBe('') + }) + + test('If query matches typeahead, make no change', () => { + const formattedQuery = formatQuery('Chi', 'Chicago') + expect(formattedQuery).toBe('Chi') + }) }) \ No newline at end of file