Skip to content

fix(stickup): fixes crashes in Suspense environment #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
140 changes: 77 additions & 63 deletions lib/Sticky.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ interface IOwnProps extends IStickyComponentProps {
experimentalNative?: boolean;
}

interface IProps extends IOwnProps, IStickyInjectedProps {}
interface IProps extends IOwnProps, IStickyInjectedProps { }

interface IState {
isSticky: boolean;
Expand All @@ -66,7 +66,7 @@ interface IState {
}

interface ILayoutSnapshot {
stickyRect: IRect;
stickyRect?: IRect;
containerRect: IRect;
}

Expand Down Expand Up @@ -106,35 +106,37 @@ class Sticky extends React.PureComponent<IProps, IState> {
return Boolean(this.props.container);
};

isNearToViewport = (rect: IRect): boolean => {
isNearToViewport = (rect?: IRect): boolean => {
const padding = 700;
return rect.top - padding < 0 && rect.bottom + padding > 0;
return (rect?.top || 0) - padding < 0 && (rect?.bottom || 0) + padding > 0;
};

getOverflowScrollType = (
rectSticky: IRect,
dimensions: IDimensions,
{ rectSticky, dimensions }: {
rectSticky?: IRect,
dimensions: IDimensions,
}
): OverflowScrollType => {
return this.props.overflowScroll === 'flow' &&
this.calcHeightDifference(rectSticky, dimensions) > 0
this.calcHeightDifference({ rectSticky, dimensions }) > 0
? 'flow'
: 'end';
};

isSticky = (rect: IRect, containerRect: IRect, dimensions: IDimensions) => {
isSticky = ({ rect, containerRect, dimensions }: { rect?: IRect, containerRect?: IRect, dimensions: IDimensions }) => {
if (!this.hasContainer()) {
return Math.round(containerRect.top) <= this.offsetTop;
return Math.round(containerRect?.top || 0) <= this.offsetTop;
}

if (Math.round(containerRect.top) > this.offsetTop) {
if (Math.round(containerRect?.top || 0) > this.offsetTop) {
return false;
}

const height =
this.props.overflowScroll === 'flow'
? Math.min(rect.height, dimensions.height)
: rect.height;
if (Math.round(containerRect.bottom) - this.offsetTop < height) {
? Math.min(rect?.height || 0, dimensions.height)
: rect?.height || 0;
if (Math.round(containerRect?.bottom || 0) - this.offsetTop < height) {
return false;
}

Expand All @@ -154,8 +156,8 @@ class Sticky extends React.PureComponent<IProps, IState> {
if (
process.env.NODE_ENV !== 'production' &&
!this.nativeStickyThrewOnce &&
(this.placeholderRef && this.placeholderRef.current.parentElement) !==
(this.props.container && this.props.container.current)
(this.placeholderRef && this.placeholderRef.current?.parentElement) !==
(this.props.container && this.props.container.current)
) {
console.warn(
'react-stickup: a sticky element was used with property `experimentalNative` but its `container` is not the parent the sticky component. As the native sticky implementation always uses its parent element as the container. This can lead to unexpected results. It is therefore recommended to change the DOM structure so that the container is a direct parent of the Sticky component or to remove the `experimentalNative` property.',
Expand All @@ -166,9 +168,11 @@ class Sticky extends React.PureComponent<IProps, IState> {
};

isDockedToBottom = (
rect: IRect,
containerRect: IRect,
dimensions: IDimensions,
{ rect, containerRect, dimensions }: {
rect?: IRect,
containerRect: IRect,
dimensions: IDimensions,
}
) => {
if (!rect || !containerRect) {
return false;
Expand All @@ -193,28 +197,30 @@ class Sticky extends React.PureComponent<IProps, IState> {
return true;
};

calcHeightDifference(rectSticky: IRect, dimensions: IDimensions) {
calcHeightDifference({ rectSticky, dimensions }: { rectSticky?: IRect, dimensions: IDimensions }) {
if (!dimensions) {
return 0;
}
return Math.max(0, Math.round(rectSticky.height) - dimensions.height);
return Math.max(0, Math.round(rectSticky?.height || 0) - dimensions.height);
}

calcOverflowScrollFlowStickyStyles(
rectSticky: IRect,
containerRect: IRect,
scroll: IScroll,
dimensions: IDimensions,
{ rectSticky, containerRect, scroll, dimensions }: {
rectSticky?: IRect,
containerRect: IRect,
scroll: IScroll,
dimensions: IDimensions,
}
): IPositionStyles {
const containerTop = Math.round(containerRect.top);
const stickyTop = Math.round(rectSticky.top);
const stickyTop = Math.round(rectSticky?.top || 0);
const scrollY = Math.round(scroll.y);
const scrollYTurn = Math.round(scroll.yTurn);
const heightDiff = this.calcHeightDifference(rectSticky, dimensions);
const heightDiff = this.calcHeightDifference({ rectSticky, dimensions });
const containerTopOffset =
containerTop + scrollY - this.props.stickyOffset.height;
const isStickyBottomReached =
Math.round(rectSticky.bottom) <= dimensions.height;
Math.round(rectSticky?.bottom || 0) <= dimensions.height;
const isContainerTopReached = containerTop < this.offsetTop;
const isTurnWithinHeightOffset =
scrollYTurn - heightDiff <= containerTopOffset;
Expand Down Expand Up @@ -272,18 +278,22 @@ class Sticky extends React.PureComponent<IProps, IState> {
}

calcPositionStyles(
rectSticky: IRect,
containerRect: IRect,
scroll: IScroll,
dimensions: IDimensions,
{ rectSticky, containerRect, scroll, dimensions }: {
rectSticky?: IRect,
containerRect: IRect,
scroll: IScroll,
dimensions: IDimensions,
}
): IPositionStyles {
if (this.isSticky(rectSticky, containerRect, dimensions)) {
if (this.getOverflowScrollType(rectSticky, dimensions) === 'flow') {
if (this.isSticky({ rect: rectSticky, containerRect, dimensions })) {
if (this.getOverflowScrollType({ rectSticky, dimensions }) === 'flow') {
return this.calcOverflowScrollFlowStickyStyles(
rectSticky,
containerRect,
scroll,
dimensions,
{
rectSticky,
containerRect,
scroll,
dimensions,
}
);
}
const stickyOffset = this.props.stickyOffset.top;
Expand All @@ -305,10 +315,10 @@ class Sticky extends React.PureComponent<IProps, IState> {
};
}

if (this.isDockedToBottom(rectSticky, containerRect, dimensions)) {
if (this.isDockedToBottom({ rect: rectSticky, containerRect, dimensions })) {
return {
position: 'absolute',
top: containerRect.height - rectSticky.height,
top: containerRect.height - (rectSticky?.height || 0),
};
}

Expand All @@ -319,33 +329,37 @@ class Sticky extends React.PureComponent<IProps, IState> {
}

getStickyStyles(
rect: IRect,
containerRect: IRect,
scroll: IScroll,
dimensions: IDimensions,
{ rect, containerRect, scroll, dimensions }: {
rect?: IRect,
containerRect: IRect,
scroll: IScroll,
dimensions: IDimensions,
}
): IPositionStyles {
const styles = this.calcPositionStyles(
rect,
containerRect,
scroll,
dimensions,
{
rectSticky: rect,
containerRect,
scroll,
dimensions,
}
);

if (!this.props.disableHardwareAcceleration) {
const shouldAccelerate = this.isNearToViewport(rect);
if (supportsWillChange) {
styles.willChange = shouldAccelerate ? 'position, top' : null;
styles.willChange = shouldAccelerate ? 'position, top' : undefined;
} else {
styles.transform = shouldAccelerate ? `translateZ(0)` : null;
styles.transform = shouldAccelerate ? `translateZ(0)` : undefined;
}
}

return styles;
}

recalculateLayoutBeforeUpdate = (): ILayoutSnapshot => {
const containerRect = this.container.current.getBoundingClientRect();
const stickyRect = this.stickyRef.current.getBoundingClientRect();
const containerRect = this.container.current?.getBoundingClientRect();
const stickyRect = this.stickyRef.current?.getBoundingClientRect();
return {
stickyRect,
containerRect,
Expand All @@ -361,23 +375,23 @@ class Sticky extends React.PureComponent<IProps, IState> {
}
// in case children is not a function renderArgs will never be used
const willRenderAsAFunction = typeof this.props.children === 'function';
const appliedOverflowScroll = this.getOverflowScrollType(
stickyRect,
const appliedOverflowScroll = this.getOverflowScrollType({
rectSticky: stickyRect,
dimensions,
);
});

const useNativeSticky = this.shouldUseNativeSticky(appliedOverflowScroll);

const styles = useNativeSticky
? {}
: this.getStickyStyles(stickyRect, containerRect, scroll, dimensions);
: this.getStickyStyles({ rect: stickyRect, containerRect, scroll, dimensions });
const stateStyles = this.state.styles;
const stylesDidChange = !shallowEqualPositionStyles(styles, stateStyles);
const isSticky = willRenderAsAFunction
? this.isSticky(stickyRect, containerRect, dimensions)
? this.isSticky({ rect: stickyRect, containerRect, dimensions })
: false;
const isDockedToBottom = willRenderAsAFunction
? this.isDockedToBottom(stickyRect, containerRect, dimensions)
? this.isDockedToBottom({ rect: stickyRect, containerRect, dimensions })
: false;
const isNearToViewport = this.isNearToViewport(stickyRect);
const useNativeStickyDidChange =
Expand Down Expand Up @@ -452,16 +466,16 @@ class Sticky extends React.PureComponent<IProps, IState> {
style={
this.state.useNativeSticky
? {
position: 'sticky',
top: this.props.defaultOffsetTop,
...style,
}
position: 'sticky',
top: this.props.defaultOffsetTop,
...style,
}
: style
}
disabled={disabled}
disabled={Boolean(disabled)}
forwardRef={this.placeholderRef}
stickyRef={this.stickyRef}
disableResizing={disableResizing}
disableResizing={Boolean(disableResizing)}
>
{this.renderSticky}
</StickyPlaceholder>
Expand Down
10 changes: 5 additions & 5 deletions lib/StickyScrollUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface IOwnProps extends IStickyComponentProps {
defaultOffsetTop?: number;
}

interface IProps extends IOwnProps, IStickyInjectedProps {}
interface IProps extends IOwnProps, IStickyInjectedProps { }

interface IState {
styles: IPositionStyles;
Expand Down Expand Up @@ -144,9 +144,9 @@ class StickyScrollUp extends React.PureComponent<IProps, IState> {
if (!this.props.disableHardwareAcceleration) {
const shouldAccelerate = this.isNearToViewport(stickyRect);
if (supportsWillChange) {
styles.willChange = shouldAccelerate ? 'position, top' : null;
styles.willChange = shouldAccelerate ? 'position, top' : undefined;
} else {
styles.transform = shouldAccelerate ? `translateZ(0)` : null;
styles.transform = shouldAccelerate ? `translateZ(0)` : undefined;
}
}

Expand Down Expand Up @@ -239,9 +239,9 @@ class StickyScrollUp extends React.PureComponent<IProps, IState> {
<StickyPlaceholder
className={className}
style={style}
disabled={disabled}
disabled={Boolean(disabled)}
stickyRef={this.stickyRef}
disableResizing={disableResizing}
disableResizing={Boolean(disableResizing)}
forwardRef={this.placeholderRef}
>
{this.renderSticky}
Expand Down
2 changes: 1 addition & 1 deletion lib/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface ResizeObserver {
/**
* Adds target to the list of observed elements.
*/
observe: (target: Element) => void;
observe: (target: Element, options?: ResizeObserverOptions) => void;

/**
* Removes target from the list of observed elements.
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"test": "jest --coverage",
"fmt": "prettier --write \"lib/*.{ts,tsx}\" \"examples/*.{ts,tsx}\"",
"prepublishOnly": "npm test && npm run compile",
"postversion": "git push && git push --tags"
"postversion": "git push && git push --tags",
"ts-check:watch": "tsc --noEmit --watch"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -55,6 +56,6 @@
"react-viewport-utils": "^1.12.1"
},
"peerDependencies": {
"react": ">=16.3.0 <18.0.0"
"react": ">=16.3.0 <=18.3.1"
}
}
Loading