• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAP / ui5-webcomponents-react / 10005734463

19 Jul 2024 09:21AM CUT coverage: 79.583% (-0.09%) from 79.67%
10005734463

Pull #6098

github

web-flow
Merge aa9112769 into 105b2da44
Pull Request #6098: docs(MigrationGuide): format headings

2514 of 3758 branches covered (66.9%)

4580 of 5755 relevant lines covered (79.58%)

70956.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.59
/packages/main/src/components/ObjectPageTitle/index.tsx
1
'use client';
2

3
import { debounce, Device, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base';
4
import { clsx } from 'clsx';
5
import type { ReactElement, ReactNode } from 'react';
6
import { cloneElement, forwardRef, isValidElement, useEffect, useRef, useState } from 'react';
7
import { FlexBoxAlignItems, FlexBoxJustifyContent } from '../../enums/index.js';
8
import { stopPropagation } from '../../internal/stopPropagation.js';
9
import type { CommonProps } from '../../types/index.js';
10
import type { ToolbarDomRef } from '../../webComponents/index.js';
11
import { FlexBox } from '../FlexBox/index.js';
12
import { classNames, styleData } from './ObjectPageTitle.module.css.js';
13

14
export interface ObjectPageTitlePropTypes extends CommonProps {
15
  /**
16
   * Defines the actions bar of the `ObjectPageTitle`.
17
   *
18
   * __Note:__ Although this prop accepts all `ReactElement`s, it is strongly recommended that you only use the `Toolbar` component in order to preserve the intended design.
19
   */
20
  actionsBar?: ReactElement;
21

22
  /**
23
   * The `breadcrumbs` displayed in the `ObjectPageTitle` top-left area.
24
   *
25
   * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `Breadcrumbs` in order to preserve the intended design.
26
   */
27
  breadcrumbs?: ReactNode | ReactNode[];
28

29
  /**
30
   * The content is positioned in the `ObjectPageTitle` middle area
31
   */
32
  children?: ReactNode | ReactNode[];
33

34
  /**
35
   * The `header` is positioned in the `ObjectPageTitle` left area.
36
   * Use this prop to display a `Title` (or any other component that serves as a heading).
37
   */
38
  header?: ReactNode;
39
  /**
40
   * The `subHeader` is positioned in the `ObjectPageTitle` left area below the `header`.
41
   * Use this aggregation to display a component like `Label` or any other component that serves as sub header.
42
   */
43
  subHeader?: ReactNode;
44
  /**
45
   * Defines navigation-actions bar of the `ObjectPageTitle`.
46
   *
47
   * *Note*: The `navigationBar` position depends on the control size.
48
   * If the control size is 1280px or bigger, they are rendered right next to the `actionsBar`.
49
   * Otherwise, they are rendered in the top-right area (above the `actionsBar`).
50
   * If a large number of elements(buttons) are used, there could be visual degradations as the space for the `navigationBar` is limited.
51
   *
52
   * __Note:__ Although this prop accepts all `ReactElement`s, it is strongly recommended that you only use the `Toolbar` component in order to preserve the intended design.
53
   */
54
  navigationBar?: ReactElement;
55
  /**
56
   * The content displayed in the `ObjectPageTitle` in expanded state.
57
   */
58
  expandedContent?: ReactNode | ReactNode[];
59
  /**
60
   * The content displayed in the `ObjectPageTitle` in collapsed (snapped) state.
61
   */
62
  snappedContent?: ReactNode | ReactNode[];
63
}
64

65
export interface InternalProps extends ObjectPageTitlePropTypes {
66
  /**
67
   * The onToggleHeaderContentVisibility show or hide the header section
68
   */
69
  onToggleHeaderContentVisibility?: (e: any) => void;
70
  /**
71
   * Defines whether the content area can be toggled
72
   */
73
  'data-not-clickable'?: boolean;
74
  /**
75
   * Defines whether the content area is visible
76
   */
77
  'data-header-content-visible'?: boolean;
78
  /**
79
   * Defines if the `snappedContent` should be rendered by the `ObjectPageTitle`
80
   */
81
  'data-is-snapped-rendered-outside'?: boolean;
82
}
83

84
/**
85
 * The `ObjectPageTitle` component is used to serve as title of the `ObjectPage`.
86
 * It can contain Breadcrumbs, Title, Subtitle, Content, KPIs and Actions.
87
 *
88
 * __Note:__ When used inside a custom component, it's essential to pass through all props, as otherwise the component won't function as intended!
89
 */
90
const ObjectPageTitle = forwardRef<HTMLDivElement, ObjectPageTitlePropTypes>((props, ref) => {
361✔
91
  const {
92
    actionsBar,
93
    breadcrumbs,
94
    children,
95
    header,
96
    subHeader,
97
    navigationBar,
98
    className,
99
    onToggleHeaderContentVisibility,
100
    expandedContent,
101
    snappedContent,
102
    ...rest
103
  } = props as InternalProps;
4,110✔
104

105
  useStylesheet(styleData, ObjectPageTitle.displayName);
4,110✔
106
  const [componentRef, dynamicPageTitleRef] = useSyncRef<HTMLDivElement>(ref);
4,110✔
107
  const [showNavigationInTopArea, setShowNavigationInTopArea] = useState(undefined);
4,110✔
108
  const isMounted = useRef(false);
4,110✔
109
  const [isPhone, setIsPhone] = useState(
4,110✔
110
    Device.getCurrentRange(dynamicPageTitleRef.current?.getBoundingClientRect().width)?.name === 'Phone'
111
  );
112
  const containerClasses = clsx(classNames.container, isPhone && classNames.phone, className);
4,110✔
113
  const toolbarContainerRef = useRef<HTMLDivElement>(null);
4,110✔
114

115
  useEffect(() => {
4,110✔
116
    isMounted.current = true;
374✔
117
    return () => {
374✔
118
      isMounted.current = false;
354✔
119
    };
120
  }, [isMounted]);
121

122
  const onHeaderClick = (e) => {
4,110✔
123
    if (typeof onToggleHeaderContentVisibility === 'function') {
76✔
124
      onToggleHeaderContentVisibility(e);
76✔
125
    }
126
  };
127

128
  useEffect(() => {
4,110✔
129
    const debouncedObserverFn = debounce(([titleContainer]: ResizeObserverEntry[]) => {
755✔
130
      // Firefox implements `borderBoxSize` as a single content rect, rather than an array
131
      const borderBoxSize = Array.isArray(titleContainer.borderBoxSize)
404!
132
        ? titleContainer.borderBoxSize[0]
133
        : titleContainer.borderBoxSize;
134
      // Safari doesn't implement `borderBoxSize`
135
      const titleContainerWidth = borderBoxSize?.inlineSize ?? titleContainer.target.getBoundingClientRect().width;
404!
136
      setIsPhone(Device.getCurrentRange(titleContainerWidth)?.name === 'Phone');
404✔
137
      if (titleContainerWidth < 1280 && !showNavigationInTopArea === true && isMounted.current) {
404✔
138
        setShowNavigationInTopArea(true);
7✔
139
      } else if (titleContainerWidth >= 1280 && !showNavigationInTopArea === false && isMounted.current) {
397!
140
        setShowNavigationInTopArea(false);
×
141
      }
142
    }, 150);
143
    const observer = new ResizeObserver(debouncedObserverFn);
755✔
144
    if (dynamicPageTitleRef.current) {
755✔
145
      observer.observe(dynamicPageTitleRef.current);
755✔
146
    }
147
    return () => {
755✔
148
      debouncedObserverFn.cancel();
735✔
149
      observer.disconnect();
735✔
150
    };
151
  }, [dynamicPageTitleRef.current, showNavigationInTopArea, isMounted]);
152

153
  const [wcrNavToolbar, setWcrNavToolbar] = useState(null);
4,110✔
154
  useEffect(() => {
4,110✔
155
    //@ts-expect-error: private identifier
156
    if (isValidElement(navigationBar) && navigationBar?.type?._displayName === 'UI5WCRToolbar') {
374!
157
      setWcrNavToolbar(
×
158
        cloneElement<any>(navigationBar, {
159
          numberOfAlwaysVisibleItems: Infinity
160
        })
161
      );
162
    }
163
  }, [navigationBar]);
164

165
  useEffect(() => {
4,110✔
166
    const toolbarContainer = toolbarContainerRef.current;
374✔
167

168
    const observer = new MutationObserver(([toolbarContainerMutation]) => {
374✔
169
      if (toolbarContainerMutation.type === 'childList') {
3✔
170
        const navigationToolbar: ToolbarDomRef | undefined = (
171
          toolbarContainerMutation.target as HTMLDivElement
3✔
172
        ).querySelector(':has(> :nth-last-child(n + 2)) > [ui5-toolbar]:last-child');
173
        if (navigationToolbar?.children) {
3!
174
          Array.from(navigationToolbar.children).forEach((item) => {
×
175
            item.setAttribute('overflow-priority', 'NeverOverflow');
×
176
          });
177
        }
178
      }
179
    });
180

181
    const config = { childList: true, subtree: true };
374✔
182

183
    if (toolbarContainer) {
374✔
184
      const navigationToolbar: ToolbarDomRef | undefined = toolbarContainer.querySelector(
285✔
185
        ':has(> :nth-last-child(n + 2)) > [ui5-toolbar]:last-child'
186
      );
187
      if (navigationToolbar?.children) {
285✔
188
        Array.from(navigationToolbar.children).forEach((item) => {
7✔
189
          item.setAttribute('overflow-priority', 'NeverOverflow');
140✔
190
        });
191
      }
192
      observer.observe(toolbarContainer, config);
285✔
193
    }
194

195
    return () => {
374✔
196
      observer.disconnect();
354✔
197
    };
198
  }, []);
199

200
  return (
4,110✔
201
    <FlexBox className={containerClasses} ref={componentRef} data-component-name="ObjectPageTitle" {...rest}>
202
      <span
203
        className={classNames.clickArea}
204
        onClick={onHeaderClick}
205
        data-component-name="ObjectPageTitleClickElement"
206
      />
8,240✔
207
      {(breadcrumbs || (navigationBar && showNavigationInTopArea)) && (
208
        <FlexBox justifyContent={FlexBoxJustifyContent.SpaceBetween} data-component-name="ObjectPageTitleBreadcrumbs">
209
          {breadcrumbs && (
5,466✔
210
            <div className={classNames.breadcrumbs} onClick={stopPropagation}>
211
              {breadcrumbs}
212
            </div>
213
          )}
214
          {showNavigationInTopArea && navigationBar && <div className={classNames.toolbar}>{navigationBar}</div>}
2,745✔
215
        </FlexBox>
216
      )}
217
      <FlexBox
218
        alignItems={FlexBoxAlignItems.Center}
219
        className={classNames.middleSection}
220
        data-component-name="ObjectPageTitleMiddleSection"
221
      >
222
        <FlexBox className={classNames.titleMainSection} onClick={onHeaderClick}>
223
          {header && (
8,176✔
224
            <div className={classNames.title} data-component-name="ObjectPageTitleHeader" /*onClick={onHeaderClick}*/>
225
              {header}
226
            </div>
227
          )}
228
          {children && (
6,838✔
229
            <div className={classNames.content} data-component-name="ObjectPageTitleContent">
230
              {children}
231
            </div>
232
          )}
233
        </FlexBox>
9,558✔
234
        {(actionsBar || (!showNavigationInTopArea && navigationBar)) && (
235
          <div className={classNames.toolbar} ref={toolbarContainerRef}>
236
            {actionsBar}
237
            {!showNavigationInTopArea && actionsBar && navigationBar && (
8,319✔
238
              <div
239
                className={classNames.actionsSpacer}
240
                data-component-name="ObjectPageTitleActionsSeparator"
241
                aria-hidden
242
              />
243
            )}
244
            {!showNavigationInTopArea && (wcrNavToolbar ? wcrNavToolbar : navigationBar)}
8,290!
245
          </div>
246
        )}
247
      </FlexBox>
248
      {subHeader && (
8,157✔
249
        <FlexBox>
250
          <div
251
            className={clsx(classNames.subTitle, classNames.subTitleBottom)}
252
            data-component-name="ObjectPageTitleSubHeader"
253
          >
254
            {subHeader}
255
          </div>
256
        </FlexBox>
257
      )}
258
      {props?.['data-header-content-visible']
4,110✔
259
        ? expandedContent
260
        : props['data-is-snapped-rendered-outside']
874✔
261
          ? undefined
262
          : snappedContent}
263
    </FlexBox>
264
  );
265
});
266

267
ObjectPageTitle.displayName = 'ObjectPageTitle';
361✔
268

269
export { ObjectPageTitle };
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc