diff --git a/packages/docusaurus-plugin-ideal-image/package.json b/packages/docusaurus-plugin-ideal-image/package.json index b67f2377bce0..8e6f93cd412a 100644 --- a/packages/docusaurus-plugin-ideal-image/package.json +++ b/packages/docusaurus-plugin-ideal-image/package.json @@ -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" diff --git a/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/index.js b/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/index.js index 4dfe4a68d9eb..5bf57e0535e2 100644 --- a/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/index.js +++ b/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/index.js @@ -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'; diff --git a/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/waypoint.tsx b/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/waypoint.tsx new file mode 100644 index 000000000000..54bd554b3f30 --- /dev/null +++ b/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/waypoint.tsx @@ -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' ? ( + {props.children} + ) : ( + 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 { + static defaultProps = { + topOffset: 0, + bottomOffset: 0, + onEnter() {}, + onLeave() {}, + }; + + scrollableAncestor?: ScrollContainer; + previousPosition: Position | null = null; + unsubscribe?: () => void; + + innerRef = createRef(); + + 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'; +} diff --git a/yarn.lock b/yarn.lock index 781ab1ef61c8..cf02d688820e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"