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

SAP / ui5-webcomponents-react / 9677606806

26 Jun 2024 10:01AM CUT coverage: 81.404% (-0.02%) from 81.426%
9677606806

Pull #5975

github

web-flow
Merge d353f6072 into c32ccab2b
Pull Request #5975: fix(MessageBox - TypeScript): adjust `onClose` type

2644 of 3840 branches covered (68.85%)

7 of 8 new or added lines in 1 file covered. (87.5%)

1 existing line in 1 file now uncovered.

4811 of 5910 relevant lines covered (81.4%)

70098.04 hits per line

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

96.77
/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 type { Ui5CustomEvent } from '../../types/index.js';
31
import type { ButtonDomRef, ButtonPropTypes, DialogDomRef, DialogPropTypes } from '../../webComponents/index.js';
32
import { Button, Dialog, Icon, Title } from '../../webComponents/index.js';
33
import { Text } from '../Text/index.js';
34
import { classNames, styleData } from './MessageBox.module.css.js';
35

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

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

106
const getIcon = (icon, type, classes) => {
405✔
107
  if (isValidElement(icon)) return icon;
457✔
108
  switch (type) {
436✔
109
    case MessageBoxType.Confirm:
110
      return <Icon name={iconSysHelp} mode={IconMode.Decorative} className={classes.confirmIcon} />;
262✔
111
    default:
112
      return null;
174✔
113
  }
114
};
115

116
const convertMessageBoxTypeToState = (type: MessageBoxType) => {
405✔
117
  switch (type) {
457✔
118
    case MessageBoxType.Information:
119
      return ValueState.Information;
36✔
120
    case MessageBoxType.Success:
121
      return ValueState.Positive;
66✔
122
    case MessageBoxType.Warning:
123
      return ValueState.Critical;
42✔
124
    case MessageBoxType.Error:
125
      return ValueState.Negative;
39✔
126
    default:
127
      return ValueState.None;
274✔
128
  }
129
};
130

131
const getActions = (actions, type): (string | ReactElement<ButtonPropTypes>)[] => {
405✔
132
  if (actions && actions.length > 0) {
457✔
133
    return actions;
69✔
134
  }
135
  if (type === MessageBoxType.Confirm) {
388✔
136
    return [MessageBoxAction.OK, MessageBoxAction.Cancel];
193✔
137
  }
138
  if (type === MessageBoxType.Error) {
195✔
139
    return [MessageBoxAction.Close];
39✔
140
  }
141
  return [MessageBoxAction.OK];
156✔
142
};
143

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

163
  useStylesheet(styleData, MessageBox.displayName);
457✔
164

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

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

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

199
  const handleDialogClose: DialogPropTypes['onBeforeClose'] = (e) => {
457✔
200
    if (typeof props.onBeforeClose === 'function') {
22!
NEW
201
      props.onBeforeClose(e);
×
202
    }
203
    if (e.detail.escPressed) {
22✔
204
      // @ts-expect-error: todo check type
205
      onClose(enrichEventWithDetails(e, { action: undefined }));
11✔
206
    }
207
  };
208

209
  const handleOnClose: ButtonPropTypes['onClick'] = (e) => {
457✔
210
    const { action } = e.currentTarget.dataset;
145✔
211
    onClose(enrichEventWithDetails(e, { action }));
145✔
212
  };
213

214
  const messageBoxId = useIsomorphicId();
457✔
215
  const internalActions = getActions(actions, type);
457✔
216

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

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

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

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

288
MessageBox.displayName = 'MessageBox';
405✔
289

290
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