Skip to content

Commit 43fdb82

Browse files
authored
fix(ideal-image): Internalize react-waypoint dependency, fix React 19 compatibility (#11014)
* copy waypoint, remove logs * remove propTypes * remove debug * remove scrollableAncestor prop * remove onPositionChange * remove horizontal prop * remove fireOnRapidScroll * remove useless render code * remove ensureRefIsUsedByChild * remove children prop * inline constants * remove consolidated-events * copy getCurrentPosition * remove computeOffsetPixels * extract findScrollableAncestor * extract getBounds * remove hasWindow * remove onNextTick() * fixes * make it work, replace waypoint * slim down * slim down * slim down * use TypeScript * slim down * slim down * revert
1 parent fcee060 commit 43fdb82

File tree

4 files changed

+242
-20
lines changed

4 files changed

+242
-20
lines changed

packages/docusaurus-plugin-ideal-image/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"@docusaurus/theme-translations": "3.7.0",
2727
"@docusaurus/types": "3.7.0",
2828
"@docusaurus/utils-validation": "3.7.0",
29-
"react-waypoint": "^10.3.0",
3029
"sharp": "^0.32.3",
3130
"tslib": "^2.6.0",
3231
"webpack": "^5.88.1"

packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/index.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, {Component} from 'react';
2-
// import PropTypes from 'prop-types'
3-
import {Waypoint} from 'react-waypoint';
2+
import {Waypoint} from './waypoint';
43
import Media from '../Media';
54
import {icons, loadStates} from '../constants';
65
import {xhrLoader, imageLoader, timeout, combineCancel} from '../loaders';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/*
2+
This is a slimmed down copy of https://github.com/civiccc/react-waypoint
3+
The MIT License (MIT)
4+
Copyright (c) 2015 Brigade
5+
*/
6+
7+
import React, {createRef, ReactNode} from 'react';
8+
9+
type ScrollContainer = Window | HTMLElement;
10+
11+
function addEventListener(
12+
element: ScrollContainer,
13+
type: 'scroll' | 'resize',
14+
listener: () => void,
15+
options: AddEventListenerOptions,
16+
) {
17+
element.addEventListener(type, listener, options);
18+
return () => element.removeEventListener(type, listener, options);
19+
}
20+
21+
type Position = 'above' | 'inside' | 'below' | 'invisible';
22+
23+
type Props = {
24+
topOffset: number;
25+
bottomOffset: number;
26+
onEnter: () => void;
27+
onLeave: () => void;
28+
children: ReactNode;
29+
};
30+
31+
export function Waypoint(props: Props) {
32+
return typeof window !== 'undefined' ? (
33+
<WaypointClient {...props}>{props.children}</WaypointClient>
34+
) : (
35+
props.children
36+
);
37+
}
38+
39+
// TODO maybe replace this with IntersectionObserver later?
40+
// IntersectionObserver doesn't support the "fast scroll" thing
41+
// but it's probably not a big deal
42+
class WaypointClient extends React.Component<Props> {
43+
static defaultProps = {
44+
topOffset: 0,
45+
bottomOffset: 0,
46+
onEnter() {},
47+
onLeave() {},
48+
};
49+
50+
scrollableAncestor?: ScrollContainer;
51+
previousPosition: Position | null = null;
52+
unsubscribe?: () => void;
53+
54+
innerRef = createRef<HTMLElement>();
55+
56+
override componentDidMount() {
57+
this.scrollableAncestor = findScrollableAncestor(this.innerRef.current!);
58+
59+
const unsubscribeScroll = addEventListener(
60+
this.scrollableAncestor!,
61+
'scroll',
62+
this._handleScroll,
63+
{passive: true},
64+
);
65+
66+
const unsubscribeResize = addEventListener(
67+
window,
68+
'resize',
69+
this._handleScroll,
70+
{passive: true},
71+
);
72+
73+
this.unsubscribe = () => {
74+
unsubscribeScroll();
75+
unsubscribeResize();
76+
};
77+
78+
this._handleScroll();
79+
}
80+
81+
override componentDidUpdate() {
82+
this._handleScroll();
83+
}
84+
85+
override componentWillUnmount() {
86+
this.unsubscribe?.();
87+
}
88+
89+
_handleScroll = () => {
90+
const node = this.innerRef.current;
91+
const {topOffset, bottomOffset, onEnter, onLeave} = this.props;
92+
93+
const bounds = getBounds({
94+
node: node!,
95+
scrollableAncestor: this.scrollableAncestor!,
96+
topOffset,
97+
bottomOffset,
98+
});
99+
100+
const currentPosition = getCurrentPosition(bounds);
101+
const previousPosition = this.previousPosition;
102+
this.previousPosition = currentPosition;
103+
104+
if (previousPosition === currentPosition) {
105+
return;
106+
}
107+
108+
if (currentPosition === 'inside') {
109+
onEnter();
110+
} else if (previousPosition === 'inside') {
111+
onLeave();
112+
}
113+
114+
const isRapidScrollDown =
115+
previousPosition === 'below' && currentPosition === 'above';
116+
const isRapidScrollUp =
117+
previousPosition === 'above' && currentPosition === 'below';
118+
if (isRapidScrollDown || isRapidScrollUp) {
119+
onEnter();
120+
onLeave();
121+
}
122+
};
123+
124+
override render() {
125+
// @ts-expect-error: fix this implicit API
126+
return React.cloneElement(this.props.children, {innerRef: this.innerRef});
127+
}
128+
}
129+
130+
/**
131+
* Traverses up the DOM to find an ancestor container which has an overflow
132+
* style that allows for scrolling.
133+
*
134+
* @return {Object} the closest ancestor element with an overflow style that
135+
* allows for scrolling. If none is found, the `window` object is returned
136+
* as a fallback.
137+
*/
138+
function findScrollableAncestor(inputNode: HTMLElement): ScrollContainer {
139+
let node: HTMLElement = inputNode;
140+
141+
while (node.parentNode) {
142+
// @ts-expect-error: it's fine
143+
node = node.parentNode!;
144+
145+
if (node === document.body) {
146+
// We've reached all the way to the root node.
147+
return window;
148+
}
149+
150+
const style = window.getComputedStyle(node);
151+
const overflow =
152+
style.getPropertyValue('overflow-y') ||
153+
style.getPropertyValue('overflow');
154+
155+
if (
156+
overflow === 'auto' ||
157+
overflow === 'scroll' ||
158+
overflow === 'overlay'
159+
) {
160+
return node;
161+
}
162+
}
163+
164+
// A scrollable ancestor element was not found, which means that we need to
165+
// do stuff on window.
166+
return window;
167+
}
168+
169+
type Bounds = {
170+
top: number;
171+
bottom: number;
172+
viewportTop: number;
173+
viewportBottom: number;
174+
};
175+
176+
function getBounds({
177+
node,
178+
scrollableAncestor,
179+
topOffset,
180+
bottomOffset,
181+
}: {
182+
node: Element;
183+
scrollableAncestor: ScrollContainer;
184+
topOffset: number;
185+
bottomOffset: number;
186+
}): Bounds {
187+
const {top, bottom} = node.getBoundingClientRect();
188+
189+
let contextHeight;
190+
let contextScrollTop;
191+
if (scrollableAncestor === window) {
192+
contextHeight = window.innerHeight;
193+
contextScrollTop = 0;
194+
} else {
195+
const ancestorElement = scrollableAncestor as HTMLElement;
196+
contextHeight = ancestorElement.offsetHeight;
197+
contextScrollTop = ancestorElement.getBoundingClientRect().top;
198+
}
199+
200+
const contextBottom = contextScrollTop + contextHeight;
201+
202+
return {
203+
top,
204+
bottom,
205+
viewportTop: contextScrollTop + topOffset,
206+
viewportBottom: contextBottom - bottomOffset,
207+
};
208+
}
209+
210+
function getCurrentPosition(bounds: Bounds): Position {
211+
if (bounds.viewportBottom - bounds.viewportTop === 0) {
212+
return 'invisible';
213+
}
214+
// top is within the viewport
215+
if (bounds.viewportTop <= bounds.top && bounds.top <= bounds.viewportBottom) {
216+
return 'inside';
217+
}
218+
// bottom is within the viewport
219+
if (
220+
bounds.viewportTop <= bounds.bottom &&
221+
bounds.bottom <= bounds.viewportBottom
222+
) {
223+
return 'inside';
224+
}
225+
// top is above the viewport and bottom is below the viewport
226+
if (
227+
bounds.top <= bounds.viewportTop &&
228+
bounds.viewportBottom <= bounds.bottom
229+
) {
230+
return 'inside';
231+
}
232+
if (bounds.viewportBottom < bounds.top) {
233+
return 'below';
234+
}
235+
if (bounds.top < bounds.viewportTop) {
236+
return 'above';
237+
}
238+
return 'invisible';
239+
}

yarn.lock

+2-17
Original file line numberDiff line numberDiff line change
@@ -6249,11 +6249,6 @@ console-control-strings@^1.1.0:
62496249
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
62506250
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
62516251

6252-
"consolidated-events@^1.1.0 || ^2.0.0":
6253-
version "2.0.2"
6254-
resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91"
6255-
integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==
6256-
62576252
62586253
version "0.5.2"
62596254
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
@@ -14841,7 +14836,7 @@ promzard@^0.3.0:
1484114836
dependencies:
1484214837
read "1"
1484314838

14844-
prop-types@^15.0.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
14839+
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
1484514840
version "15.8.1"
1484614841
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
1484714842
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -15052,7 +15047,7 @@ react-fast-compare@^3.2.0:
1505215047
react-fast-compare "^3.2.0"
1505315048
shallowequal "^1.1.0"
1505415049

15055-
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", "react-is@^17.0.1 || ^18.0.0", react-is@^18.0.0, react-is@^18.3.1:
15050+
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.3.1:
1505615051
version "18.3.1"
1505715052
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
1505815053
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
@@ -15152,16 +15147,6 @@ react-test-renderer@^18.0.0:
1515215147
react-shallow-renderer "^16.15.0"
1515315148
scheduler "^0.23.2"
1515415149

15155-
react-waypoint@^10.3.0:
15156-
version "10.3.0"
15157-
resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-10.3.0.tgz#fcc60e86c6c9ad2174fa58d066dc6ae54e3df71d"
15158-
integrity sha512-iF1y2c1BsoXuEGz08NoahaLFIGI9gTUAAOKip96HUmylRT6DUtpgoBPjk/Y8dfcFVmfVDvUzWjNXpZyKTOV0SQ==
15159-
dependencies:
15160-
"@babel/runtime" "^7.12.5"
15161-
consolidated-events "^1.1.0 || ^2.0.0"
15162-
prop-types "^15.0.0"
15163-
react-is "^17.0.1 || ^18.0.0"
15164-
1516515150
1516615151
version "16.14.0"
1516715152
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"

0 commit comments

Comments
 (0)