Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 106 additions & 1 deletion src/components/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,22 @@ describe('Accordion component', () => {
},
]

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

// The last expanded item "wins" if multiple new items have expanded:true
expect(getByTestId(`accordionItem_${testItems[0].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${testItems[1].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${testItems[2].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${testItems[3].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${testItems[4].id}`)).toBeVisible()
})

it('shows all expanded items when multiselectable is true', () => {
const { getByTestId } = render(
<Accordion multiselectable items={testExpandedItems} />
)

expect(getByTestId(`accordionItem_${testItems[0].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${testItems[1].id}`)).toBeVisible()
expect(getByTestId(`accordionItem_${testItems[2].id}`)).not.toBeVisible()
Expand Down Expand Up @@ -399,4 +412,96 @@ describe('Accordion component', () => {
expect(customToggleFunction).toHaveBeenCalledOnce()
})
})

describe('when new items are added', () => {
let oldItems: AccordionItemProps[]
let newItems: AccordionItemProps[]

beforeEach(() => {
oldItems = testItems.slice(0, 2).map((item) => ({ ...item }))
newItems = testItems.slice(2).map((item) => ({ ...item }))
})

it('renders new items', () => {
const { getByTestId, rerender } = render(<Accordion items={oldItems} />)
rerender(<Accordion items={[...oldItems, ...newItems]} />)
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).toBeInTheDocument()
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).toBeInTheDocument()
expect(getByTestId(`accordionItem_${newItems[0].id}`)).toBeInTheDocument()
expect(getByTestId(`accordionItem_${newItems[1].id}`)).toBeInTheDocument()
expect(getByTestId(`accordionItem_${newItems[2].id}`)).toBeInTheDocument()
})

describe('when multiselectable is false', () => {
it('maintains existing expansion if new unexpanded items are added', () => {
const { getByText, getByTestId, rerender } = render(
<Accordion items={oldItems} />
)
fireEvent.click(getByText(oldItems[1].title as string))

rerender(<Accordion items={[...oldItems, ...newItems]} />)
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).toBeVisible()
expect(getByTestId(`accordionItem_${newItems[0].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${newItems[1].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${newItems[2].id}`)).not.toBeVisible()
})

it('collapses existing expansion if new expanded items are added', () => {
const { getByText, getByTestId, rerender } = render(
<Accordion items={oldItems} />
)
fireEvent.click(getByText(oldItems[1].title as string))

// The last expanded item "wins" if multiple new items have expanded:true
newItems[0].expanded = true
newItems[1].expanded = true
rerender(<Accordion items={[...oldItems, ...newItems]} />)
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${newItems[0].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${newItems[1].id}`)).toBeVisible()
expect(getByTestId(`accordionItem_${newItems[2].id}`)).not.toBeVisible()
})
})

describe('when multiselectable is true', () => {
it('maintains existing expansions if new unexpanded items are added', () => {
const { getByText, getByTestId, rerender } = render(
<Accordion multiselectable items={oldItems} />
)
fireEvent.click(getByText(oldItems[0].title as string))
fireEvent.click(getByText(oldItems[1].title as string))

rerender(
<Accordion multiselectable items={[...oldItems, ...newItems]} />
)
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).toBeVisible()
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).toBeVisible()
expect(getByTestId(`accordionItem_${newItems[0].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${newItems[1].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${newItems[2].id}`)).not.toBeVisible()
})

it('maintains existing expansions if new expanded items are added', () => {
const { getByText, getByTestId, rerender } = render(
<Accordion multiselectable items={oldItems} />
)
fireEvent.click(getByText(oldItems[0].title as string))
fireEvent.click(getByText(oldItems[1].title as string))

// The last expanded item "wins" if multiple new items have expanded:true
newItems[1].expanded = true
newItems[2].expanded = true
rerender(
<Accordion multiselectable items={[...oldItems, ...newItems]} />
)
expect(getByTestId(`accordionItem_${oldItems[0].id}`)).toBeVisible()
expect(getByTestId(`accordionItem_${oldItems[1].id}`)).toBeVisible()
expect(getByTestId(`accordionItem_${newItems[0].id}`)).not.toBeVisible()
expect(getByTestId(`accordionItem_${newItems[1].id}`)).toBeVisible()
expect(getByTestId(`accordionItem_${newItems[2].id}`)).toBeVisible()
})
})
})
})
57 changes: 41 additions & 16 deletions src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,44 @@ export const AccordionItem = ({
)
}

function buildExpansions(
items: AccordionItemProps[],
multiselectable: boolean,
savedExpansions = new Map<string, boolean | undefined>()
) {
const lastExpandedItem = multiselectable
? undefined
: items.findLast((item) => item.expanded || savedExpansions.get(item.id))
return items.reduce((map, item) => {
map.set(
item.id,
multiselectable
? (savedExpansions.get(item.id) ?? item.expanded)
: !!lastExpandedItem && item.id === lastExpandedItem.id
)
return map
}, new Map<string, boolean | undefined>())
}

export const Accordion = ({
bordered,
items,
className,
multiselectable = false,
}: AccordionProps): JSX.Element => {
const [openItems, setOpenState] = useState(
items.filter((i) => !!i.expanded).map((i) => i.id)
const [savedExpansions, setSavedExpansions] = useState(() =>
buildExpansions(items, multiselectable)
)

// Update saved expansions with new items as the appear
const [prevItems, setPrevItems] = useState(items)
if (items !== prevItems) {
setPrevItems(items)
setSavedExpansions((prevExpansions) =>
buildExpansions(items, multiselectable, prevExpansions)
)
}

const classes = classnames(
'usa-accordion',
{
Expand All @@ -81,21 +109,18 @@ export const Accordion = ({
)

const toggleItem = (itemId: AccordionItemProps['id']): void => {
const newOpenItems = [...openItems]
const itemIndex = openItems.indexOf(itemId)
const isMultiselectable = multiselectable

if (itemIndex > -1) {
newOpenItems.splice(itemIndex, 1)
} else {
if (isMultiselectable) {
newOpenItems.push(itemId)
setSavedExpansions((prevExpansions) => {
const updatedExpansions = new Map(prevExpansions)
if (updatedExpansions.get(itemId)) {
updatedExpansions.set(itemId, false)
} else {
newOpenItems.splice(0, newOpenItems.length)
newOpenItems.push(itemId)
if (!multiselectable) {
updatedExpansions.forEach((_val, key, map) => map.set(key, false))
}
updatedExpansions.set(itemId, true)
}
}
setOpenState(newOpenItems)
return updatedExpansions
})
}

return (
Expand All @@ -107,7 +132,7 @@ export const Accordion = ({
<AccordionItem
key={item.id}
{...item}
expanded={openItems.indexOf(item.id) > -1}
expanded={savedExpansions.get(item.id) ?? false}
handleToggle={(e): void => {
if (item.handleToggle) item.handleToggle(e)
toggleItem(item.id)
Expand Down