1
1
'use client' ;
2
2
3
- import { debounce , enrichEventWithDetails , ThemingParameters , useSyncRef } from '@ui5/webcomponents-react-base' ;
3
+ import { debounce , ThemingParameters , useSyncRef } from '@ui5/webcomponents-react-base' ;
4
4
import { clsx } from 'clsx' ;
5
5
import type { ReactElement , ReactNode } from 'react' ;
6
6
import React , { cloneElement , forwardRef , useEffect , useRef , useState } from 'react' ;
@@ -54,6 +54,20 @@ export interface DynamicPagePropTypes extends Omit<CommonProps, 'title' | 'child
54
54
* __Note:__ Assigning `children` as function is recommended when implementing sticky sub-headers. You can find out more about this [here](https://sap.github.io/ui5-webcomponents-react/?path=/story/layouts-floorplans-dynamicpage--sticky-sub-headers).
55
55
*/
56
56
children ?: ReactNode | ReactNode [ ] | ( ( payload : { stickyHeaderHeight : number } ) => ReactElement ) ;
57
+ /**
58
+ * Determines whether the header is expanded. You can use this to initialize the component with a collapsed header.
59
+ *
60
+ * __Note:__ Changes through user interaction (scrolling, manually expanding/collapsing the header, etc.) are still applied.
61
+ *
62
+ * @since 1.23.0
63
+ */
64
+ headerCollapsed ?: boolean ;
65
+ /**
66
+ * Preserves the current header state when scrolling. For example, if the user expands the header by clicking on the title and then scrolls down the page, the header will remain expanded.
67
+ *
68
+ * @since 1.23.0
69
+ */
70
+ preserveHeaderStateOnScroll ?: boolean ;
57
71
/**
58
72
* Defines internally used a11y properties.
59
73
*/
@@ -111,6 +125,8 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
111
125
a11yConfig,
112
126
onToggleHeaderContent,
113
127
onPinnedStateChange,
128
+ headerCollapsed : headerCollapsedProp ,
129
+ preserveHeaderStateOnScroll,
114
130
...rest
115
131
} = props ;
116
132
const { onScroll : _1 , ...propsWithoutOmitted } = rest ;
@@ -129,7 +145,7 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
129
145
const isToggledRef = useRef ( false ) ;
130
146
const [ isOverflowing , setIsOverflowing ] = useState ( false ) ;
131
147
132
- const [ headerCollapsedInternal , setHeaderCollapsedInternal ] = useState < undefined | boolean > ( undefined ) ;
148
+ const [ headerCollapsedInternal , setHeaderCollapsedInternal ] = useState < undefined | boolean > ( headerCollapsedProp ) ;
133
149
// observe heights of header parts
134
150
const { topHeaderHeight, headerCollapsed } = useObserveHeights (
135
151
dynamicPageRef ,
@@ -140,10 +156,32 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
140
156
{
141
157
noHeader : false ,
142
158
fixedHeader : headerState === HEADER_STATES . VISIBLE_PINNED || headerState === HEADER_STATES . HIDDEN_PINNED ,
143
- scrollTimeout
159
+ scrollTimeout,
160
+ preserveHeaderStateOnScroll
144
161
}
145
162
) ;
146
163
164
+ useEffect ( ( ) => {
165
+ if ( preserveHeaderStateOnScroll && headerState === HEADER_STATES . AUTO ) {
166
+ if (
167
+ dynamicPageRef . current . scrollTop <=
168
+ ( topHeaderRef ?. current . offsetHeight ?? 0 ) +
169
+ Math . max ( 0 , headerContentRef . current . offsetHeight ?? 0 - topHeaderRef ?. current . offsetHeight ?? 0 )
170
+ ) {
171
+ setHeaderState ( HEADER_STATES . VISIBLE ) ;
172
+ } else {
173
+ setHeaderState ( HEADER_STATES . HIDDEN ) ;
174
+ }
175
+ }
176
+ } , [ preserveHeaderStateOnScroll , headerState ] ) ;
177
+
178
+ useEffect ( ( ) => {
179
+ if ( headerCollapsedProp != null ) {
180
+ setHeaderCollapsedInternal ( headerCollapsedProp ) ;
181
+ onToggleHeaderContentInternal ( undefined , headerCollapsedProp ) ;
182
+ }
183
+ } , [ headerCollapsedProp ] ) ;
184
+
147
185
const classes = useStyles ( ) ;
148
186
const dynamicPageClasses = clsx (
149
187
classes . dynamicPage ,
@@ -173,6 +211,7 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
173
211
} ;
174
212
} , [ ] ) ;
175
213
214
+ const timeoutRef = useRef < undefined | ReturnType < typeof setTimeout > > ( undefined ) ;
176
215
useEffect ( ( ) => {
177
216
const dynamicPage = dynamicPageRef . current ;
178
217
const oneTimeScrollHandler = ( e ) => {
@@ -186,11 +225,15 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
186
225
setHeaderCollapsedInternal ( true ) ;
187
226
}
188
227
} ;
189
- if ( headerState === HEADER_STATES . VISIBLE || headerState === HEADER_STATES . HIDDEN ) {
228
+ if (
229
+ ! preserveHeaderStateOnScroll &&
230
+ ( headerState === HEADER_STATES . VISIBLE || headerState === HEADER_STATES . HIDDEN )
231
+ ) {
190
232
// only reset state after scroll if scroll isn't invoked by expanding the header
191
233
const timeout = scrollTimeout . current - performance . now ( ) ;
234
+ clearTimeout ( timeoutRef . current ) ;
192
235
if ( timeout > 0 ) {
193
- setTimeout ( ( ) => {
236
+ timeoutRef . current = setTimeout ( ( ) => {
194
237
dynamicPage ?. addEventListener ( 'scroll' , oneTimeScrollHandler , {
195
238
once : true
196
239
} ) ;
@@ -204,16 +247,23 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
204
247
return ( ) => {
205
248
dynamicPage ?. removeEventListener ( 'scroll' , oneTimeScrollHandler ) ;
206
249
} ;
207
- } , [ dynamicPageRef , headerState ] ) ;
250
+ } , [ dynamicPageRef , headerState , preserveHeaderStateOnScroll ] ) ;
208
251
209
- const onToggleHeaderContentVisibility = ( e ) => {
252
+ const onToggleHeaderContentInternal = ( e ?, headerCollapsedProp ?) => {
253
+ e ?. stopPropagation ( ) ;
254
+ if ( ! isToggledRef . current ) {
255
+ isToggledRef . current = true ;
256
+ }
257
+ onToggleHeaderContentVisibility ( headerCollapsedProp ?? ! headerCollapsed ) ;
258
+ } ;
259
+
260
+ const onToggleHeaderContentVisibility = ( localHeaderCollapsed ) => {
210
261
scrollTimeout . current = performance . now ( ) + 500 ;
211
- const shouldHideHeader = ! e . detail . visible ;
212
262
setHeaderState ( ( oldState ) => {
213
263
if ( oldState === HEADER_STATES . VISIBLE_PINNED || oldState === HEADER_STATES . HIDDEN_PINNED ) {
214
- return shouldHideHeader ? HEADER_STATES . HIDDEN_PINNED : HEADER_STATES . VISIBLE_PINNED ;
264
+ return localHeaderCollapsed ? HEADER_STATES . HIDDEN_PINNED : HEADER_STATES . VISIBLE_PINNED ;
215
265
}
216
- return shouldHideHeader ? HEADER_STATES . HIDDEN : HEADER_STATES . VISIBLE ;
266
+ return localHeaderCollapsed ? HEADER_STATES . HIDDEN : HEADER_STATES . VISIBLE ;
217
267
} ) ;
218
268
} ;
219
269
@@ -232,14 +282,6 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
232
282
}
233
283
} ;
234
284
235
- const onToggleHeaderContentInternal = ( e ) => {
236
- e . stopPropagation ( ) ;
237
- if ( ! isToggledRef . current ) {
238
- isToggledRef . current = true ;
239
- }
240
- onToggleHeaderContentVisibility ( enrichEventWithDetails ( e , { visible : headerCollapsed } ) ) ;
241
- } ;
242
-
243
285
const handleHeaderPinnedChange = ( headerWillPin ) => {
244
286
if ( headerWillPin ) {
245
287
setHeaderState ( HEADER_STATES . VISIBLE_PINNED ) ;
@@ -259,6 +301,9 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
259
301
} , [ alwaysShowContentHeader ] ) ;
260
302
261
303
const onDynamicPageScroll = ( e ) => {
304
+ if ( preserveHeaderStateOnScroll ) {
305
+ return ;
306
+ }
262
307
if ( ! isToggledRef . current ) {
263
308
isToggledRef . current = true ;
264
309
}
@@ -314,7 +359,10 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
314
359
? { ...headerContent . props . style , position : 'relative' , visibility : 'hidden' }
315
360
: headerContent . props . style ,
316
361
className : clsx ( classes . header , headerContent ?. props ?. className ) ,
317
- headerPinned : headerState === HEADER_STATES . VISIBLE_PINNED || headerState === HEADER_STATES . VISIBLE ,
362
+ headerPinned :
363
+ preserveHeaderStateOnScroll ||
364
+ headerState === HEADER_STATES . VISIBLE_PINNED ||
365
+ headerState === HEADER_STATES . VISIBLE ,
318
366
topHeaderHeight
319
367
} ) }
320
368
< FlexBox
0 commit comments