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

SAP / ui5-webcomponents-react / 14974207177

12 May 2025 01:58PM CUT coverage: 87.827% (-0.4%) from 88.18%
14974207177

Pull #7327

github

web-flow
Merge a9e21cb3e into b5963be79
Pull Request #7327: fix(SelectDialog): use `headerText` as `accessibleName` per default

2893 of 3894 branches covered (74.29%)

5072 of 5775 relevant lines covered (87.83%)

107705.4 hits per line

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

91.55
/packages/main/src/components/SelectDialog/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 InputType from '@ui5/webcomponents/dist/types/InputType.js';
6
import ListSelectionMode from '@ui5/webcomponents/dist/types/ListSelectionMode.js';
7
import iconDecline from '@ui5/webcomponents-icons/dist/decline.js';
8
import iconSearch from '@ui5/webcomponents-icons/dist/search.js';
9
import { enrichEventWithDetails, useI18nBundle, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base';
10
import { clsx } from 'clsx';
11
import type { ReactNode } from 'react';
12
import { forwardRef, useEffect, useState } from 'react';
13
import { CANCEL, CLEAR, RESET, SEARCH, SELECT, SELECTED } from '../../i18n/i18n-defaults.js';
14
import { Button, Dialog, FlexBox, FlexBoxAlignItems, Icon, Input, List, Text, Title } from '../../index.js';
15
import type { Ui5CustomEvent } from '../../types/index.js';
16
import type {
17
  ButtonDomRef,
18
  ButtonPropTypes,
19
  DialogDomRef,
20
  DialogPropTypes,
21
  IconDomRef,
22
  InputDomRef,
23
  ListDomRef,
24
  ListItemStandardDomRef,
25
  ListPropTypes
26
} from '../../webComponents/index.js';
27
import { classNames, styleData } from './SelectDialog.module.css.js';
28

29
interface ListDomRefWithPrivateAPIs extends ListDomRef {
30
  get hasData(): boolean;
31

32
  getSelectedItems(): HTMLElement[];
33

34
  deselectSelectedItems(): void;
35

36
  focusFirstItem(): void;
37
}
38

39
export interface SelectDialogPropTypes
40
  extends Omit<DialogPropTypes, 'header' | 'headerText' | 'footer' | 'children' | 'state'>,
41
    Pick<ListPropTypes, 'growing' | 'onLoadMore'> {
42
  /**
43
   * Defines the list items of the component.
44
   *
45
   * __Note:__ Although this prop accepts all HTML Elements and therefore also all list items, it is strongly recommended that you only use `ListItemStandard` in order to preserve the intended design.
46
   */
47
  children?: ReactNode | ReactNode[];
48
  /**
49
   * This flag controls whether the Clear button is shown. When set to `true`, it provides a way to clear selections. We recommend enabling the Clear button in cases where a mechanism to delete the selection is required: In single selection mode (default mode) or when `rememberSelections` is set to `true`.
50
   */
51
  showClearButton?: boolean;
52
  /**
53
   * Defines the header text.
54
   */
55
  headerText?: string;
56
  /**
57
   * Specifies the `headerText` alignment.
58
   */
59
  headerTextAlignCenter?: boolean;
60
  /**
61
   * Overwrites the default text for the confirmation button.
62
   */
63
  confirmButtonText?: string;
64
  /**
65
   * This flag controls whether the dialog clears the selection after the confirm event has been fired. If the dialog needs to be opened multiple times in the same context to allow for corrections of previous user inputs, set this flag to `true`.
66
   *
67
   * __Note:__ This won't work if the dialog is unmounted, if you want to unmount the dialog when closed, you need to persist the selection yourself.
68
   */
69
  rememberSelections?: boolean;
70
  /**
71
   * Defines the number of selected list items displayed above the list in `MultiSelect` mode. Programmatically setting the counter is necessary if all previously selected elements are to remain selected during search.
72
   */
73
  numberOfSelectedItems?: number;
74
  /**
75
   * Defines the mode of the SelectDialog list.
76
   *
77
   * __Note:__ Although this prop accepts all `ListSelectionMode`s, it is strongly recommended that you only use `Single` or `Multiple` in order to preserve the intended design.
78
   *
79
   * @default ListSelectionMode.Single
80
   */
81
  selectionMode?: ListPropTypes['selectionMode'];
82
  /**
83
   * Defines props you can pass to the internal `List` component.
84
   *
85
   * __Note:__ `selectionMode`, `children`, `growing`, `onLoadMore` and `footerText` are not supported.
86
   *
87
   * @default {}
88
   */
89
  listProps?: Omit<ListPropTypes, 'selectionMode' | 'children' | 'footerText' | 'growing' | 'onLoadMore'>;
90
  /**
91
   * Defines the props of the confirm button.
92
   *
93
   * __Note:__`onClick` and `design` are not supported.
94
   *
95
   * @since 1.25.0
96
   */
97
  confirmButtonProps?: Omit<ButtonPropTypes, 'onClick' | 'design'>;
98
  /**
99
   * This event will be fired when the value of the search field is changed by a user - e.g. at each key press
100
   */
101
  onSearchInput?: (event: Ui5CustomEvent<InputDomRef, { value: string }>) => void;
102
  /**
103
   * This event will be fired when the search button has been clicked or the ENTER key has been pressed in the search field.
104
   */
105
  onSearch?:
106
    | ((event: Ui5CustomEvent<InputDomRef, { value: string }>) => void)
107
    | ((event: Ui5CustomEvent<IconDomRef, { value: string }>) => void);
108
  /**
109
   * This event will be fired when the reset button has been clicked in the search field or when the dialog is closed.
110
   */
111
  onSearchReset?: (event: Ui5CustomEvent<{ prevValue: string; nativeDetail?: number }>) => void;
112
  /**
113
   * This event will be fired when the clear button has been clicked.
114
   */
115
  onClear?: (
116
    event: Ui5CustomEvent<ButtonDomRef, { prevSelectedItems: ListItemStandardDomRef[]; nativeDetail: number }>
117
  ) => void;
118
  /**
119
   * This event will be fired when the dialog is confirmed by selecting an item in single selection mode or by pressing the confirmation button in multi selection mode.
120
   */
121
  onConfirm?:
122
    | ((event: Ui5CustomEvent<ListDomRef, { selectedItems: ListItemStandardDomRef[] }>) => void)
123
    | ((event: Ui5CustomEvent<ButtonDomRef, { selectedItems: ListItemStandardDomRef[] }>) => void);
124
  /**
125
   * This event will be fired when the cancel button is clicked or ESC key is pressed.
126
   */
127
  onCancel?: ButtonPropTypes['onClick'] | DialogPropTypes['onBeforeClose'];
128
}
129

130
/**
131
 * The SelectDialog enables users to filter a comprehensive list via a search field and to select one or more items.
132
 */
133
const SelectDialog = forwardRef<DialogDomRef, SelectDialogPropTypes>((props, ref) => {
393✔
134
  const {
135
    open,
136
    children,
137
    className,
138
    confirmButtonText,
139
    confirmButtonProps,
140
    growing,
141
    headerText,
20✔
142
    headerTextAlignCenter,
143
    listProps = {},
458✔
144
    selectionMode = ListSelectionMode.Single,
572✔
145
    numberOfSelectedItems,
146
    rememberSelections,
147
    showClearButton,
148
    onClose,
149
    onClear,
150
    onConfirm,
151
    onLoadMore,
152
    onSearch,
×
153
    onSearchInput,
×
154
    onSearchReset,
155
    onBeforeOpen,
156
    onBeforeClose,
157
    onOpen,
158
    onCancel,
159
    ...rest
160
  } = props;
1,018✔
161

162
  useStylesheet(styleData, SelectDialog.displayName);
1,018✔
163
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
1,018✔
164
  const [searchValue, setSearchValue] = useState('');
1,018✔
165
  const [selectedItems, setSelectedItems] = useState([]);
1,018✔
166
  const [listComponentRef, listRef] = useSyncRef<ListDomRefWithPrivateAPIs>((listProps as any).ref);
1,018✔
167
  const [internalOpen, setInternalOpen] = useState(open);
1,018✔
168
  useEffect(() => {
1,018✔
169
    setInternalOpen(open);
260✔
170
  }, [open]);
171

172
  const handleBeforeOpen = (e) => {
1,018✔
173
    const localSelectedItems = listRef.current?.getSelectedItems() ?? [];
40!
174
    if (typeof onBeforeOpen === 'function') {
40!
175
      onBeforeOpen(e);
×
176
    }
177
    if (selectionMode === ListSelectionMode.Multiple && listRef.current?.hasData) {
40✔
178
      setSelectedItems(localSelectedItems);
13✔
179
    }
180
  };
181

182
  const handleAfterOpen = (e) => {
1,018!
183
    if (typeof onOpen === 'function') {
103!
184
      onOpen(e);
×
185
    }
186
    listRef.current?.focusFirstItem();
103!
187
  };
188

189
  const handleSearchInput = (e) => {
1,018✔
190
    if (typeof onSearchInput === 'function') {
24✔
191
      onSearchInput(enrichEventWithDetails(e, { value: e.target.value }));
24✔
192
    }
×
193
    setSearchValue(e.target.value);
24✔
194
  };
195
  const handleSearchSubmit = (e) => {
1,018✔
196
    if (typeof onSearch === 'function') {
36✔
197
      if (e.type === 'keyup' && e.code === 'Enter') {
36✔
198
        onSearch(enrichEventWithDetails(e, { value: e.target.value }));
6✔
199
      }
×
200
      if (e.type === 'click') {
36✔
201
        onSearch(enrichEventWithDetails(e, { value: searchValue }));
6✔
202
      }
203
    }
204
  };
205
  const handleResetSearch = (e) => {
1,018!
206
    if (typeof onSearchReset === 'function') {
6!
207
      onSearchReset(enrichEventWithDetails(e, { prevValue: searchValue }));
6✔
208
    }
209
    setSearchValue('');
6!
210
  };
211

212
  const handleSelectionChange = (e) => {
1,018✔
213
    if (typeof listProps?.onSelectionChange === 'function') {
42✔
214
      listProps.onSelectionChange(e);
42✔
215
    }
×
216
    if (selectionMode === ListSelectionMode.Multiple) {
42✔
217
      setSelectedItems(e.detail.selectedItems);
28✔
218
    } else {
219
      if (typeof onConfirm === 'function') {
14✔
220
        onConfirm(e);
14✔
221
      }
222
      setInternalOpen(false);
14!
223
    }
224
  };
225

×
226
  const handleClose = (e) => {
1,018✔
227
    setInternalOpen(false);
22✔
228
    if (typeof onCancel === 'function') {
22!
229
      onCancel(e);
6✔
230
    }
231
  };
232

233
  const handleClear = (e) => {
1,018✔
234
    if (typeof onClear === 'function') {
×
235
      onClear(enrichEventWithDetails(e, { prevSelectedItems: selectedItems }));
×
236
    }
237
    setSelectedItems([]);
×
238
    listRef.current?.deselectSelectedItems();
×
239
  };
240

241
  const handleConfirm = (e) => {
1,018✔
242
    if (typeof onConfirm === 'function') {
19✔
243
      onConfirm(enrichEventWithDetails(e, { selectedItems }));
19!
244
    }
245
    setInternalOpen(false);
19✔
246
  };
247

248
  const handleAfterClose = (e) => {
1,018✔
249
    setInternalOpen(false);
68✔
250
    if (typeof onClose === 'function') {
68✔
251
      onClose(e);
54!
252
    }
253
    if (typeof onSearchReset === 'function') {
68!
254
      onSearchReset(enrichEventWithDetails(e, { prevValue: searchValue }));
×
255
    }
256
    setSearchValue('');
68✔
257
    if (!rememberSelections) {
68✔
258
      listRef.current?.deselectSelectedItems();
54✔
259
    }
×
260
  };
261

262
  const handleBeforeClose = (e) => {
1,018!
263
    if (typeof onBeforeClose === 'function') {
68!
264
      onBeforeClose(e);
265
    }
266
    if (typeof onCancel === 'function' && e.detail.escPressed) {
68!
267
      onCancel(e);
6✔
268
    }
269
  };
270

271
  return (
1,018✔
272
    <Dialog
×
273
      {...rest}
274
      open={internalOpen}
275
      data-component-name="SelectDialog"
×
276
      ref={ref}
277
      className={clsx(classNames.dialog, className)}
278
      onClose={handleAfterClose}
279
      onBeforeOpen={handleBeforeOpen}
280
      onOpen={handleAfterOpen}
281
      onBeforeClose={handleBeforeClose}
282
    >
283
      <div className={classNames.headerContent} slot="header">
284
        {showClearButton && headerTextAlignCenter && (
1,018!
285
          <Button
286
            onClick={handleClear}
287
            design={ButtonDesign.Transparent}
288
            className={classNames.hiddenClearBtn}
289
            tabIndex={-1}
290
            aria-hidden="true"
291
          >
×
292
            {i18nBundle.getText(CLEAR)}
293
          </Button>
294
        )}
×
295
        <Title className={clsx(classNames.title, headerTextAlignCenter && classNames.titleCenterAlign)}>
1,050✔
296
          {headerText}
297
        </Title>
298
        {showClearButton && (
1,018!
299
          <Button onClick={handleClear} design={ButtonDesign.Transparent} className={classNames.clearBtn}>
300
            {i18nBundle.getText(CLEAR)}
301
          </Button>
302
        )}
303
        <Input
304
          className={classNames.input}
305
          accessibleName={i18nBundle.getText(SEARCH)}
×
306
          value={searchValue}
307
          placeholder={i18nBundle.getText(SEARCH)}
308
          onInput={handleSearchInput}
×
309
          onKeyUp={handleSearchSubmit}
310
          type={InputType.Search}
311
          icon={
312
            <>
313
              {searchValue && (
1,078✔
314
                <Icon
315
                  accessibleName={i18nBundle.getText(RESET)}
316
                  title={i18nBundle.getText(RESET)}
317
                  name={iconDecline}
318
                  mode={IconMode.Interactive}
319
                  onClick={handleResetSearch}
320
                  className={classNames.inputIcon}
321
                />
322
              )}
323
              <Icon
×
324
                mode={IconMode.Interactive}
325
                name={iconSearch}
326
                className={classNames.inputIcon}
327
                onClick={handleSearchSubmit}
328
                accessibleName={i18nBundle.getText(SEARCH)}
329
                title={i18nBundle.getText(SEARCH)}
330
              />
331
            </>
332
          }
333
        />
334
      </div>
335

336
      {selectionMode === ListSelectionMode.Multiple && (!!selectedItems.length || numberOfSelectedItems > 0) && (
1,794✔
337
        <FlexBox alignItems={FlexBoxAlignItems.Center} className={classNames.infoBar}>
338
          <Text>{`${i18nBundle.getText(SELECTED)}: ${numberOfSelectedItems ?? selectedItems.length}`}</Text>
352✔
339
        </FlexBox>
340
      )}
341
      <List
342
        {...listProps}
343
        ref={listComponentRef}
344
        growing={growing}
345
        onLoadMore={onLoadMore}
346
        selectionMode={selectionMode}
×
347
        onSelectionChange={handleSelectionChange}
348
      >
×
349
        {children}
350
      </List>
351
      <div slot="footer" className={classNames.footer}>
352
        {selectionMode === ListSelectionMode.Multiple && (
1,398✔
353
          <Button {...confirmButtonProps} onClick={handleConfirm} design={ButtonDesign.Emphasized}>
354
            {confirmButtonText ?? i18nBundle.getText(SELECT)}
720✔
355
          </Button>
356
        )}
357
        <Button onClick={handleClose} design={ButtonDesign.Transparent}>
358
          {i18nBundle.getText(CANCEL)}
359
        </Button>
360
      </div>
361
    </Dialog>
362
  );
×
363
});
364

×
365
SelectDialog.displayName = 'SelectDialog';
393✔
366

367
export { SelectDialog };
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