Skip to content

Commit

Permalink
Merge pull request #1577 from Adslot/fix-resize-observer-error
Browse files Browse the repository at this point in the history
fix: [useCollapse] cleanup resize observer, overflow css
  • Loading branch information
xiaofan2406 authored Mar 10, 2023
2 parents 70db3fd + 38345fd commit 115b990
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 128 deletions.
1 change: 1 addition & 0 deletions src/components/Accordion/__snapshots__/index.spec.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ exports[`<Accordion /> should have default props 1`] = `
</div>
<div
class="panel-component-content-wrapper animate"
style="height: 0px;"
>
<div
class="panel-component-content"
Expand Down
16 changes: 13 additions & 3 deletions src/components/Panel/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,29 @@ const Panel = ({ onClick, className, children, dts, icon, id, isCollapsed, title
onClick(id);
};

const { collapsed, height, containerRef } = useCollapse({
const { height, containerRef, transitionState } = useCollapse({
collapsed: isCollapsed,
transitionMs: animate ? 250 : null,
});

const classesCombined = classnames(['panel-component', { collapsed }, className]);
const classesCombined = classnames([
'panel-component',
{ collapsed: isCollapsed, [transitionState]: transitionState },
className,
]);

return (
<div data-testid="panel-wrapper" className={classesCombined} data-test-selector={dts}>
<div data-testid="panel-header" className="panel-component-header clearfix" onClick={onHeaderClick}>
{icon}
{title}
</div>
<div style={{ height }} className={classnames('panel-component-content-wrapper', { animate })}>
<div
style={{ height }}
className={classnames('panel-component-content-wrapper', {
animate,
})}
>
<div ref={containerRef} data-testid="panel-content" className="panel-component-content">
{children}
</div>
Expand Down
21 changes: 13 additions & 8 deletions src/components/Panel/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
.panel-component {
background-color: $color-white;
transition: background-color 250ms 75ms ease-out;

&.is-expanding,
&.is-collapsing {
overflow: hidden;
}
}

.panel-component ~ .panel-component {
Expand Down Expand Up @@ -38,8 +43,6 @@
}

.panel-component-content-wrapper {
overflow: hidden;

&.animate {
transition: all 250ms ease;
}
Expand All @@ -54,16 +57,18 @@
background-color: $color-grey-100;
}

.panel-component.collapsed .panel-component-header {
border-bottom: 0;
}

.panel-component.collapsed .panel-component-header::before {
transform: rotate(0);
}

.panel-component.collapsed .panel-component-content {
display: none;
.panel-component.collapsed:not(.is-expanding, .is-collapsing) {
& .panel-component-header {
border-bottom: 0;
}

& .panel-component-content {
display: none;
}
}

.panel-component hr {
Expand Down
6 changes: 3 additions & 3 deletions src/components/Paragraph/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ export interface ParagraphProps {
* A fallback maximum height for the brief content.
* This height won't be exceeded, even if props.briefCharCount isn't reached
* (e.g due to new lines in HTML)
* @default 100
* @default 200
*/
briefMaxHeight?: number;
/**
* Removes Read More button, only showing text in the current collapsed state
*/
hideReadMore?: boolean;
/**
* Control collapsed state
* initial collapsed state
*/
collapsed?: boolean;
defaultCollapsed?: boolean;
/**
* Content inside paragraph
*/
Expand Down
23 changes: 9 additions & 14 deletions src/components/Paragraph/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,13 @@ import './styles.css';

const baseClass = 'aui--paragraph';

const Paragraph = ({
briefMaxHeight = 200,
hideReadMore,
collapsed: collapsedProp = true,
className,
children,
dts,
}) => {
const Paragraph = ({ briefMaxHeight = 200, hideReadMore, defaultCollapsed = true, className, children, dts }) => {
const hideReadMoreRef = React.useRef();
const { collapsed, toggleCollapsed, height, collapsedHeightExceeded, containerRef } = useCollapse({
const [collapsed, setCollapsed] = React.useState(defaultCollapsed);

const { height, collapsedHeightExceeded, containerRef } = useCollapse({
collapsedHeight: briefMaxHeight,
collapsed: collapsedProp,
collapsed,
});

return (
Expand All @@ -43,7 +38,7 @@ const Paragraph = ({
data-testid="paragraph-read-more-button"
variant="link"
className={`${baseClass}-read-more`}
onClick={toggleCollapsed}
onClick={() => setCollapsed(!collapsed)}
>
{collapsed ? `Read More` : `Read Less`}
</Button>
Expand All @@ -62,17 +57,17 @@ Paragraph.propTypes = {
* A fallback maximum height for the brief content.
* This height won't be exceeded, even if props.briefCharCount isn't reached
* (e.g due to new lines in HTML)
* @default 100
* @default 200
*/
briefMaxHeight: PropTypes.number,
/**
* Removes Read More button, only showing text in the current collapsed state
*/
hideReadMore: PropTypes.bool,
/**
* Control collapsed state
* initial collapsed state
*/
collapsed: PropTypes.bool,
defaultCollapsed: PropTypes.bool,
/**
* Content inside paragraph
*/
Expand Down
21 changes: 8 additions & 13 deletions src/components/Paragraph/index.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { render, cleanup, fireEvent } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import Paragraph from '.';

beforeEach(() => {
jest.useFakeTimers();
});
afterEach(cleanup);

describe('<Paragraph />', () => {
Expand Down Expand Up @@ -70,6 +73,7 @@ describe('<Paragraph />', () => {
},
},
]);
jest.runAllTimers();
});

expect(queryByTestId('paragraph-wrapper')).toBeInTheDocument();
Expand Down Expand Up @@ -116,6 +120,7 @@ describe('<Paragraph />', () => {
},
},
]);
jest.runAllTimers();
});

expect(queryByTestId('paragraph-wrapper')).toBeInTheDocument();
Expand All @@ -142,6 +147,7 @@ describe('<Paragraph />', () => {
},
},
]);
jest.runAllTimers();
});

expect(queryByTestId('paragraph-wrapper')).toBeInTheDocument();
Expand All @@ -161,6 +167,7 @@ describe('<Paragraph />', () => {
},
},
]);
jest.runAllTimers();
});

expect(queryByTestId('paragraph-wrapper')).toBeInTheDocument();
Expand All @@ -175,21 +182,9 @@ describe('<Paragraph />', () => {
},
},
]);
jest.runAllTimers();
});
expect(getByTestId('expandable-content')).toHaveStyle(`height: 200px`);

act(() => {
resizeListener([
{
contentRect: {
height: 50,
},
},
]);
});

fireEvent.click(getByTestId('paragraph-read-more-button'));
expect(getByTestId('expandable-content')).toHaveStyle(`height: 50px`);
});

it('should be able to click read more button to expand paragraph', () => {
Expand Down
114 changes: 72 additions & 42 deletions src/hooks/useCollapse.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,90 @@ import React from 'react';

/**
* @param {Object} useCollapse
* @param {number} useCollapse.collapsedHeight height to collapse to (default 0)
* @param {number} useCollapse.collapsedHeight height to collapse to if the collapsed content's height is greater (default 0)
* @param {number} useCollapse.collapsed optionally control collapsed state
* @param {number} useCollapse.transitionMs transition time in ms, for transitionState
*/
export const useCollapse = ({ collapsedHeight = 0, collapsed: collapsedProp = false }) => {
export const useCollapse = ({ collapsedHeight = 0, collapsed = false, transitionMs }) => {
const containerRef = React.useRef();
const elHeightRef = React.useRef();
const collapsedHeightRef = React.useRef();

const [height, _setHeight] = React.useState();
const [collapsed, setCollapsed] = React.useState(collapsedProp);

const setHeight = React.useCallback(
(elHeight) => {
if (!containerRef.current) return;
collapsedHeightRef.current = _.isNumber(collapsedHeight) ? Math.min(collapsedHeight, elHeight) : elHeight;
if (!collapsed) {
_setHeight(elHeight);
} else {
_setHeight(collapsedHeightRef.current);
}
},
[collapsedHeight, collapsed]
);
const animationFrameRef = React.useRef();
const transitionTimerRef = React.useRef();

React.useLayoutEffect(() => {
setCollapsed(collapsedProp);
}, [collapsedProp]);
const [_height, _setHeight] = React.useState();
const [animationState, setAnimationState] = React.useState();

React.useEffect(() => {
if (!containerRef.current) return;
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// borderBoxSize is not an array in old versions of Firefox
elHeightRef.current = _.castArray(entry.borderBoxSize)[0]?.blockSize ?? entry.contentRect.height;
const collapseTo = _height && (_.isNumber(collapsedHeight) ? Math.min(collapsedHeight, _height) : _height);

const height = collapsed ? collapseTo : _height;

setHeight(elHeightRef.current);
const collapsedHeightExceeded = _.isNil(collapsedHeight) || collapseTo === collapsedHeight;

const transitionType = collapsed ? 'is-collapsing' : 'is-expanding';
const collapsedRef = React.useRef(collapsed);
const transitionState = animationState ? transitionType : '';

React.useLayoutEffect(() => {
if (collapsed !== collapsedRef.current) {
const node = containerRef.current;
if (node) {
_setHeight(node.getBoundingClientRect().height);
}
});
resizeObserver.observe(containerRef.current);
if (transitionMs) {
setAnimationState(1);
transitionTimerRef.current = setTimeout(() => {
setAnimationState(0);
}, transitionMs);
}
}
collapsedRef.current = collapsed;
}, [collapsed, transitionMs]);

React.useEffect(() => {
return () => {
resizeObserver.disconnect();
if (transitionTimerRef.current) {
window.clearTimeout(transitionTimerRef.current);
}
};
}, [setHeight]);
}, []);

const collapsedHeightExceeded = _.isNil(collapsedHeight) || collapsedHeightRef.current === collapsedHeight;
// set the height on mount, as resize observer's animation frame hasn't fired yet
React.useLayoutEffect(() => {
const node = containerRef.current;
if (node) {
_setHeight(node.getBoundingClientRect().height);
}
}, [collapsedHeight]);

const resizeObserverRef = React.useRef();

React.useLayoutEffect(() => {
if (!containerRef.current || !elHeightRef.current) return;
setHeight(elHeightRef.current);
}, [setHeight]);
if (!containerRef.current) return;
if (!resizeObserverRef.current) {
// the resize observer's job is to keep the height in sync when the
// size of the container changes
resizeObserverRef.current = new ResizeObserver((entries) => {
// see https://github.com/WICG/resize-observer/issues/38
animationFrameRef.current = window.requestAnimationFrame(() => {
for (const entry of entries) {
// borderBoxSize is not an array in old versions of Firefox
_setHeight(_.castArray(entry.borderBoxSize)[0]?.blockSize ?? entry.contentRect.height);
}
});
});
}

const toggleCollapsed = () => {
setCollapsed(!collapsed);
};
resizeObserverRef.current.observe(containerRef.current);

return { collapsed, toggleCollapsed, height, collapsedHeightExceeded, containerRef };
return () => {
animationFrameRef.current && window.cancelAnimationFrame(animationFrameRef.current);
resizeObserverRef.current && resizeObserverRef.current.disconnect();
};
}, []);

return {
height,
collapsedHeightExceeded,
containerRef,
transitionState,
};
};
Loading

0 comments on commit 115b990

Please sign in to comment.