Skip to content

Commit ecfabc9

Browse files
Add useExpansionToggle hook (#957)
Add useExpansionToggle react hook that allows components to centrally manage the expanded state of various items in a list (logic borrowed from workflow-history's useEventExpansionToggle hook).
1 parent b213011 commit ecfabc9

File tree

4 files changed

+221
-0
lines changed

4 files changed

+221
-0
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { renderHook, act } from '@/test-utils/rtl';
2+
3+
import useExpansionToggle from '../use-expansion-toggle';
4+
5+
describe('useExpansionToggle', () => {
6+
const mockItems = ['1', '2', '3'];
7+
8+
it('should initialize with provided initialState as true (all items expanded)', () => {
9+
const { result } = renderHook(() =>
10+
useExpansionToggle({
11+
initialState: true,
12+
items: mockItems,
13+
})
14+
);
15+
expect(result.current.expandedItems).toBe(true);
16+
});
17+
18+
it('should initialize with provided initialState as an object with specific items expanded', () => {
19+
const { result } = renderHook(() =>
20+
useExpansionToggle({
21+
initialState: { '1': true, '2': false },
22+
items: mockItems,
23+
})
24+
);
25+
expect(result.current.expandedItems).toEqual({ '1': true, '2': false });
26+
});
27+
28+
it('should toggle all items expansion state', () => {
29+
const { result } = renderHook(() =>
30+
useExpansionToggle({
31+
initialState: { '1': true, '2': false },
32+
items: mockItems,
33+
})
34+
);
35+
36+
act(() => {
37+
result.current.toggleAreAllItemsExpanded();
38+
});
39+
expect(result.current.expandedItems).toBe(true);
40+
41+
act(() => {
42+
result.current.toggleAreAllItemsExpanded();
43+
});
44+
expect(result.current.expandedItems).toEqual({});
45+
});
46+
47+
it('should expand a specific item when toggled', () => {
48+
const { result } = renderHook(() =>
49+
useExpansionToggle({
50+
initialState: {},
51+
items: mockItems,
52+
})
53+
);
54+
55+
act(() => {
56+
result.current.toggleIsItemExpanded('1');
57+
});
58+
expect(result.current.expandedItems).toEqual({ '1': true });
59+
});
60+
61+
it('should collapse a specific item when toggled if already expanded', () => {
62+
const { result } = renderHook(() =>
63+
useExpansionToggle({
64+
initialState: { '1': true },
65+
items: mockItems,
66+
})
67+
);
68+
69+
act(() => {
70+
result.current.toggleIsItemExpanded('1');
71+
});
72+
expect(result.current.expandedItems).toEqual({});
73+
});
74+
75+
it('should collapse only the toggled item when all items are expanded', () => {
76+
const { result } = renderHook(() =>
77+
useExpansionToggle({
78+
initialState: true,
79+
items: mockItems,
80+
})
81+
);
82+
83+
act(() => {
84+
result.current.toggleIsItemExpanded('1');
85+
});
86+
87+
expect(result.current.expandedItems).toEqual({
88+
'2': true,
89+
'3': true,
90+
});
91+
});
92+
93+
it('should expand all items when each item has been individually expanded', () => {
94+
const { result } = renderHook(() =>
95+
useExpansionToggle({
96+
initialState: {},
97+
items: mockItems,
98+
})
99+
);
100+
101+
act(() => {
102+
result.current.toggleIsItemExpanded('1');
103+
result.current.toggleIsItemExpanded('2');
104+
result.current.toggleIsItemExpanded('3');
105+
});
106+
107+
expect(result.current.expandedItems).toBe(true);
108+
});
109+
110+
it('should check if an item is expanded using getIsItemExpanded', () => {
111+
const { result } = renderHook(() =>
112+
useExpansionToggle({
113+
initialState: { '1': true, '2': false },
114+
items: mockItems,
115+
})
116+
);
117+
118+
expect(result.current.getIsItemExpanded('1')).toBe(true);
119+
expect(result.current.getIsItemExpanded('2')).toBe(false);
120+
expect(result.current.getIsItemExpanded('3')).toBe(false);
121+
});
122+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useCallback, useState } from 'react';
2+
3+
import {
4+
type UseExpansionToggleReturn,
5+
type Props,
6+
type ExpansionState,
7+
} from './use-expansion-toggle.types';
8+
9+
export default function useExpansionToggle<T extends string>({
10+
items,
11+
initialState,
12+
}: Props<T>): UseExpansionToggleReturn<T> {
13+
const [expandedItems, setExpandedItems] =
14+
useState<ExpansionState<T>>(initialState);
15+
16+
const areAllItemsExpanded = expandedItems === true;
17+
18+
const toggleAreAllItemsExpanded = useCallback(() => {
19+
setExpandedItems((prev) =>
20+
prev === true ? ({} as ExpansionState<T>) : true
21+
);
22+
}, []);
23+
24+
const getIsItemExpanded = useCallback(
25+
(item: T) => {
26+
if (expandedItems === true) {
27+
return true;
28+
}
29+
30+
return Boolean(expandedItems[item]);
31+
},
32+
[expandedItems]
33+
);
34+
35+
const toggleIsItemExpanded = useCallback(
36+
(item: T) => {
37+
setExpandedItems((prev) => {
38+
let newState: Record<T, boolean>;
39+
if (prev === true) {
40+
newState = items.reduce(
41+
(result, i) => {
42+
if (i !== item) {
43+
result[i] = true;
44+
}
45+
return result;
46+
},
47+
{} as Record<T, boolean>
48+
);
49+
} else {
50+
if (prev[item] === true) {
51+
newState = prev;
52+
delete newState[item];
53+
} else {
54+
newState = {
55+
...prev,
56+
[item]: true,
57+
};
58+
}
59+
}
60+
if (items.every((item) => newState[item])) {
61+
return true;
62+
}
63+
return newState;
64+
});
65+
},
66+
[items]
67+
);
68+
69+
return {
70+
expandedItems,
71+
setExpandedItems,
72+
73+
areAllItemsExpanded,
74+
toggleAreAllItemsExpanded,
75+
76+
getIsItemExpanded,
77+
toggleIsItemExpanded,
78+
};
79+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { type Dispatch, type SetStateAction } from 'react';
2+
3+
export type ExpansionState<T extends string> = Record<T, boolean> | true;
4+
5+
export type Props<T extends string> = {
6+
initialState: ExpansionState<T>;
7+
items: Array<T>;
8+
};
9+
10+
export type UseExpansionToggleReturn<T extends string> = {
11+
expandedItems: ExpansionState<T>;
12+
setExpandedItems: Dispatch<SetStateAction<ExpansionState<T>>>;
13+
14+
areAllItemsExpanded: boolean;
15+
toggleAreAllItemsExpanded: () => void;
16+
17+
getIsItemExpanded: (item: T) => boolean;
18+
toggleIsItemExpanded: (item: T) => void;
19+
};

src/views/workflow-history/hooks/use-event-expansion-toggle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type ToggleIsExpandAllEvents,
1212
} from './use-event-expansion-toggle.types';
1313

14+
// TODO @adhitya.mamallan - replace this with the generic useExpansionToggle hook
1415
export default function useEventExpansionToggle({
1516
initialState = {},
1617
visibleEvents,

0 commit comments

Comments
 (0)