@@ -118,9 +118,12 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
118118 } , [ mode , children , internalSelectedSectionId ] ) ;
119119
120120 const onSelectedSectionChangeRef = useRef ( onSelectedSectionChange ) ;
121- // Keep ref in sync with prop to avoid stale closure in debounced function
122- // eslint-disable-next-line react-hooks/refs
121+ const onToggleHeaderAreaRef = useRef ( onToggleHeaderArea ) ;
122+ const onScrollRef = useRef ( rest . onScroll ) ;
123+ // Keep refs in sync with props to avoid stale closure
123124 onSelectedSectionChangeRef . current = onSelectedSectionChange ;
125+ onToggleHeaderAreaRef . current = onToggleHeaderArea ;
126+ onScrollRef . current = rest . onScroll ;
124127
125128 const fireOnSelectedChangedEvent = ( targetEvent , index : number | string , id : string , section ) => {
126129 if (
@@ -138,14 +141,16 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
138141 prevInternalSelectedSectionId . current = id ;
139142 }
140143 } ;
141- // Extracting .current immediately is safe - useRef creates stable reference on mount
142- // eslint-disable-next-line react-hooks/refs
143144 const debouncedOnSectionChange = useRef ( debounce ( fireOnSelectedChangedEvent , 500 ) ) . current ;
144145 useEffect ( ( ) => {
145146 return ( ) => {
146147 debouncedOnSectionChange . cancel ( ) ;
147- clearTimeout ( selectionScrollTimeout . current ) ;
148+ if ( selectionScrollTimeout . current ) {
149+ clearTimeout ( selectionScrollTimeout . current ) ;
150+ }
148151 } ;
152+ // debouncedOnSectionChange and selectionScrollTimeout are stable refs
153+ // eslint-disable-next-line react-hooks/exhaustive-deps
149154 } , [ ] ) ;
150155
151156 // observe heights of header parts
@@ -162,30 +167,33 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
162167 ) ;
163168 const scrollPaddingBlock = `${ Math . ceil ( 12 + topHeaderHeight + TAB_CONTAINER_HEADER_HEIGHT + ( ! headerCollapsed && headerPinned ? headerContentHeight : 0 ) ) } px ${ footerArea ? 'calc(var(--_ui5wcr-BarHeight) + 1.25rem)' : 0 } ` ;
164169
165- const onToggleHeaderContentVisibility = ( e ) => {
166- isToggledRef . current = true ;
167- scrollTimeout . current = performance . now ( ) + 500 ;
168- setToggledCollapsedHeaderWasVisible ( false ) ;
169- if ( ! e . detail . visible ) {
170- if ( objectPageRef . current . scrollTop <= headerContentHeight ) {
171- setToggledCollapsedHeaderWasVisible ( true ) ;
172- if ( firstSectionId === internalSelectedSectionId || mode === ObjectPageMode . IconTabBar ) {
173- objectPageRef . current . scrollTop = 0 ;
170+ const onToggleHeaderContentVisibility = useCallback (
171+ ( e ) => {
172+ isToggledRef . current = true ;
173+ scrollTimeout . current = performance . now ( ) + 500 ;
174+ setToggledCollapsedHeaderWasVisible ( false ) ;
175+ if ( ! e . detail . visible ) {
176+ if ( objectPageRef . current . scrollTop <= headerContentHeight ) {
177+ setToggledCollapsedHeaderWasVisible ( true ) ;
178+ if ( firstSectionId === internalSelectedSectionId || mode === ObjectPageMode . IconTabBar ) {
179+ objectPageRef . current . scrollTop = 0 ;
180+ }
181+ }
182+ setHeaderCollapsedInternal ( true ) ;
183+ setScrolledHeaderExpanded ( false ) ;
184+ } else {
185+ setHeaderCollapsedInternal ( false ) ;
186+ if ( objectPageRef . current . scrollTop >= headerContentHeight && objectPageRef . current . scrollTop > 0 ) {
187+ setScrolledHeaderExpanded ( true ) ;
174188 }
175189 }
176- setHeaderCollapsedInternal ( true ) ;
177- setScrolledHeaderExpanded ( false ) ;
178- } else {
179- setHeaderCollapsedInternal ( false ) ;
180- if ( objectPageRef . current . scrollTop >= headerContentHeight && objectPageRef . current . scrollTop > 0 ) {
181- setScrolledHeaderExpanded ( true ) ;
182- }
183- }
184- } ;
190+ } ,
191+ [ headerContentHeight , firstSectionId , internalSelectedSectionId , mode , objectPageRef ] ,
192+ ) ;
185193
186194 useEffect ( ( ) => {
187- if ( typeof onToggleHeaderArea === 'function' && isToggledRef . current ) {
188- onToggleHeaderArea ( headerCollapsed !== true ) ;
195+ if ( typeof onToggleHeaderAreaRef . current === 'function' && isToggledRef . current ) {
196+ onToggleHeaderAreaRef . current ( headerCollapsed !== true ) ;
189197 }
190198 } , [ headerCollapsed ] ) ;
191199
@@ -202,7 +210,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
202210 } ,
203211 } ) ;
204212 }
205- } , [ headerCollapsed ] ) ;
213+ } , [ headerCollapsed , onToggleHeaderContentVisibility , objectPageRef ] ) ;
206214
207215 const avatar = useMemo ( ( ) => {
208216 if ( ! image ) {
@@ -228,52 +236,59 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
228236 }
229237 } , [ image , imageShapeCircle ] ) ;
230238
231- const scrollToSectionById = ( id : string | undefined , isSubSection = false ) => {
232- const scroll = ( ) => {
233- const section = getSectionElementById ( objectPageRef . current , isSubSection , id ) ;
234- scrollTimeout . current = performance . now ( ) + 500 ;
235- if ( section ) {
236- const safeTopHeaderHeight = topHeaderHeight || prevTopHeaderHeight . current ;
237-
238- const scrollMargin =
239- - 1 /* reduce margin-block so that intersection observer detects correct section*/ +
240- safeTopHeaderHeight +
241- TAB_CONTAINER_HEADER_HEIGHT +
242- ( headerPinned && ! headerCollapsed ? headerContentHeight : 0 ) ;
243- section . style . scrollMarginBlockStart = scrollMargin + 'px' ;
244- if ( isSubSection ) {
245- section . focus ( ) ;
246- }
239+ const scrollToSectionById = useCallback (
240+ ( id : string | undefined , isSubSection = false ) => {
241+ const scroll = ( ) => {
242+ const section = getSectionElementById ( objectPageRef . current , isSubSection , id ) ;
243+ scrollTimeout . current = performance . now ( ) + 500 ;
244+ if ( section ) {
245+ const safeTopHeaderHeight = topHeaderHeight || prevTopHeaderHeight . current ;
246+
247+ const scrollMargin =
248+ - 1 /* reduce margin-block so that intersection observer detects correct section*/ +
249+ safeTopHeaderHeight +
250+ TAB_CONTAINER_HEADER_HEIGHT +
251+ ( headerPinned && ! headerCollapsed ? headerContentHeight : 0 ) ;
252+ section . style . scrollMarginBlockStart = scrollMargin + 'px' ;
253+ if ( isSubSection ) {
254+ section . focus ( ) ;
255+ }
247256
248- const sectionRect = section . getBoundingClientRect ( ) ;
249- const objectPageElement = objectPageRef . current ;
250- const objectPageRect = objectPageElement . getBoundingClientRect ( ) ;
257+ const sectionRect = section . getBoundingClientRect ( ) ;
258+ const objectPageElement = objectPageRef . current ;
259+ const objectPageRect = objectPageElement . getBoundingClientRect ( ) ;
251260
252- // Calculate the top position of the section relative to the container
253- objectPageElement . scrollTop = sectionRect . top - objectPageRect . top + objectPageElement . scrollTop - scrollMargin ;
261+ // Calculate the top position of the section relative to the container
262+ objectPageElement . scrollTop =
263+ sectionRect . top - objectPageRect . top + objectPageElement . scrollTop - scrollMargin ;
254264
255- section . style . scrollMarginBlockStart = '' ;
265+ section . style . scrollMarginBlockStart = '' ;
266+ }
267+ } ;
268+ // In TabBar mode the section is only rendered when selected: delay scroll for subsection
269+ if ( mode === ObjectPageMode . IconTabBar && isSubSection ) {
270+ setTimeout ( scroll , 300 ) ;
271+ } else {
272+ scroll ( ) ;
256273 }
257- } ;
258- // In TabBar mode the section is only rendered when selected: delay scroll for subsection
259- if ( mode === ObjectPageMode . IconTabBar && isSubSection ) {
260- setTimeout ( scroll , 300 ) ;
261- } else {
262- scroll ( ) ;
263- }
264- } ;
274+ } ,
275+ [ mode , topHeaderHeight , headerPinned , headerCollapsed , headerContentHeight , objectPageRef ] ,
276+ ) ;
265277
266- const scrollToSection = ( sectionId ?: string ) => {
267- if ( ! sectionId ) {
268- return ;
269- }
270- if ( firstSectionId === sectionId ) {
271- objectPageRef . current ?. scrollTo ( { top : 0 } ) ;
272- } else {
273- scrollToSectionById ( sectionId ) ;
274- }
275- isProgrammaticallyScrolled . current = false ;
276- } ;
278+ const scrollToSection = useCallback (
279+ ( sectionId ?: string ) => {
280+ if ( ! sectionId ) {
281+ return ;
282+ }
283+ if ( firstSectionId === sectionId ) {
284+ objectPageRef . current ?. scrollTo ( { top : 0 } ) ;
285+ } else {
286+ scrollToSectionById ( sectionId ) ;
287+ }
288+ isProgrammaticallyScrolled . current = false ;
289+ } ,
290+ [ firstSectionId , scrollToSectionById , objectPageRef ] ,
291+ ) ;
277292
278293 // section was selected by clicking on the tab bar buttons
279294 const handleOnSectionSelected : HandleOnSectionSelectedType = ( targetEvent , newSelectionSectionId , index , section ) => {
@@ -325,15 +340,15 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
325340 if ( mode === ObjectPageMode . Default && isProgrammaticallyScrolled . current === true && ! selectedSubSectionId ) {
326341 scrollToSection ( internalSelectedSectionId ) ;
327342 }
328- } , [ internalSelectedSectionId , mode , selectedSubSectionId ] ) ;
343+ } , [ internalSelectedSectionId , mode , selectedSubSectionId , scrollToSection ] ) ;
329344
330345 // Scrolling for Sub Section Selection
331346 useEffect ( ( ) => {
332347 if ( selectedSubSectionId && isProgrammaticallyScrolled . current === true ) {
333348 scrollToSectionById ( selectedSubSectionId , true ) ;
334349 isProgrammaticallyScrolled . current = false ;
335350 }
336- } , [ selectedSubSectionId , sectionSpacer ] ) ;
351+ } , [ selectedSubSectionId , sectionSpacer , scrollToSectionById ] ) ;
337352
338353 useEffect ( ( ) => {
339354 if ( headerPinnedProp !== undefined ) {
@@ -342,7 +357,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
342357 if ( headerPinnedProp ) {
343358 onToggleHeaderContentVisibility ( { detail : { visible : true } } ) ;
344359 }
345- } , [ headerPinnedProp ] ) ;
360+ } , [ headerPinnedProp , onToggleHeaderContentVisibility ] ) ;
346361
347362 const prevHeaderPinned = useRef ( headerPinned ) ;
348363 useEffect ( ( ) => {
@@ -353,7 +368,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
353368 if ( ! prevHeaderPinned . current && headerPinned ) {
354369 prevHeaderPinned . current = true ;
355370 }
356- } , [ headerPinned , topHeaderHeight ] ) ;
371+ } , [ headerPinned , topHeaderHeight , onToggleHeaderContentVisibility , objectPageRef ] ) ;
357372
358373 const isInitialTabBarMode = useRef ( true ) ;
359374 useEffect ( ( ) => {
@@ -394,7 +409,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
394409 }
395410 }
396411 isInitialTabBarMode . current = false ;
397- } , [ props . selectedSubSectionId , isMounted ] ) ;
412+ } , [ props . selectedSubSectionId , isMounted , childrenArray , debouncedOnSectionChange , mode ] ) ;
398413
399414 const tabContainerContainerRef = useRef ( null ) ;
400415 const isHeaderPinnedAndExpanded = headerPinned && ! headerCollapsed ;
@@ -457,7 +472,15 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
457472 return ( ) => {
458473 observer . disconnect ( ) ;
459474 } ;
460- } , [ topHeaderHeight , headerContentHeight , currentTabModeSection , children , mode , isHeaderPinnedAndExpanded ] ) ;
475+ } , [
476+ topHeaderHeight ,
477+ headerContentHeight ,
478+ currentTabModeSection ,
479+ children ,
480+ mode ,
481+ isHeaderPinnedAndExpanded ,
482+ objectPageRef ,
483+ ] ) ;
461484
462485 const { onScroll : _0 , selectedSubSectionId : _1 , ...propsWithoutOmitted } = rest ;
463486
@@ -519,6 +542,8 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
519542 childrenArray . length ,
520543 scrolledHeaderExpanded ,
521544 mode ,
545+ objectPageRef ,
546+ debouncedOnSectionChange ,
522547 ] ) ;
523548
524549 const onTitleClick = ( e ) => {
@@ -568,8 +593,8 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
568593 }
569594 setToggledCollapsedHeaderWasVisible ( false ) ;
570595 scrollEvent . current = e ;
571- if ( typeof props . onScroll === 'function' ) {
572- props . onScroll ( e ) ;
596+ if ( typeof onScrollRef . current === 'function' ) {
597+ onScrollRef . current ( e ) ;
573598 }
574599 if ( selectedSubSectionId ) {
575600 setSelectedSubSectionId ( undefined ) ;
@@ -591,7 +616,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
591616 setScrolledHeaderExpanded ( false ) ;
592617 }
593618 } ,
594- [ topHeaderHeight , headerPinned , props . onScroll , scrolledHeaderExpanded , selectedSubSectionId ] ,
619+ [ headerPinned , scrolledHeaderExpanded , selectedSubSectionId , objectPageRef , scrollEndHandler ] ,
595620 ) ;
596621
597622 const onHoverToggleButton : MouseEventHandler < HTMLHeadElement > = useCallback ( ( e ) => {
@@ -602,8 +627,6 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
602627 }
603628 } , [ ] ) ;
604629
605- // Passing refs to custom hooks is normal React usage
606- // eslint-disable-next-line react-hooks/refs
607630 const handleTabSelect = useHandleTabSelect ( {
608631 onBeforeNavigate,
609632 headerPinned,
@@ -692,8 +715,6 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
692715 data-component-name = "ObjectPageTitleAreaClickElement"
693716 />
694717 { titleArea &&
695- // Passing props (including refs via cloneElement) is normal React usage
696- // eslint-disable-next-line react-hooks/refs
697718 cloneElement ( titleArea as ReactElement < ObjectPageTitlePropsWithDataAttributes > , {
698719 className : clsx ( titleArea ?. props ?. className ) ,
699720 onToggleHeaderContentVisibility : onTitleClick ,
0 commit comments