@ui5/webcomponents-react
Version:
React Wrapper for UI5 Web Components and additional components
766 lines (757 loc) • 33.3 kB
JavaScript
'use client';
import AvatarSize from '@ui5/webcomponents/dist/types/AvatarSize.js';
import { debounce, enrichEventWithDetails, ThemingParameters, useIsomorphicLayoutEffect, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base';
import { clsx } from 'clsx';
import { cloneElement, Children, forwardRef, isValidElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ObjectPageMode } from '../../enums/ObjectPageMode.js';
import { safeGetChildrenArray } from '../../internal/safeGetChildrenArray.js';
import { useObserveHeights } from '../../internal/useObserveHeights.js';
import { Tab } from '../../webComponents/Tab/index.js';
import { TabContainer } from '../../webComponents/TabContainer/index.js';
import { ObjectPageAnchorBar } from '../ObjectPageAnchorBar/index.js';
import { CollapsedAvatar } from './CollapsedAvatar.js';
import { ObjectPageContext } from './context.js';
import { classNames, styleData } from './ObjectPage.module.css.js';
import { getSectionById, getSectionElementById } from './ObjectPageUtils.js';
import { useHandleTabSelect } from './useHandleTabSelect.js';
import { useOnScrollEnd } from './useOnScrollEnd.js';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const ObjectPageCssVariables = {
headerDisplay: '--_ui5wcr_ObjectPage_header_display',
titleFontSize: '--_ui5wcr_ObjectPage_title_fontsize',
fullHeaderHeight: '--_ui5wcr_ObjectPage_header_height'
};
const TAB_CONTAINER_HEADER_HEIGHT = 44 + 4; // tabbar height + custom 4px padding-block-start
/**
* A component that allows apps to easily display information related to a business object.
*
* The `ObjectPage` is composed of a header (title and content) and block content wrapped in sections and subsections that structure the information.
*/
const ObjectPage = /*#__PURE__*/forwardRef((props, ref) => {
const {
titleArea,
image,
footerArea,
mode = ObjectPageMode.Default,
imageShapeCircle,
className,
style,
slot,
children,
selectedSectionId,
headerPinned: headerPinnedProp,
headerArea,
hidePinButton,
preserveHeaderStateOnClick,
accessibilityAttributes,
placeholder,
onSelectedSectionChange,
onToggleHeaderArea,
onPinButtonToggle,
onBeforeNavigate,
...rest
} = props;
useStylesheet(styleData, ObjectPage.displayName);
// memo necessary due to side effects triggered on each update
const childrenArray = useMemo(() => safeGetChildrenArray(children), [children]);
const firstSectionId = childrenArray[0]?.props?.id;
const [internalSelectedSectionId, setInternalSelectedSectionId] = useState(selectedSectionId ?? firstSectionId);
const [tabSelectId, setTabSelectId] = useState(null);
const titleAreaInteractive = headerArea && !preserveHeaderStateOnClick;
const hasOnlySingleSection = Children.count(children) === 1;
const tabContainerHeaderHeight = hasOnlySingleSection ? 1 : TAB_CONTAINER_HEADER_HEIGHT;
const isProgrammaticallyScrolled = useRef(false);
const [componentRef, objectPageRef] = useSyncRef(ref);
const topHeaderRef = useRef(null);
const prevTopHeaderHeight = useRef(0);
// @ts-expect-error: useSyncRef will create a ref if not present
const [componentRefHeaderContent, headerContentRef] = useSyncRef(headerArea?.ref);
const scrollEvent = useRef(undefined);
const objectPageContentRef = useRef(null);
const selectionScrollTimeout = useRef(null);
const isToggledRef = useRef(false);
const scrollTimeout = useRef(0);
const prevInternalSelectedSectionId = useRef(internalSelectedSectionId);
const [selectedSubSectionId, setSelectedSubSectionId] = useState(undefined);
const [headerPinned, setHeaderPinned] = useState(headerPinnedProp);
const [isMounted, setIsMounted] = useState(false);
const [headerCollapsedInternal, setHeaderCollapsedInternal] = useState(undefined);
const [scrolledHeaderExpanded, setScrolledHeaderExpanded] = useState(false);
const [sectionSpacer, setSectionSpacer] = useState(0);
const currentTabModeSection = useMemo(() => mode === ObjectPageMode.IconTabBar ? getSectionById(children, internalSelectedSectionId) : null, [mode, children, internalSelectedSectionId]);
const [toggledCollapsedHeaderWasVisible, setToggledCollapsedHeaderWasVisible] = useState(false);
const sections = mode === ObjectPageMode.IconTabBar ? currentTabModeSection : children;
const scrollEndHandler = useOnScrollEnd({
objectPageRef,
setTabSelectId
});
// only required for IconTabBar mode
const [wasUserSectionChange, setWasUserSectionChange] = useState(false);
const onSelectedSectionChangeRef = useRef(onSelectedSectionChange);
const onToggleHeaderAreaRef = useRef(onToggleHeaderArea);
const onScrollRef = useRef(rest.onScroll);
// Keep refs in sync with props to avoid stale closure
onSelectedSectionChangeRef.current = onSelectedSectionChange;
onToggleHeaderAreaRef.current = onToggleHeaderArea;
onScrollRef.current = rest.onScroll;
const fireOnSelectedChangedEvent = (targetEvent, index, id, section) => {
if (typeof onSelectedSectionChangeRef.current === 'function' && targetEvent && prevInternalSelectedSectionId.current !== id) {
onSelectedSectionChangeRef.current(enrichEventWithDetails(targetEvent, {
selectedSectionIndex: typeof index === 'number' ? index : parseInt(index, 10),
selectedSectionId: id,
section
}));
prevInternalSelectedSectionId.current = id;
}
};
const debouncedOnSectionChange = useRef(debounce(fireOnSelectedChangedEvent, 500)).current;
useEffect(() => {
return () => {
debouncedOnSectionChange.cancel();
if (selectionScrollTimeout.current) {
// Access .current at cleanup time to clear the actual timeout, not the stale value from mount
// eslint-disable-next-line react-hooks/exhaustive-deps
clearTimeout(selectionScrollTimeout.current);
}
};
// debouncedOnSectionChange and selectionScrollTimeout are stable refs
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// observe heights of header parts
const {
topHeaderHeight,
headerContentHeight,
totalHeaderHeight,
headerCollapsed
} = useObserveHeights(objectPageRef, topHeaderRef, headerContentRef, [headerCollapsedInternal, setHeaderCollapsedInternal], {
noHeader: !titleArea && !headerArea,
fixedHeader: headerPinned,
scrollTimeout
});
const scrollPaddingBlock = `${Math.ceil(12 + topHeaderHeight + tabContainerHeaderHeight + (!headerCollapsed && headerPinned ? headerContentHeight : 0))}px ${footerArea ? 'calc(var(--_ui5wcr-BarHeight) + 1.25rem)' : 0}`;
const onToggleHeaderContentVisibility = useCallback(e => {
isToggledRef.current = true;
scrollTimeout.current = performance.now() + 500;
setToggledCollapsedHeaderWasVisible(false);
if (!e.detail.visible) {
if (objectPageRef.current.scrollTop <= headerContentHeight) {
setToggledCollapsedHeaderWasVisible(true);
if (firstSectionId === internalSelectedSectionId || mode === ObjectPageMode.IconTabBar) {
objectPageRef.current.scrollTop = 0;
}
}
setHeaderCollapsedInternal(true);
setScrolledHeaderExpanded(false);
} else {
setHeaderCollapsedInternal(false);
if (objectPageRef.current.scrollTop >= headerContentHeight && objectPageRef.current.scrollTop > 0) {
setScrolledHeaderExpanded(true);
}
}
}, [headerContentHeight, firstSectionId, internalSelectedSectionId, mode, objectPageRef]);
useEffect(() => {
if (typeof onToggleHeaderAreaRef.current === 'function' && isToggledRef.current) {
onToggleHeaderAreaRef.current(headerCollapsed !== true);
}
}, [headerCollapsed]);
useEffect(() => {
const objectPageNode = objectPageRef.current;
if (objectPageNode) {
Object.assign(objectPageNode, {
toggleHeaderArea(snapped) {
if (typeof snapped === 'boolean') {
onToggleHeaderContentVisibility({
detail: {
visible: !snapped
}
});
} else {
onToggleHeaderContentVisibility({
detail: {
visible: !!headerCollapsed
}
});
}
}
});
}
}, [headerCollapsed, onToggleHeaderContentVisibility, objectPageRef]);
const avatar = useMemo(() => {
if (!image) {
return null;
}
if (typeof image === 'string') {
return /*#__PURE__*/_jsx("span", {
className: classNames.headerImage,
style: {
borderRadius: imageShapeCircle ? '50%' : 0,
overflow: 'hidden'
},
"data-component-name": "ObjectPageHeaderImage",
children: /*#__PURE__*/_jsx("img", {
src: image,
className: classNames.image,
alt: "Company Logo"
})
});
} else {
return /*#__PURE__*/cloneElement(image, {
size: AvatarSize.L,
className: clsx(classNames.headerImage, image.props?.className),
'data-component-name': 'ObjectPageHeaderImage'
});
}
}, [image, imageShapeCircle]);
const scrollToSectionById = useCallback((id, isSubSection = false) => {
const scroll = () => {
const section = getSectionElementById(objectPageRef.current, isSubSection, id);
scrollTimeout.current = performance.now() + 500;
if (section) {
const safeTopHeaderHeight = topHeaderHeight || prevTopHeaderHeight.current;
const scrollMargin = -1 /* reduce margin-block so that intersection observer detects correct section*/ + safeTopHeaderHeight + tabContainerHeaderHeight + (headerPinned && !headerCollapsed ? headerContentHeight : 0);
section.style.scrollMarginBlockStart = scrollMargin + 'px';
if (isSubSection) {
section.focus();
}
const sectionRect = section.getBoundingClientRect();
const objectPageElement = objectPageRef.current;
const objectPageRect = objectPageElement.getBoundingClientRect();
// Calculate the top position of the section relative to the container
objectPageElement.scrollTop = sectionRect.top - objectPageRect.top + objectPageElement.scrollTop - scrollMargin;
section.style.scrollMarginBlockStart = '';
}
};
// In TabBar mode the section is only rendered when selected: delay scroll for subsection
if (mode === ObjectPageMode.IconTabBar && isSubSection) {
setTimeout(scroll, 300);
} else {
scroll();
}
}, [mode, objectPageRef, topHeaderHeight, tabContainerHeaderHeight, headerPinned, headerCollapsed, headerContentHeight]);
const scrollToSection = useCallback(sectionId => {
if (!sectionId) {
return;
}
if (firstSectionId === sectionId) {
objectPageRef.current?.scrollTo({
top: 0
});
} else {
scrollToSectionById(sectionId);
}
isProgrammaticallyScrolled.current = false;
}, [firstSectionId, scrollToSectionById, objectPageRef]);
// section was selected by clicking on the tab bar buttons
const handleOnSectionSelected = (targetEvent, newSelectionSectionId, index, section) => {
isProgrammaticallyScrolled.current = true;
debouncedOnSectionChange.cancel();
setSelectedSubSectionId(undefined);
setInternalSelectedSectionId(prevSelectedSection => {
if (prevSelectedSection === newSelectionSectionId && mode !== ObjectPageMode.IconTabBar) {
scrollToSection(newSelectionSectionId);
}
return newSelectionSectionId;
});
// Reset scroll for section swap; scrollTimeout preserves current header collapsed/expanded state.
if (mode === ObjectPageMode.IconTabBar) {
scrollTimeout.current = performance.now() + 500;
objectPageRef.current?.scrollTo({
top: 0
});
}
setTabSelectId(newSelectionSectionId);
scrollEvent.current = targetEvent;
if (isMounted && mode === ObjectPageMode.Default) {
getSectionElementById(objectPageContentRef.current, false, newSelectionSectionId)?.focus({
preventScroll: true
});
}
fireOnSelectedChangedEvent(targetEvent, index, newSelectionSectionId, section);
};
useIsomorphicLayoutEffect(() => {
if (selectedSectionId) {
const fireSelectEvent = () => {
const selectedSection = getSectionElementById(objectPageRef.current, false, selectedSectionId);
if (selectedSection) {
const selectedSectionIndex = Array.from(selectedSection.parentElement.querySelectorAll(':scope > [data-component-name="ObjectPageSection"]')).indexOf(selectedSection);
handleOnSectionSelected({}, selectedSectionId, selectedSectionIndex, selectedSection);
}
};
if (mode === ObjectPageMode.IconTabBar) {
scrollTimeout.current = performance.now() + 500;
setInternalSelectedSectionId(selectedSectionId);
getSectionElementById(objectPageContentRef.current, false, selectedSectionId)?.focus({
preventScroll: true
});
// In TabBar mode the section is only rendered when selected, therefore delay firing the event until the section is available in the DOM
setTimeout(fireSelectEvent);
} else {
fireSelectEvent();
}
}
}, [selectedSectionId, mode]);
// do internal scrolling
useEffect(() => {
if (mode === ObjectPageMode.Default && isProgrammaticallyScrolled.current === true && !selectedSubSectionId) {
scrollToSection(internalSelectedSectionId);
}
}, [internalSelectedSectionId, mode, selectedSubSectionId, scrollToSection]);
// Scrolling for Sub Section Selection
useEffect(() => {
if (selectedSubSectionId && isProgrammaticallyScrolled.current === true) {
scrollToSectionById(selectedSubSectionId, true);
isProgrammaticallyScrolled.current = false;
}
}, [selectedSubSectionId, sectionSpacer, scrollToSectionById]);
useEffect(() => {
if (headerPinnedProp !== undefined) {
setHeaderPinned(headerPinnedProp);
}
if (headerPinnedProp) {
onToggleHeaderContentVisibility({
detail: {
visible: true
}
});
}
}, [headerPinnedProp, onToggleHeaderContentVisibility]);
const prevHeaderPinned = useRef(headerPinned);
useEffect(() => {
if (prevHeaderPinned.current && !headerPinned && objectPageRef.current.scrollTop > topHeaderHeight) {
onToggleHeaderContentVisibility({
detail: {
visible: false
}
});
prevHeaderPinned.current = false;
}
if (!prevHeaderPinned.current && headerPinned) {
prevHeaderPinned.current = true;
}
}, [headerPinned, topHeaderHeight, onToggleHeaderContentVisibility, objectPageRef]);
const isInitialTabBarMode = useRef(true);
useEffect(() => {
if (!isMounted) {
requestAnimationFrame(() => setIsMounted(true));
return;
}
setSelectedSubSectionId(props.selectedSubSectionId);
if (props.selectedSubSectionId) {
isProgrammaticallyScrolled.current = true;
if (mode === ObjectPageMode.IconTabBar) {
let sectionId;
let curSection;
let sectionIndex = -1;
childrenArray.forEach((section, index) => {
if (/*#__PURE__*/isValidElement(section) && section.props && section.props.children) {
safeGetChildrenArray(section.props.children).forEach(subSection => {
if (/*#__PURE__*/isValidElement(subSection) && subSection.props && subSection.props.id === props.selectedSubSectionId) {
curSection = section;
sectionId = section.props?.id;
sectionIndex = index;
}
});
}
});
if (sectionId) {
if (!isInitialTabBarMode.current) {
//In TabBar mode the section is often not scrolled when subsection changes, thus the onSelectedSectionChange isn't fired
debouncedOnSectionChange({}, sectionIndex, sectionId, curSection);
}
setInternalSelectedSectionId(sectionId);
}
}
}
isInitialTabBarMode.current = false;
}, [props.selectedSubSectionId, isMounted, childrenArray, debouncedOnSectionChange, mode]);
const tabContainerContainerRef = useRef(null);
const isHeaderPinnedAndExpanded = headerPinned && !headerCollapsed;
useEffect(() => {
const objectPage = objectPageRef.current;
const tabContainerContainer = tabContainerContainerRef.current;
if (!objectPage || !tabContainerContainer) {
return;
}
const footerElement = objectPage.querySelector('[data-component-name="ObjectPageFooter"]');
const topHeaderElement = objectPage.querySelector('[data-component-name="ObjectPageTopHeader"]');
const calculateSpacer = ([lastSectionNodeEntry]) => {
const lastSectionNode = lastSectionNodeEntry?.target;
if (!lastSectionNode) {
setSectionSpacer(0);
return;
}
const subSections = lastSectionNode.querySelectorAll('[id^="ObjectPageSubSection"]');
const lastSubSection = subSections[subSections.length - 1];
const lastSubSectionOrSection = lastSubSection ?? lastSectionNode;
if (currentTabModeSection && !lastSubSection || sectionNodes.length === 1 && !lastSubSection) {
setSectionSpacer(0);
return;
}
// batching DOM-reads together minimizes reflow
const footerHeight = footerElement?.offsetHeight ?? 0;
const objectPageRect = objectPage.getBoundingClientRect();
const tabContainerContainerRect = tabContainerContainer.getBoundingClientRect();
const lastSubSectionOrSectionRect = lastSubSectionOrSection.getBoundingClientRect();
let stickyHeaderBottom = 0;
if (!isHeaderPinnedAndExpanded) {
const topHeaderBottom = topHeaderElement?.getBoundingClientRect().bottom ?? 0;
stickyHeaderBottom = topHeaderBottom + tabContainerContainerRect.height;
} else {
stickyHeaderBottom = tabContainerContainerRect.bottom;
}
const spacer = Math.ceil(objectPageRect.bottom - stickyHeaderBottom - lastSubSectionOrSectionRect.height - footerHeight // section padding (8px) not included, so that the intersection observer is triggered correctly
);
setSectionSpacer(Math.max(spacer, 0));
};
const observer = new ResizeObserver(calculateSpacer);
const sectionNodes = objectPage.querySelectorAll('[id^="ObjectPageSection"]');
const lastSectionNode = sectionNodes[sectionNodes.length - 1];
if (lastSectionNode) {
observer.observe(lastSectionNode, {
box: 'border-box'
});
}
return () => {
observer.disconnect();
};
}, [topHeaderHeight, headerContentHeight, currentTabModeSection, children, mode, isHeaderPinnedAndExpanded, hasOnlySingleSection, objectPageRef]);
const {
onScroll: _0,
selectedSubSectionId: _1,
...propsWithoutOmitted
} = rest;
const visibleSectionIds = useRef(new Set());
useEffect(() => {
// section observers are not required in TabBar mode
if (mode === ObjectPageMode.IconTabBar) {
return;
}
const sectionNodes = objectPageRef.current?.querySelectorAll('section[data-component-name="ObjectPageSection"]');
// only the sticky part of the header must be added as margin
const rootMargin = `-${(headerPinned && !headerCollapsed || scrolledHeaderExpanded ? totalHeaderHeight : topHeaderHeight) + tabContainerHeaderHeight}px 0px 0px 0px`;
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const sectionId = entry.target.id;
if (entry.isIntersecting) {
visibleSectionIds.current.add(sectionId);
} else {
visibleSectionIds.current.delete(sectionId);
}
let currentIndex;
const sortedVisibleSections = Array.from(sectionNodes).filter((section, index) => {
const isVisibleSection = visibleSectionIds.current.has(section.id);
if (currentIndex === undefined && isVisibleSection) {
currentIndex = index;
}
return visibleSectionIds.current.has(section.id);
});
if (sortedVisibleSections.length > 0) {
const section = sortedVisibleSections[0];
const id = sortedVisibleSections[0].id.slice(18);
setInternalSelectedSectionId(id);
debouncedOnSectionChange(scrollEvent.current, currentIndex, id, section);
}
});
}, {
root: objectPageRef.current,
rootMargin,
threshold: [0]
});
sectionNodes.forEach(el => {
observer.observe(el);
});
return () => {
observer.disconnect();
};
}, [totalHeaderHeight, headerPinned, headerCollapsed, topHeaderHeight, childrenArray.length, scrolledHeaderExpanded, mode, objectPageRef, debouncedOnSectionChange, tabContainerHeaderHeight]);
const onTitleClick = e => {
e.stopPropagation();
if (titleAreaInteractive) {
onToggleHeaderContentVisibility(enrichEventWithDetails(e, {
visible: headerCollapsed
}));
}
};
const renderHeaderContentSection = () => {
if (headerArea?.props) {
return /*#__PURE__*/cloneElement(headerArea, {
...headerArea.props,
topHeaderHeight,
style: headerCollapsed === true ? {
...headerArea.props.style,
position: 'absolute',
visibility: 'hidden',
flexShrink: 0,
insetInline: 0
} : {
...headerArea.props.style,
flexShrink: 0
},
headerPinned: headerPinned || scrolledHeaderExpanded,
//@ts-expect-error: todo remove me when forwardref has been replaced
ref: componentRefHeaderContent,
children: /*#__PURE__*/_jsxs("div", {
className: clsx(classNames.headerContainer, avatar && classNames.hasAvatar),
"data-component-name": "ObjectPageHeaderContainer",
children: [avatar, headerArea.props.children && /*#__PURE__*/_jsx("div", {
"data-component-name": "ObjectPageHeaderContent",
children: headerArea.props.children
})]
})
});
}
};
const prevScrollTop = useRef(undefined);
const onObjectPageScroll = useCallback(e => {
const target = e.target;
scrollEndHandler(e);
if (!isToggledRef.current) {
isToggledRef.current = true;
}
if (scrollTimeout.current >= performance.now()) {
return;
}
setToggledCollapsedHeaderWasVisible(false);
scrollEvent.current = e;
if (typeof onScrollRef.current === 'function') {
onScrollRef.current(e);
}
if (selectedSubSectionId) {
setSelectedSubSectionId(undefined);
}
if (selectionScrollTimeout.current) {
clearTimeout(selectionScrollTimeout.current);
}
if (!headerPinned || target.scrollTop === 0) {
objectPageRef.current?.classList.remove(classNames.headerCollapsed);
}
if (scrolledHeaderExpanded && target.scrollTop !== prevScrollTop.current) {
if (target.scrollHeight - target.scrollTop === target.clientHeight) {
return;
}
prevScrollTop.current = target.scrollTop;
if (!headerPinned) {
setHeaderCollapsedInternal(true);
}
setScrolledHeaderExpanded(false);
}
}, [headerPinned, scrolledHeaderExpanded, selectedSubSectionId, objectPageRef, scrollEndHandler]);
const onHoverToggleButton = useCallback(e => {
if (e.type === 'mouseover') {
topHeaderRef.current?.classList.add(classNames.headerHoverStyles);
} else {
topHeaderRef.current?.classList.remove(classNames.headerHoverStyles);
}
}, []);
const handleTabSelect = useHandleTabSelect({
onBeforeNavigate,
headerPinned,
mode,
setHeaderCollapsedInternal,
setScrolledHeaderExpanded,
childrenArray,
handleOnSectionSelected,
isProgrammaticallyScrolled,
setInternalSelectedSectionId,
objectPageRef,
debouncedOnSectionChange,
scrollTimeout,
setSelectedSubSectionId,
setTabSelectId,
setWasUserSectionChange
});
const objectPageStyles = {
...style,
[ObjectPageCssVariables.fullHeaderHeight]: headerPinned || scrolledHeaderExpanded ? `${topHeaderHeight + (headerCollapsed === true ? 0 : headerContentHeight) + tabContainerHeaderHeight}px` : `${topHeaderHeight + tabContainerHeaderHeight}px`
};
if (headerCollapsed === true && headerArea) {
objectPageStyles[ObjectPageCssVariables.titleFontSize] = ThemingParameters.sapObjectHeader_Title_SnappedFontSize;
}
useEffect(() => {
if (isMounted && children && mode === ObjectPageMode.Default) {
const firstSection = objectPageContentRef.current.querySelector('[data-component-name="ObjectPageSection"]');
if (firstSection) {
firstSection.tabIndex = 0;
}
}
}, [isMounted, children, mode]);
const handleContentBlur = e => {
const opNode = objectPageRef.current;
if (!opNode) return;
if (e.relatedTarget && !e.currentTarget.contains(e.relatedTarget)) {
opNode.style.scrollPaddingBlock = '0px';
// Fallback: Some (ui5-table) ui5wc components don't implement `relatedTarget` as expected.
} else if (!e.relatedTarget) {
const currentTarget = e.currentTarget;
opNode.style.scrollPaddingBlock = '0px';
requestAnimationFrame(() => {
if (currentTarget.contains(document.activeElement)) {
opNode.style.scrollPaddingBlock = scrollPaddingBlock;
document.activeElement.scrollIntoView({
block: 'nearest'
});
}
});
}
};
return /*#__PURE__*/_jsx(ObjectPageContext.Provider, {
value: mode,
children: /*#__PURE__*/_jsxs("div", {
ref: componentRef,
"data-component-name": "ObjectPage",
slot: slot,
className: clsx(classNames.objectPage, className, mode === ObjectPageMode.IconTabBar && classNames.iconTabBarMode),
style: objectPageStyles,
onScroll: onObjectPageScroll,
"data-in-iframe": window && window.self !== window.top,
...propsWithoutOmitted,
children: [/*#__PURE__*/_jsxs("header", {
onMouseOver: onHoverToggleButton,
onMouseLeave: onHoverToggleButton,
"data-component-name": "ObjectPageTopHeader",
ref: topHeaderRef,
role: accessibilityAttributes?.objectPageTopHeader?.role,
"data-not-clickable": !titleAreaInteractive,
"aria-roledescription": accessibilityAttributes?.objectPageTopHeader?.ariaRoledescription ?? 'Object Page header',
className: classNames.header,
children: [/*#__PURE__*/_jsx("span", {
className: classNames.clickArea,
onClick: onTitleClick,
"data-component-name": "ObjectPageTitleAreaClickElement"
}), titleArea && /*#__PURE__*/cloneElement(titleArea, {
className: clsx(titleArea?.props?.className),
onToggleHeaderContentVisibility: onTitleClick,
'data-not-clickable': !titleAreaInteractive,
'data-header-content-visible': headerArea && headerCollapsed !== true,
_snappedAvatar: /*#__PURE__*/_jsx(CollapsedAvatar, {
image: image,
imageShapeCircle: imageShapeCircle,
hideCollapsedAvatar: !image || headerArea && !headerCollapsed
})
})]
}), renderHeaderContentSection(), headerArea && titleArea && /*#__PURE__*/_jsx("div", {
"data-component-name": "ObjectPageAnchorBar",
className: classNames.anchorBar,
style: {
top: scrolledHeaderExpanded || headerPinned ? `${topHeaderHeight + (headerCollapsed === true ? 0 : headerContentHeight)}px` : `${topHeaderHeight}px`
},
children: /*#__PURE__*/_jsx(ObjectPageAnchorBar, {
headerContentVisible: headerCollapsed !== true,
hidePinButton: !!hidePinButton,
headerPinned: headerPinned,
accessibilityAttributes: accessibilityAttributes?.objectPageAnchorBar,
onToggleHeaderContentVisibility: onToggleHeaderContentVisibility,
setHeaderPinned: setHeaderPinned,
onHoverToggleButton: onHoverToggleButton,
onPinButtonToggle: onPinButtonToggle
})
}), hasOnlySingleSection && /*#__PURE__*/_jsx("div", {
ref: tabContainerContainerRef,
"aria-hidden": "true",
"data-component-name": "ObjectPageTabContainerPlaceholder",
className: classNames.tabContainerPlaceholder,
style: {
insetBlockStart: headerPinned || scrolledHeaderExpanded ? `${topHeaderHeight + (headerCollapsed === true ? 0 : headerContentHeight)}px` : `${topHeaderHeight}px`
}
}), !placeholder && !hasOnlySingleSection && /*#__PURE__*/_jsxs("div", {
ref: tabContainerContainerRef,
className: classNames.tabContainer,
"data-component-name": "ObjectPageTabContainer",
style: {
insetBlockStart: headerPinned || scrolledHeaderExpanded ? `${topHeaderHeight + (headerCollapsed === true ? 0 : headerContentHeight)}px` : `${topHeaderHeight}px`
},
children: [/*#__PURE__*/_jsx("div", {
"aria-hidden": "true",
className: classNames.tabContainerSpacer
}), /*#__PURE__*/_jsx(TabContainer, {
collapsed: true,
onTabSelect: handleTabSelect,
"data-component-name": "ObjectPageTabContainer",
className: classNames.tabContainerComponent,
children: childrenArray.map((section, index) => {
if (! /*#__PURE__*/isValidElement(section) || !section.props) return null;
const subTabs = safeGetChildrenArray(section.props.children).filter(subSection =>
// @ts-expect-error: if the `ObjectPageSubSection` component is passed as children, the `displayName` is available. Otherwise, the default children should be rendered w/o additional logic.
/*#__PURE__*/isValidElement(subSection) && subSection?.type?.displayName === 'ObjectPageSubSection');
return /*#__PURE__*/_jsx(Tab, {
ref: section.props.tabRef,
"data-index": index,
"data-section-id": section.props.id,
text: section.props.titleText,
selected: tabSelectId && tabSelectId === section.props?.id || !tabSelectId && internalSelectedSectionId === section.props?.id || undefined,
items: subTabs.map(item => {
if (! /*#__PURE__*/isValidElement(item)) {
return null;
}
return /*#__PURE__*/_jsx(Tab, {
"data-parent-id": section.props.id,
"data-is-sub-tab": true,
"data-section-id": item.props.id,
text: item.props.titleText,
selected: item.props.id === selectedSubSectionId || undefined,
"data-index": index,
children: /*#__PURE__*/_jsx("span", {
style: {
display: 'none'
}
})
}, item.props.id);
}),
children: /*#__PURE__*/_jsx("span", {
style: {
display: 'none'
}
})
}, `Anchor-${section.props?.id}`);
})
})]
}), /*#__PURE__*/_jsxs("div", {
"data-component-name": "ObjectPageContent",
className: classNames.content,
ref: node => {
if (node) {
if (mode === ObjectPageMode.IconTabBar && wasUserSectionChange) {
node.querySelector('[data-component-name="ObjectPageSection"]')?.focus({
preventScroll: true
});
}
objectPageContentRef.current = node;
}
setWasUserSectionChange(false);
}
// prevent content scroll when elements outside the content are focused
,
onFocus: () => {
const opNode = objectPageRef.current;
if (opNode) {
// 12px or 0.75rem margin for ui5wc border and input margins
opNode.style.scrollPaddingBlock = scrollPaddingBlock;
}
},
onBlur: handleContentBlur,
children: [/*#__PURE__*/_jsx("div", {
style: {
height: (headerCollapsed && !headerPinned || scrolledHeaderExpanded) && !toggledCollapsedHeaderWasVisible && !(mode === ObjectPageMode.IconTabBar && scrollTimeout.current >= performance.now()) ? `${headerContentHeight}px` : 0
},
"aria-hidden": "true"
}), placeholder ? placeholder : sections, /*#__PURE__*/_jsx("div", {
style: {
height: `${sectionSpacer}px`
},
"aria-hidden": "true"
})]
}), footerArea && mode === ObjectPageMode.IconTabBar && !sectionSpacer && /*#__PURE__*/_jsx("div", {
className: classNames.footerSpacer,
"data-component-name": "ObjectPageFooterSpacer",
"aria-hidden": "true"
}), footerArea && /*#__PURE__*/_jsx("footer", {
role: accessibilityAttributes?.objectPageFooterArea?.role,
className: classNames.footer,
"data-component-name": "ObjectPageFooter",
children: footerArea
})]
})
});
});
ObjectPage.displayName = 'ObjectPage';
export { ObjectPage };