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

SAP / ui5-webcomponents-react / 14246877380

03 Apr 2025 03:29PM CUT coverage: 87.519% (+0.02%) from 87.5%
14246877380

Pull #7186

github

web-flow
Merge 8e5320e1e into a0f234dd0
Pull Request #7186: fix(ObjectPage): announce first section heading with screen readers

2944 of 3957 branches covered (74.4%)

5126 of 5857 relevant lines covered (87.52%)

91375.9 hits per line

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

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

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

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

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

1,058✔
117
  useEffect(() => {
1,058✔
118
    isMounted.current = true;
1,036✔
119
    return () => {
120
      isMounted.current = false;
121
    };
122
  }, [isMounted]);
12,364✔
123

88✔
124
  const onHeaderClick = (e) => {
88✔
125
    if (typeof onToggleHeaderContentVisibility === 'function') {
×
126
      onToggleHeaderContentVisibility(e);
127
    }
128
  };
12,364✔
129

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

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

1,058✔
167
  useEffect(() => {
168
    const toolbarContainer = toolbarContainerRef.current;
1,058✔
169

853✔
170
    const updateNavigationToolbar = (container: HTMLDivElement) => {
14✔
171
      if (container.children.length >= 2) {
14!
172
        const lastChild = container.lastElementChild as ToolbarDomRef;
14✔
173
        if (lastChild && lastChild.matches('[ui5-toolbar]')) {
280!
174
          Array.from(lastChild.children).forEach((item) => {
175
            item.setAttribute('overflow-priority', 'NeverOverflow');
176
          });
177
        }
178
      }
179
    };
1,058✔
180

3✔
181
    const observer = new MutationObserver(([toolbarContainerMutation]) => {
3✔
182
      if (toolbarContainerMutation.type === 'childList') {
×
183
        updateNavigationToolbar(toolbarContainerMutation.target as HTMLDivElement);
184
      }
185
    });
1,058✔
186

187
    const config = { childList: true, subtree: true };
1,058✔
188

850✔
189
    if (toolbarContainer) {
850!
190
      updateNavigationToolbar(toolbarContainer);
191
      observer.observe(toolbarContainer, config);
192
    }
1,058✔
193

1,036✔
194
    return () => {
195
      observer.disconnect();
196
    };
197
  }, []);
12,364✔
198

199
  return (
200
    <FlexBox
201
      className={containerClasses}
202
      ref={componentRef}
203
      data-component-name="ObjectPageTitle"
204
      direction={FlexBoxDirection.Column}
205
      justifyContent={FlexBoxJustifyContent.SpaceBetween}
206
      {...rest}
207
    >
208
      <span
209
        className={classNames.clickArea}
210
        onClick={onHeaderClick}
24,764✔
211
        data-component-name="ObjectPageTitleClickElement"
212
      />
×
213
      {(breadcrumbs || (navigationBar && showNavigationInTopArea)) && (
18,604✔
214
        <FlexBox justifyContent={FlexBoxJustifyContent.SpaceBetween} data-component-name="ObjectPageTitleBreadcrumbs">
215
          {breadcrumbs && (
×
216
            <div className={classNames.breadcrumbs} onClick={stopPropagation}>
217
              {breadcrumbs}
218
            </div>
9,330✔
219
          )}
220
          {showNavigationInTopArea && navigationBar && <div className={classNames.toolbar}>{navigationBar}</div>}
×
221
        </FlexBox>
222
      )}
223
      <FlexBox
224
        alignItems={FlexBoxAlignItems.Center}
225
        className={classNames.middleSection}
226
        data-component-name="ObjectPageTitleMiddleSection"
227
      >
24,648✔
228
        <FlexBox className={classNames.titleMainSection} onClick={onHeaderClick}>
229
          {header && (
×
230
            <div className={classNames.title} data-component-name="ObjectPageTitleHeader">
231
              {header}
232
            </div>
21,660✔
233
          )}
234
          {children && (
×
235
            <div className={classNames.content} data-component-name="ObjectPageTitleContent">
236
              {children}
237
            </div>
27,716✔
238
          )}
239
        </FlexBox>
×
240
        {(actionsBar || (!showNavigationInTopArea && navigationBar)) && (
241
          <div className={classNames.toolbar} ref={toolbarContainerRef}>
28,126✔
242
            {actionsBar}
243
            {!showNavigationInTopArea && actionsBar && navigationBar && (
×
244
              <div
245
                className={classNames.actionsSeparator}
246
                data-component-name="ObjectPageTitleActionsSeparator"
247
                aria-hidden
248
              />
28,072!
249
            )}
250
            {!showNavigationInTopArea && (wcrNavToolbar ? wcrNavToolbar : navigationBar)}
×
251
          </div>
252
        )}
24,610✔
253
      </FlexBox>
254
      {subHeader && (
×
255
        <FlexBox>
256
          <div
257
            className={clsx(classNames.subTitle, classNames.subTitleBottom)}
258
            data-component-name="ObjectPageTitleSubHeader"
259
          >
260
            {subHeader}
261
          </div>
262
        </FlexBox>
12,364✔
263
      )}
264
      {props?.['data-header-content-visible']
1,936!
265
        ? expandedContent
266
        : props['data-is-snapped-rendered-outside']
×
267
          ? undefined
268
          : snappedContent}
269
    </FlexBox>
270
  );
271
});
245✔
272

273
ObjectPageTitle.displayName = 'ObjectPageTitle';
181✔
274

275
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