Consolidate itemGroups and data props into a single listbox prop

This commit is contained in:
Tom Southall
2022-02-21 21:07:52 +00:00
parent ac6f749762
commit 583726fcb9
13 changed files with 107 additions and 114 deletions

View File

@@ -1,4 +1,4 @@
const defaultItemGroups = [
const defaultListbox = [
{ name: 'Recent Searches', displayField: 'name', data: [
{ name: 'New Orleans, Louisiana, United States', coords: '29.95465,-90.07507' },
{ name: 'Chicago, Illinois, United States', coords: '41.85003,-87.65005' },
@@ -17,4 +17,4 @@ const defaultItemGroups = [
}
]
export default defaultItemGroups
export default defaultListbox

View File

@@ -2,14 +2,14 @@ import React, { useCallback, useState } from 'react'
import Turnstone from '../../src/lib'
import styles from './styles/App.module.css'
import autocompleteStyles from './styles/autocomplete.module.css'
import defaultItemGroups from '../_shared/defaultItemGroups'
import defaultListbox from '../_shared/defaultListbox'
const maxItems = 10
const placeholder = 'Enter a city or airport'
const splitChar = ','
const noItemsMessage = 'We found no places that match your search'
const itemGroups = [
const listbox = [
{
name: 'Cities',
ratio: 8,
@@ -66,11 +66,11 @@ const App = () => {
autoFocus={false}
clearButton={true}
debounceWait={250}
defaultItemGroups={defaultItemGroups}
defaultItemGroupsAreImmutable={false}
defaultListbox={defaultListbox}
defaultListboxIsImmutable={false}
id='autocomplete'
itemGroups={itemGroups}
itemGroupsAreImmutable={true}
listbox={listbox}
listboxIsImmutable={true}
maxItems={maxItems}
minQueryLength={1}
noItemsMessage={noItemsMessage}

View File

@@ -6,6 +6,11 @@ const styles = {
highlightedItem: 'highlightedItem'
}
const listbox = {
data: fruits,
dataSearchType: 'startswith'
}
const App = () => {
return (
<>
@@ -13,10 +18,9 @@ const App = () => {
<Turnstone
autoFocus={true}
clearButton={true}
data={fruits}
dataSearchType={'startswith'}
debounceWait={0}
itemGroupsAreImmutable={true}
listbox={listbox}
listboxIsImmutable={true}
maxItems={10}
name={'search'}
noItemsMessage={'No matching fruit found'}

View File

@@ -12,8 +12,10 @@ describe('Integration tests', () => {
const placeholder = 'test'
render(<Turnstone
data={fruits}
dataSearchType={'startswith'}
listbox={{
data: fruits,
dataSearchType: 'startswith'
}}
placeholder={placeholder}
/>)
@@ -31,8 +33,10 @@ describe('Integration tests', () => {
render(<Turnstone
autoFocus={true}
data={fruits}
dataSearchType={'startswith'}
listbox={{
data: fruits,
dataSearchType: 'startswith'
}}
text={text}
/>)
@@ -46,8 +50,10 @@ describe('Integration tests', () => {
const text = 'pe'
render(<Turnstone
data={fruits}
dataSearchType={'startswith'}
listbox={{
data: fruits,
dataSearchType: 'startswith'
}}
text={text}
/>)
@@ -58,9 +64,11 @@ describe('Integration tests', () => {
const placeholder = 'test'
render(<Turnstone
data={fruits}
dataSearchType={'startswith'}
disabled={true}
listbox={{
data: fruits,
dataSearchType: 'startswith'
}}
placeholder={placeholder}
/>)
@@ -74,8 +82,10 @@ describe('Integration tests', () => {
const placeholder = 'test'
render(<Turnstone
data={fruits}
dataSearchType={'startswith'}
listbox={{
data: fruits,
dataSearchType: 'startswith'
}}
placeholder={placeholder}
/>)

View File

@@ -29,14 +29,11 @@ export default function Container(props) {
clearButtonAriaLabel,
clearButtonText,
debounceWait,
defaultItemGroups,
defaultItemGroupsAreImmutable,
defaultListbox,
defaultListboxIsImmutable,
disabled,
displayField,
data,
dataSearchType,
id,
itemGroupsAreImmutable,
listboxIsImmutable,
maxItems,
minQueryLength,
name,
@@ -49,18 +46,10 @@ export default function Container(props) {
tabIndex
} = props
// Destructure itemGroups prop
const {
itemGroups = [
{
name: '',
data,
dataSearchType,
displayField,
ratio: maxItems
}
]
} = props
// Destructure listbox prop
const listbox = Array.isArray(props.listbox)
? props.listbox
: [{ ...props.listbox, ...{ name: '', ratio: maxItems } }]
const listboxId = `${id}-listbox`
@@ -78,10 +67,10 @@ export default function Container(props) {
// Checks whether or not SWR data is to be treated as immutable
const isImmutable = (() => {
return itemGroupsAreImmutable &&
return listboxIsImmutable &&
!(
defaultItemGroups &&
!defaultItemGroupsAreImmutable &&
defaultListbox &&
!defaultListboxIsImmutable &&
debouncedQuery.length === 0
)
})()
@@ -90,8 +79,8 @@ export default function Container(props) {
const swrData = useData(
debouncedQuery.toLowerCase(),
isImmutable,
itemGroups,
defaultItemGroups,
listbox,
defaultListbox,
minQueryLength,
maxItems,
dispatch

View File

@@ -6,7 +6,7 @@ import { StateContextProvider } from '../context/state'
import Container from './container'
import { fruits } from '../../data'
vi.mock('./listbox', () => ({ default: () => 'Items' }))
vi.mock('./listbox', () => ({ default: () => 'Listbox' }))
vi.mock('./hooks/containerEffects', () => ({
useItemsState: vi.fn(),
useAutoFocus: vi.fn(),

View File

@@ -75,13 +75,13 @@ const swrOptions = (isImmutable) => {
: swrBaseOptions
}
export const fetcher = (query, itemGroups, defaultItemGroups, minQueryLength, maxItems) => {
if (defaultItemGroups && query.length > 0 && query.length < minQueryLength)
export const fetcher = (query, listbox, defaultListbox, minQueryLength, maxItems) => {
if (defaultListbox && query.length > 0 && query.length < minQueryLength)
return []
else if (!defaultItemGroups && query.length < minQueryLength) return []
else if (!defaultListbox && query.length < minQueryLength) return []
const groupsProp =
defaultItemGroups && !query.length ? defaultItemGroups : itemGroups
defaultListbox && !query.length ? defaultListbox : listbox
const promises = groupsProp.map((group) => {
if (typeof group.data === 'function') {
@@ -110,7 +110,7 @@ export const fetcher = (query, itemGroups, defaultItemGroups, minQueryLength, ma
})
}
const useData = (query, isImmutable, itemGroups, defaultItemGroups, minQueryLength, maxItems) => {
const useData = (query, isImmutable, listbox, defaultListbox, minQueryLength, maxItems) => {
// See: https://github.com/vercel/swr/discussions/1810
const dummyArgToEnsureCachingOfZeroLengthStrings = 'X'
@@ -120,7 +120,7 @@ const useData = (query, isImmutable, itemGroups, defaultItemGroups, minQueryLeng
query.toLowerCase(),
dummyArgToEnsureCachingOfZeroLengthStrings
],
(query) => fetcher(query, itemGroups, defaultItemGroups, minQueryLength, maxItems),
(query) => fetcher(query, listbox, defaultListbox, minQueryLength, maxItems),
swrOptions(isImmutable)
)

View File

@@ -27,7 +27,7 @@ const server = setupServer(
})
)
const apiItemGroups = [
const apiListbox = [
{
name: 'Books',
ratio: 4,
@@ -55,7 +55,7 @@ describe('Fetching API data', () => {
})
test('Returns expected results', () => {
return fetcher('L', apiItemGroups, undef, 1, 6).then(items => {
return fetcher('L', apiListbox, undef, 1, 6).then(items => {
expect(items).toEqual([
{
value: { title: 'Last Argument of Kings', author: 'Joe Abercrombie' },
@@ -97,13 +97,13 @@ describe('Fetching API data', () => {
// // Test Static data //
// ///////////////////////////////////////////
const singleItemGroup = [{
const singleGroupListbox = [{
name: '',
data: fruits,
dataSearchType: 'startswith'
}]
const multiItemGroup = [
const multiGroupListbox = [
{
name: 'Fruits',
data: fruits,
@@ -118,14 +118,14 @@ const multiItemGroup = [
describe('Fetching static data', () => {
afterEach(() => {
delete multiItemGroup[0].ratio
delete multiItemGroup[1].ratio
multiItemGroup[0].dataSearchType = 'startswith'
multiItemGroup[1].dataSearchType = 'startswith'
delete multiGroupListbox[0].ratio
delete multiGroupListbox[1].ratio
multiGroupListbox[0].dataSearchType = 'startswith'
multiGroupListbox[1].dataSearchType = 'startswith'
})
test('Returns expected results for a single group', () => {
return fetcher('Pe', singleItemGroup, undef, 1, 10).then(items => {
return fetcher('Pe', singleGroupListbox, undef, 1, 10).then(items => {
expect(items).toEqual([
{ value: 'Peach', text: 'Peach', groupIndex: 0, groupName: '' },
{ value: 'Pear', text: 'Pear', groupIndex: 0, groupName: '' }
@@ -134,7 +134,7 @@ describe('Fetching static data', () => {
})
test('Returns expected results for multiple groups with equal ratios', () => {
return fetcher('P', multiItemGroup, undef, undef, 10).then(items => {
return fetcher('P', multiGroupListbox, undef, undef, 10).then(items => {
expect(items).toEqual([
{ value: 'Peach', text: 'Peach', groupIndex: 0, groupName: 'Fruits' },
{ value: 'Pear', text: 'Pear', groupIndex: 0, groupName: 'Fruits' },
@@ -151,7 +151,7 @@ describe('Fetching static data', () => {
})
test('Returns expected results for multiple groups limited to 6 results', () => {
return fetcher('P', multiItemGroup, undef, 1, 6).then(items => {
return fetcher('P', multiGroupListbox, undef, 1, 6).then(items => {
expect(items).toEqual([
{ value: 'Peach', text: 'Peach', groupIndex: 0, groupName: 'Fruits' },
{ value: 'Pear', text: 'Pear', groupIndex: 0, groupName: 'Fruits' },
@@ -164,10 +164,10 @@ describe('Fetching static data', () => {
})
test('Returns expected ratios for multiple groups', () => {
multiItemGroup[0].ratio = 4
multiItemGroup[0].ratio = 2
multiGroupListbox[0].ratio = 4
multiGroupListbox[0].ratio = 2
return fetcher('P', multiItemGroup, undef, 1, 6).then(items => {
return fetcher('P', multiGroupListbox, undef, 1, 6).then(items => {
expect(items).toEqual([
{ value: 'Peach', text: 'Peach', groupIndex: 0, groupName: 'Fruits' },
{ value: 'Pear', text: 'Pear', groupIndex: 0, groupName: 'Fruits' },
@@ -180,10 +180,10 @@ describe('Fetching static data', () => {
})
test('Returns results containing query', () => {
multiItemGroup[0].dataSearchType = 'contains'
multiItemGroup[1].dataSearchType = 'contains'
multiGroupListbox[0].dataSearchType = 'contains'
multiGroupListbox[1].dataSearchType = 'contains'
return fetcher('Pe', multiItemGroup, undef, 1, 6).then(items => {
return fetcher('Pe', multiGroupListbox, undef, 1, 6).then(items => {
expect(items).toEqual([
{ value: 'Bartlett pear', text: 'Bartlett pear', groupIndex: 0, groupName: 'Fruits' },
{ value: 'Cantaloupe', text: 'Cantaloupe', groupIndex: 0, groupName: 'Fruits' },

View File

@@ -3,7 +3,7 @@ import renderer from 'react-test-renderer'
import { vi, describe, expect, test } from 'vitest'
import { StateContextProvider } from '../context/state'
import Listbox from './listBox'
import Listbox from './listbox'
vi.mock('./item', () => ({ default: () => 'Item' }))
vi.mock('./item', () => ({ default: () => 'ItemFirst' }))

View File

@@ -3,7 +3,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import { StateContextProvider } from './context/state'
import isUndefined from './utils/isUndefined'
import Container from './components/container'
const randomId = () => `turnstone-${(0|Math.random()*6.04e7).toString(36)}`
@@ -15,10 +14,10 @@ const propDefaults = {
clearButtonAriaLabel: 'Clear contents',
clearButtonText: '\u00d7',
debounceWait: 250,
defaultItemGroupsAreImmutable: true,
defaultListboxIsImmutable: true,
disabled: false,
id: randomId(),
itemGroupsAreImmutable: true,
listboxIsImmutable: true,
maxItems: 10,
minQueryLength: 1,
placeholder: ''
@@ -38,51 +37,42 @@ export default function Turnstone(props) {
// Prop validation //
//////////////////////////////////////////////////////
const msgBothRequired = `Either a "data" prop or an "itemGroups" prop must be provided. Both are missing.`
const msgOneOnly = `Both a "data" prop and an "itemGroups" prop were provided. Provide one only.`
const requiredPropsAreMissing = (props) => isUndefined(props.data) && isUndefined(props.itemGroups)
const dataSearchTypes = ['startswith', 'contains']
const listboxRules = PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.exact({
data: PropTypes.oneOfType([
PropTypes.func,
PropTypes.array
]).isRequired,
dataSearchType: PropTypes.oneOf(dataSearchTypes),
displayField: PropTypes.string,
name: PropTypes.string.isRequired,
ratio: PropTypes.number
})),
PropTypes.exact({
data: PropTypes.oneOfType([
PropTypes.func,
PropTypes.array
]).isRequired,
dataSearchType: PropTypes.oneOf(dataSearchTypes),
displayField: PropTypes.string
})
])
Turnstone.propTypes = {
autoFocus: PropTypes.bool,
clearButton: PropTypes.bool,
clearButtonAriaLabel: PropTypes.string,
clearButtonText: PropTypes.string,
data: (props) => {
if(requiredPropsAreMissing(props)) return new Error(msgBothRequired)
if(!isUndefined(props.data) && !isUndefined(props.itemGroups)) return new Error(msgOneOnly)
PropTypes.checkPropTypes(
{data: PropTypes.oneOfType([PropTypes.array, PropTypes.func])},
{data: props.data},
'prop', 'Turnstone'
)
},
dataSearchType: PropTypes.oneOf(['startswith', 'contains']),
debounceWait: PropTypes.number,
defaultItemGroups: PropTypes.array,
defaultItemGroupsAreImmutable: PropTypes.bool,
defaultListbox: listboxRules,
defaultListboxIsImmutable: PropTypes.bool,
disabled: PropTypes.bool,
displayField: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
id: PropTypes.string,
itemGroups: (props) => {
if(requiredPropsAreMissing(props)) return new Error(msgBothRequired)
if(!isUndefined(props.data) && !isUndefined(props.itemGroups)) return new Error(msgOneOnly)
PropTypes.checkPropTypes(
{
itemGroups: PropTypes.arrayOf(PropTypes.exact({
name: PropTypes.string.isRequired,
data: PropTypes.oneOfType([
PropTypes.func,
PropTypes.array
]).isRequired,
dataSearchType: PropTypes.oneOf(['startswith', 'contains']),
displayField: PropTypes.string,
ratio: PropTypes.number
}))
},
{itemGroups: props.itemGroups},
'prop', 'Turnstone'
)
},
itemGroupsAreImmutable: PropTypes.bool,
listbox: listboxRules.isRequired,
listboxIsImmutable: PropTypes.bool,
minQueryLength: (props) => {
PropTypes.checkPropTypes(
{minQueryLength: PropTypes.number},

View File

@@ -35,9 +35,9 @@ describe('Turnstone', () => {
clearButtonAriaLabel: 'Clear contents',
clearButtonText: '×',
debounceWait: 250,
defaultItemGroupsAreImmutable: true,
defaultListboxIsImmutable: true,
disabled: false,
itemGroupsAreImmutable: true,
listboxIsImmutable: true,
maxItems: 10,
minQueryLength: 1,
placeholder: '',

View File

@@ -21,7 +21,7 @@ const reducer = (state, action) => {
newState.itemsLoaded = false
// Allow listbox if there is no query and we have default items to show
if(action.query.length === 0 && state.props.defaultItemGroups)
if(action.query.length === 0 && state.props.defaultListbox)
newState.itemsLoaded = true
return newState

View File

@@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest'
import reducer from './reducer'
import * as actions from '../actions/actions'
import undef from '../utils/undef'
import defaultItemGroups from '../../../examples/_shared/defaultItemGroups'
import defaultListbox from '../../../examples/_shared/defaultListbox'
describe('SET_QUERY action', () => {
test('produces expected new state', () => {
@@ -69,7 +69,7 @@ describe('SET_QUERY action', () => {
const state = {
props: {
minQueryLength: 1,
defaultItemGroups
defaultListbox
}
}
@@ -81,7 +81,7 @@ describe('SET_QUERY action', () => {
itemsLoaded: true,
props: {
minQueryLength: 1,
defaultItemGroups
defaultListbox
}
})
})