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

SAP / ui5-webcomponents-react / 3658276346

pending completion
3658276346

Pull #3856

github

GitHub
Merge 5d0b46815 into beb032e09
Pull Request #3856: fix(Modals - TypeScript): allow typing the container element

3040 of 6031 branches covered (50.41%)

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

4225 of 5253 relevant lines covered (80.43%)

1079.1 hits per line

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

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

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

95
interface ListDomRefWithPrivateAPIs extends ListDomRef {
96
  get hasData(): boolean;
97

98
  getSelectedItems(): HTMLElement[];
99

100
  deselectSelectedItems(): void;
101

102
  focusFirstItem(): void;
103
}
104

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

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

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

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

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

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

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

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

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

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

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

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

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

398
SelectDialog.defaultProps = {
21✔
399
  mode: ListMode.SingleSelect,
400
  listProps: {}
401
};
402

403
SelectDialog.displayName = 'SelectDialog';
21✔
404

405
export { SelectDialog };
38✔
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