Skip to content

Commit dc8bf86

Browse files
feat: Update Accordion expansion behavior (#3262)
Co-authored-by: Brandon Lenz <15805554+brandonlenz@users.noreply.github.com>
1 parent d1d8f55 commit dc8bf86

File tree

2 files changed

+147
-17
lines changed

2 files changed

+147
-17
lines changed

src/components/Accordion/Accordion.test.tsx

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,22 @@ describe('Accordion component', () => {
279279
},
280280
]
281281

282-
it('shows the expanded items by default', () => {
282+
it('shows one expanded item when multiselectable is false', () => {
283283
const { getByTestId } = render(<Accordion items={testExpandedItems} />)
284284

285+
// The last expanded item "wins" if multiple new items have expanded:true
286+
expect(getByTestId(`accordionItem_${testItems[0].id}`)).not.toBeVisible()
287+
expect(getByTestId(`accordionItem_${testItems[1].id}`)).not.toBeVisible()
288+
expect(getByTestId(`accordionItem_${testItems[2].id}`)).not.toBeVisible()
289+
expect(getByTestId(`accordionItem_${testItems[3].id}`)).not.toBeVisible()
290+
expect(getByTestId(`accordionItem_${testItems[4].id}`)).toBeVisible()
291+
})
292+
293+
it('shows all expanded items when multiselectable is true', () => {
294+
const { getByTestId } = render(
295+
<Accordion multiselectable items={testExpandedItems} />
296+
)
297+
285298
expect(getByTestId(`accordionItem_${testItems[0].id}`)).not.toBeVisible()
286299
expect(getByTestId(`accordionItem_${testItems[1].id}`)).toBeVisible()
287300
expect(getByTestId(`accordionItem_${testItems[2].id}`)).not.toBeVisible()
@@ -399,4 +412,96 @@ describe('Accordion component', () => {
399412
expect(customToggleFunction).toHaveBeenCalledOnce()
400413
})
401414
})
415+
416+
describe('when new items are added', () => {
417+
let oldItems: AccordionItemProps[]
418+
let newItems: AccordionItemProps[]
419+
420+
beforeEach(() => {
421+
oldItems = testItems.slice(0, 2).map((item) => ({ ...item }))
422+
newItems = testItems.slice(2).map((item) => ({ ...item }))
423+
})
424+
425+
it('renders new items', () => {
426+
const { getByTestId, rerender } = render(<Accordion items={oldItems} />)
427+
rerender(<Accordion items={[...oldItems, ...newItems]} />)
428+
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).toBeInTheDocument()
429+
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).toBeInTheDocument()
430+
expect(getByTestId(`accordionItem_${newItems[0].id}`)).toBeInTheDocument()
431+
expect(getByTestId(`accordionItem_${newItems[1].id}`)).toBeInTheDocument()
432+
expect(getByTestId(`accordionItem_${newItems[2].id}`)).toBeInTheDocument()
433+
})
434+
435+
describe('when multiselectable is false', () => {
436+
it('maintains existing expansion if new unexpanded items are added', () => {
437+
const { getByText, getByTestId, rerender } = render(
438+
<Accordion items={oldItems} />
439+
)
440+
fireEvent.click(getByText(oldItems[1].title as string))
441+
442+
rerender(<Accordion items={[...oldItems, ...newItems]} />)
443+
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).not.toBeVisible()
444+
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).toBeVisible()
445+
expect(getByTestId(`accordionItem_${newItems[0].id}`)).not.toBeVisible()
446+
expect(getByTestId(`accordionItem_${newItems[1].id}`)).not.toBeVisible()
447+
expect(getByTestId(`accordionItem_${newItems[2].id}`)).not.toBeVisible()
448+
})
449+
450+
it('collapses existing expansion if new expanded items are added', () => {
451+
const { getByText, getByTestId, rerender } = render(
452+
<Accordion items={oldItems} />
453+
)
454+
fireEvent.click(getByText(oldItems[1].title as string))
455+
456+
// The last expanded item "wins" if multiple new items have expanded:true
457+
newItems[0].expanded = true
458+
newItems[1].expanded = true
459+
rerender(<Accordion items={[...oldItems, ...newItems]} />)
460+
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).not.toBeVisible()
461+
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).not.toBeVisible()
462+
expect(getByTestId(`accordionItem_${newItems[0].id}`)).not.toBeVisible()
463+
expect(getByTestId(`accordionItem_${newItems[1].id}`)).toBeVisible()
464+
expect(getByTestId(`accordionItem_${newItems[2].id}`)).not.toBeVisible()
465+
})
466+
})
467+
468+
describe('when multiselectable is true', () => {
469+
it('maintains existing expansions if new unexpanded items are added', () => {
470+
const { getByText, getByTestId, rerender } = render(
471+
<Accordion multiselectable items={oldItems} />
472+
)
473+
fireEvent.click(getByText(oldItems[0].title as string))
474+
fireEvent.click(getByText(oldItems[1].title as string))
475+
476+
rerender(
477+
<Accordion multiselectable items={[...oldItems, ...newItems]} />
478+
)
479+
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).toBeVisible()
480+
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).toBeVisible()
481+
expect(getByTestId(`accordionItem_${newItems[0].id}`)).not.toBeVisible()
482+
expect(getByTestId(`accordionItem_${newItems[1].id}`)).not.toBeVisible()
483+
expect(getByTestId(`accordionItem_${newItems[2].id}`)).not.toBeVisible()
484+
})
485+
486+
it('maintains existing expansions if new expanded items are added', () => {
487+
const { getByText, getByTestId, rerender } = render(
488+
<Accordion multiselectable items={oldItems} />
489+
)
490+
fireEvent.click(getByText(oldItems[0].title as string))
491+
fireEvent.click(getByText(oldItems[1].title as string))
492+
493+
// The last expanded item "wins" if multiple new items have expanded:true
494+
newItems[1].expanded = true
495+
newItems[2].expanded = true
496+
rerender(
497+
<Accordion multiselectable items={[...oldItems, ...newItems]} />
498+
)
499+
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).toBeVisible()
500+
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).toBeVisible()
501+
expect(getByTestId(`accordionItem_${newItems[0].id}`)).not.toBeVisible()
502+
expect(getByTestId(`accordionItem_${newItems[1].id}`)).toBeVisible()
503+
expect(getByTestId(`accordionItem_${newItems[2].id}`)).toBeVisible()
504+
})
505+
})
506+
})
402507
})

src/components/Accordion/Accordion.tsx

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,44 @@ export const AccordionItem = ({
6262
)
6363
}
6464

65+
function buildExpansions(
66+
items: AccordionItemProps[],
67+
multiselectable: boolean,
68+
savedExpansions = new Map<string, boolean | undefined>()
69+
) {
70+
const lastExpandedItem = multiselectable
71+
? undefined
72+
: items.findLast((item) => item.expanded || savedExpansions.get(item.id))
73+
return items.reduce((map, item) => {
74+
map.set(
75+
item.id,
76+
multiselectable
77+
? (savedExpansions.get(item.id) ?? item.expanded)
78+
: !!lastExpandedItem && item.id === lastExpandedItem.id
79+
)
80+
return map
81+
}, new Map<string, boolean | undefined>())
82+
}
83+
6584
export const Accordion = ({
6685
bordered,
6786
items,
6887
className,
6988
multiselectable = false,
7089
}: AccordionProps): JSX.Element => {
71-
const [openItems, setOpenState] = useState(
72-
items.filter((i) => !!i.expanded).map((i) => i.id)
90+
const [savedExpansions, setSavedExpansions] = useState(() =>
91+
buildExpansions(items, multiselectable)
7392
)
7493

94+
// Update saved expansions with new items as the appear
95+
const [prevItems, setPrevItems] = useState(items)
96+
if (items !== prevItems) {
97+
setPrevItems(items)
98+
setSavedExpansions((prevExpansions) =>
99+
buildExpansions(items, multiselectable, prevExpansions)
100+
)
101+
}
102+
75103
const classes = classnames(
76104
'usa-accordion',
77105
{
@@ -81,21 +109,18 @@ export const Accordion = ({
81109
)
82110

83111
const toggleItem = (itemId: AccordionItemProps['id']): void => {
84-
const newOpenItems = [...openItems]
85-
const itemIndex = openItems.indexOf(itemId)
86-
const isMultiselectable = multiselectable
87-
88-
if (itemIndex > -1) {
89-
newOpenItems.splice(itemIndex, 1)
90-
} else {
91-
if (isMultiselectable) {
92-
newOpenItems.push(itemId)
112+
setSavedExpansions((prevExpansions) => {
113+
const updatedExpansions = new Map(prevExpansions)
114+
if (updatedExpansions.get(itemId)) {
115+
updatedExpansions.set(itemId, false)
93116
} else {
94-
newOpenItems.splice(0, newOpenItems.length)
95-
newOpenItems.push(itemId)
117+
if (!multiselectable) {
118+
updatedExpansions.forEach((_val, key, map) => map.set(key, false))
119+
}
120+
updatedExpansions.set(itemId, true)
96121
}
97-
}
98-
setOpenState(newOpenItems)
122+
return updatedExpansions
123+
})
99124
}
100125

101126
return (
@@ -107,7 +132,7 @@ export const Accordion = ({
107132
<AccordionItem
108133
key={item.id}
109134
{...item}
110-
expanded={openItems.indexOf(item.id) > -1}
135+
expanded={savedExpansions.get(item.id) ?? false}
111136
handleToggle={(e): void => {
112137
if (item.handleToggle) item.handleToggle(e)
113138
toggleItem(item.id)

0 commit comments

Comments
 (0)