Skip to content

Commit 58b708d

Browse files
authored
fix(ObjectPage): enable scroll by dragging scrollbar (#209)
1 parent e8120b0 commit 58b708d

File tree

8 files changed

+140
-61
lines changed

8 files changed

+140
-61
lines changed

config/jestsetup.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import jssSerializer from '@shared/tests/serializer/jss-snapshot-serializer';
33
import Enzyme from 'enzyme';
44
import Adapter from 'enzyme-adapter-react-16';
55
import { createSerializer } from 'enzyme-to-json';
6+
import ResizeObserver from 'resize-observer-polyfill';
67

78
process.env.NODE_ENV = 'test';
89
process.env.BABEL_ENV = 'test';
@@ -36,4 +37,10 @@ export const setupMatchMedia = () => {
3637
};
3738
};
3839

40+
export const setupResizeObserver = () => {
41+
// @ts-ignore
42+
window.ResizeObserver = ResizeObserver;
43+
};
44+
3945
setupMatchMedia();
46+
setupResizeObserver();

packages/base/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"dependencies": {
1919
"core-js": "^3.1.4",
2020
"hoist-non-react-statics": "^3.3.0",
21-
"react-jss": "10.0.0"
21+
"react-jss": "10.0.0",
22+
"resize-observer-polyfill": "^1.5.1"
2223
},
2324
"peerDependencies": {
2425
"react": "^16.8.0"

packages/base/src/polyfill/Edge.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import ResizeObserver from 'resize-observer-polyfill';
2+
3+
// @ts-ignore
4+
window.ResizeObserver = ResizeObserver;

packages/base/src/polyfill/IE11.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
import 'core-js/modules/es.object.assign';
22
import 'core-js/modules/es.object.values';
33
import 'core-js/modules/es.array.flat';
4+
import ResizeObserver from 'resize-observer-polyfill';
5+
6+
// @ts-ignore
7+
window.ResizeObserver = ResizeObserver;

packages/charts/src/internal/useSizeMonitor.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useCallback, useEffect, useRef, useState } from 'react';
2-
import ResizeObserver from 'resize-observer-polyfill';
32

43
export const useSizeMonitor = (props, container) => {
54
const { height: heightProp, width: widthProp, minHeight, minWidth } = props;

packages/main/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
"react-content-loader": "^4.3.2",
2626
"react-table": "7.0.0-beta.12",
2727
"react-toastify": "^5.0.1",
28-
"react-window": "^1.8.5",
29-
"resize-observer-polyfill": "^1.5.1"
28+
"react-window": "^1.8.5"
3029
},
3130
"devDependencies": {
3231
"diff": "^4.0.1",

packages/main/src/components/ObjectPage/demo.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const renderDemo = () => {
4747
selectedSectionId={text('selectedSectionId', '1')}
4848
onSelectedSectionChanged={action('onSelectedSectionChanged')}
4949
noHeader={boolean('noHeader', false)}
50-
alwaysShowContentHeader={boolean('alwaysShowContentHeader', false)}
50+
alwaysShowContentHeader={boolean('alwaysShowContentHeader', true)}
5151
showTitleInHeaderContent={boolean('showTitleInHeaderContent', true)}
5252
style={{ height: '700px' }}
5353
>

packages/main/src/components/ObjectPage/index.tsx

Lines changed: 121 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@ import { JSSTheme } from '../../interfaces/JSSTheme';
2323
import { ObjectPageMode } from '@ui5/webcomponents-react/lib/ObjectPageMode';
2424
import styles from './ObjectPage.jss';
2525
import { ObjectPageAnchorButton } from './ObjectPageAnchorButton';
26-
import { Button } from '../../webComponents/Button';
26+
import { Button } from '@ui5/webcomponents-react/lib/Button';
2727
import { CollapsedAvatar } from './CollapsedAvatar';
2828
import { ObjectPageScroller } from './scroll/ObjectPageScroller';
29-
import { Avatar } from '@ui5/webcomponents-react/lib/Avatar';
3029
import { AvatarSize } from '@ui5/webcomponents-react/lib/AvatarSize';
31-
import { AvatarShape } from '@ui5/webcomponents-react/lib/AvatarShape';
3230
import { ContentDensity } from '@ui5/webcomponents-react/lib/ContentDensity';
3331
import '@ui5/webcomponents/dist/icons/navigation-up-arrow.js';
3432
import { getScrollBarWidth } from '@ui5/webcomponents-react-base/lib/Utils';
33+
import '@ui5/webcomponents/dist/icons/navigation-down-arrow.js';
3534

3635
export interface ObjectPagePropTypes extends CommonProps {
3736
title?: string;
@@ -101,9 +100,10 @@ const ObjectPage: FC<ObjectPagePropTypes> = forwardRef((props: ObjectPagePropTyp
101100
const innerScrollBar: RefObject<HTMLDivElement> = useRef();
102101
const contentScrollContainer: RefObject<HTMLDivElement> = useRef();
103102
const collapsedHeaderFiller: RefObject<HTMLDivElement> = useRef();
104-
const expandedHeaderHeight = useRef(0);
103+
const lastScrolledContainer = useRef();
105104
const hideHeaderButtonPressed = useRef(false);
106-
const stableOnScrollRef = useRef(null);
105+
const stableContentOnScrollRef = useRef(null);
106+
const stableBarOnScrollRef = useRef(null);
107107
const scroller = useRef(null);
108108
const [scrollbarWidth, setScrollbarWidth] = useState(defaultScrollbarWidth);
109109

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

@@ -167,6 +167,8 @@ const ObjectPage: FC<ObjectPagePropTypes> = forwardRef((props: ObjectPagePropTyp
167167
});
168168
};
169169

170+
const observer = useRef(new ResizeObserver(adjustDummyDivHeight));
171+
170172
const renderAnchorBar = () => {
171173
return (
172174
<section className={classes.anchorBar} role="navigation">
@@ -190,7 +192,6 @@ const ObjectPage: FC<ObjectPagePropTypes> = forwardRef((props: ObjectPagePropTyp
190192

191193
const changeHeader = useCallback(() => {
192194
hideHeaderButtonPressed.current = true;
193-
contentContainer.current.removeEventListener('scroll', onScroll);
194195

195196
if (!expandHeaderActive && collapsedHeader) {
196197
setExpandHeaderActive(true);
@@ -342,9 +343,9 @@ const ObjectPage: FC<ObjectPagePropTypes> = forwardRef((props: ObjectPagePropTyp
342343

343344
// register resize handler
344345
useEffect(() => {
345-
window.addEventListener('resize', adjustDummyDivHeight);
346-
return window.removeEventListener('resize', adjustDummyDivHeight);
347-
}, []);
346+
observer.current.observe(contentScrollContainer.current);
347+
return () => observer.current.disconnect();
348+
}, [adjustDummyDivHeight]);
348349

349350
useLayoutEffect(() => {
350351
if (!isMounted) return;
@@ -376,86 +377,150 @@ const ObjectPage: FC<ObjectPagePropTypes> = forwardRef((props: ObjectPagePropTyp
376377
}
377378
}, [selectedSectionIndex]);
378379

379-
const getProportionateScrollTop = useCallback(
380-
(base) => {
381-
const contentContainerHeightFull = contentScrollContainer.current.getBoundingClientRect().height;
382-
const scrollBarHeight = innerScrollBar.current.getBoundingClientRect().height;
383-
384-
return (base / contentContainerHeightFull) * scrollBarHeight;
385-
},
386-
[contentScrollContainer.current, innerScrollBar.current]
387-
);
380+
const getProportionateScrollTop = useCallback((activeContainer, passiveContainer, base) => {
381+
const activeHeight = activeContainer.current.getBoundingClientRect().height;
382+
const passiveHeight = passiveContainer.current.getBoundingClientRect().height;
388383

389-
const onScroll = useMemo(() => {
390-
if (!contentContainer.current) return;
384+
return (base / activeHeight) * passiveHeight;
385+
}, []);
391386

392-
if (stableOnScrollRef.current) {
393-
contentContainer.current.removeEventListener('scroll', stableOnScrollRef.current);
387+
const bindScrollEvent = useCallback((scrollContainer, handler) => {
388+
if (scrollContainer.current && handler.current) {
389+
scrollContainer.current.addEventListener('scroll', handler.current, { passive: true });
394390
}
391+
}, []);
395392

396-
stableOnScrollRef.current = function innerOnScroll(e) {
397-
requestAnimationFrame(() => {
398-
if (noHeader || alwaysShowContentHeader) {
399-
scrollBar.current.scrollTop = getProportionateScrollTop(e.target.scrollTop);
400-
scroller.current.scroll(e);
401-
return;
402-
}
393+
const removeScrollEvent = useCallback((scrollContainer, handler) => {
394+
if (scrollContainer.current && handler.current) {
395+
scrollContainer.current.removeEventListener('scroll', handler.current);
396+
}
397+
}, []);
403398

399+
const checkForHeaderCollapse = useCallback(
400+
// activeContainer contains the scrollContainer thats being actively scrolled
401+
// passiveContainer contains the container that needs to reflect activeContainers scroll position
402+
(activeContainer, activeInnerContainer, passiveContainer, passiveInnerContainer, e) => {
403+
if (noHeader || alwaysShowContentHeader) {
404+
passiveContainer.current.scrollTop = alwaysShowContentHeader
405+
? e.target.scrollTop
406+
: getProportionateScrollTop(activeContainer, passiveContainer, e.target.scrollTop);
407+
scroller.current.scroll(e);
408+
} else {
404409
if (expandHeaderActive) {
405410
setExpandHeaderActive(false);
406411
}
407-
const innerHeaderHeight = innerHeader.current.getBoundingClientRect().height;
408-
const threshold = collapsedHeader ? expandedHeaderHeight.current + 4 : innerHeaderHeight - 45;
409-
const shouldBeCollapsed = e.target.scrollTop > threshold;
412+
413+
const threshold = 64;
414+
const baseScrollValue =
415+
activeContainer.current === contentContainer.current
416+
? e.target.scrollTop
417+
: getProportionateScrollTop(activeInnerContainer, passiveInnerContainer, e.target.scrollTop);
418+
419+
const shouldBeCollapsed = baseScrollValue > threshold;
410420
if (collapsedHeader !== shouldBeCollapsed) {
411-
// contentContainer.current.removeEventListener('scroll', onScroll);
421+
lastScrolledContainer.current = activeContainer.current;
412422
if (shouldBeCollapsed) {
413-
expandedHeaderHeight.current = innerHeaderHeight - 45;
414-
collapsedHeaderFiller.current.style.height = `${expandedHeaderHeight.current}px`;
423+
collapsedHeaderFiller.current.style.height = `${64}px`;
415424
} else {
416425
collapsedHeaderFiller.current.style.height = `${0}px`;
417426
}
427+
lastScrolledContainer.current = activeContainer.current;
428+
removeScrollEvent(contentContainer, stableContentOnScrollRef);
429+
removeScrollEvent(scrollBar, stableBarOnScrollRef);
418430
setCollapsedHeader(shouldBeCollapsed);
419431
} else {
420-
scrollBar.current.scrollTop = collapsedHeader
421-
? e.target.scrollTop
422-
: getProportionateScrollTop(e.target.scrollTop);
432+
const newScrollValue =
433+
collapsedHeader && e.target.scrollTop > threshold + 50
434+
? e.target.scrollTop
435+
: getProportionateScrollTop(activeInnerContainer, passiveInnerContainer, e.target.scrollTop);
436+
437+
passiveContainer.current.scrollTop = newScrollValue;
423438
scroller.current.scroll(e);
424439
}
440+
}
441+
},
442+
[
443+
innerHeader.current,
444+
collapsedHeader,
445+
contentContainer.current,
446+
collapsedHeaderFiller.current,
447+
setCollapsedHeader,
448+
scrollBar.current,
449+
scroller.current
450+
]
451+
);
452+
453+
useEffect(() => {
454+
if (!isMounted) return;
455+
adjustDummyDivHeight().then(() => {
456+
if (!hideHeaderButtonPressed.current) {
457+
removeScrollEvent(contentContainer, stableContentOnScrollRef);
458+
removeScrollEvent(scrollBar, stableBarOnScrollRef);
459+
if (lastScrolledContainer.current === contentContainer.current) {
460+
contentContainer.current.scrollTop = collapsedHeader ? 64 + 2 : 64 - 2;
461+
} else {
462+
contentContainer.current.scrollTop = collapsedHeader ? 64 + 2 : 64 - 2;
463+
scrollBar.current.scrollTop = getProportionateScrollTop(
464+
contentScrollContainer,
465+
innerScrollBar,
466+
contentContainer.current.scrollTop
467+
);
468+
}
469+
requestAnimationFrame(() => {
470+
bindScrollEvent(contentContainer, stableContentOnScrollRef);
471+
bindScrollEvent(scrollBar, stableBarOnScrollRef);
472+
});
473+
}
474+
hideHeaderButtonPressed.current = false;
475+
});
476+
}, [collapsedHeader]);
477+
478+
useEffect(() => {
479+
if (!contentContainer.current) return;
480+
481+
removeScrollEvent(contentContainer, stableContentOnScrollRef);
482+
removeScrollEvent(scrollBar, stableBarOnScrollRef);
483+
484+
stableContentOnScrollRef.current = function innerOnScroll(e) {
485+
requestAnimationFrame(() => {
486+
removeScrollEvent(scrollBar, stableBarOnScrollRef);
487+
checkForHeaderCollapse(contentContainer, contentScrollContainer, scrollBar, innerScrollBar, e);
488+
requestAnimationFrame(() => {
489+
bindScrollEvent(scrollBar, stableBarOnScrollRef);
490+
});
425491
});
426492
};
427493

428-
contentContainer.current.addEventListener('scroll', stableOnScrollRef.current);
494+
stableBarOnScrollRef.current = function innerBarOnScroll(e) {
495+
requestAnimationFrame(() => {
496+
removeScrollEvent(contentContainer, stableContentOnScrollRef);
497+
checkForHeaderCollapse(scrollBar, innerScrollBar, contentContainer, contentScrollContainer, e);
429498

430-
return stableOnScrollRef.current;
499+
requestAnimationFrame(() => {
500+
bindScrollEvent(contentContainer, stableContentOnScrollRef);
501+
});
502+
});
503+
};
504+
505+
bindScrollEvent(contentContainer, stableContentOnScrollRef);
506+
bindScrollEvent(scrollBar, stableBarOnScrollRef);
431507
}, [
432508
noHeader,
509+
checkForHeaderCollapse,
433510
alwaysShowContentHeader,
434511
scrollBar.current,
512+
innerScrollBar.current,
435513
innerHeader.current,
436514
contentContainer.current,
437-
expandedHeaderHeight.current,
515+
contentScrollContainer.current,
438516
collapsedHeaderFiller.current,
439517
collapsedHeader,
440518
setCollapsedHeader,
441519
expandHeaderActive,
442520
getProportionateScrollTop
443521
]);
444522

445-
useLayoutEffect(() => {
446-
if (!isMounted) return;
447-
adjustDummyDivHeight().then(() => {
448-
if (!hideHeaderButtonPressed.current) {
449-
const innerHeaderHeight = innerHeader.current.getBoundingClientRect().height;
450-
const base = collapsedHeader ? expandedHeaderHeight.current + 5 : innerHeaderHeight - 50;
451-
contentContainer.current.scrollTop = base;
452-
scrollBar.current.scrollTop = collapsedHeader ? base : getProportionateScrollTop(base);
453-
}
454-
hideHeaderButtonPressed.current = false;
455-
});
456-
}, [collapsedHeader]);
457-
458-
useLayoutEffect(() => {
523+
useEffect(() => {
459524
if (!isMounted) return;
460525
adjustDummyDivHeight();
461526
}, [expandHeaderActive]);

0 commit comments

Comments
 (0)