Skip to content

Commit

Permalink
fix(ObjectPage): enable scroll by dragging scrollbar (#209)
Browse files Browse the repository at this point in the history
  • Loading branch information
vbersch authored Oct 29, 2019
1 parent e8120b0 commit 58b708d
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 61 deletions.
7 changes: 7 additions & 0 deletions config/jestsetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -36,4 +37,10 @@ export const setupMatchMedia = () => {
};
};

export const setupResizeObserver = () => {
// @ts-ignore
window.ResizeObserver = ResizeObserver;
};

setupMatchMedia();
setupResizeObserver();
3 changes: 2 additions & 1 deletion packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions packages/base/src/polyfill/Edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ResizeObserver from 'resize-observer-polyfill';

// @ts-ignore
window.ResizeObserver = ResizeObserver;
4 changes: 4 additions & 0 deletions packages/base/src/polyfill/IE11.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 0 additions & 1 deletion packages/charts/src/internal/useSizeMonitor.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
3 changes: 1 addition & 2 deletions packages/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/components/ObjectPage/demo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
>
Expand Down
177 changes: 121 additions & 56 deletions packages/main/src/components/ObjectPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -101,9 +100,10 @@ const ObjectPage: FC<ObjectPagePropTypes> = forwardRef((props: ObjectPagePropTyp
const innerScrollBar: RefObject<HTMLDivElement> = useRef();
const contentScrollContainer: RefObject<HTMLDivElement> = useRef();
const collapsedHeaderFiller: RefObject<HTMLDivElement> = 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);

Expand Down Expand Up @@ -139,7 +139,7 @@ const ObjectPage: FC<ObjectPagePropTypes> = forwardRef((props: ObjectPagePropTyp
requestAnimationFrame(() => {
if (!objectPage.current) {
// in case componentWillUnmount didn´t fire
window.removeEventListener('resize', adjustDummyDivHeight);
observer.current.disconnect();
return;
}

Expand Down Expand Up @@ -167,6 +167,8 @@ const ObjectPage: FC<ObjectPagePropTypes> = forwardRef((props: ObjectPagePropTyp
});
};

const observer = useRef(new ResizeObserver(adjustDummyDivHeight));

const renderAnchorBar = () => {
return (
<section className={classes.anchorBar} role="navigation">
Expand All @@ -190,7 +192,6 @@ const ObjectPage: FC<ObjectPagePropTypes> = forwardRef((props: ObjectPagePropTyp

const changeHeader = useCallback(() => {
hideHeaderButtonPressed.current = true;
contentContainer.current.removeEventListener('scroll', onScroll);

if (!expandHeaderActive && collapsedHeader) {
setExpandHeaderActive(true);
Expand Down Expand Up @@ -342,9 +343,9 @@ const ObjectPage: FC<ObjectPagePropTypes> = 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;
Expand Down Expand Up @@ -376,86 +377,150 @@ const ObjectPage: FC<ObjectPagePropTypes> = 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,
expandHeaderActive,
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]);
Expand Down

0 comments on commit 58b708d

Please sign in to comment.