diff --git a/package-lock.json b/package-lock.json index 8bcb3c653c..1f1d748f2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@loadable/component": "5.15.3", "@mui/icons-material": "5.11.16", "@mui/material": "5.13.3", + "@react-hook/resize-observer": "1.2.6", "@tanstack/react-query": "4.29.12", "@tanstack/react-query-devtools": "4.29.12", "blurhash": "2.0.5", @@ -2980,6 +2981,11 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -3375,6 +3381,35 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-hook/latest": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz", + "integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@react-hook/passive-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz", + "integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@react-hook/resize-observer": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz", + "integrity": "sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA==", + "dependencies": { + "@juggle/resize-observer": "^3.3.1", + "@react-hook/latest": "^1.0.2", + "@react-hook/passive-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/@remix-run/router": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.1.tgz", @@ -22455,6 +22490,11 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" + }, "@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -22677,6 +22717,28 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==" }, + "@react-hook/latest": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz", + "integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==", + "requires": {} + }, + "@react-hook/passive-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz", + "integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==", + "requires": {} + }, + "@react-hook/resize-observer": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz", + "integrity": "sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA==", + "requires": { + "@juggle/resize-observer": "^3.3.1", + "@react-hook/latest": "^1.0.2", + "@react-hook/passive-layout-effect": "^1.2.0" + } + }, "@remix-run/router": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.1.tgz", diff --git a/package.json b/package.json index fe5c1b6d98..0741a67a2b 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@loadable/component": "5.15.3", "@mui/icons-material": "5.11.16", "@mui/material": "5.13.3", + "@react-hook/resize-observer": "1.2.6", "@tanstack/react-query": "4.29.12", "@tanstack/react-query-devtools": "4.29.12", "blurhash": "2.0.5", diff --git a/src/elements/emby-scrollbuttons/ScrollButtons.tsx b/src/elements/emby-scrollbuttons/ScrollButtons.tsx index 6f4bce3826..aaeb9bc23e 100644 --- a/src/elements/emby-scrollbuttons/ScrollButtons.tsx +++ b/src/elements/emby-scrollbuttons/ScrollButtons.tsx @@ -10,7 +10,6 @@ enum Direction { } interface ScrollButtonsProps { - scrollRef?: React.MutableRefObject; scrollerFactoryRef: React.MutableRefObject; scrollState: { scrollSize: number; diff --git a/src/elements/emby-scroller/Scroller.tsx b/src/elements/emby-scroller/Scroller.tsx index 773d184c59..57349bad4c 100644 --- a/src/elements/emby-scroller/Scroller.tsx +++ b/src/elements/emby-scroller/Scroller.tsx @@ -1,5 +1,6 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; +import useElementSize from 'hooks/useElementSize'; import layoutManager from '../../components/layoutManager'; import dom from '../../scripts/dom'; import browser from '../../scripts/browser'; @@ -32,14 +33,14 @@ const Scroller: FC = ({ isAllowNativeSmoothScrollEnabled, children }) => { + const [scrollRef, size] = useElementSize(); + const [showControls, setShowControls] = useState(false); const [scrollState, setScrollState] = useState({ - scrollSize: 0, + scrollSize: size.width, scrollPos: 0, scrollWidth: 0 }); - - const scrollRef = useRef(null); const scrollerFactoryRef = useRef(null); const getScrollSlider = useCallback(() => { @@ -125,7 +126,7 @@ const Scroller: FC = ({ }); }, [getScrollPosition, getScrollSize, getScrollWidth]); - const initCenterFocus = useCallback((elem: EventTarget, scrollerInstance: scrollerFactory) => { + const initCenterFocus = useCallback((elem, scrollerInstance: scrollerFactory) => { dom.addEventListener(elem, 'focus', function (e: FocusEvent) { const focused = focusManager.focusableParent(e.target); if (focused) { @@ -150,15 +151,10 @@ const Scroller: FC = ({ }, [scrollerFactoryRef]); useEffect(() => { - const scrollerElement = scrollRef.current as HTMLDivElement; - const horizontal = isHorizontalEnabled !== false; const scrollbuttons = isScrollButtonsEnabled !== false; const mousewheel = isMouseWheelEnabled !== false; - const slider = scrollerElement.querySelector('.scrollSlider'); - - const scrollFrame = scrollerElement; const enableScrollButtons = layoutManager.desktop && horizontal && scrollbuttons; const options = { @@ -166,7 +162,7 @@ const Scroller: FC = ({ mouseDragging: 1, mouseWheel: mousewheel, touchDragging: 1, - slidee: slider, + slidee: scrollRef.current?.querySelector('.scrollSlider'), scrollBy: 200, speed: horizontal ? 270 : 240, elasticBounds: 1, @@ -183,12 +179,12 @@ const Scroller: FC = ({ }; // If just inserted it might not have any height yet - yes this is a hack - scrollerFactoryRef.current = new scrollerFactory(scrollFrame, options); + scrollerFactoryRef.current = new scrollerFactory(scrollRef.current, options); scrollerFactoryRef.current.init(); scrollerFactoryRef.current.reload(); if (layoutManager.tv && isCenterFocusEnabled) { - initCenterFocus(scrollerElement, scrollerFactoryRef.current); + initCenterFocus(scrollRef.current, scrollerFactoryRef.current); } if (enableScrollButtons) { @@ -200,9 +196,8 @@ const Scroller: FC = ({ } return () => { - const scrollerInstance = scrollerFactoryRef.current; - if (scrollerInstance) { - scrollerInstance.destroy(); + if (scrollerFactoryRef.current) { + scrollerFactoryRef.current.destroy(); scrollerFactoryRef.current = null; } @@ -223,7 +218,8 @@ const Scroller: FC = ({ isScrollEventEnabled, isSkipFocusWhenVisibleEnabled, onScroll, - removeScrollEventListener + removeScrollEventListener, + scrollRef ]); return ( @@ -231,7 +227,6 @@ const Scroller: FC = ({ { showControls && scrollState.scrollWidth > scrollState.scrollSize + 20 && diff --git a/src/hooks/useElementSize.ts b/src/hooks/useElementSize.ts new file mode 100644 index 0000000000..aeb7000ee4 --- /dev/null +++ b/src/hooks/useElementSize.ts @@ -0,0 +1,25 @@ +import { MutableRefObject, useLayoutEffect, useRef, useState } from 'react'; +import useResizeObserver from '@react-hook/resize-observer'; + +interface Size { + width: number; + height: number; +} + +export default function useElementSize< + T extends HTMLElement = HTMLDivElement +>(): [MutableRefObject, Size] { + const target = useRef(null); + const [size, setSize] = useState({ + width: 0, + height: 0 + }); + + useLayoutEffect(() => { + target.current && setSize(target.current.getBoundingClientRect()); + }, [target]); + + useResizeObserver(target, (entry) => setSize(entry.contentRect)); + + return [target, size]; +} diff --git a/webpack.common.js b/webpack.common.js index b1fccfd5f1..c4b67af589 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -166,6 +166,9 @@ const config = { path.resolve(__dirname, 'node_modules/event-target-polyfill'), path.resolve(__dirname, 'node_modules/rvfc-polyfill'), path.resolve(__dirname, 'node_modules/@jellyfin/sdk'), + path.resolve(__dirname, 'node_modules/@react-hook/latest'), + path.resolve(__dirname, 'node_modules/@react-hook/passive-layout-effect'), + path.resolve(__dirname, 'node_modules/@react-hook/resize-observer'), path.resolve(__dirname, 'node_modules/@remix-run/router'), path.resolve(__dirname, 'node_modules/@tanstack/query-core'), path.resolve(__dirname, 'node_modules/@tanstack/react-query'),