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

SAP / ui5-webcomponents-react / 9742178811

01 Jul 2024 10:00AM CUT coverage: 81.445% (+0.06%) from 81.387%
9742178811

Pull #5978

github

web-flow
Merge 362695332 into 2cf618399
Pull Request #5978: feat(ThemeProvider): apply Fiori scrollbar styling to all scroll containers

2649 of 3845 branches covered (68.89%)

2 of 2 new or added lines in 2 files covered. (100.0%)

164 existing lines in 4 files now uncovered.

4815 of 5912 relevant lines covered (81.44%)

70456.25 hits per line

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

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

31
interface ListDomRefWithPrivateAPIs extends ListDomRef {
32
  get hasData(): boolean;
33

34
  getSelectedItems(): HTMLElement[];
35

36
  deselectSelectedItems(): void;
37

38
  focusFirstItem(): void;
39
}
40

41
export interface SelectDialogPropTypes
42
  extends Omit<DialogPropTypes, 'header' | 'headerText' | 'footer' | 'children' | 'state'>,
43
    Pick<ListPropTypes, 'growing' | 'onLoadMore'> {
44
  /**
45
   * Defines the list items of the component.
46
   *
47
   * __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.
48
   */
49
  children?: ReactNode | ReactNode[];
50
  /**
51
   * 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`.
52
   */
53
  showClearButton?: boolean;
54
  /**
55
   * Defines the header text.
56
   */
57
  headerText?: string;
58
  /**
59
   * Specifies the `headerText` alignment.
60
   */
61
  headerTextAlignCenter?: boolean;
62
  /**
63
   * Overwrites the default text for the confirmation button.
64
   */
65
  confirmButtonText?: string;
66
  /**
67
   * 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`.
68
   *
69
   * __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.
70
   */
71
  rememberSelections?: boolean;
72
  /**
73
   * 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.
74
   */
75
  numberOfSelectedItems?: number;
76
  /**
77
   * Defines the mode of the SelectDialog list.
78
   *
79
   * __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.
80
   *
81
   * @default ListSelectionMode.Single
82
   */
83
  selectionMode?: ListPropTypes['selectionMode'];
84
  /**
85
   * Defines props you can pass to the internal `List` component.
86
   *
87
   * __Note:__ `selectionMode`, `children`, `growing`, `onLoadMore` and `footerText` are not supported.
88
   *
89
   * @default {}
90
   */
91
  listProps?: Omit<ListPropTypes, 'selectionMode' | 'children' | 'footerText' | 'growing' | 'onLoadMore'>;
92
  /**
93
   * Defines the props of the confirm button.
94
   *
95
   * __Note:__`onClick` and `design` are not supported.
96
   *
97
   * @since 1.25.0
98
   */
99
  confirmButtonProps?: Omit<ButtonPropTypes, 'onClick' | 'design'>;
100
  /**
101
   * This event will be fired when the value of the search field is changed by a user - e.g. at each key press
102
   */
103
  onSearchInput?: (event: Ui5CustomEvent<InputDomRef, { value: string }>) => void;
104
  /**
105
   * This event will be fired when the search button has been clicked or the ENTER key has been pressed in the search field.
106
   */
107
  onSearch?:
108
    | ((event: Ui5CustomEvent<InputDomRef, { value: string }>) => void)
109
    | ((event: Ui5CustomEvent<IconDomRef, { value: string }>) => void);
110
  /**
111
   * This event will be fired when the reset button has been clicked in the search field or when the dialog is closed.
112
   */
113
  onSearchReset?: (event: Ui5CustomEvent<{ prevValue: string; nativeDetail?: number }>) => void;
114
  /**
115
   * This event will be fired when the clear button has been clicked.
116
   */
117
  onClear?: (
118
    event: Ui5CustomEvent<ButtonDomRef, { prevSelectedItems: ListItemStandardDomRef[]; nativeDetail: number }>
119
  ) => void;
120
  /**
121
   * 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.
122
   */
123
  onConfirm?:
124
    | ((event: Ui5CustomEvent<ListDomRef, { selectedItems: ListItemStandardDomRef[] }>) => void)
125
    | ((event: Ui5CustomEvent<ButtonDomRef, { selectedItems: ListItemStandardDomRef[] }>) => void);
126
  /**
127
   * This event will be fired when the cancel button is clicked or ESC key is pressed.
128
   */
129
  onCancel?: ButtonPropTypes['onClick'] | DialogPropTypes['onBeforeClose'];
130
}
131

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

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

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

184
  const handleAfterOpen = (e) => {
574✔
185
    if (typeof onOpen === 'function') {
87!
UNCOV
186
      onOpen(e);
×
187
    }
188
    listRef.current?.focusFirstItem();
87✔
189
  };
190

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

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

228
  const handleClose = (e) => {
574✔
229
    setInternalOpen(false);
22✔
230
    if (typeof onCancel === 'function') {
22✔
231
      onCancel(e);
6✔
232
    }
233
  };
234

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

243
  const handleConfirm = (e) => {
574✔
244
    if (typeof onConfirm === 'function') {
19✔
245
      onConfirm(enrichEventWithDetails(e, { selectedItems }));
19✔
246
    }
247
    setInternalOpen(false);
19✔
248
  };
249

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

264
  const handleBeforeClose = (e) => {
574✔
265
    if (typeof onBeforeClose === 'function') {
68!
UNCOV
266
      onBeforeClose(e);
×
267
    }
268
    if (typeof onCancel === 'function' && e.detail.escPressed) {
68✔
269
      onCancel(e);
6✔
270
    }
271
  };
272

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

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

366
SelectDialog.displayName = 'SelectDialog';
405✔
367

368
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