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

SAP / ui5-webcomponents-react / 10486318980

21 Aug 2024 08:28AM CUT coverage: 86.822% (+7.0%) from 79.858%
10486318980

Pull #6214

github

web-flow
Merge 1081ba16b into 7a4697321
Pull Request #6214: refactor(FilterBar): remove reference copying of filter/input elements

2769 of 3747 branches covered (73.9%)

116 of 119 new or added lines in 3 files covered. (97.48%)

22 existing lines in 3 files now uncovered.

4961 of 5714 relevant lines covered (86.82%)

72997.84 hits per line

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

96.83
/packages/main/src/components/MessageBox/index.tsx
1
'use client';
2

3
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
4
import IconMode from '@ui5/webcomponents/dist/types/IconMode.js';
5
import PopupAccessibleRole from '@ui5/webcomponents/dist/types/PopupAccessibleRole.js';
6
import TitleLevel from '@ui5/webcomponents/dist/types/TitleLevel.js';
7
import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js';
8
import iconSysHelp from '@ui5/webcomponents-icons/dist/sys-help-2.js';
9
import { useI18nBundle, useStylesheet } from '@ui5/webcomponents-react-base';
10
import { clsx } from 'clsx';
11
import type { ReactElement, ReactNode } from 'react';
12
import { cloneElement, forwardRef, isValidElement, useId } from 'react';
13
import { MessageBoxAction, MessageBoxType } from '../../enums/index.js';
14
import {
15
  ABORT,
16
  CANCEL,
17
  CLOSE,
18
  CONFIRMATION,
19
  DELETE,
20
  ERROR,
21
  IGNORE,
22
  INFORMATION,
23
  NO,
24
  OK,
25
  RETRY,
26
  SUCCESS,
27
  WARNING,
28
  YES
29
} from '../../i18n/i18n-defaults.js';
30
import type { ButtonPropTypes, DialogDomRef, DialogPropTypes } from '../../webComponents/index.js';
31
import { Button, Dialog, Icon, Text, Title } from '../../webComponents/index.js';
32
import { classNames, styleData } from './MessageBox.module.css.js';
33

34
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
35
type MessageBoxActionType = MessageBoxAction | keyof typeof MessageBoxAction | string;
36

37
export interface MessageBoxPropTypes
38
  extends Omit<
39
    DialogPropTypes,
40
    'children' | 'footer' | 'headerText' | 'onClose' | 'state' | 'accessibleNameRef' | 'open' | 'initialFocus'
41
  > {
42
  /**
43
   * Defines the IDs of the elements that label the component.
44
   *
45
   * __Note:__ Per default the prop receives the IDs of the header and the content.
46
   */
47
  accessibleNameRef?: DialogPropTypes['accessibleNameRef'];
48
  /**
49
   * Flag whether the Message Box should be opened or closed
50
   */
51
  open?: DialogPropTypes['open'];
52
  /**
53
   * A custom title for the MessageBox. If not present, it will be derived from the `MessageBox` type.
54
   */
55
  titleText?: DialogPropTypes['headerText'];
56
  /**
57
   * Defines the content of the `MessageBox`.
58
   *
59
   * **Note:** Although this prop accepts HTML Elements, it is strongly recommended that you only use text in order to preserve the intended design and accessibility capabilities.
60
   */
61
  children: ReactNode | ReactNode[];
62
  /**
63
   * Array of actions of the MessageBox. Those actions will be transformed into buttons in the `MessageBox` footer.
64
   *
65
   * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `MessageBoxAction`s (text) or the `Button` component in order to preserve the intended.
66
   */
67
  actions?: (MessageBoxActionType | ReactNode)[];
68
  /**
69
   * Specifies which action of the created dialog will be emphasized.
70
   *
71
   * @since 0.16.3
72
   *
73
   * @default `"OK"`
74
   */
75
  emphasizedAction?: MessageBoxActionType;
76
  /**
77
   * A custom icon. If not present, it will be derived from the `MessageBox` type.
78
   */
79
  icon?: ReactNode;
80
  /**
81
   * Defines the type of the `MessageBox` with predefined title, icon, actions and a visual highlight color.
82
   *
83
   * @default `"Confirm"`
84
   */
85
  type?: MessageBoxType | keyof typeof MessageBoxType;
86
  /**
87
   * Defines the ID of the HTML Element or the `MessageBoxAction`, which will get the initial focus.
88
   */
89
  initialFocus?: MessageBoxActionType;
90
  /**
91
   * Callback to be executed when the `MessageBox` is closed (either by pressing on one of the `actions` or by pressing the Escape key).
92
   * `action` is the pressed action button, it's `undefined` when closed via ESC.
93
   */
94
  onClose?: (action: MessageBoxActionType | undefined, escPressed?: true) => void;
95
}
96

97
const getIcon = (icon, type, classes) => {
376✔
98
  if (isValidElement(icon)) return icon;
422✔
99
  switch (type) {
406✔
100
    case MessageBoxType.Confirm:
101
      return <Icon name={iconSysHelp} mode={IconMode.Decorative} className={classes.confirmIcon} />;
233✔
102
    default:
103
      return null;
173✔
104
  }
105
};
106

107
const convertMessageBoxTypeToState = (type: MessageBoxType) => {
376✔
108
  switch (type) {
422✔
109
    case MessageBoxType.Information:
110
      return ValueState.Information;
26✔
111
    case MessageBoxType.Success:
112
      return ValueState.Positive;
48✔
113
    case MessageBoxType.Warning:
114
      return ValueState.Critical;
77✔
115
    case MessageBoxType.Error:
116
      return ValueState.Negative;
28✔
117
    default:
118
      return ValueState.None;
243✔
119
  }
120
};
121

122
const getActions = (actions, type): (string | ReactElement<ButtonPropTypes>)[] => {
376✔
123
  if (actions && actions.length > 0) {
422✔
124
    return actions;
99✔
125
  }
126
  if (type === MessageBoxType.Confirm) {
323✔
127
    return [MessageBoxAction.OK, MessageBoxAction.Cancel];
181✔
128
  }
129
  if (type === MessageBoxType.Error) {
142✔
130
    return [MessageBoxAction.Close];
28✔
131
  }
132
  return [MessageBoxAction.OK];
114✔
133
};
134

135
/**
136
 * The `MessageBox` component provides easier methods to create a `Dialog`, such as standard alerts, confirmation dialogs, or arbitrary message dialogs.
137
 */
138
const MessageBox = forwardRef<DialogDomRef, MessageBoxPropTypes>((props, ref) => {
376✔
139
  const {
140
    open,
141
    type = MessageBoxType.Confirm,
142✔
142
    children,
143
    className,
144
    titleText,
145
    icon,
146
    actions = [],
323✔
147
    emphasizedAction = MessageBoxAction.OK,
422✔
148
    onClose,
149
    initialFocus,
150
    ...rest
151
  } = props;
422✔
152

153
  useStylesheet(styleData, MessageBox.displayName);
422✔
154

155
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
422✔
156

157
  const actionTranslations = {
422✔
158
    [MessageBoxAction.Abort]: i18nBundle.getText(ABORT),
159
    [MessageBoxAction.Cancel]: i18nBundle.getText(CANCEL),
160
    [MessageBoxAction.Close]: i18nBundle.getText(CLOSE),
161
    [MessageBoxAction.Delete]: i18nBundle.getText(DELETE),
162
    [MessageBoxAction.Ignore]: i18nBundle.getText(IGNORE),
163
    [MessageBoxAction.No]: i18nBundle.getText(NO),
164
    [MessageBoxAction.OK]: i18nBundle.getText(OK),
165
    [MessageBoxAction.Retry]: i18nBundle.getText(RETRY),
166
    [MessageBoxAction.Yes]: i18nBundle.getText(YES)
167
  };
168

169
  const titleToRender = () => {
422✔
170
    if (titleText) {
665✔
171
      return titleText;
68✔
172
    }
173
    switch (type) {
597✔
174
      case MessageBoxType.Confirm:
175
        return i18nBundle.getText(CONFIRMATION);
424✔
176
      case MessageBoxType.Error:
177
        return i18nBundle.getText(ERROR);
28✔
178
      case MessageBoxType.Information:
179
        return i18nBundle.getText(INFORMATION);
26✔
180
      case MessageBoxType.Success:
181
        return i18nBundle.getText(SUCCESS);
32✔
182
      case MessageBoxType.Warning:
183
        return i18nBundle.getText(WARNING);
77✔
184
      default:
185
        return null;
10✔
186
    }
187
  };
188

189
  const handleDialogClose: DialogPropTypes['onBeforeClose'] = (e) => {
422✔
190
    if (typeof props.onBeforeClose === 'function') {
38!
191
      props.onBeforeClose(e);
×
192
    }
193
    if (e.detail.escPressed && typeof onClose === 'function') {
38✔
194
      onClose(undefined, e.detail.escPressed);
26✔
195
    }
196
  };
197

198
  const handleOnClose: ButtonPropTypes['onClick'] = (e) => {
422✔
199
    const { action } = e.currentTarget.dataset;
184✔
200
    if (typeof onClose === 'function') {
184✔
201
      onClose(action);
182✔
202
    }
203
  };
204

205
  const messageBoxId = useId();
422✔
206
  const internalActions = getActions(actions, type);
422✔
207

208
  const getInitialFocus = () => {
422✔
209
    const actionToFocus = internalActions.find((action) => action === initialFocus);
746✔
210
    if (typeof actionToFocus === 'string') {
422✔
211
      return `${messageBoxId}-action-${actionToFocus}`;
8✔
212
    }
213
    return initialFocus;
414✔
214
  };
215

216
  // @ts-expect-error: footer, headerText and onClose are already omitted via prop types
217
  const { footer: _0, headerText: _1, onClose: _2, onBeforeClose: _3, ...restWithoutOmitted } = rest;
422✔
218

219
  const iconToRender = getIcon(icon, type, classNames);
422✔
220
  const needsCustomHeader = !props.header && !!iconToRender;
422✔
221

222
  return (
422✔
223
    <Dialog
224
      open={open}
225
      ref={ref}
226
      className={clsx(classNames.messageBox, className)}
227
      onBeforeClose={handleDialogClose}
228
      accessibleNameRef={needsCustomHeader ? `${messageBoxId}-title ${messageBoxId}-text` : undefined}
422✔
229
      accessibleRole={PopupAccessibleRole.AlertDialog}
230
      {...restWithoutOmitted}
231
      headerText={titleToRender()}
232
      state={convertMessageBoxTypeToState(type as MessageBoxType)}
233
      initialFocus={getInitialFocus()}
234
      data-type={type}
235
    >
236
      {needsCustomHeader && (
665✔
237
        <div slot="header" className={classNames.header}>
238
          {iconToRender}
239
          {iconToRender && <span className={classNames.spacer} />}
486✔
240
          <Title id={`${messageBoxId}-title`} level={TitleLevel.H1}>
241
            {titleToRender()}
242
          </Title>
243
        </div>
244
      )}
245
      <Text id={`${messageBoxId}-text`}>{children}</Text>
246
      <div slot="footer" className={classNames.footer}>
247
        {internalActions.map((action, index) => {
248
          if (typeof action !== 'string' && isValidElement(action)) {
746✔
249
            return cloneElement<ButtonPropTypes | { 'data-action': string }>(action, {
22✔
250
              onClick: action?.props?.onClick
22!
251
                ? (e) => {
252
                    action?.props?.onClick(e);
11✔
253
                    handleOnClose(e);
11✔
254
                  }
255
                : handleOnClose,
256
              'data-action': action?.props?.['data-action'] ?? `${index}: custom action`
44✔
257
            });
258
          }
259
          if (typeof action === 'string') {
724✔
260
            return (
724✔
261
              <Button
262
                id={`${messageBoxId}-action-${action}`}
263
                key={`${action}-${index}`}
264
                design={emphasizedAction === action ? ButtonDesign.Emphasized : ButtonDesign.Transparent}
724✔
265
                onClick={handleOnClose}
266
                data-action={action}
267
              >
268
                {actionTranslations[action] ?? action}
758✔
269
              </Button>
270
            );
271
          }
UNCOV
272
          return null;
×
273
        })}
274
      </div>
275
    </Dialog>
276
  );
277
});
278

279
MessageBox.displayName = 'MessageBox';
376✔
280

281
export { MessageBox };
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