mirror of
https://github.com/fergalmoran/turnstone.git
synced 2026-01-01 22:59:38 +00:00
Consolidate itemGroups and data props into a single listbox prop
This commit is contained in:
@@ -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
|
||||
@@ -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}
|
||||
|
||||
10
src/App.jsx
10
src/App.jsx
@@ -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'}
|
||||
|
||||
@@ -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}
|
||||
/>)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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' }))
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user