Skip to content

Commit 2e09839

Browse files
authored
fix(ObjectPage): improve selection & scroll behavior (SAP#6492)
Fixes SAP#6478
1 parent 76b925b commit 2e09839

File tree

3 files changed

+35
-77
lines changed

3 files changed

+35
-77
lines changed

packages/main/src/components/ObjectPage/ObjectPage.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ Also, when using the legacy `Toolbar` there are some things to consider that wor
105105

106106
```tsx
107107
import { useRef } from 'react';
108-
import { Toolbar as LegacyToolbar, ToolbarSpacer as LegacyToolbarSpacer } from '@ui5/webcomponents-react-compat';
108+
import { Toolbar as LegacyToolbar } from '@ui5/webcomponents-react-compat/dist/components/Toolbar/index.js';
109+
import { ToolbarSpacer as LegacyToolbarSpacer } from '@ui5/webcomponents-react-compat/dist/components/ToolbarSpacer/index.js';
109110
import { Button, ButtonDesign, ObjectPage, ObjectPageSection, ObjectPageTitle } from '@ui5/webcomponents-react';
110111
import type { ObjectPageDomRef } from '@ui5/webcomponents-react';
111112

packages/main/src/components/ObjectPage/ObjectPageUtils.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,3 @@ export const getSectionById = (sections, id) => {
1111
);
1212
});
1313
};
14-
15-
export const extractSectionIdFromHtmlId = (id: string) => {
16-
if (!id) return null;
17-
return id.replace(/^ObjectPageSection-/, '');
18-
};

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

Lines changed: 33 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { clsx } from 'clsx';
1313
import type { CSSProperties, ReactElement, ReactNode } from 'react';
1414
import { cloneElement, forwardRef, isValidElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
1515
import { ObjectPageMode } from '../../enums/index.js';
16-
import { addCustomCSSWithScoping } from '../../internal/addCustomCSSWithScoping.js';
1716
import { safeGetChildrenArray } from '../../internal/safeGetChildrenArray.js';
1817
import { useObserveHeights } from '../../internal/useObserveHeights.js';
1918
import type { CommonProps, Ui5CustomEvent } from '../../types/index.js';
@@ -32,17 +31,7 @@ import type {
3231
} from '../ObjectPageTitle/index.js';
3332
import { CollapsedAvatar } from './CollapsedAvatar.js';
3433
import { classNames, styleData } from './ObjectPage.module.css.js';
35-
import { extractSectionIdFromHtmlId, getSectionById } from './ObjectPageUtils.js';
36-
37-
addCustomCSSWithScoping(
38-
'ui5-tabcontainer',
39-
// todo: the additional text span adds 3px to the container - needs to be investigated why
40-
`
41-
:host([data-component-name="ObjectPageTabContainer"]) [id$="additionalText"] {
42-
display: none;
43-
}
44-
`
45-
);
34+
import { getSectionById } from './ObjectPageUtils.js';
4635

4736
const ObjectPageCssVariables = {
4837
headerDisplay: '--_ui5wcr_ObjectPage_header_display',
@@ -239,7 +228,6 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
239228
const anchorBarRef = useRef<HTMLDivElement>(null);
240229
const objectPageContentRef = useRef<HTMLDivElement>(null);
241230
const selectionScrollTimeout = useRef(null);
242-
const [isAfterScroll, setIsAfterScroll] = useState(false);
243231
const isToggledRef = useRef(false);
244232
const [headerCollapsedInternal, setHeaderCollapsedInternal] = useState<undefined | boolean>(undefined);
245233
const [scrolledHeaderExpanded, setScrolledHeaderExpanded] = useState(false);
@@ -256,7 +244,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
256244

257245
const prevInternalSelectedSectionId = useRef(internalSelectedSectionId);
258246
const fireOnSelectedChangedEvent = (targetEvent, index, id, section) => {
259-
if (typeof onSelectedSectionChange === 'function' && prevInternalSelectedSectionId.current !== id) {
247+
if (typeof onSelectedSectionChange === 'function' && targetEvent && prevInternalSelectedSectionId.current !== id) {
260248
onSelectedSectionChange(
261249
enrichEventWithDetails(targetEvent, {
262250
selectedSectionIndex: parseInt(index, 10),
@@ -344,7 +332,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
344332
safeTopHeaderHeight +
345333
anchorBarHeight +
346334
TAB_CONTAINER_HEADER_HEIGHT +
347-
(headerPinned ? headerContentHeight : 0) +
335+
(headerPinned && !headerCollapsed ? headerContentHeight : 0) +
348336
'px';
349337
section.focus();
350338
section.scrollIntoView({ behavior: 'smooth' });
@@ -558,76 +546,53 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
558546

559547
const { onScroll: _0, selectedSubSectionId: _1, ...propsWithoutOmitted } = rest;
560548

549+
const visibleSectionIds = useRef<Set<string>>(new Set());
561550
useEffect(() => {
562551
const sectionNodes = objectPageRef.current?.querySelectorAll('section[data-component-name="ObjectPageSection"]');
563-
const objectPageHeight = objectPageRef.current?.clientHeight ?? 1000;
564-
const marginBottom = objectPageHeight - totalHeaderHeight - /*TabContainer*/ TAB_CONTAINER_HEADER_HEIGHT;
565-
const rootMargin = `-${totalHeaderHeight}px 0px -${marginBottom < 0 ? 0 : marginBottom}px 0px`;
552+
// only the sticky part of the header must be added as margin
553+
const rootMargin = `-${(headerPinned && !headerCollapsed ? totalHeaderHeight : topHeaderHeight) + TAB_CONTAINER_HEADER_HEIGHT}px 0px 0px 0px`;
554+
566555
const observer = new IntersectionObserver(
567-
([section]) => {
568-
if (section.isIntersecting && isProgrammaticallyScrolled.current === false) {
569-
if (
570-
objectPageRef.current.getBoundingClientRect().top + totalHeaderHeight + TAB_CONTAINER_HEADER_HEIGHT <=
571-
section.target.getBoundingClientRect().bottom
572-
) {
573-
const currentId = extractSectionIdFromHtmlId(section.target.id);
574-
setInternalSelectedSectionId(currentId);
575-
const currentIndex = safeGetChildrenArray(children).findIndex((objectPageSection) => {
576-
return (
577-
isValidElement(objectPageSection) &&
578-
(objectPageSection as ReactElement<ObjectPageSectionPropTypes>).props?.id === currentId
579-
);
580-
});
581-
debouncedOnSectionChange(scrollEvent.current, currentIndex, currentId, section.target);
556+
(entries) => {
557+
entries.forEach((entry) => {
558+
const sectionId = entry.target.id;
559+
if (entry.isIntersecting) {
560+
visibleSectionIds.current.add(sectionId);
561+
} else {
562+
visibleSectionIds.current.delete(sectionId);
582563
}
583-
}
564+
565+
let currentIndex: undefined | number;
566+
const sortedVisibleSections = Array.from(sectionNodes).filter((section, index) => {
567+
const isVisibleSection = visibleSectionIds.current.has(section.id);
568+
if (currentIndex === undefined && isVisibleSection) {
569+
currentIndex = index;
570+
}
571+
return visibleSectionIds.current.has(section.id);
572+
});
573+
574+
if (sortedVisibleSections.length > 0) {
575+
const section = sortedVisibleSections[0];
576+
const id = sortedVisibleSections[0].id.slice(18);
577+
setInternalSelectedSectionId(id);
578+
debouncedOnSectionChange(scrollEvent.current, currentIndex, id, section);
579+
}
580+
});
584581
},
585582
{
586583
root: objectPageRef.current,
587584
rootMargin,
588585
threshold: [0]
589586
}
590587
);
591-
592588
sectionNodes.forEach((el) => {
593589
observer.observe(el);
594590
});
595591

596592
return () => {
597593
observer.disconnect();
598594
};
599-
}, [children, totalHeaderHeight, setInternalSelectedSectionId, isProgrammaticallyScrolled]);
600-
601-
// Fallback when scrolling faster than the IntersectionObserver can observe (in most cases faster than 60fps)
602-
useEffect(() => {
603-
const sectionNodes = objectPageRef.current?.querySelectorAll('section[data-component-name="ObjectPageSection"]');
604-
if (isAfterScroll) {
605-
let currentSection = sectionNodes[sectionNodes.length - 1];
606-
let currentIndex: number;
607-
for (let i = 0; i <= sectionNodes.length - 1; i++) {
608-
const sectionNode = sectionNodes[i];
609-
if (
610-
objectPageRef.current.getBoundingClientRect().top + totalHeaderHeight + TAB_CONTAINER_HEADER_HEIGHT <=
611-
sectionNode.getBoundingClientRect().bottom
612-
) {
613-
currentSection = sectionNode;
614-
currentIndex = i;
615-
break;
616-
}
617-
}
618-
const currentSectionId = extractSectionIdFromHtmlId(currentSection?.id);
619-
if (currentSectionId !== internalSelectedSectionId) {
620-
setInternalSelectedSectionId(currentSectionId);
621-
debouncedOnSectionChange(
622-
scrollEvent.current,
623-
currentIndex ?? sectionNodes.length - 1,
624-
currentSectionId,
625-
currentSection
626-
);
627-
}
628-
setIsAfterScroll(false);
629-
}
630-
}, [isAfterScroll]);
595+
}, [children, totalHeaderHeight, setInternalSelectedSectionId, headerPinned, debouncedOnSectionChange]);
631596

632597
const onTitleClick = (e) => {
633598
e.stopPropagation();
@@ -721,9 +686,6 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
721686
if (selectionScrollTimeout.current) {
722687
clearTimeout(selectionScrollTimeout.current);
723688
}
724-
selectionScrollTimeout.current = setTimeout(() => {
725-
setIsAfterScroll(true);
726-
}, 100);
727689
if (!headerPinned || e.target.scrollTop === 0) {
728690
objectPageRef.current?.classList.remove(classNames.headerCollapsed);
729691
}
@@ -894,7 +856,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
894856
</div>
895857
)}
896858
<div data-component-name="ObjectPageContent" className={classNames.content} ref={objectPageContentRef}>
897-
<div style={{ height: headerCollapsed ? `${headerContentHeight}px` : 0 }} aria-hidden />
859+
<div style={{ height: headerCollapsed && !headerPinned ? `${headerContentHeight}px` : 0 }} aria-hidden />
898860
{placeholder ? placeholder : sections}
899861
<div style={{ height: `${sectionSpacer}px` }} aria-hidden />
900862
</div>

0 commit comments

Comments
 (0)