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
This commit is contained in:
Sébastien Lorber 2025-03-20 12:33:27 +01:00 committed by GitHub
parent fcee060f40
commit 43fdb825e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 242 additions and 20 deletions

View file

@ -26,7 +26,6 @@
"@docusaurus/theme-translations": "3.7.0",
"@docusaurus/types": "3.7.0",
"@docusaurus/utils-validation": "3.7.0",
"react-waypoint": "^10.3.0",
"sharp": "^0.32.3",
"tslib": "^2.6.0",
"webpack": "^5.88.1"

View file

@ -1,6 +1,5 @@
import React, {Component} from 'react';
// import PropTypes from 'prop-types'
import {Waypoint} from 'react-waypoint';
import {Waypoint} from './waypoint';
import Media from '../Media';
import {icons, loadStates} from '../constants';
import {xhrLoader, imageLoader, timeout, combineCancel} from '../loaders';

View file

@ -0,0 +1,239 @@
/*
This is a slimmed down copy of https://github.com/civiccc/react-waypoint
The MIT License (MIT)
Copyright (c) 2015 Brigade
*/
import React, {createRef, ReactNode} from 'react';
type ScrollContainer = Window | HTMLElement;
function addEventListener(
element: ScrollContainer,
type: 'scroll' | 'resize',
listener: () => void,
options: AddEventListenerOptions,
) {
element.addEventListener(type, listener, options);
return () => element.removeEventListener(type, listener, options);
}
type Position = 'above' | 'inside' | 'below' | 'invisible';
type Props = {
topOffset: number;
bottomOffset: number;
onEnter: () => void;
onLeave: () => void;
children: ReactNode;
};
export function Waypoint(props: Props) {
return typeof window !== 'undefined' ? (
<WaypointClient {...props}>{props.children}</WaypointClient>
) : (
props.children
);
}
// TODO maybe replace this with IntersectionObserver later?
// IntersectionObserver doesn't support the "fast scroll" thing
// but it's probably not a big deal
class WaypointClient extends React.Component<Props> {
static defaultProps = {
topOffset: 0,
bottomOffset: 0,
onEnter() {},
onLeave() {},
};
scrollableAncestor?: ScrollContainer;
previousPosition: Position | null = null;
unsubscribe?: () => void;
innerRef = createRef<HTMLElement>();
override componentDidMount() {
this.scrollableAncestor = findScrollableAncestor(this.innerRef.current!);
const unsubscribeScroll = addEventListener(
this.scrollableAncestor!,
'scroll',
this._handleScroll,
{passive: true},
);
const unsubscribeResize = addEventListener(
window,
'resize',
this._handleScroll,
{passive: true},
);
this.unsubscribe = () => {
unsubscribeScroll();
unsubscribeResize();
};
this._handleScroll();
}
override componentDidUpdate() {
this._handleScroll();
}
override componentWillUnmount() {
this.unsubscribe?.();
}
_handleScroll = () => {
const node = this.innerRef.current;
const {topOffset, bottomOffset, onEnter, onLeave} = this.props;
const bounds = getBounds({
node: node!,
scrollableAncestor: this.scrollableAncestor!,
topOffset,
bottomOffset,
});
const currentPosition = getCurrentPosition(bounds);
const previousPosition = this.previousPosition;
this.previousPosition = currentPosition;
if (previousPosition === currentPosition) {
return;
}
if (currentPosition === 'inside') {
onEnter();
} else if (previousPosition === 'inside') {
onLeave();
}
const isRapidScrollDown =
previousPosition === 'below' && currentPosition === 'above';
const isRapidScrollUp =
previousPosition === 'above' && currentPosition === 'below';
if (isRapidScrollDown || isRapidScrollUp) {
onEnter();
onLeave();
}
};
override render() {
// @ts-expect-error: fix this implicit API
return React.cloneElement(this.props.children, {innerRef: this.innerRef});
}
}
/**
* Traverses up the DOM to find an ancestor container which has an overflow
* style that allows for scrolling.
*
* @return {Object} the closest ancestor element with an overflow style that
* allows for scrolling. If none is found, the `window` object is returned
* as a fallback.
*/
function findScrollableAncestor(inputNode: HTMLElement): ScrollContainer {
let node: HTMLElement = inputNode;
while (node.parentNode) {
// @ts-expect-error: it's fine
node = node.parentNode!;
if (node === document.body) {
// We've reached all the way to the root node.
return window;
}
const style = window.getComputedStyle(node);
const overflow =
style.getPropertyValue('overflow-y') ||
style.getPropertyValue('overflow');
if (
overflow === 'auto' ||
overflow === 'scroll' ||
overflow === 'overlay'
) {
return node;
}
}
// A scrollable ancestor element was not found, which means that we need to
// do stuff on window.
return window;
}
type Bounds = {
top: number;
bottom: number;
viewportTop: number;
viewportBottom: number;
};
function getBounds({
node,
scrollableAncestor,
topOffset,
bottomOffset,
}: {
node: Element;
scrollableAncestor: ScrollContainer;
topOffset: number;
bottomOffset: number;
}): Bounds {
const {top, bottom} = node.getBoundingClientRect();
let contextHeight;
let contextScrollTop;
if (scrollableAncestor === window) {
contextHeight = window.innerHeight;
contextScrollTop = 0;
} else {
const ancestorElement = scrollableAncestor as HTMLElement;
contextHeight = ancestorElement.offsetHeight;
contextScrollTop = ancestorElement.getBoundingClientRect().top;
}
const contextBottom = contextScrollTop + contextHeight;
return {
top,
bottom,
viewportTop: contextScrollTop + topOffset,
viewportBottom: contextBottom - bottomOffset,
};
}
function getCurrentPosition(bounds: Bounds): Position {
if (bounds.viewportBottom - bounds.viewportTop === 0) {
return 'invisible';
}
// top is within the viewport
if (bounds.viewportTop <= bounds.top && bounds.top <= bounds.viewportBottom) {
return 'inside';
}
// bottom is within the viewport
if (
bounds.viewportTop <= bounds.bottom &&
bounds.bottom <= bounds.viewportBottom
) {
return 'inside';
}
// top is above the viewport and bottom is below the viewport
if (
bounds.top <= bounds.viewportTop &&
bounds.viewportBottom <= bounds.bottom
) {
return 'inside';
}
if (bounds.viewportBottom < bounds.top) {
return 'below';
}
if (bounds.top < bounds.viewportTop) {
return 'above';
}
return 'invisible';
}

View file

@ -6249,11 +6249,6 @@ console-control-strings@^1.1.0:
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
"consolidated-events@^1.1.0 || ^2.0.0":
version "2.0.2"
resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91"
integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==
content-disposition@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
@ -14841,7 +14836,7 @@ promzard@^0.3.0:
dependencies:
read "1"
prop-types@^15.0.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -15052,7 +15047,7 @@ react-fast-compare@^3.2.0:
react-fast-compare "^3.2.0"
shallowequal "^1.1.0"
"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:
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
@ -15152,16 +15147,6 @@ react-test-renderer@^18.0.0:
react-shallow-renderer "^16.15.0"
scheduler "^0.23.2"
react-waypoint@^10.3.0:
version "10.3.0"
resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-10.3.0.tgz#fcc60e86c6c9ad2174fa58d066dc6ae54e3df71d"
integrity sha512-iF1y2c1BsoXuEGz08NoahaLFIGI9gTUAAOKip96HUmylRT6DUtpgoBPjk/Y8dfcFVmfVDvUzWjNXpZyKTOV0SQ==
dependencies:
"@babel/runtime" "^7.12.5"
consolidated-events "^1.1.0 || ^2.0.0"
prop-types "^15.0.0"
react-is "^17.0.1 || ^18.0.0"
react@16.14.0:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"