Skip to content

Commit 0133f18

Browse files
author
damfinkel
authored
Merge pull request #132 from Wolox/react-list-cmpc
react-list-cmpc
2 parents 000f9a1 + 4fc8c5b commit 0133f18

File tree

20 files changed

+616
-57
lines changed

20 files changed

+616
-57
lines changed

Diff for: cookbook-react/src/hooks/useCollapse/README.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
## useCollapse
2+
3+
Hook to handle collapsible HTML elements.
4+
5+
**Generic Value**
6+
7+
This hook requires a generic value `Element` which extends from `HTMLElement` in order to create a ref for any kind of element.
8+
9+
**Parameters**
10+
11+
| Variable | Type | Default | Description |
12+
|:---|:---|:---:|:---|
13+
| defaultIsOpen | boolean | - | Determines if the element starts collapsed or expanded |
14+
| onChange (optional) | - | (isOpen: boolean) => void | Function that executes after the collapse value changes |
15+
| animateOnCollapseEffect (optional) | boolean | true | Determines if the onChange function executes immediatly or after the collapse animation |
16+
| collapseTime | number | 300 | Time to execute the collapse animation in milliseconds |
17+
18+
**Return values**
19+
20+
| Variable | Type | Description |
21+
|:---|:---|:---:|:---|
22+
| handleCollapse | () => void | Function to collapse/expand |
23+
| collapsibleRef | Ref<Element> | Ref to pass to the parent container of the collapsible area |
24+
| collapsed | boolean | Determines if the element is collapsed or expanded |
25+
26+
**Usage**
27+
28+
```tsx
29+
import useCollapse from './hooks/useCollapse';
30+
31+
const { handleCollapse, collapsibleRef, collapsed } = useCollapse<HTMLDivElement>({
32+
defaultIsOpen: !defaultIsClosed,
33+
onChange,
34+
animateOnCollapseEffect: false,
35+
collapseTime: 250
36+
});
37+
```

Diff for: cookbook-react/src/hooks/useCollapse/cookbook.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"thumbnail": {
3+
"type": "img",
4+
"url": "https://cookbook.wolox-fearmy.vercel.app/logo192.png"
5+
}
6+
}

Diff for: cookbook-react/src/hooks/useCollapse/index.test.tsx

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
5+
import useCollapse, { CollapsibleParameters } from '.';
6+
7+
function WrapperComponent({
8+
defaultIsOpen,
9+
onChange,
10+
animateOnCollapseEffect,
11+
collapseTime
12+
}: CollapsibleParameters) {
13+
const { handleCollapse, collapsibleRef, collapsed } = useCollapse<HTMLDivElement>({
14+
defaultIsOpen,
15+
onChange,
16+
animateOnCollapseEffect,
17+
collapseTime
18+
});
19+
20+
return (
21+
<div>
22+
<span>is collapsed: {collapsed ? 'true' : 'false'}</span>
23+
<button type="button" onClick={handleCollapse} />
24+
<div ref={collapsibleRef}>
25+
<span>collapsible content</span>
26+
</div>
27+
</div>
28+
);
29+
}
30+
31+
test('returns a handler that executes the collapse logic', () => {
32+
const collapseHandler = jest.fn();
33+
render(<WrapperComponent onChange={collapseHandler} animateOnCollapseEffect={false} />);
34+
const collapseButton = screen.getByRole('button');
35+
userEvent.click(collapseButton);
36+
37+
expect(collapseHandler).toHaveBeenCalled();
38+
});
39+
40+
test('returns a handler that executes after `collapseTime` when animateOnCollapse is true', () => {
41+
jest.useFakeTimers();
42+
const collapseHandler = jest.fn();
43+
44+
render(<WrapperComponent onChange={collapseHandler} />);
45+
46+
const collapseButton = screen.getByRole('button');
47+
userEvent.click(collapseButton);
48+
49+
jest.runAllTimers();
50+
51+
expect(collapseHandler).toHaveBeenCalled();
52+
jest.runOnlyPendingTimers();
53+
jest.useRealTimers();
54+
});
55+
56+
test('collapses the element when the return handler is called', () => {
57+
render(<WrapperComponent animateOnCollapseEffect={false} />);
58+
59+
const collapseButton = screen.getByRole('button');
60+
userEvent.click(collapseButton);
61+
62+
const collapsedText = screen.getByText('is collapsed: true');
63+
expect(collapsedText).toBeVisible();
64+
});
65+
66+
test('sets the correct styles when it uncollapses', () => {
67+
render(<WrapperComponent animateOnCollapseEffect={false} />);
68+
69+
const collapseButton = screen.getByRole('button');
70+
userEvent.click(collapseButton);
71+
userEvent.click(collapseButton);
72+
73+
const collapsedText = screen.getByText('is collapsed: false');
74+
expect(collapsedText).toBeVisible();
75+
});

Diff for: cookbook-react/src/hooks/useCollapse/index.tsx

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { useCallback, useEffect, useRef, useState } from 'react';
2+
3+
const DEFAULT_COLLAPSE_TIME = 300;
4+
5+
export interface CollapsibleParameters {
6+
defaultIsOpen?: boolean;
7+
onChange?: (isOpen: boolean) => void;
8+
animateOnCollapseEffect?: boolean;
9+
collapseTime?: number;
10+
}
11+
12+
const useCollapse = <Element extends HTMLElement>({
13+
defaultIsOpen = true,
14+
onChange,
15+
animateOnCollapseEffect = true,
16+
collapseTime = DEFAULT_COLLAPSE_TIME
17+
}: CollapsibleParameters) => {
18+
const [collapsed, setCollapsed] = useState(!defaultIsOpen);
19+
const [originalHeight, setOriginalHeight] = useState(0);
20+
const collapsibleRef = useRef<Element>(null);
21+
22+
const setCollapsedStyles = (height: number, isCollapsed: boolean) => {
23+
const { style } = collapsibleRef.current!;
24+
const heightString = isCollapsed ? '0' : `${height}px`;
25+
26+
style.height = heightString;
27+
28+
style.paddingTop = isCollapsed ? '0' : '';
29+
style.paddingBottom = isCollapsed ? '0' : '';
30+
style.marginTop = isCollapsed ? '0' : '';
31+
style.marginBottom = isCollapsed ? '0' : '';
32+
};
33+
34+
const calculateHeight = useCallback(() => {
35+
const { scrollHeight } = collapsibleRef.current!;
36+
37+
const newHeight = scrollHeight ?? 0;
38+
setOriginalHeight(newHeight);
39+
}, []);
40+
41+
// Sets initial height values
42+
useEffect(() => {
43+
if (collapsibleRef.current) {
44+
collapsibleRef.current.style.transition = `height ${collapseTime}ms ease-out, padding ${collapseTime}ms ease-out, margin ${collapseTime}ms ease-out`;
45+
collapsibleRef.current.style.overflow = 'hidden';
46+
}
47+
48+
calculateHeight();
49+
}, [calculateHeight, collapseTime]);
50+
51+
useEffect(() => {
52+
// Note: There's a small bug that causes the resize not to correctly reduce the collapsible's
53+
// height when the screen is enlargened. The reason is because the scroll height does not
54+
// change because the height value is already set. The screen does not break though, the only
55+
// problem is that the collapsible element has more height than it should but it works.
56+
window.addEventListener('resize', calculateHeight);
57+
return () => {
58+
window.removeEventListener('resize', calculateHeight);
59+
};
60+
}, [calculateHeight]);
61+
62+
// Changes height values when height or collapsible state changes
63+
useEffect(() => {
64+
setCollapsedStyles(originalHeight, collapsed);
65+
}, [originalHeight, collapsed]);
66+
67+
const handleCollapse = () => {
68+
setCollapsed(!collapsed);
69+
setCollapsedStyles(originalHeight, !collapsed);
70+
71+
if (animateOnCollapseEffect) {
72+
setTimeout(() => {
73+
onChange?.(collapsed);
74+
}, collapseTime);
75+
} else {
76+
onChange?.(collapsed);
77+
}
78+
};
79+
80+
return { handleCollapse, collapsibleRef, collapsed };
81+
};
82+
83+
export default useCollapse;

Diff for: cookbook-react/src/recipes/HOCs/Spinner/components/Loading/__snapshots__/index.test.tsx.snap

-13
This file was deleted.

Diff for: cookbook-react/src/recipes/collapsibles/valparaiso/Collapsible/index.tsx

+8-34
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import React, { ReactNode, useState, useRef, useEffect } from 'react';
1+
import React, { ReactNode } from 'react';
22
import cn from 'classnames';
33

4+
import useCollapse from '../../../../hooks/useCollapse';
5+
46
import styles from './styles.module.scss';
57

68
interface CollapsibleProps {
@@ -12,37 +14,6 @@ interface CollapsibleProps {
1214
collapsibleId: string;
1315
}
1416

15-
const useCollapse = (defaultIsOpen: boolean, onCollapse?: (isOpen: boolean) => void) => {
16-
const [collapsed, setCollapsed] = useState(!defaultIsOpen);
17-
const collapsibleRef = useRef<HTMLDivElement>(null);
18-
19-
const setCollapsedValue = (height: number, isCollapsed: boolean) => {
20-
const heightString = isCollapsed ? '0px' : `${height}px`;
21-
collapsibleRef.current!.style.height = heightString;
22-
};
23-
24-
useEffect(() => {
25-
const calculateHeight = () => {
26-
const maxHeight = collapsibleRef.current?.scrollHeight ?? 0;
27-
setCollapsedValue(maxHeight, collapsed);
28-
};
29-
30-
calculateHeight();
31-
window.addEventListener('resize', calculateHeight);
32-
return () => {
33-
window.removeEventListener('resize', calculateHeight);
34-
};
35-
}, [collapsed]);
36-
37-
const handleCollapse = () => {
38-
setCollapsed(!collapsed);
39-
setCollapsedValue(collapsibleRef.current?.scrollHeight!, !collapsed);
40-
onCollapse?.(collapsed);
41-
};
42-
43-
return { handleCollapse, collapsibleRef, collapsed };
44-
};
45-
4617
function Collapsible({
4718
collapsibleId,
4819
title,
@@ -51,7 +22,10 @@ function Collapsible({
5122
children,
5223
onChange
5324
}: CollapsibleProps) {
54-
const { handleCollapse, collapsibleRef, collapsed } = useCollapse(!defaultIsClosed, onChange);
25+
const { handleCollapse, collapsibleRef, collapsed } = useCollapse<HTMLDivElement>({
26+
defaultIsOpen: !defaultIsClosed,
27+
onChange
28+
});
5529

5630
return (
5731
<div className={cn(className, styles.collapsibleContainer, 'column')}>
@@ -65,7 +39,7 @@ function Collapsible({
6539
onClick={handleCollapse}
6640
/>
6741
</div>
68-
<div id={collapsibleId} ref={collapsibleRef} className={cn(styles.content)} role="region">
42+
<div id={collapsibleId} ref={collapsibleRef} role="region">
6943
{children}
7044
</div>
7145
</div>

Diff for: cookbook-react/src/recipes/collapsibles/valparaiso/Collapsible/styles.module.scss

-6
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,6 @@ $icon-width: 18px;
1414
padding: 20px;
1515
}
1616

17-
.content {
18-
overflow: hidden;
19-
transition: height 0.3s ease-out;
20-
transform-origin: top;
21-
}
22-
2317
.title {
2418
color: $title-color;
2519
font-size: 32px;

Diff for: cookbook-react/src/recipes/collapsibles/valparaiso/__snapshots__/index.test.tsx.snap

+1-2
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ exports[`Should match snapshot 1`] = `
2121
/>
2222
</div>
2323
<div
24-
class="content"
2524
id="collapsable-example"
2625
role="region"
27-
style="height: 0px;"
26+
style="transition: height 300ms ease-out, padding 300ms ease-out, margin 300ms ease-out; overflow: hidden; height: 0px;"
2827
>
2928
<p
3029
class="m-bottom-3 text"
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"thumbnail": {
33
"type": "iframe",
4-
"url": "https://cookbook-react.now.sh/?category=collapses&component=collapse-cmpc&compact=true"
4+
"url": "https://cookbook-react.now.sh/?category=collapsibles&component=valparaiso&compact=true"
55
},
66
"detail": {
77
"type": "iframe",
8-
"url": "https://cookbook-react.now.sh/?category=collapses&component=collapse-cmpc&compact=true"
8+
"url": "https://cookbook-react.now.sh/?category=collapsibles&component=valparaiso&compact=true"
99
}
1010
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
3+
import useCollapse from '../../../../hooks/useCollapse';
4+
5+
import styles from './styles.module.scss';
6+
7+
interface ItemType {
8+
id: number;
9+
name: string;
10+
}
11+
12+
interface ItemProps {
13+
item: ItemType;
14+
onDelete: (id: number) => void;
15+
}
16+
17+
function Item({ item, onDelete }: ItemProps) {
18+
const handleDelete = () => onDelete(item.id);
19+
20+
const { handleCollapse, collapsibleRef } = useCollapse<HTMLLIElement>({ onChange: handleDelete });
21+
22+
return (
23+
<li key={item.id} ref={collapsibleRef} className={`${styles.listItem} row middle space-between`}>
24+
<div className="item-1">{item.name}</div>
25+
<button type="button" className={styles.closeIcon} onClick={handleCollapse} />
26+
</li>
27+
);
28+
}
29+
30+
export default Item;

0 commit comments

Comments
 (0)