From 58b708dda2603febee548bc2c26a0d2411016ad3 Mon Sep 17 00:00:00 2001 From: vbersch Date: Tue, 29 Oct 2019 11:08:30 +0100 Subject: [PATCH] fix(ObjectPage): enable scroll by dragging scrollbar (#209) --- config/jestsetup.ts | 7 + packages/base/package.json | 3 +- packages/base/src/polyfill/Edge.ts | 4 + packages/base/src/polyfill/IE11.ts | 4 + .../charts/src/internal/useSizeMonitor.ts | 1 - packages/main/package.json | 3 +- .../components/ObjectPage/demo.stories.tsx | 2 +- .../main/src/components/ObjectPage/index.tsx | 177 ++++++++++++------ 8 files changed, 140 insertions(+), 61 deletions(-) create mode 100644 packages/base/src/polyfill/Edge.ts diff --git a/config/jestsetup.ts b/config/jestsetup.ts index 33fdcc78cc5..2df5ed0c08f 100644 --- a/config/jestsetup.ts +++ b/config/jestsetup.ts @@ -3,6 +3,7 @@ import jssSerializer from '@shared/tests/serializer/jss-snapshot-serializer'; import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import { createSerializer } from 'enzyme-to-json'; +import ResizeObserver from 'resize-observer-polyfill'; process.env.NODE_ENV = 'test'; process.env.BABEL_ENV = 'test'; @@ -36,4 +37,10 @@ export const setupMatchMedia = () => { }; }; +export const setupResizeObserver = () => { + // @ts-ignore + window.ResizeObserver = ResizeObserver; +}; + setupMatchMedia(); +setupResizeObserver(); diff --git a/packages/base/package.json b/packages/base/package.json index f82c12cf298..b18b9b60840 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -18,7 +18,8 @@ "dependencies": { "core-js": "^3.1.4", "hoist-non-react-statics": "^3.3.0", - "react-jss": "10.0.0" + "react-jss": "10.0.0", + "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": "^16.8.0" diff --git a/packages/base/src/polyfill/Edge.ts b/packages/base/src/polyfill/Edge.ts new file mode 100644 index 00000000000..73a04c96f57 --- /dev/null +++ b/packages/base/src/polyfill/Edge.ts @@ -0,0 +1,4 @@ +import ResizeObserver from 'resize-observer-polyfill'; + +// @ts-ignore +window.ResizeObserver = ResizeObserver; diff --git a/packages/base/src/polyfill/IE11.ts b/packages/base/src/polyfill/IE11.ts index 2cd49efc78e..d8053519932 100644 --- a/packages/base/src/polyfill/IE11.ts +++ b/packages/base/src/polyfill/IE11.ts @@ -1,3 +1,7 @@ import 'core-js/modules/es.object.assign'; import 'core-js/modules/es.object.values'; import 'core-js/modules/es.array.flat'; +import ResizeObserver from 'resize-observer-polyfill'; + +// @ts-ignore +window.ResizeObserver = ResizeObserver; diff --git a/packages/charts/src/internal/useSizeMonitor.ts b/packages/charts/src/internal/useSizeMonitor.ts index 131e7f1537e..b0e9231f5b2 100644 --- a/packages/charts/src/internal/useSizeMonitor.ts +++ b/packages/charts/src/internal/useSizeMonitor.ts @@ -1,5 +1,4 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; export const useSizeMonitor = (props, container) => { const { height: heightProp, width: widthProp, minHeight, minWidth } = props; diff --git a/packages/main/package.json b/packages/main/package.json index fff85e2f380..0b8a46c8b81 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -25,8 +25,7 @@ "react-content-loader": "^4.3.2", "react-table": "7.0.0-beta.12", "react-toastify": "^5.0.1", - "react-window": "^1.8.5", - "resize-observer-polyfill": "^1.5.1" + "react-window": "^1.8.5" }, "devDependencies": { "diff": "^4.0.1", diff --git a/packages/main/src/components/ObjectPage/demo.stories.tsx b/packages/main/src/components/ObjectPage/demo.stories.tsx index 3f49fa65b04..b92491cbec5 100644 --- a/packages/main/src/components/ObjectPage/demo.stories.tsx +++ b/packages/main/src/components/ObjectPage/demo.stories.tsx @@ -47,7 +47,7 @@ export const renderDemo = () => { selectedSectionId={text('selectedSectionId', '1')} onSelectedSectionChanged={action('onSelectedSectionChanged')} noHeader={boolean('noHeader', false)} - alwaysShowContentHeader={boolean('alwaysShowContentHeader', false)} + alwaysShowContentHeader={boolean('alwaysShowContentHeader', true)} showTitleInHeaderContent={boolean('showTitleInHeaderContent', true)} style={{ height: '700px' }} > diff --git a/packages/main/src/components/ObjectPage/index.tsx b/packages/main/src/components/ObjectPage/index.tsx index 302f3a533cc..ecc4a336b26 100644 --- a/packages/main/src/components/ObjectPage/index.tsx +++ b/packages/main/src/components/ObjectPage/index.tsx @@ -23,15 +23,14 @@ import { JSSTheme } from '../../interfaces/JSSTheme'; import { ObjectPageMode } from '@ui5/webcomponents-react/lib/ObjectPageMode'; import styles from './ObjectPage.jss'; import { ObjectPageAnchorButton } from './ObjectPageAnchorButton'; -import { Button } from '../../webComponents/Button'; +import { Button } from '@ui5/webcomponents-react/lib/Button'; import { CollapsedAvatar } from './CollapsedAvatar'; import { ObjectPageScroller } from './scroll/ObjectPageScroller'; -import { Avatar } from '@ui5/webcomponents-react/lib/Avatar'; import { AvatarSize } from '@ui5/webcomponents-react/lib/AvatarSize'; -import { AvatarShape } from '@ui5/webcomponents-react/lib/AvatarShape'; import { ContentDensity } from '@ui5/webcomponents-react/lib/ContentDensity'; import '@ui5/webcomponents/dist/icons/navigation-up-arrow.js'; import { getScrollBarWidth } from '@ui5/webcomponents-react-base/lib/Utils'; +import '@ui5/webcomponents/dist/icons/navigation-down-arrow.js'; export interface ObjectPagePropTypes extends CommonProps { title?: string; @@ -101,9 +100,10 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp const innerScrollBar: RefObject = useRef(); const contentScrollContainer: RefObject = useRef(); const collapsedHeaderFiller: RefObject = useRef(); - const expandedHeaderHeight = useRef(0); + const lastScrolledContainer = useRef(); const hideHeaderButtonPressed = useRef(false); - const stableOnScrollRef = useRef(null); + const stableContentOnScrollRef = useRef(null); + const stableBarOnScrollRef = useRef(null); const scroller = useRef(null); const [scrollbarWidth, setScrollbarWidth] = useState(defaultScrollbarWidth); @@ -139,7 +139,7 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp requestAnimationFrame(() => { if (!objectPage.current) { // in case componentWillUnmount didnĀ“t fire - window.removeEventListener('resize', adjustDummyDivHeight); + observer.current.disconnect(); return; } @@ -167,6 +167,8 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp }); }; + const observer = useRef(new ResizeObserver(adjustDummyDivHeight)); + const renderAnchorBar = () => { return (
@@ -190,7 +192,6 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp const changeHeader = useCallback(() => { hideHeaderButtonPressed.current = true; - contentContainer.current.removeEventListener('scroll', onScroll); if (!expandHeaderActive && collapsedHeader) { setExpandHeaderActive(true); @@ -342,9 +343,9 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp // register resize handler useEffect(() => { - window.addEventListener('resize', adjustDummyDivHeight); - return window.removeEventListener('resize', adjustDummyDivHeight); - }, []); + observer.current.observe(contentScrollContainer.current); + return () => observer.current.disconnect(); + }, [adjustDummyDivHeight]); useLayoutEffect(() => { if (!isMounted) return; @@ -376,65 +377,142 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp } }, [selectedSectionIndex]); - const getProportionateScrollTop = useCallback( - (base) => { - const contentContainerHeightFull = contentScrollContainer.current.getBoundingClientRect().height; - const scrollBarHeight = innerScrollBar.current.getBoundingClientRect().height; - - return (base / contentContainerHeightFull) * scrollBarHeight; - }, - [contentScrollContainer.current, innerScrollBar.current] - ); + const getProportionateScrollTop = useCallback((activeContainer, passiveContainer, base) => { + const activeHeight = activeContainer.current.getBoundingClientRect().height; + const passiveHeight = passiveContainer.current.getBoundingClientRect().height; - const onScroll = useMemo(() => { - if (!contentContainer.current) return; + return (base / activeHeight) * passiveHeight; + }, []); - if (stableOnScrollRef.current) { - contentContainer.current.removeEventListener('scroll', stableOnScrollRef.current); + const bindScrollEvent = useCallback((scrollContainer, handler) => { + if (scrollContainer.current && handler.current) { + scrollContainer.current.addEventListener('scroll', handler.current, { passive: true }); } + }, []); - stableOnScrollRef.current = function innerOnScroll(e) { - requestAnimationFrame(() => { - if (noHeader || alwaysShowContentHeader) { - scrollBar.current.scrollTop = getProportionateScrollTop(e.target.scrollTop); - scroller.current.scroll(e); - return; - } + const removeScrollEvent = useCallback((scrollContainer, handler) => { + if (scrollContainer.current && handler.current) { + scrollContainer.current.removeEventListener('scroll', handler.current); + } + }, []); + const checkForHeaderCollapse = useCallback( + // activeContainer contains the scrollContainer thats being actively scrolled + // passiveContainer contains the container that needs to reflect activeContainers scroll position + (activeContainer, activeInnerContainer, passiveContainer, passiveInnerContainer, e) => { + if (noHeader || alwaysShowContentHeader) { + passiveContainer.current.scrollTop = alwaysShowContentHeader + ? e.target.scrollTop + : getProportionateScrollTop(activeContainer, passiveContainer, e.target.scrollTop); + scroller.current.scroll(e); + } else { if (expandHeaderActive) { setExpandHeaderActive(false); } - const innerHeaderHeight = innerHeader.current.getBoundingClientRect().height; - const threshold = collapsedHeader ? expandedHeaderHeight.current + 4 : innerHeaderHeight - 45; - const shouldBeCollapsed = e.target.scrollTop > threshold; + + const threshold = 64; + const baseScrollValue = + activeContainer.current === contentContainer.current + ? e.target.scrollTop + : getProportionateScrollTop(activeInnerContainer, passiveInnerContainer, e.target.scrollTop); + + const shouldBeCollapsed = baseScrollValue > threshold; if (collapsedHeader !== shouldBeCollapsed) { - // contentContainer.current.removeEventListener('scroll', onScroll); + lastScrolledContainer.current = activeContainer.current; if (shouldBeCollapsed) { - expandedHeaderHeight.current = innerHeaderHeight - 45; - collapsedHeaderFiller.current.style.height = `${expandedHeaderHeight.current}px`; + collapsedHeaderFiller.current.style.height = `${64}px`; } else { collapsedHeaderFiller.current.style.height = `${0}px`; } + lastScrolledContainer.current = activeContainer.current; + removeScrollEvent(contentContainer, stableContentOnScrollRef); + removeScrollEvent(scrollBar, stableBarOnScrollRef); setCollapsedHeader(shouldBeCollapsed); } else { - scrollBar.current.scrollTop = collapsedHeader - ? e.target.scrollTop - : getProportionateScrollTop(e.target.scrollTop); + const newScrollValue = + collapsedHeader && e.target.scrollTop > threshold + 50 + ? e.target.scrollTop + : getProportionateScrollTop(activeInnerContainer, passiveInnerContainer, e.target.scrollTop); + + passiveContainer.current.scrollTop = newScrollValue; scroller.current.scroll(e); } + } + }, + [ + innerHeader.current, + collapsedHeader, + contentContainer.current, + collapsedHeaderFiller.current, + setCollapsedHeader, + scrollBar.current, + scroller.current + ] + ); + + useEffect(() => { + if (!isMounted) return; + adjustDummyDivHeight().then(() => { + if (!hideHeaderButtonPressed.current) { + removeScrollEvent(contentContainer, stableContentOnScrollRef); + removeScrollEvent(scrollBar, stableBarOnScrollRef); + if (lastScrolledContainer.current === contentContainer.current) { + contentContainer.current.scrollTop = collapsedHeader ? 64 + 2 : 64 - 2; + } else { + contentContainer.current.scrollTop = collapsedHeader ? 64 + 2 : 64 - 2; + scrollBar.current.scrollTop = getProportionateScrollTop( + contentScrollContainer, + innerScrollBar, + contentContainer.current.scrollTop + ); + } + requestAnimationFrame(() => { + bindScrollEvent(contentContainer, stableContentOnScrollRef); + bindScrollEvent(scrollBar, stableBarOnScrollRef); + }); + } + hideHeaderButtonPressed.current = false; + }); + }, [collapsedHeader]); + + useEffect(() => { + if (!contentContainer.current) return; + + removeScrollEvent(contentContainer, stableContentOnScrollRef); + removeScrollEvent(scrollBar, stableBarOnScrollRef); + + stableContentOnScrollRef.current = function innerOnScroll(e) { + requestAnimationFrame(() => { + removeScrollEvent(scrollBar, stableBarOnScrollRef); + checkForHeaderCollapse(contentContainer, contentScrollContainer, scrollBar, innerScrollBar, e); + requestAnimationFrame(() => { + bindScrollEvent(scrollBar, stableBarOnScrollRef); + }); }); }; - contentContainer.current.addEventListener('scroll', stableOnScrollRef.current); + stableBarOnScrollRef.current = function innerBarOnScroll(e) { + requestAnimationFrame(() => { + removeScrollEvent(contentContainer, stableContentOnScrollRef); + checkForHeaderCollapse(scrollBar, innerScrollBar, contentContainer, contentScrollContainer, e); - return stableOnScrollRef.current; + requestAnimationFrame(() => { + bindScrollEvent(contentContainer, stableContentOnScrollRef); + }); + }); + }; + + bindScrollEvent(contentContainer, stableContentOnScrollRef); + bindScrollEvent(scrollBar, stableBarOnScrollRef); }, [ noHeader, + checkForHeaderCollapse, alwaysShowContentHeader, scrollBar.current, + innerScrollBar.current, innerHeader.current, contentContainer.current, - expandedHeaderHeight.current, + contentScrollContainer.current, collapsedHeaderFiller.current, collapsedHeader, setCollapsedHeader, @@ -442,20 +520,7 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp getProportionateScrollTop ]); - useLayoutEffect(() => { - if (!isMounted) return; - adjustDummyDivHeight().then(() => { - if (!hideHeaderButtonPressed.current) { - const innerHeaderHeight = innerHeader.current.getBoundingClientRect().height; - const base = collapsedHeader ? expandedHeaderHeight.current + 5 : innerHeaderHeight - 50; - contentContainer.current.scrollTop = base; - scrollBar.current.scrollTop = collapsedHeader ? base : getProportionateScrollTop(base); - } - hideHeaderButtonPressed.current = false; - }); - }, [collapsedHeader]); - - useLayoutEffect(() => { + useEffect(() => { if (!isMounted) return; adjustDummyDivHeight(); }, [expandHeaderActive]);