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

SAP / ui5-webcomponents-react / 14304969491

07 Apr 2025 08:53AM CUT coverage: 87.54% (+0.08%) from 87.46%
14304969491

Pull #7187

github

web-flow
Merge f9635f7ca into 8d652b4c7
Pull Request #7187: feat(ObjectPage): allow customizing `role` of `footerArea` container

2966 of 3932 branches covered (75.43%)

5185 of 5923 relevant lines covered (87.54%)

84562.43 hits per line

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

96.05
/packages/main/src/components/MessageView/index.tsx
1
'use client';
2

3
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
4
import ListSeparator from '@ui5/webcomponents/dist/types/ListSeparator.js';
5
import TitleLevel from '@ui5/webcomponents/dist/types/TitleLevel.js';
6
import WrappingType from '@ui5/webcomponents/dist/types/WrappingType.js';
7
import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js';
8
import announce from '@ui5/webcomponents-base/dist/util/InvisibleMessage.js';
9
import iconSlimArrowLeft from '@ui5/webcomponents-icons/dist/slim-arrow-left.js';
10
import type { Ui5DomRef } from '@ui5/webcomponents-react-base';
11
import { useI18nBundle, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base';
12
import { clsx } from 'clsx';
13
import type { ReactElement, ReactNode } from 'react';
14
import { useRef, Children, forwardRef, Fragment, isValidElement, useCallback, useEffect, useState } from 'react';
15
import { FlexBoxDirection } from '../../enums/index.js';
16
import { ALL, LIST_NO_DATA, NAVIGATE_BACK, MESSAGE_DETAILS, MESSAGE_TYPES } from '../../i18n/i18n-defaults.js';
17
import type { SelectedMessage } from '../../internal/MessageViewContext.js';
18
import { MessageViewContext } from '../../internal/MessageViewContext.js';
19
import type { CommonProps } from '../../types/index.js';
20
import { Bar } from '../../webComponents/Bar/index.js';
21
import type { ButtonDomRef } from '../../webComponents/Button/index.js';
22
import { Button } from '../../webComponents/Button/index.js';
23
import { Icon } from '../../webComponents/Icon/index.js';
24
import type { ListDomRef, ListPropTypes } from '../../webComponents/List/index.js';
25
import { List } from '../../webComponents/List/index.js';
26
import { ListItemGroup } from '../../webComponents/ListItemGroup/index.js';
27
import { SegmentedButton } from '../../webComponents/SegmentedButton/index.js';
28
import type { SegmentedButtonPropTypes } from '../../webComponents/SegmentedButton/index.js';
29
import { SegmentedButtonItem } from '../../webComponents/SegmentedButtonItem/index.js';
30
import { Title } from '../../webComponents/Title/index.js';
31
import { FlexBox } from '../FlexBox/index.js';
32
import type { MessageItemPropTypes } from './MessageItem.js';
33
import { classNames, styleData } from './MessageView.module.css.js';
34
import { getIconNameForType, getValueStateMap } from './utils.js';
35

36
export interface MessageViewDomRef extends HTMLDivElement {
37
  /**
38
   * Navigates back to the list page
39
   */
40
  navigateBack: () => void;
41
}
42

43
export interface MessageViewPropTypes extends CommonProps {
44
  /**
45
   * Defines whether the messages are grouped or not.
46
   */
47
  groupItems?: boolean;
48

49
  /**
50
   * Defines whether the header of the details page will be shown.
51
   */
52
  showDetailsPageHeader?: boolean;
53

54
  /**
55
   * A list with message items. If only one item is provided, the initial page will be the details page for the item.
56
   *
57
   * * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `Message` in order to preserve the intended design.
58
   */
59
  children: ReactNode | ReactNode[];
60

61
  /**
62
   * Event is fired when the details of a message are shown.
63
   */
64
  onItemSelect?: ListPropTypes['onItemClick'];
65
}
66

67
export const resolveMessageTypes = (children: ReactElement<MessageItemPropTypes>[]) => {
427✔
68
  return (children ?? [])
246!
69
    .map((message) => message?.props?.type)
1,014✔
70
    .reduce(
71
      (acc, type) => {
72
        const finalType = type === ValueState.None ? ValueState.Information : type;
1,014✔
73
        if (Object.prototype.hasOwnProperty.call(acc, finalType)) {
1,014✔
74
          acc[finalType]++;
974✔
75
        }
76
        return acc;
1,014✔
77
      },
78
      {
79
        [ValueState.Negative]: 0,
80
        [ValueState.Critical]: 0,
81
        [ValueState.Positive]: 0,
82
        [ValueState.Information]: 0
83
      }
84
    );
85
};
86

87
export const resolveMessageGroups = (children: ReactElement<MessageItemPropTypes>[]) => {
427✔
88
  const groups = (children ?? []).reduce((acc, val) => {
246!
89
    const groupName = val?.props?.groupName ?? '';
834✔
90
    if (Object.prototype.hasOwnProperty.call(acc, groupName)) {
834✔
91
      acc[groupName].push(val);
264✔
92
    } else {
93
      acc[groupName] = [val];
570✔
94
    }
95
    return acc;
834✔
96
  }, {});
97

98
  return Object.entries<ReactElement<MessageItemPropTypes>[]>(groups).sort(([keyA], [keyB]) => {
246✔
99
    if (keyA === '' && keyB !== '') {
456✔
100
      return -1;
132✔
101
    }
102
    if (keyA !== '' && keyB === '') {
324✔
103
      return 1;
168✔
104
    }
105
    return 0;
156✔
106
  });
107
};
108

109
/**
110
 * The `MessageView` is used to display a summarized list of different types of messages (error, warning, success, and information messages).
111
 */
112
const MessageView = forwardRef<MessageViewDomRef, MessageViewPropTypes>((props, ref) => {
427✔
113
  const { children, groupItems, showDetailsPageHeader, className, onItemSelect, ...rest } = props;
246✔
114
  const navBtnRef = useRef<ButtonDomRef>(null);
246✔
115
  const listRef = useRef<ListDomRef>(null);
246✔
116
  const transitionTrigger = useRef<'btn' | 'list' | null>(null);
246✔
117
  const prevSelectedMessage = useRef<SelectedMessage>(null);
246✔
118

119
  useStylesheet(styleData, MessageView.displayName);
246✔
120

121
  const [componentRef, internalRef] = useSyncRef<MessageViewDomRef>(ref);
246✔
122

123
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
246✔
124

125
  const [listFilter, setListFilter] = useState<ValueState | 'All'>('All');
246✔
126
  const [selectedMessage, setSelectedMessage] = useState<SelectedMessage>(null);
246✔
127

128
  const childrenArray = Children.toArray(children);
246✔
129
  const messageTypes = resolveMessageTypes(childrenArray as ReactElement<MessageItemPropTypes>[]);
246✔
130
  const filledTypes = Object.values(messageTypes).filter((count) => count > 0).length;
984✔
131

132
  const filteredChildren =
133
    listFilter === 'All'
246✔
134
      ? childrenArray
135
      : childrenArray.filter((message) => {
136
          if (!isValidElement(message)) {
240!
137
            return false;
×
138
          }
139
          const castMessage = message as ReactElement<MessageItemPropTypes>;
240✔
140
          if (listFilter === ValueState.Information) {
240✔
141
            return castMessage?.props?.type === ValueState.Information || castMessage?.props?.type === ValueState.None;
60✔
142
          }
143
          return castMessage?.props?.type === listFilter;
180✔
144
        });
145

146
  const groupedMessages = resolveMessageGroups(filteredChildren as ReactElement<MessageItemPropTypes>[]);
246✔
147

148
  const navigateBack = useCallback(() => {
246✔
149
    transitionTrigger.current = 'btn';
14✔
150
    prevSelectedMessage.current = selectedMessage;
14✔
151
    setSelectedMessage(null);
14✔
152
  }, [setSelectedMessage, selectedMessage]);
153

154
  useEffect(() => {
246✔
155
    if (internalRef.current) {
117✔
156
      internalRef.current.navigateBack = navigateBack;
117✔
157
    }
158
  }, [internalRef.current, navigateBack]);
159

160
  const handleListFilterChange: SegmentedButtonPropTypes['onSelectionChange'] = (e) => {
246✔
161
    setListFilter(e.detail.selectedItems.at(0).dataset.key as never);
30✔
162
  };
163

164
  const handleTransitionEnd: MessageViewPropTypes['onTransitionEnd'] = (e) => {
246✔
165
    if (typeof props?.onTransitionEnd === 'function') {
44!
166
      props.onTransitionEnd(e);
×
167
    }
168
    if (showDetailsPageHeader && transitionTrigger.current === 'list') {
44✔
169
      requestAnimationFrame(() => {
9✔
170
        void navBtnRef.current?.focus();
9✔
171
      });
172
      setTimeout(() => {
9✔
173
        announce(i18nBundle.getText(MESSAGE_DETAILS), 'Polite');
9✔
174
      }, 300);
175
    }
176
    if (transitionTrigger.current === 'btn') {
44✔
177
      requestAnimationFrame(() => {
13✔
178
        const selectedItem = listRef.current.querySelector<Ui5DomRef>(
13✔
179
          `[data-title="${CSS.escape(prevSelectedMessage.current.titleTextStr)}"]`
180
        );
181
        void selectedItem.focus();
13✔
182
      });
183
    }
184
    transitionTrigger.current = null;
44✔
185
  };
186

187
  const handleListItemClick: ListPropTypes['onItemClick'] = (e) => {
246✔
188
    transitionTrigger.current = 'list';
17✔
189
    onItemSelect(e);
17✔
190
  };
191

192
  const outerClasses = clsx(classNames.container, className, selectedMessage && classNames.showDetails);
246✔
193
  return (
246✔
194
    <div ref={componentRef} {...rest} className={outerClasses} onTransitionEnd={handleTransitionEnd}>
195
      <MessageViewContext.Provider
196
        value={{
197
          selectMessage: setSelectedMessage
198
        }}
199
      >
200
        <div style={{ visibility: selectedMessage ? 'hidden' : 'visible' }} className={classNames.messagesContainer}>
246✔
201
          {!selectedMessage && (
458✔
202
            <>
203
              {filledTypes > 1 && (
372✔
204
                <Bar
205
                  startContent={
206
                    <SegmentedButton
207
                      onSelectionChange={handleListFilterChange}
208
                      accessibleName={i18nBundle.getText(MESSAGE_TYPES)}
209
                    >
210
                      <SegmentedButtonItem data-key="All" selected={listFilter === 'All'}>
211
                        {i18nBundle.getText(ALL)}
212
                      </SegmentedButtonItem>
213
                      {/* @ts-expect-error: The key can't be typed, it's always `string`, but since the `ValueState` enum only contains strings it's fine to use here*/}
214
                      {Object.entries(messageTypes).map(([valueState, count]: [ValueState, number]) => {
215
                        if (count === 0) {
640!
216
                          return null;
×
217
                        }
218
                        return (
640✔
219
                          <SegmentedButtonItem
220
                            key={valueState}
221
                            data-key={valueState}
222
                            selected={listFilter === valueState}
223
                            icon={getIconNameForType(valueState)}
224
                            className={classNames.button}
225
                            tooltip={getValueStateMap(i18nBundle)[valueState]}
226
                            accessibleName={getValueStateMap(i18nBundle)[valueState]}
227
                          >
228
                            {count}
229
                          </SegmentedButtonItem>
230
                        );
231
                      })}
232
                    </SegmentedButton>
233
                  }
234
                />
235
              )}
236
              <List
237
                ref={listRef}
238
                onItemClick={handleListItemClick}
239
                noDataText={i18nBundle.getText(LIST_NO_DATA)}
240
                separators={ListSeparator.Inner}
241
              >
242
                {groupItems
212✔
243
                  ? groupedMessages.map(([groupName, items]) => {
244
                      if (!groupName) {
168✔
245
                        return items;
60✔
246
                      }
247
                      return (
108✔
248
                        <Fragment key={groupName}>
249
                          {groupName && <ListItemGroup headerText={groupName}>{items}</ListItemGroup>}
216✔
250
                        </Fragment>
251
                      );
252
                    })
253
                  : filteredChildren}
254
              </List>
255
            </>
256
          )}
257
        </div>
258
        <div className={classNames.detailsContainer} data-component-name="MessageViewDetailsContainer">
259
          {childrenArray.length > 0 ? (
246!
260
            <>
261
              {showDetailsPageHeader && selectedMessage && (
358✔
262
                <Bar
263
                  startContent={
264
                    <Button
265
                      ref={navBtnRef}
266
                      design={ButtonDesign.Transparent}
267
                      icon={iconSlimArrowLeft}
268
                      onClick={navigateBack}
269
                      tooltip={i18nBundle.getText(NAVIGATE_BACK)}
270
                      accessibleName={i18nBundle.getText(NAVIGATE_BACK)}
271
                      data-component-name="MessageViewDetailsNavBackBtn"
272
                    />
273
                  }
274
                />
275
              )}
276
              {selectedMessage && (
280✔
277
                <FlexBox className={classNames.details}>
278
                  <Icon
279
                    data-type={selectedMessage.type ?? ValueState.Negative}
38✔
280
                    name={getIconNameForType(selectedMessage.type)}
281
                    className={classNames.detailsIcon}
282
                  />
283
                  <FlexBox direction={FlexBoxDirection.Column} className={classNames.detailsTextContainer}>
284
                    <Title level={TitleLevel.H5} className={classNames.detailsTitle} wrappingType={WrappingType.Normal}>
285
                      {selectedMessage.titleText}
286
                    </Title>
287
                    <div className={classNames.detailsText}>{selectedMessage.children}</div>
288
                  </FlexBox>
289
                </FlexBox>
290
              )}
291
            </>
292
          ) : null}
293
        </div>
294
      </MessageViewContext.Provider>
295
    </div>
296
  );
297
});
298

299
MessageView.displayName = 'MessageView';
427✔
300

301
export { MessageView };
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