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

SAP / ui5-webcomponents-react / 9675719002

26 Jun 2024 07:54AM CUT coverage: 81.357% (-0.07%) from 81.426%
9675719002

Pull #5975

github

web-flow
Merge 2c21b2d24 into 186638f17
Pull Request #5975: fix(MessageBox - TypeScript): adjust `onClose` type

2642 of 3840 branches covered (68.8%)

3 of 7 new or added lines in 1 file covered. (42.86%)

1 existing line in 1 file now uncovered.

4809 of 5911 relevant lines covered (81.36%)

70611.41 hits per line

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

92.06
/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 { enrichEventWithDetails, useI18nBundle, useIsomorphicId, useStylesheet } from '@ui5/webcomponents-react-base';
10
import { clsx } from 'clsx';
11
import type { ReactElement, ReactNode } from 'react';
12
import { cloneElement, forwardRef, isValidElement } 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 { stopPropagation } from '../../internal/stopPropagation.js';
31
import type { Ui5CustomEvent } from '../../types/index.js';
32
import type { ButtonDomRef, ButtonPropTypes, DialogDomRef, DialogPropTypes } from '../../webComponents/index.js';
33
import { Button, Dialog, Icon, Title } from '../../webComponents/index.js';
34
import { Text } from '../Text/index.js';
35
import { classNames, styleData } from './MessageBox.module.css.js';
36

37
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
38
type MessageBoxActionType = MessageBoxAction | keyof typeof MessageBoxAction | string;
39

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

107
const getIcon = (icon, type, classes) => {
404✔
108
  if (isValidElement(icon)) return icon;
366✔
109
  switch (type) {
345✔
110
    case MessageBoxType.Confirm:
111
      return <Icon name={iconSysHelp} mode={IconMode.Decorative} className={classes.confirmIcon} />;
183✔
112
    default:
113
      return null;
162✔
114
  }
115
};
116

117
const convertMessageBoxTypeToState = (type: MessageBoxType) => {
404✔
118
  switch (type) {
366✔
119
    case MessageBoxType.Information:
120
      return ValueState.Information;
33✔
121
    case MessageBoxType.Success:
122
      return ValueState.Positive;
63✔
123
    case MessageBoxType.Warning:
124
      return ValueState.Critical;
39✔
125
    case MessageBoxType.Error:
126
      return ValueState.Negative;
36✔
127
    default:
128
      return ValueState.None;
195✔
129
  }
130
};
131

132
const getActions = (actions, type): (string | ReactElement<ButtonPropTypes>)[] => {
404✔
133
  if (actions && actions.length > 0) {
366✔
134
    return actions;
69✔
135
  }
136
  if (type === MessageBoxType.Confirm) {
297✔
137
    return [MessageBoxAction.OK, MessageBoxAction.Cancel];
114✔
138
  }
139
  if (type === MessageBoxType.Error) {
183✔
140
    return [MessageBoxAction.Close];
36✔
141
  }
142
  return [MessageBoxAction.OK];
147✔
143
};
144

145
/**
146
 * The `MessageBox` component provides easier methods to create a `Dialog`, such as standard alerts, confirmation dialogs, or arbitrary message dialogs.
147
 * For convenience, it also provides an `open` prop, so it is not necessary to attach a `ref` to open the `MessageBox`.
148
 */
149
const MessageBox = forwardRef<DialogDomRef, MessageBoxPropTypes>((props, ref) => {
404✔
150
  const {
151
    open,
152
    type = MessageBoxType.Confirm,
102✔
153
    children,
154
    className,
155
    titleText,
156
    icon,
157
    actions = [],
297✔
158
    emphasizedAction = MessageBoxAction.OK,
366✔
159
    onClose,
160
    initialFocus,
161
    ...rest
162
  } = props;
366✔
163

164
  useStylesheet(styleData, MessageBox.displayName);
366✔
165

166
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
366✔
167

168
  const actionTranslations = {
366✔
169
    [MessageBoxAction.Abort]: i18nBundle.getText(ABORT),
170
    [MessageBoxAction.Cancel]: i18nBundle.getText(CANCEL),
171
    [MessageBoxAction.Close]: i18nBundle.getText(CLOSE),
172
    [MessageBoxAction.Delete]: i18nBundle.getText(DELETE),
173
    [MessageBoxAction.Ignore]: i18nBundle.getText(IGNORE),
174
    [MessageBoxAction.No]: i18nBundle.getText(NO),
175
    [MessageBoxAction.OK]: i18nBundle.getText(OK),
176
    [MessageBoxAction.Retry]: i18nBundle.getText(RETRY),
177
    [MessageBoxAction.Yes]: i18nBundle.getText(YES)
178
  };
179

180
  const titleToRender = () => {
366✔
181
    if (titleText) {
564✔
182
      return titleText;
90✔
183
    }
184
    switch (type) {
474✔
185
      case MessageBoxType.Confirm:
186
        return i18nBundle.getText(CONFIRMATION);
312✔
187
      case MessageBoxType.Error:
188
        return i18nBundle.getText(ERROR);
36✔
189
      case MessageBoxType.Information:
190
        return i18nBundle.getText(INFORMATION);
33✔
191
      case MessageBoxType.Success:
192
        return i18nBundle.getText(SUCCESS);
42✔
193
      case MessageBoxType.Warning:
194
        return i18nBundle.getText(WARNING);
39✔
195
      default:
196
        return null;
12✔
197
    }
198
  };
199

200
  const handleDialogClose: DialogPropTypes['onBeforeClose'] = (e) => {
366✔
NEW
201
    if (typeof props.onBeforeClose === 'function') {
×
NEW
202
      props.onBeforeClose(e);
×
203
    }
NEW
204
    if (e.detail.escPressed) {
×
NEW
205
      onClose(enrichEventWithDetails(e, { action: undefined }));
×
206
    }
207
  };
208

209
  const handleOnClose: ButtonPropTypes['onClick'] = (e) => {
366✔
210
    const { action } = e.target.dataset;
129✔
211
    stopPropagation(e);
129✔
212
    onClose(enrichEventWithDetails(e, { action }));
129✔
213
  };
214

215
  const messageBoxId = useIsomorphicId();
366✔
216
  const internalActions = getActions(actions, type);
366✔
217

218
  const getInitialFocus = () => {
366✔
219
    const actionToFocus = internalActions.find((action) => action === initialFocus);
609✔
220
    if (typeof actionToFocus === 'string') {
366✔
221
      return `${messageBoxId}-action-${actionToFocus}`;
9✔
222
    }
223
    return initialFocus;
357✔
224
  };
225

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

229
  const iconToRender = getIcon(icon, type, classNames);
366✔
230
  const needsCustomHeader = !props.header && !!iconToRender;
366✔
231

232
  return (
366✔
233
    <Dialog
234
      open={open}
235
      ref={ref}
236
      className={clsx(classNames.messageBox, className)}
237
      onBeforeClose={handleDialogClose}
238
      accessibleNameRef={needsCustomHeader ? `${messageBoxId}-title ${messageBoxId}-text` : undefined}
366✔
239
      accessibleRole={PopupAccessibleRole.AlertDialog}
240
      {...restWithoutOmitted}
241
      headerText={titleToRender()}
242
      state={convertMessageBoxTypeToState(type as MessageBoxType)}
243
      initialFocus={getInitialFocus()}
244
      data-type={type}
245
    >
246
      {needsCustomHeader && (
564✔
247
        <div slot="header" className={classNames.header}>
248
          {iconToRender}
249
          {iconToRender && <span className={classNames.spacer} />}
396✔
250
          <Title id={`${messageBoxId}-title`} level={TitleLevel.H1}>
251
            {titleToRender()}
252
          </Title>
253
        </div>
254
      )}
255
      <Text id={`${messageBoxId}-text`}>{children}</Text>
256
      <div slot="footer" className={classNames.footer}>
257
        {internalActions.map((action, index) => {
258
          if (typeof action !== 'string' && isValidElement(action)) {
609✔
259
            return cloneElement<ButtonPropTypes | { 'data-action': string }>(action, {
30✔
260
              onClick: action?.props?.onClick
30!
261
                ? (e) => {
262
                    action?.props?.onClick(e);
10✔
263
                    handleOnClose(e);
10✔
264
                  }
265
                : handleOnClose,
266
              'data-action': action?.props?.['data-action'] ?? `${index}: custom action`
60✔
267
            });
268
          }
269
          if (typeof action === 'string') {
579✔
270
            return (
579✔
271
              <Button
272
                id={`${messageBoxId}-action-${action}`}
273
                key={`${action}-${index}`}
274
                design={emphasizedAction === action ? ButtonDesign.Emphasized : ButtonDesign.Transparent}
579✔
275
                onClick={handleOnClose}
276
                data-action={action}
277
              >
278
                {actionTranslations[action] ?? action}
624✔
279
              </Button>
280
            );
281
          }
282
          return null;
×
283
        })}
284
      </div>
285
    </Dialog>
286
  );
287
});
288

289
MessageBox.displayName = 'MessageBox';
404✔
290

291
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