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

SAP / ui5-webcomponents-react / 4159167214

pending completion
4159167214

Pull #4138

github

GitHub
Merge 626dc4209 into 54724044c
Pull Request #4138: chore(deps-dev): Bump @types/react from 18.0.27 to 18.0.28

3414 of 6299 branches covered (54.2%)

4612 of 5451 relevant lines covered (84.61%)

3051.3 hits per line

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

51.52
/packages/main/src/components/SelectDialog/index.tsx
1
'use client';
657✔
2

3
import iconDecline from '@ui5/webcomponents-icons/dist/decline.js';
4
import iconSearch from '@ui5/webcomponents-icons/dist/search.js';
5
import {
6
  CssSizeVariables,
7
  enrichEventWithDetails,
8
  ThemingParameters,
9
  useI18nBundle,
10
  useSyncRef
11
} from '@ui5/webcomponents-react-base';
12
import { clsx } from 'clsx';
13
import React, { forwardRef, ReactNode, useState } from 'react';
14
import { createUseStyles } from 'react-jss';
15
import { ButtonDesign, ListGrowingMode, ListMode, ToolbarDesign } from '../../enums';
16
import { CANCEL, CLEAR, RESET, SEARCH, SELECT, SELECTED } from '../../i18n/i18n-defaults';
17
import { Ui5CustomEvent } from '../../interfaces/Ui5CustomEvent';
18
import {
19
  Button,
20
  Dialog,
21
  DialogDomRef,
22
  DialogPropTypes,
23
  Icon,
24
  Input,
25
  List,
26
  ListDomRef,
27
  ListPropTypes,
28
  Title
29
} from '../../webComponents';
30
import { Text } from '../Text';
31
import { Toolbar } from '../Toolbar';
32

33
const useStyles = createUseStyles(
73✔
34
  {
35
    dialog: {
36
      '&::part(header)': {
37
        paddingBottom: '0.25rem',
38
        flexDirection: 'column',
39
        marginBottom: 0
40
      },
41
      '&::part(content)': {
42
        padding: 0
43
      }
44
    },
45
    headerContent: {
46
      display: 'grid',
47
      gridTemplateColumns: 'fit-content(100px) minmax(0, 1fr) fit-content(100px)',
48
      gridTemplateAreas: `
49
      "titleStart titleCenter cancel"
50
      "input input input"
51
      `,
52
      gridTemplateRows: `${CssSizeVariables.sapWcrDialogHeaderHeight} ${CssSizeVariables.sapWcrDialogSubHeaderHeight}`,
53
      width: '100%',
54
      alignItems: 'center'
55
    },
56
    title: {
57
      fontSize: ThemingParameters.sapFontLargeSize,
58
      fontFamily: ThemingParameters.sapFontHeaderFamily,
59
      gridColumnStart: 'titleStart',
60
      gridColumnEnd: 'titleCenter',
61
      maxWidth: '100%',
62
      overflow: 'hidden',
63
      textOverflow: 'ellipsis'
64
    },
65
    titleCenterAlign: {
66
      gridArea: 'titleCenter',
67
      justifySelf: 'center'
68
    },
69
    hiddenClearBtn: {
70
      gridArea: 'titleStart',
71
      visibility: 'hidden'
72
    },
73
    clearBtn: {
74
      gridArea: 'cancel',
75
      justifySelf: 'end'
76
    },
77
    input: {
78
      gridArea: 'input',
79
      width: '100%'
80
    },
81
    footer: {
82
      display: 'flex',
83
      alignItems: 'center',
84
      justifyContent: 'end',
85
      width: '100%',
86
      boxSizing: 'border-box',
87
      '& > *': {
88
        marginInlineStart: '0.5rem'
89
      }
90
    },
91
    inputIcon: { cursor: 'pointer', color: ThemingParameters.sapContent_IconColor },
92
    infoBar: { padding: '0 0.5rem', position: 'sticky', top: 0, zIndex: 1 }
93
  },
94
  { name: 'SelectDialog' }
95
);
96

97
interface ListDomRefWithPrivateAPIs extends ListDomRef {
98
  get hasData(): boolean;
99

100
  getSelectedItems(): HTMLElement[];
101

102
  deselectSelectedItems(): void;
103

104
  focusFirstItem(): void;
105
}
106

107
export interface SelectDialogPropTypes extends Omit<DialogPropTypes, 'header' | 'headerText' | 'footer' | 'children'> {
108
  /**
109
   * Defines the list items of the component.
110
   *
111
   * __Note:__ Although this prop accepts all HTML Elements and therefore also all list items, it is strongly recommended that you only use `StandardListItem` in order to preserve the intended design.
112
   */
113
  children?: ReactNode | ReactNode[];
114
  /**
115
   * 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`.
116
   */
117
  showClearButton?: boolean;
118
  /**
119
   * Defines the header text.
120
   */
121
  headerText?: string;
122
  /**
123
   * Specifies the `headerText` alignment.
124
   */
125
  headerTextAlignCenter?: boolean;
126
  /**
127
   * Overwrites the default text for the confirmation button.
128
   */
129
  confirmButtonText?: string;
130
  /**
131
   * 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`.
132
   *
133
   * __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.
134
   */
135
  rememberSelections?: boolean;
136
  /**
137
   * Defines the mode of the SelectDialog list.
138
   *
139
   * __Note:__ Although this prop accepts all `ListMode`s, it is strongly recommended that you only use `SingleSelect` or `MultiSelect` in order to preserve the intended design.
140
   */
141
  mode?: ListMode | keyof typeof ListMode;
142
  /**
143
   * Defines whether the `List` will have growing capability either by pressing a `More` button, or via user scroll. In both cases the `onLoadMore` event is fired.
144
   *
145
   * Available options:
146
   *
147
   * `Button` - Shows a `More` button at the bottom of the list, pressing of which triggers the `load-more` event.
148
   * `Scroll` - The `load-more` event is triggered when the user scrolls to the bottom of the list;
149
   * `None` (default) - The growing is off.
150
   *
151
   * **Limitations:** `growing="Scroll"` is not supported for Internet Explorer, on IE the component will fallback to `growing="Button"`.
152
   */
153
  growing?: ListGrowingMode | keyof typeof ListGrowingMode;
154
  /**
155
   * Defines props you can pass to the internal `List` component.
156
   *
157
   * __Note:__ `mode`, `children`, `growing`, `onLoadMore` and `footerText` are not supported.
158
   */
159
  listProps?: Omit<ListPropTypes, 'mode' | 'children' | 'footerText' | 'growing' | 'onLoadMore'>;
160
  /**
161
   * 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.
162
   */
163
  numberOfSelectedItems?: number;
164
  /**
165
   * This event will be fired when the value of the search field is changed by a user - e.g. at each key press
166
   */
167
  onSearchInput?: (event: Ui5CustomEvent<HTMLInputElement, { value: string }>) => void;
168
  /**
169
   * This event will be fired when the search button has been clicked or the ENTER key has been pressed in the search field.
170
   */
171
  onSearch?: (event: Ui5CustomEvent<{ value: string }>) => void;
172
  /**
173
   * This event will be fired when the reset button has been clicked in the search field or when the dialog is closed.
174
   */
175
  onSearchReset?: (event: Ui5CustomEvent<{ prevValue: string }>) => void;
176
  /**
177
   * This event will be fired when the clear button has been clicked.
178
   */
179
  onClear?: (event: Ui5CustomEvent<{ prevSelectedItems: HTMLElement[] }>) => void;
180
  /**
181
   * 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.
182
   */
183
  onConfirm?: (event: Ui5CustomEvent<{ selectedItems: HTMLElement[] }>) => void;
184
  /**
185
   * Fired when the user scrolls to the bottom of the list.
186
   *
187
   * **Note:** The event is fired when the `growing='Scroll'` property is enabled.
188
   */
189
  onLoadMore?: (event: Ui5CustomEvent) => void;
190
}
191

192
/**
193
 * The SelectDialog enables users to filter a comprehensive list via a search field and to select one or more items.
194
 */
195
const SelectDialog = forwardRef<DialogDomRef, SelectDialogPropTypes>((props, ref) => {
73✔
196
  const {
197
    children,
198
    className,
199
    confirmButtonText,
200
    growing,
201
    headerText,
202
    headerTextAlignCenter,
203
    listProps,
204
    mode,
205
    numberOfSelectedItems,
206
    rememberSelections,
207
    showClearButton,
208
    onAfterClose,
209
    onClear,
210
    onConfirm,
211
    onLoadMore,
212
    onSearch,
213
    onSearchInput,
214
    onSearchReset,
215
    onBeforeOpen,
216
    onAfterOpen,
217
    ...rest
218
  } = props;
16✔
219

220
  const classes = useStyles();
15✔
221
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
15✔
222
  const [searchValue, setSearchValue] = useState('');
15✔
223
  const [selectedItems, setSelectedItems] = useState([]);
15✔
224
  const [componentRef, selectDialogRef] = useSyncRef(ref);
15✔
225
  const [listComponentRef, listRef] = useSyncRef<ListDomRefWithPrivateAPIs>((listProps as any).ref);
15✔
226

227
  const handleBeforeOpen = (e) => {
15✔
228
    if (typeof onBeforeOpen === 'function') {
3!
229
      onBeforeOpen(e);
×
230
    }
231
    if (mode === ListMode.MultiSelect && listRef.current?.hasData) {
3!
232
      setSelectedItems(listRef.current?.getSelectedItems() ?? []);
2!
233
    }
234
  };
235

236
  const handleAfterOpen = (e) => {
15✔
237
    if (typeof onAfterOpen === 'function') {
×
238
      onAfterOpen(e);
×
239
    }
240
    listRef.current?.focusFirstItem();
×
241
  };
242

243
  const handleSearchInput = (e) => {
15✔
244
    if (typeof onSearchInput === 'function') {
1!
245
      onSearchInput(enrichEventWithDetails(e, { value: e.target.value }));
1✔
246
    }
247
    setSearchValue(e.target.value);
1✔
248
  };
249
  const handleSearchSubmit = (e) => {
15✔
250
    if (typeof onSearch === 'function') {
×
251
      if (e.type === 'keyup' && e.code === 'Enter') {
×
252
        onSearch(enrichEventWithDetails(e, { value: e.target.value }));
×
253
      }
254
      if (e.type === 'click') {
×
255
        onSearch(enrichEventWithDetails(e, { value: searchValue }));
×
256
      }
257
    }
258
  };
259
  const handleResetSearch = (e) => {
15✔
260
    if (typeof onSearchReset === 'function') {
×
261
      onSearchReset(enrichEventWithDetails(e, { prevValue: searchValue }));
×
262
    }
263
    setSearchValue('');
×
264
  };
265

266
  const handleSelectionChange = (e) => {
15✔
267
    if (typeof listProps?.onSelectionChange === 'function') {
3!
268
      listProps.onSelectionChange(e);
3✔
269
    }
270
    if (mode === ListMode.MultiSelect) {
3!
271
      setSelectedItems(e.detail.selectedItems);
2✔
272
    } else {
273
      if (typeof onConfirm === 'function') {
1!
274
        onConfirm(e);
1✔
275
      }
276
      selectDialogRef.current.close();
1✔
277
    }
278
  };
279

280
  const handleClose = () => {
15✔
281
    selectDialogRef.current.close();
×
282
  };
283

284
  const handleClear = (e) => {
15✔
285
    if (typeof onClear === 'function') {
1!
286
      onClear(enrichEventWithDetails(e, { prevSelectedItems: selectedItems }));
1✔
287
    }
288
    setSelectedItems([]);
1✔
289
    listRef.current?.deselectSelectedItems();
1✔
290
  };
291

292
  const handleConfirm = (e) => {
15✔
293
    if (typeof onConfirm === 'function') {
1!
294
      onConfirm(enrichEventWithDetails(e, { selectedItems }));
1✔
295
    }
296
    selectDialogRef.current.close();
1✔
297
  };
298

299
  const handleAfterClose = (e) => {
15✔
300
    if (typeof onAfterClose === 'function') {
2!
301
      onAfterClose(e);
2✔
302
    }
303
    if (typeof onSearchReset === 'function') {
2!
304
      onSearchReset(enrichEventWithDetails(e, { prevValue: searchValue }));
×
305
    }
306
    setSearchValue('');
2✔
307
    if (!rememberSelections) {
2!
308
      listRef.current?.deselectSelectedItems();
×
309
    }
310
  };
311

312
  return (
15✔
313
    <Dialog
314
      {...rest}
315
      data-component-name="SelectDialog"
316
      ref={componentRef}
317
      className={clsx(classes.dialog, className)}
318
      onAfterClose={handleAfterClose}
319
      onBeforeOpen={handleBeforeOpen}
320
      onAfterOpen={handleAfterOpen}
321
    >
322
      <div className={classes.headerContent} slot="header">
323
        {showClearButton && headerTextAlignCenter && (
21!
324
          <Button
325
            onClick={handleClear}
326
            design={ButtonDesign.Transparent}
327
            className={classes.hiddenClearBtn}
328
            tabIndex={-1}
329
            aria-hidden="true"
330
          >
331
            {i18nBundle.getText(CLEAR)}
332
          </Button>
333
        )}
334
        <Title className={clsx(classes.title, headerTextAlignCenter && classes.titleCenterAlign)}>{headerText}</Title>
16!
335
        {showClearButton && (
21!
336
          <Button onClick={handleClear} design={ButtonDesign.Transparent} className={classes.clearBtn}>
337
            {i18nBundle.getText(CLEAR)}
338
          </Button>
339
        )}
340
        <Input
341
          className={classes.input}
342
          accessibleName={i18nBundle.getText(SEARCH)}
343
          value={searchValue}
344
          placeholder={i18nBundle.getText(SEARCH)}
345
          onInput={handleSearchInput}
346
          onKeyUp={handleSearchSubmit}
347
          icon={
348
            <>
349
              {searchValue && (
16!
350
                <Icon
351
                  accessibleName={i18nBundle.getText(RESET)}
352
                  title={i18nBundle.getText(RESET)}
353
                  name={iconDecline}
354
                  interactive
355
                  onClick={handleResetSearch}
356
                  className={classes.inputIcon}
357
                />
358
              )}
359
              <Icon
360
                name={iconSearch}
361
                className={classes.inputIcon}
362
                onClick={handleSearchSubmit}
363
                accessibleName={i18nBundle.getText(SEARCH)}
364
                title={i18nBundle.getText(SEARCH)}
365
              />
366
            </>
367
          }
368
        />
369
      </div>
370

371
      {mode === ListMode.MultiSelect && (!!selectedItems.length || numberOfSelectedItems > 0) && (
32!
372
        <Toolbar design={ToolbarDesign.Info} className={classes.infoBar}>
373
          <Text>{`${i18nBundle.getText(SELECTED)}: ${numberOfSelectedItems ?? selectedItems.length}`}</Text>
7!
374
        </Toolbar>
375
      )}
376
      <List
377
        {...listProps}
378
        ref={listComponentRef}
379
        growing={growing}
380
        onLoadMore={onLoadMore}
381
        mode={mode}
382
        onSelectionChange={handleSelectionChange}
383
      >
384
        {children}
385
      </List>
386
      <div slot="footer" className={classes.footer}>
387
        {mode === ListMode.MultiSelect && (
23!
388
          <Button onClick={handleConfirm} design={ButtonDesign.Emphasized}>
389
            {confirmButtonText ?? i18nBundle.getText(SELECT)}
15!
390
          </Button>
391
        )}
392
        <Button onClick={handleClose} design={ButtonDesign.Transparent}>
393
          {i18nBundle.getText(CANCEL)}
394
        </Button>
395
      </div>
396
    </Dialog>
397
  );
398
});
73✔
399

400
SelectDialog.defaultProps = {
73✔
401
  mode: ListMode.SingleSelect,
402
  listProps: {}
403
};
404

405
SelectDialog.displayName = 'SelectDialog';
73✔
406

407
export { SelectDialog };
146✔
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