UNPKG

@ui5/webcomponents-react

Version:

React Wrapper for UI5 Web Components and additional components

766 lines (757 loc) 33.3 kB
'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 };