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

SAP / ui5-webcomponents-react / 3619939827

pending completion
3619939827

Pull #3829

github

GitHub
Merge 84c32f6c4 into 0fa305c6b
Pull Request #3829: fix(FilterBar): fix alignment of table cells in dialog

3007 of 6031 branches covered (49.86%)

1 of 1 new or added line in 1 file covered. (100.0%)

4178 of 5253 relevant lines covered (79.54%)

4085.7 hits per line

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

39.06
/packages/main/src/components/FilterBar/FilterDialog.tsx
1
import group2Icon from '@ui5/webcomponents-icons/dist/group-2.js';
2
import listIcon from '@ui5/webcomponents-icons/dist/list.js';
3
import searchIcon from '@ui5/webcomponents-icons/dist/search.js';
4
import { enrichEventWithDetails, useI18nBundle } from '@ui5/webcomponents-react-base';
5
import React, { Children, cloneElement, useEffect, useReducer, useRef, useState } from 'react';
6
import { createPortal } from 'react-dom';
7
import { createUseStyles } from 'react-jss';
8
import {
9
  BarDesign,
10
  ButtonDesign,
11
  FlexBoxDirection,
12
  FlexBoxJustifyContent,
13
  TableMode,
14
  TitleLevel,
15
  ToolbarStyle
16
} from '../../enums';
17
import {
18
  ACTIVE,
19
  ALL,
20
  BASIC,
21
  CANCEL,
22
  FIELD,
23
  FIELDS_BY_ATTRIBUTE,
24
  FILTERS,
25
  GROUP_VIEW,
26
  HIDE_VALUES,
27
  LIST_VIEW,
28
  MANDATORY,
29
  OK,
30
  RESET,
31
  SEARCH_FOR_FILTERS,
32
  SHOW_VALUES,
33
  VISIBLE,
34
  VISIBLE_AND_ACTIVE
35
} from '../../i18n/i18n-defaults';
36
import { Ui5CustomEvent } from '../../interfaces';
37
import { addCustomCSSWithScoping } from '../../internal/addCustomCSSWithScoping';
38
import { stopPropagation } from '../../internal/stopPropagation';
39
import {
40
  Bar,
41
  Button,
42
  Dialog,
43
  DialogDomRef,
44
  Icon,
45
  Input,
46
  Option,
47
  Panel,
48
  SegmentedButton,
49
  SegmentedButtonItem,
50
  Select,
51
  Table,
52
  TableColumn,
53
  TableDomRef,
54
  TableRowDomRef,
55
  Title
56
} from '../../webComponents';
57
import { FilterGroupItemPropTypes } from '../FilterGroupItem';
58
import { FlexBox } from '../FlexBox';
59
import { Toolbar } from '../Toolbar';
60
import { ToolbarSpacer } from '../ToolbarSpacer';
61
import styles from './FilterBarDialog.jss';
62
import { filterValue, syncRef } from './utils';
63

64
addCustomCSSWithScoping(
21✔
65
  'ui5-table',
66
  `
67
/* hide table header of panel table */
68
:host([data-component-name="FilterBarDialogPanelTable"]) thead {
69
  visibility: collapse;
70
}
71
/* don't display border of panel table */
72
:host([data-component-name="FilterBarDialogPanelTable"]) table {
73
  border-collapse: unset;
74
}
75

76
:host([data-component-name="FilterBarDialogPanelTable"]) .ui5-table-root {
77
  border-bottom: none;
78
}
79
 `
80
);
81

82
const getActiveFilters = (activeFilterAttribute, filter) => {
21✔
83
  switch (activeFilterAttribute) {
18!
84
    case 'all':
85
      return true;
18✔
86
    case 'visible':
87
      return filter.props?.visibleInFilterBar;
×
88
    case 'active':
89
      return filter.props?.active;
×
90
    case 'visibleAndActive':
91
      return filter.props?.visibleInFilterBar && filter.props?.active;
×
92
    case 'mandatory':
93
      return filter.props?.required;
×
94
    default:
95
      return true;
×
96
  }
97
};
98

99
const compareObjects = (firstObj, secondObj) =>
21✔
100
  Object.keys(firstObj).find((first) =>
×
101
    Object.keys(secondObj).every((second) => firstObj[second] !== secondObj[first])
×
102
  );
103

104
const useStyles = createUseStyles(styles, { name: 'FilterBarDialog' });
21✔
105

106
interface FilterDialogPropTypes {
107
  filterBarRefs: any;
108
  open: boolean;
109
  handleDialogClose: (event: Ui5CustomEvent<DialogDomRef>) => void;
110
  children: any;
111
  showRestoreButton: boolean;
112
  handleRestoreFilters: (e, source, filterElements) => void;
113
  handleDialogSave: (e, newRefs, updatedToggledFilters) => void;
114
  handleSearchValueChange: React.Dispatch<React.SetStateAction<string>>;
115
  handleSelectionChange?: (
116
    event: Ui5CustomEvent<
117
      TableDomRef,
118
      { element: TableRowDomRef; checked: boolean; selectedRows: unknown[]; previouslySelectedRows: unknown[] }
119
    >
120
  ) => void;
121
  handleDialogSearch?: (event: CustomEvent<{ value: string; element: HTMLElement }>) => void;
122
  handleDialogCancel?: (event: Ui5CustomEvent<HTMLElement>) => void;
123
  portalContainer: Element;
124
  dialogRef: React.MutableRefObject<DialogDomRef>;
125
  isListView: boolean;
126
  setIsListView: React.Dispatch<React.SetStateAction<boolean>>;
127
  filteredAttribute: string;
128
  setFilteredAttribute: React.Dispatch<React.SetStateAction<string>>;
129
  onAfterFiltersDialogOpen: (event: Ui5CustomEvent<DialogDomRef>) => void;
130
}
131

132
export const FilterDialog = (props: FilterDialogPropTypes) => {
21✔
133
  const {
134
    filterBarRefs,
135
    open,
136
    handleDialogClose,
137
    children,
138
    showRestoreButton,
139
    handleRestoreFilters,
140
    handleDialogSave,
141
    handleSelectionChange,
142
    handleDialogSearch,
143
    handleDialogCancel,
144
    onAfterFiltersDialogOpen,
145
    portalContainer,
146
    dialogRef,
147
    isListView,
148
    setIsListView,
149
    filteredAttribute,
150
    setFilteredAttribute
151
  } = props;
6✔
152
  const classes = useStyles();
6✔
153
  const [searchString, setSearchString] = useState('');
6✔
154
  const [toggledFilters, setToggledFilters] = useState({});
6✔
155
  const dialogRefs = useRef({});
6✔
156
  const dialogSearchRef = useRef(null);
6✔
157
  const [showValues, toggleValues] = useReducer((prev) => !prev, false);
6✔
158
  const [selectedFilters, setSelectedFilters] = useState(null);
6✔
159
  const [forceRequired, setForceRequired] = useState<undefined | TableRowDomRef>();
6✔
160

161
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
21✔
162

163
  const basicText = i18nBundle.getText(BASIC);
6✔
164
  const cancelText = i18nBundle.getText(CANCEL);
6✔
165
  const okText = i18nBundle.getText(OK);
6✔
166
  const searchForFiltersText = i18nBundle.getText(SEARCH_FOR_FILTERS);
6✔
167
  const filtersTitle = i18nBundle.getText(FILTERS);
6✔
168
  const resetText = i18nBundle.getText(RESET);
6✔
169
  const allText = i18nBundle.getText(ALL);
6✔
170
  const activeText = i18nBundle.getText(ACTIVE);
6✔
171
  const visibleText = i18nBundle.getText(VISIBLE);
6✔
172
  const visibleAndActiveText = i18nBundle.getText(VISIBLE_AND_ACTIVE);
6✔
173
  const mandatoryText = i18nBundle.getText(MANDATORY);
6✔
174
  const listViewText = i18nBundle.getText(LIST_VIEW);
6✔
175
  const groupViewText = i18nBundle.getText(GROUP_VIEW);
6✔
176
  const showValuesText = i18nBundle.getText(SHOW_VALUES);
6✔
177
  const hideValuesText = i18nBundle.getText(HIDE_VALUES);
6✔
178
  const fieldText = i18nBundle.getText(FIELD);
6✔
179
  const fieldsByAttributeText = i18nBundle.getText(FIELDS_BY_ATTRIBUTE);
6✔
180

181
  useEffect(() => {
6✔
182
    if (open) {
2!
183
      dialogRef.current.show();
2✔
184
    }
185
  }, [open]);
186

187
  const handleSearch = (e) => {
6✔
188
    if (handleDialogSearch) {
×
189
      handleDialogSearch(enrichEventWithDetails(e, { value: e.target.value, element: e.target }));
×
190
    }
191
    setSearchString(e.target.value);
×
192
  };
193
  const handleSave = (e) => {
6✔
194
    handleDialogSave(e, dialogRefs.current, toggledFilters);
×
195
  };
196

197
  const handleClose = (e, isCancel = false) => {
6!
198
    setSelectedFilters(null);
×
199
    stopPropagation(e);
×
200
    if (!isCancel) {
×
201
      handleSave(e);
×
202
      return;
×
203
    }
204
    handleDialogClose(e);
×
205
  };
206

207
  const handleCancel = (e) => {
6✔
208
    if (handleDialogCancel) {
×
209
      handleDialogCancel(enrichEventWithDetails(e));
×
210
    }
211
    handleDialogClose(e);
×
212
  };
213

214
  const handleRestore = (e) => {
6✔
215
    setSelectedFilters(null);
×
216
    handleRestoreFilters(e, 'dialog', { filters: Array.from(dialogRef.current.querySelectorAll('ui5-table-row')) });
×
217
  };
218
  const handleViewChange = (e) => {
6✔
219
    setIsListView(e.detail.selectedItem.dataset.id === 'list');
2✔
220
  };
221

222
  const renderChildren = () => {
6✔
223
    return children
6✔
224
      .filter((item) => {
225
        return (
18✔
226
          !!item?.props &&
72!
227
          item.props?.visible &&
228
          (item.props?.label?.toLowerCase().includes(searchString.toLowerCase()) || searchString.length === 0) &&
229
          getActiveFilters(filteredAttribute, item)
230
        );
231
      })
232
      .map((child) => {
233
        const filterBarItemRef = filterBarRefs.current[child.key];
18✔
234
        let filterItemProps = {};
18✔
235
        if (filterBarItemRef) {
18!
236
          filterItemProps = filterValue(filterBarItemRef, child);
18✔
237
        }
238
        if (!child.props.children) return child;
18!
239

240
        return cloneElement<
18✔
241
          FilterGroupItemPropTypes & {
242
            'data-with-values': boolean;
243
            'data-selected': boolean;
244
            'data-react-key': boolean;
245
          }
246
        >(child, {
247
          'data-with-values': showValues,
248
          'data-selected':
249
            selectedFilters !== null
18!
250
              ? !!selectedFilters?.[child.key]?.selected
251
              : child.props.visibleInFilterBar || child.props.required || child.type.displayName !== 'FilterGroupItem',
18!
252
          'data-react-key': child.key,
253
          children: {
254
            ...child.props.children,
255
            props: {
256
              ...child.props.children.props,
257
              ...filterItemProps
258
            },
259
            ref: (node) => {
260
              if (node) {
×
261
                dialogRefs.current[child.key] = node;
×
262
                syncRef(child.props.children.ref, node);
×
263
              }
264
            }
265
          }
266
        });
267
      });
268
  };
269

270
  const handleAttributeFilterChange = (e) => {
6✔
271
    setFilteredAttribute(e.detail.selectedOption.dataset.id);
×
272
  };
273

274
  const handleCheckBoxChange = (e) => {
6✔
275
    e.preventDefault();
×
276
    const prevRowsByKey = e.detail.previouslySelectedRows.reduce(
×
277
      (acc, prevSelRow) => ({ ...acc, [prevSelRow.dataset.reactKey]: prevSelRow }),
×
278
      {}
279
    );
280
    const rowsByKey = e.detail.selectedRows.reduce(
×
281
      (acc, selRow) => ({ ...acc, [selRow.dataset.reactKey]: selRow }),
×
282
      {}
283
    );
284

285
    const changedRowKey =
286
      e.detail.previouslySelectedRows > e.detail.selectedRows
×
287
        ? compareObjects(prevRowsByKey, rowsByKey)
288
        : compareObjects(rowsByKey, prevRowsByKey);
289

290
    const element = rowsByKey[changedRowKey] || prevRowsByKey[changedRowKey];
×
291

292
    // todo: workaround until specific rows can be disabled
293
    if (element.dataset?.required === 'true') {
×
294
      setForceRequired(element);
×
295
      return;
×
296
    }
297

298
    setSelectedFilters({ ...prevRowsByKey, ...rowsByKey });
×
299

300
    if (typeof handleSelectionChange === 'function') {
×
301
      handleSelectionChange(enrichEventWithDetails(e, { element, checked: element.selected }));
×
302
    }
303

304
    setToggledFilters((prev) => {
×
305
      return { ...prev, [changedRowKey]: element.selected };
×
306
    });
307
  };
308

309
  useEffect(() => {
6✔
310
    if (forceRequired) {
2!
311
      forceRequired.setAttribute('selected', 'true');
×
312
      setForceRequired(undefined);
×
313
    }
314
  }, [forceRequired]);
315

316
  const renderGroups = () => {
6✔
317
    const groups = {};
2✔
318
    Children.forEach(renderChildren(), (child) => {
2✔
319
      const childGroups = child.props.groupName ?? 'default';
6!
320
      if (groups[childGroups]) {
6!
321
        groups[childGroups].push(child);
4✔
322
      } else {
323
        groups[childGroups] = [child];
2✔
324
      }
325
    });
326

327
    const filterGroups = Object.keys(groups)
2✔
328
      .sort((x, y) => (x === 'default' ? -1 : y === 'role' ? 1 : 0))
×
329
      .map((item, index) => {
330
        return (
2✔
331
          <Panel
332
            headerText={item === 'default' ? basicText : item}
2!
333
            className={classes.groupPanel}
334
            key={`${item === 'default' ? basicText : item}${index}`}
2!
335
          >
336
            <Table
337
              className={classes.table}
338
              mode={TableMode.MultiSelect}
339
              data-component-name="FilterBarDialogPanelTable"
340
              onSelectionChange={handleCheckBoxChange}
341
            >
342
              {groups[item]}
343
            </Table>
344
          </Panel>
345
        );
346
      });
347
    return filterGroups;
2✔
348
  };
349

350
  return createPortal(
6✔
351
    <Dialog
352
      ref={dialogRef}
353
      data-component-name="FilterBarDialog"
354
      onAfterClose={handleClose}
355
      onAfterOpen={onAfterFiltersDialogOpen}
356
      resizable
357
      draggable
358
      className={classes.dialogComponent}
359
      preventFocusRestore
360
      header={
361
        <Bar
362
          design={BarDesign.Header}
363
          startContent={
364
            <Title level={TitleLevel.H4} title={filtersTitle}>
365
              {filtersTitle}
366
            </Title>
367
          }
368
          endContent={
369
            showRestoreButton && (
6!
370
              <Button design={ButtonDesign.Transparent} onClick={handleRestore}>
371
                {resetText}
372
              </Button>
373
            )
374
          }
375
        />
376
      }
377
      footer={
378
        <Bar
379
          design={BarDesign.Footer}
380
          endContent={
381
            <FlexBox justifyContent={FlexBoxJustifyContent.End} className={classes.footer}>
382
              <Button
383
                onClick={handleSave}
384
                data-component-name="FilterBarDialogSaveBtn"
385
                design={ButtonDesign.Emphasized}
386
              >
387
                {okText}
388
              </Button>
389
              <Button
390
                design={ButtonDesign.Transparent}
391
                onClick={handleCancel}
392
                data-component-name="FilterBarDialogCancelBtn"
393
              >
394
                {cancelText}
395
              </Button>
396
            </FlexBox>
397
          }
398
        />
399
      }
400
    >
401
      <FlexBox direction={FlexBoxDirection.Column} className={classes.subheaderContainer}>
402
        <Toolbar className={classes.subheader} toolbarStyle={ToolbarStyle.Clear}>
403
          <Select
404
            onChange={handleAttributeFilterChange}
405
            title={fieldsByAttributeText}
406
            accessibleName={fieldsByAttributeText}
407
          >
408
            <Option selected={filteredAttribute === 'all'} data-id="all">
409
              {allText}
410
            </Option>
411
            <Option selected={filteredAttribute === 'visible'} data-id="visible">
412
              {visibleText}
413
            </Option>
414
            <Option selected={filteredAttribute === 'active'} data-id="active">
415
              {activeText}
416
            </Option>
417
            <Option selected={filteredAttribute === 'visibleAndActive'} data-id="visibleAndActive">
418
              {visibleAndActiveText}
419
            </Option>
420
            <Option selected={filteredAttribute === 'mandatory'} data-id="mandatory">
421
              {mandatoryText}
422
            </Option>
423
          </Select>
424
          <ToolbarSpacer />
425
          <Button design={ButtonDesign.Transparent} onClick={toggleValues} aria-live="polite">
426
            {showValues ? hideValuesText : showValuesText}
6!
427
          </Button>
428
          <SegmentedButton onSelectionChange={handleViewChange}>
429
            <SegmentedButtonItem icon={listIcon} data-id="list" pressed={isListView} accessibleName={listViewText} />
430
            <SegmentedButtonItem
431
              icon={group2Icon}
432
              data-id="group"
433
              pressed={!isListView}
434
              accessibleName={groupViewText}
435
            />
436
          </SegmentedButton>
437
        </Toolbar>
438
        <FlexBox className={classes.searchInputContainer}>
439
          <Input
440
            noTypeahead
441
            placeholder={searchForFiltersText}
442
            onInput={handleSearch}
443
            showClearIcon
444
            icon={<Icon name={searchIcon} />}
445
            ref={dialogSearchRef}
446
            className={classes.searchInput}
447
          />
448
        </FlexBox>
449
      </FlexBox>
450
      <Table
451
        data-component-name="FilterBarDialogTable"
452
        hideNoData={!isListView}
453
        className={classes.table}
454
        mode={TableMode.MultiSelect}
455
        onSelectionChange={handleCheckBoxChange}
456
        columns={
457
          <>
458
            <TableColumn>{fieldText}</TableColumn>
459
            {!showValues && <TableColumn className={classes.tHactive}>{activeText}</TableColumn>}
12!
460
          </>
461
        }
462
      >
463
        {isListView && renderChildren()}
10!
464
      </Table>
465
      {!isListView && renderGroups()}
8!
466
    </Dialog>,
467
    portalContainer
468
  );
469
};
21✔
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