• 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

80.14
/packages/main/src/components/VariantManagement/index.tsx
1
import '@ui5/webcomponents-fiori/dist/illustrations/UnableToLoad.js';
2
import navDownIcon from '@ui5/webcomponents-icons/dist/navigation-down-arrow.js';
3
import searchIcon from '@ui5/webcomponents-icons/dist/search.js';
4
import { enrichEventWithDetails, ThemingParameters, useI18nBundle } from '@ui5/webcomponents-react-base';
5
import clsx from 'clsx';
6
import React, {
7
  Children,
8
  cloneElement,
9
  ComponentElement,
10
  forwardRef,
11
  isValidElement,
12
  ReactNode,
13
  useCallback,
14
  useEffect,
15
  useRef,
16
  useState
17
} from 'react';
18
import { createPortal } from 'react-dom';
19
import { createUseStyles } from 'react-jss';
20
import {
21
  BarDesign,
22
  ButtonDesign,
23
  IllustrationMessageType,
24
  ListMode,
25
  PopoverPlacementType,
26
  TitleLevel
27
} from '../../enums';
28
import { MANAGE, MY_VIEWS, SAVE, SAVE_AS, SEARCH, SEARCH_VARIANT, SELECT_VIEW } from '../../i18n/i18n-defaults';
29
import { CommonProps, Ui5CustomEvent } from '../../interfaces';
30
import { stopPropagation } from '../../internal/stopPropagation';
31
import { SelectedVariant, VariantManagementContext } from '../../internal/VariantManagementContext';
32
import {
33
  Bar,
34
  Button,
35
  Icon,
36
  IllustratedMessage,
37
  Input,
38
  List,
39
  ResponsivePopover,
40
  ResponsivePopoverDomRef,
41
  Title
42
} from '../../webComponents';
43
import { FlexBox } from '../FlexBox';
44
import { ManageViewsDialog } from './ManageViewsDialog';
45
import { SaveViewDialog } from './SaveViewDialog';
46
import { VariantItemPropTypes } from './VariantItem';
47

48
interface UpdatedVariant extends SelectedVariant {
49
  prevVariant?: VariantItemPropTypes;
50
}
51

52
export interface VariantManagementPropTypes extends Omit<CommonProps, 'onSelect'> {
53
  /**
54
   * Variant items displayed by the VariantManagement component.
55
   *
56
   * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `VariantItem` in order to preserve the intended design.
57
   */
58
  children?: ReactNode | ReactNode[];
59
  /**
60
   * Determines on which side the VariantManagement popover is placed at.
61
   */
62
  placement?: PopoverPlacementType | keyof typeof PopoverPlacementType;
63
  /**
64
   * Describes the title of the VariantManagement popover.
65
   *
66
   * __Note:__ If not set, the default title is used.
67
   */
68
  titleText?: string;
69
  /**
70
   * Defines whether the VariantManagement should be closed if an item was selected.
71
   */
72
  closeOnItemSelect?: boolean;
73
  /**
74
   * Describes the `HTML Title` level of the variants.
75
   */
76
  level?: TitleLevel | keyof typeof TitleLevel;
77
  /**
78
   * Defines whether the VariantManagement is disabled.
79
   */
80
  disabled?: boolean;
81
  /**
82
   * Fired after a variant has been selected.
83
   */
84
  onSelect?: (
85
    event: Ui5CustomEvent<
86
      HTMLElement,
87
      {
88
        selectedVariant: SelectedVariant;
89
        selectedItems: unknown[];
90
        previouslySelectedItems: unknown[];
91
      }
92
    >
93
  ) => void;
94
  /**
95
   * Indicator for modified but not saved variants.
96
   *
97
   * __Note:__ You can change the indicator by setting `dirtyStateText`.
98
   */
99
  dirtyState?: boolean;
100
  /**
101
   * Text for the dirty state indicator.
102
   */
103
  dirtyStateText?: string;
104
  /**
105
   * Indicates that the 'Favorites' feature is used. Only variants marked as favorites will be displayed in the variant list.
106
   *
107
   * __Note:__ Only if `showOnlyFavorites` is set to `true` favorites can be changed.
108
   */
109
  showOnlyFavorites?: boolean;
110
  /**
111
   * Indicates that set as default is visible in the Save View and the Manage Views dialogs.
112
   */
113
  hideSetAsDefault?: boolean;
114
  /**
115
   * Indicates that the Public indicator is visible in the Save View and the Manage Views dialogs.
116
   */
117
  hideShare?: boolean;
118
  /**
119
   * Indicates that Apply Automatically is visible in the Save View and the Manage Views dialogs.
120
   */
121
  hideApplyAutomatically?: boolean;
122
  /**
123
   * Indicates that the Author is visible in the Manage Views dialog.
124
   */
125
  hideCreatedBy?: boolean;
126
  /**
127
   * Indicates that the Save View dialog button is visible.
128
   */
129
  hideSaveAs?: boolean;
130
  /**
131
   * Indicates that the Manage Views dialog button is visible.
132
   */
133
  hideManageVariants?: boolean;
134
  /**
135
   * Indicates that the control is in error state. If set to true error message will be displayed whenever the variant is opened.
136
   */
137
  inErrorState?: boolean;
138
  /**
139
   * Defines where modals are rendered into via `React.createPortal`.
140
   *
141
   * You can find out more about this [here](https://sap.github.io/ui5-webcomponents-react/?path=/docs/knowledge-base-working-with-portals--page).
142
   *
143
   * Defaults to: `document.body`
144
   */
145
  portalContainer?: Element;
146
  /**
147
   * The event is fired when the "Save" button is clicked inside the Save View dialog.
148
   */
149
  onSaveAs?: (e: CustomEvent<SelectedVariant>) => void;
150
  /**
151
   * The event is fired when the "Save" button is clicked inside the Manage Views dialog.
152
   */
153
  onSaveManageViews?: (
154
    e: CustomEvent<{
155
      deletedVariants: VariantItemPropTypes[];
156
      prevVariants: VariantItemPropTypes[];
157
      updatedVariants: UpdatedVariant[];
158
      variants: SelectedVariant[];
159
    }>
160
  ) => void;
161
  /**
162
   * The event is fired when the "Save" button is clicked in the `VariantManagement` popover.
163
   *
164
   * __Note:__ The save button is only displayed if the `VariantManagement` is in `dirtyState` and the selected variant is not in `readOnly` mode.
165
   */
166
  onSave?: (e: CustomEvent<SelectedVariant>) => void;
167
}
168

169
const styles = {
28✔
170
  container: {
171
    display: 'flex',
172
    alignItems: 'center',
173
    textAlign: 'center'
174
  },
175
  title: {
176
    cursor: 'pointer',
177
    color: ThemingParameters.sapLinkColor,
178
    textShadow: 'none',
179
    '&:hover': {
180
      color: ThemingParameters.sapLink_Hover_Color
181
    },
182
    '&:active': {
183
      color: ThemingParameters.sapLink_Active_Color
184
    }
185
  },
186
  disabled: {
187
    '& $title': {
188
      color: ThemingParameters.sapGroup_TitleTextColor,
189
      cursor: 'default',
190
      '&:hover': {
191
        color: 'ThemingParameters.sapGroup_TitleTextColor'
192
      }
193
    }
194
  },
195
  dirtyState: {
196
    color: ThemingParameters.sapGroup_TitleTextColor,
197
    paddingInline: '0.125rem',
198
    fontWeight: 'bold',
199
    font: ThemingParameters.sapFontFamily,
200
    fontSize: ThemingParameters.sapFontSize,
201
    flexGrow: 1
202
  },
203
  dirtyStateText: {
204
    fontSize: ThemingParameters.sapFontSmallSize,
205
    fontWeight: 'normal'
206
  },
207
  navDownBtn: {
208
    marginInlineStart: '0.125rem'
209
  },
210
  footer: {
211
    '& > :last-child': {
212
      marginInlineEnd: 0
213
    }
214
  },
215
  inputIcon: { cursor: 'pointer', color: ThemingParameters.sapContent_IconColor },
216
  searchInput: { padding: '0.25rem 1rem' },
217
  popover: {
218
    minWidth: '25rem',
219
    '&::part(content), &::part(footer)': {
220
      padding: 0
221
    },
222
    '&::part(footer)': {
223
      borderBlockStart: 'none'
224
    }
225
  }
226
};
227

228
const useStyles = createUseStyles(styles, { name: 'VariantManagement' });
28✔
229
/**
230
 * The `VariantManagement` component can be used to manage variants, such as FilterBar variants or AnalyticalTable variants.
231
 */
232
const VariantManagement = forwardRef<HTMLDivElement, VariantManagementPropTypes>((props, ref) => {
28✔
233
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
67✔
234
  const {
235
    titleText = i18nBundle.getText(MY_VIEWS),
81✔
236
    className,
237
    style,
238
    placement,
239
    level,
240
    onSelect,
241
    closeOnItemSelect,
242
    disabled,
243
    onSaveAs,
244
    onSaveManageViews,
245
    showOnlyFavorites,
246
    inErrorState,
247
    hideShare,
248
    children,
249
    hideManageVariants,
250
    hideApplyAutomatically,
251
    hideSetAsDefault,
252
    hideCreatedBy,
253
    hideSaveAs,
254
    dirtyStateText,
255
    dirtyState,
256
    onSave,
257
    portalContainer,
258
    ...rest
259
  } = props;
67✔
260

261
  const classes = useStyles();
67✔
262
  const popoverRef = useRef<ResponsivePopoverDomRef>(null);
67✔
263

264
  const [safeChildren, setSafeChildren] = useState(Children.toArray(children));
67✔
265
  const [showInput, setShowInput] = useState(safeChildren.length > 9);
67✔
266

267
  useEffect(() => {
67✔
268
    setSafeChildren(Children.toArray(children));
19✔
269
  }, [children]);
270

271
  useEffect(() => {
67✔
272
    if (safeChildren.length > 9) {
19!
273
      setShowInput(true);
2✔
274
    } else {
275
      setShowInput(false);
17✔
276
    }
277
  }, [safeChildren.length]);
278

279
  const [manageViewsDialogOpen, setManageViewsDialogOpen] = useState(false);
67✔
280
  const [saveAsDialogOpen, setSaveAsDialogOpen] = useState(false);
67✔
281
  const [selectedVariant, setSelectedVariant] = useState<SelectedVariant | undefined>(() => {
67✔
282
    const currentSelectedVariant = safeChildren.find(
17✔
283
      (item) => isValidElement(item) && item.props.selected
52✔
284
    ) as ComponentElement<any, any>;
285
    if (currentSelectedVariant) {
17✔
286
      return { ...currentSelectedVariant.props, variantItem: currentSelectedVariant.ref };
15✔
287
    }
288
  });
289

290
  const handleClose = () => {
67✔
291
    popoverRef.current.close();
1✔
292
  };
293

294
  const handleManageClick = () => {
67✔
295
    setManageViewsDialogOpen(true);
5✔
296
  };
297
  const handleManageClose = () => {
67✔
298
    setManageViewsDialogOpen(false);
1✔
299
  };
300
  const handleOpenSaveAsDialog = () => {
67✔
301
    setSaveAsDialogOpen(true);
3✔
302
  };
303
  const handleSaveAsClose = () => {
67✔
304
    setSaveAsDialogOpen(false);
3✔
305
  };
306

307
  const handleSave = (e) => {
67✔
308
    if (typeof onSave === 'function') {
1!
309
      onSave(enrichEventWithDetails(e, selectedVariant as Record<string, any>));
1✔
310
    }
311
  };
312

313
  const handleSaveView = (e, selectedVariant) => {
67✔
314
    if (typeof onSaveAs === 'function') {
1!
315
      onSaveAs(enrichEventWithDetails(e, selectedVariant));
1✔
316
    }
317
    handleSaveAsClose();
1✔
318
  };
319

320
  const handleSaveManageViews = (e, payload) => {
67✔
321
    const { defaultView, updatedRows, deletedRows } = payload;
1✔
322
    const callbackProperties = { deletedVariants: [], prevVariants: [], updatedVariants: [], variants: [] };
1✔
323
    setSafeChildren((prev) =>
1✔
324
      Children.toArray(
1✔
325
        prev.map((child: ComponentElement<any, any>) => {
326
          let updatedProps: Omit<SelectedVariant, 'children' | 'variantItem'> = {};
3✔
327
          const currentVariant = popoverRef.current.querySelector(`ui5-li[data-text="${child.props.children}"]`);
3✔
328
          callbackProperties.prevVariants.push(child.props);
3✔
329
          if (defaultView) {
3!
330
            if (defaultView === child.props.children) {
×
331
              updatedProps.isDefault = true;
×
332
            } else if (child.props.isDefault) {
×
333
              updatedProps.isDefault = false;
×
334
            }
335
          }
336
          if (Object.keys(updatedRows).includes(child.props.children)) {
3!
337
            const { currentVariant: _0, ...rest } = updatedRows[child.props.children];
×
338
            updatedProps = { ...updatedProps, ...rest };
×
339
          }
340
          if (deletedRows.has(child.props.children)) {
3!
341
            callbackProperties.deletedVariants.push(child.props);
2✔
342
            return false;
2✔
343
          }
344
          if (Object.keys(updatedProps).length > 0) {
1!
345
            callbackProperties.updatedVariants.push({
×
346
              ...child.props,
347
              ...updatedProps,
348
              variantItem: currentVariant,
349
              prevVariant: { ...child.props }
350
            });
351
          }
352
          callbackProperties.variants.push({ ...child.props, ...updatedProps, variantItem: currentVariant });
1✔
353
          return cloneElement(child, updatedProps);
1✔
354
        })
355
      )
356
    );
357
    if (typeof onSaveManageViews === 'function') {
1!
358
      onSaveManageViews(enrichEventWithDetails(e, callbackProperties));
1✔
359
    }
360
    handleManageClose();
1✔
361
  };
362

363
  const handleOpenVariantManagement = useCallback(
67✔
364
    (e) => {
365
      popoverRef.current.showAt(e.target);
6✔
366
    },
367
    [popoverRef]
368
  );
369

370
  const searchText = i18nBundle.getText(SEARCH);
67✔
371
  const saveAsText = i18nBundle.getText(SAVE_AS);
67✔
372
  const manageText = i18nBundle.getText(MANAGE);
67✔
373
  const saveText = i18nBundle.getText(SAVE);
67✔
374
  const a11ySearchText = i18nBundle.getText(SEARCH_VARIANT);
67✔
375
  const selectViewText = i18nBundle.getText(SELECT_VIEW);
67✔
376

377
  const variantManagementClasses = clsx(classes.container, disabled && classes.disabled, className);
67!
378

379
  const dirtyStateClasses = clsx(classes.dirtyState, dirtyStateText !== '*' && classes.dirtyStateText);
67!
380

381
  const selectVariantEventRef = useRef();
67✔
382
  useEffect(() => {
67✔
383
    if (selectVariantEventRef.current) {
34!
384
      if (typeof onSelect === 'function') {
3!
385
        onSelect(enrichEventWithDetails(selectVariantEventRef.current, { selectedVariant }));
1✔
386
        selectVariantEventRef.current = undefined;
1✔
387
      }
388
    }
389
  }, [selectedVariant, onSelect]);
390

391
  const handleVariantItemSelect = (e) => {
67✔
392
    setSelectedVariant({ ...e.detail.selectedItems[0].dataset, variantItem: e.detail.selectedItems[0] });
3✔
393
    selectVariantEventRef.current = e;
3✔
394
    if (closeOnItemSelect) {
3!
395
      handleClose();
1✔
396
    }
397
  };
398

399
  const variantNames = safeChildren.map((item: ComponentElement<any, any>) =>
67✔
400
    typeof item.props?.children === 'string' ? item.props.children : ''
274!
401
  );
402

403
  const [favoriteChildren, setFavoriteChildren] = useState(undefined);
67✔
404

405
  useEffect(() => {
67✔
406
    if (showOnlyFavorites) {
39!
407
      setFavoriteChildren(
6✔
408
        safeChildren.filter((child: ComponentElement<any, any>) => child.props.favorite || child.props.isDefault)
36!
409
      );
410
    }
411
    if (!showOnlyFavorites && favoriteChildren?.length > 0) {
39!
412
      setFavoriteChildren(undefined);
1✔
413
    }
414
  }, [showOnlyFavorites, safeChildren]);
415

416
  const safeChildrenWithFavorites = favoriteChildren ?? safeChildren;
67✔
417

418
  const [filteredChildren, setFilteredChildren] = useState(undefined);
67✔
419
  const [searchValue, setSearchValue] = useState('');
67✔
420
  const handleSearchInput = (e) => {
67✔
421
    setSearchValue(e.target.value);
1✔
422
    setFilteredChildren(
1✔
423
      safeChildrenWithFavorites.filter(
424
        (child: ComponentElement<any, any>) =>
425
          typeof child?.props?.children === 'string' &&
10!
426
          child.props.children.toLowerCase().includes(e.target.value.toLowerCase())
427
      )
428
    );
429
  };
430
  useEffect(() => {
67✔
431
    if (filteredChildren) {
41!
432
      setFilteredChildren(
1✔
433
        safeChildrenWithFavorites.filter(
434
          (child: ComponentElement<any, any>) =>
435
            typeof child?.props?.children === 'string' && child.props.children.toLowerCase().includes(searchValue)
9!
436
        )
437
      );
438
    }
439
  }, [safeChildrenWithFavorites]);
440

441
  const showSaveBtn = dirtyState && !selectedVariant?.readOnly;
67!
442

443
  return (
67✔
444
    <div className={variantManagementClasses} style={style} {...rest} ref={ref}>
445
      <VariantManagementContext.Provider
446
        value={{
447
          selectVariantItem: setSelectedVariant
448
        }}
449
      >
450
        <FlexBox onClick={disabled ? undefined : handleOpenVariantManagement}>
83!
451
          <Title level={level} className={classes.title}>
452
            {selectedVariant?.children}
453
          </Title>
454
          {dirtyState && <div className={dirtyStateClasses}>{dirtyStateText}</div>}
87!
455
        </FlexBox>
456
        <Button
457
          className={clsx(classes.navDownBtn, 'ui5-content-density-compact')}
458
          tooltip={selectViewText}
459
          accessibleName={selectViewText}
460
          onClick={handleOpenVariantManagement}
461
          design={ButtonDesign.Transparent}
462
          icon={navDownIcon}
463
          disabled={disabled}
464
        />
465
        {createPortal(
466
          <ResponsivePopover
467
            className={classes.popover}
468
            ref={popoverRef}
469
            headerText={titleText}
470
            placementType={placement}
471
            footer={
48!
472
              (showSaveBtn || !hideSaveAs || !hideManageVariants) && (
200✔
473
                <Bar
474
                  design={BarDesign.Footer}
475
                  className={classes.footer}
476
                  endContent={
477
                    <>
478
                      {!inErrorState && showSaveBtn && (
166!
479
                        <Button onClick={handleSave} design={ButtonDesign.Emphasized}>
480
                          {saveText}
481
                        </Button>
482
                      )}
483
                      {!inErrorState && !hideSaveAs && (
244✔
484
                        <Button
485
                          onClick={handleOpenSaveAsDialog}
486
                          design={showSaveBtn ? ButtonDesign.Transparent : ButtonDesign.Emphasized}
80!
487
                          disabled={!selectedVariant || Object.keys(selectedVariant).length === 0}
146✔
488
                        >
489
                          {saveAsText}
490
                        </Button>
491
                      )}
492
                      {!inErrorState && !hideManageVariants && (
244✔
493
                        <Button
494
                          onClick={handleManageClick}
495
                          design={showSaveBtn || !hideSaveAs ? ButtonDesign.Transparent : ButtonDesign.Emphasized}
238!
496
                        >
497
                          {manageText}
498
                        </Button>
499
                      )}
500
                    </>
501
                  }
502
                />
503
              )
504
            }
505
            onAfterClose={stopPropagation}
506
          >
507
            {inErrorState ? (
83!
508
              <IllustratedMessage name={IllustrationMessageType.UnableToLoad} />
509
            ) : (
510
              <List
511
                onSelectionChange={handleVariantItemSelect}
512
                mode={ListMode.SingleSelect}
513
                header={
514
                  showInput ? (
81!
515
                    <div className={classes.searchInput} tabIndex={-1}>
516
                      <Input
517
                        accessibleName={a11ySearchText}
518
                        value={searchValue}
519
                        placeholder={searchText}
520
                        onInput={handleSearchInput}
521
                        showClearIcon
522
                        icon={<Icon name={searchIcon} className={classes.inputIcon} />}
523
                      />
524
                    </div>
525
                  ) : undefined
526
                }
527
              >
528
                {filteredChildren ?? safeChildrenWithFavorites}
158✔
529
              </List>
530
            )}
531
          </ResponsivePopover>,
532
          portalContainer
533
        )}
534
        {manageViewsDialogOpen && (
92!
535
          <ManageViewsDialog
536
            onAfterClose={handleManageClose}
537
            handleSaveManageViews={handleSaveManageViews}
538
            showShare={!hideShare}
539
            showApplyAutomatically={!hideApplyAutomatically}
540
            showCreatedBy={!hideCreatedBy}
541
            showSetAsDefault={!hideSetAsDefault}
542
            variantNames={variantNames}
543
            portalContainer={portalContainer}
544
            showOnlyFavorites={showOnlyFavorites}
545
          >
546
            {safeChildren}
547
          </ManageViewsDialog>
548
        )}
549
        {saveAsDialogOpen && (
86!
550
          <SaveViewDialog
551
            portalContainer={portalContainer}
552
            showShare={!hideShare}
553
            showApplyAutomatically={!hideApplyAutomatically}
554
            showSetAsDefault={!hideSetAsDefault}
555
            onAfterClose={handleSaveAsClose}
556
            handleSave={handleSaveView}
557
            selectedVariant={selectedVariant}
558
            variantNames={variantNames}
559
          />
560
        )}
561
      </VariantManagementContext.Provider>
562
    </div>
563
  );
564
});
28✔
565

566
VariantManagement.defaultProps = {
28✔
567
  placement: PopoverPlacementType.Bottom,
568
  level: TitleLevel.H4,
569
  dirtyStateText: '*',
570
  portalContainer: document.body
571
};
572
VariantManagement.displayName = 'VariantManagement';
28✔
573

574
export { VariantManagement };
56✔
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