@ui5/webcomponents-react
Version:
React Wrapper for UI5 Web Components and additional components
562 lines (553 loc) • 22.4 kB
JavaScript
import BarDesign from '@ui5/webcomponents/dist/types/BarDesign.js';
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import TitleLevel from '@ui5/webcomponents/dist/types/TitleLevel.js';
import group2Icon from '@ui5/webcomponents-icons/dist/group-2.js';
import listIcon from '@ui5/webcomponents-icons/dist/list.js';
import searchIcon from '@ui5/webcomponents-icons/dist/search.js';
import { enrichEventWithDetails, useI18nBundle, useStylesheet } from '@ui5/webcomponents-react-base';
import { addCustomCSSWithScoping } from '@ui5/webcomponents-react-base/internal/utils';
import { Children, cloneElement, useCallback, useEffect, useId, useReducer, useRef, useState } from 'react';
import { FlexBoxDirection } from '../../enums/FlexBoxDirection.js';
import { FlexBoxJustifyContent } from '../../enums/FlexBoxJustifyContent.js';
import { MessageBoxAction } from '../../enums/MessageBoxAction.js';
import { MessageBoxType } from '../../enums/MessageBoxType.js';
import { ACTIVE, ALL, BASIC, CANCEL, FIELDS_BY_ATTRIBUTE, FILTER, FILTER_DIALOG_RESET_WARNING, FILTERS, GROUP_VIEW, HIDE_VALUES, LIST_VIEW, MANDATORY, OK, RESET, SEARCH_FOR_FILTERS, SHOW_VALUES, VISIBLE, VISIBLE_AND_ACTIVE } from '../../i18n/i18n-defaults.js';
import { FilterBarDialogContext } from '../../internal/FilterBarDialogContext.js';
import { stopPropagation } from '../../internal/stopPropagation.js';
import { Bar } from '../../webComponents/Bar/index.js';
import { Button } from '../../webComponents/Button/index.js';
import { Dialog } from '../../webComponents/Dialog/index.js';
import { Icon } from '../../webComponents/Icon/index.js';
import { Input } from '../../webComponents/Input/index.js';
import { Option } from '../../webComponents/Option/index.js';
import { Panel } from '../../webComponents/Panel/index.js';
import { SegmentedButton } from '../../webComponents/SegmentedButton/index.js';
import { SegmentedButtonItem } from '../../webComponents/SegmentedButtonItem/index.js';
import { Select } from '../../webComponents/Select/index.js';
import { Table } from '../../webComponents/Table/index.js';
import { TableHeaderCell } from '../../webComponents/TableHeaderCell/index.js';
import { TableHeaderRow } from '../../webComponents/TableHeaderRow/index.js';
import { TableSelectionMulti } from '../../webComponents/TableSelectionMulti/index.js';
import { Title } from '../../webComponents/Title/index.js';
import { FlexBox } from '../FlexBox/index.js';
import { MessageBox } from '../MessageBox/index.js';
import { classNames, styleData } from './FilterBarDialog.module.css.js';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
addCustomCSSWithScoping('ui5-table', `
:host([data-component-name="FilterBarDialogTable"][data-is-grouped="true"]) #no-data-row {
display: none;
}
`);
const getActiveFilters = (activeFilterAttribute, filter) => {
switch (activeFilterAttribute) {
case 'all':
return true;
case 'visible':
return filter.props?.hiddenInFilterBar !== true;
case 'active':
return filter.props?.active;
case 'visibleAndActive':
return filter.props?.hiddenInFilterBar !== true && filter.props?.active;
case 'mandatory':
return filter.props?.required;
default:
return true;
}
};
export const FilterDialog = props => {
const {
open,
handleDialogClose,
children,
showRestoreButton,
dialogRef,
enableReordering,
isPhone,
handleRestoreFilters,
handleDialogSave,
onFiltersDialogSelectionChange,
handleDialogSearch,
handleDialogCancel,
onAfterFiltersDialogOpen,
onReorder
} = props;
useStylesheet(styleData, 'FilterBarDialog');
const uniqueId = useId();
const [searchString, setSearchString] = useState('');
const [selectedFilters, setSelectedFilters] = useState(undefined);
const [requiredFilters, setRequiredKeys] = useState({});
const [showValues, toggleValues] = useReducer(prev => !prev, false);
const [messageBoxOpen, setMessageBoxOpen] = useState(false);
const [forceRequired, setForceRequired] = useState();
const [showBtnsOnHover, setShowBtnsOnHover] = useState(true);
const [isListView, setIsListView] = useState(true);
const [filteredAttribute, setFilteredAttribute] = useState('all');
const [currentReorderedItem, setCurrentReorderedItem] = useState({});
const [orderedChildren, setOrderedChildren] = useState([]);
const [updatedIndex, setUpdatedIndex] = useState(undefined);
const currentReorderedItemOrderId = currentReorderedItem?.filterKey;
const selected = (selectedFilters ?? []).join(' ');
const dialogSearchRef = useRef(null);
const tableRef = useRef(null);
const okBtnRef = useRef(null);
const prevIsListView = useRef(true);
const selectionChangePayloadRef = useRef({
selectedFilterKeys: selectedFilters ?? []
});
const initialSelected = useRef(undefined);
const prevRowKey = useRef(undefined);
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
const basicText = i18nBundle.getText(BASIC);
const cancelText = i18nBundle.getText(CANCEL);
const okText = i18nBundle.getText(OK);
const searchForFiltersText = i18nBundle.getText(SEARCH_FOR_FILTERS);
const filtersTitle = i18nBundle.getText(FILTERS);
const resetText = i18nBundle.getText(RESET);
const allText = i18nBundle.getText(ALL);
const activeText = i18nBundle.getText(ACTIVE);
const visibleText = i18nBundle.getText(VISIBLE);
const visibleAndActiveText = i18nBundle.getText(VISIBLE_AND_ACTIVE);
const mandatoryText = i18nBundle.getText(MANDATORY);
const listViewText = i18nBundle.getText(LIST_VIEW);
const groupViewText = i18nBundle.getText(GROUP_VIEW);
const showValuesText = i18nBundle.getText(SHOW_VALUES);
const hideValuesText = i18nBundle.getText(HIDE_VALUES);
const filterText = i18nBundle.getText(FILTER);
const fieldsByAttributeText = i18nBundle.getText(FIELDS_BY_ATTRIBUTE);
const wasReordered = useRef(false);
const handleReorder = e => {
wasReordered.current = true;
setCurrentReorderedItem(e);
};
const handleFocusFallback = () => {
const rowKey = currentReorderedItem?.target?.rowKey;
if (rowKey && tableRef.current && rowKey !== prevRowKey.current) {
// we have to retrigger the internal item navigation logic after reordering,
// otherwise keyboard nav and general focus handling is not working properly
setTimeout(() => {
const itemNav = tableRef.current._tableNavigation;
itemNav._gridWalker.setGrid(itemNav._getNavigationItemsOfGrid());
tableRef.current.querySelector(`[row-key="${rowKey}"]`).focus();
}, 0);
prevRowKey.current = rowKey;
}
};
const visibleChildren = useCallback(() => children.filter(item => {
return !!item?.props && !item?.props?.hidden;
}), [children]);
// orderedChildren syncs from children but also has independent mutations (reorder, restore)
/* eslint-disable react-hooks/set-state-in-effect */
useEffect(() => {
if (children.length) {
setOrderedChildren(visibleChildren());
}
}, [children, visibleChildren]);
/* eslint-enable react-hooks/set-state-in-effect */
const renderChildren = () => {
const searchStringLower = searchString.toLowerCase();
const filteredChildren = searchStringLower.length > 0 || filteredAttribute !== 'all' ? orderedChildren.filter(item => (searchStringLower === '' || item.props.label?.toLowerCase().includes(searchStringLower)) && getActiveFilters(filteredAttribute, item)) : orderedChildren;
return filteredChildren.map((child, index, arr) => {
return /*#__PURE__*/cloneElement(child, {
'data-index': index,
'data-filters-count': arr.length
});
});
};
const handleSearch = e => {
if (typeof handleDialogSearch === 'function') {
handleDialogSearch(enrichEventWithDetails(e, {
value: e.target.value,
element: e.target
}));
}
setSearchString(e.target.value);
};
const handleSave = e => {
const orderedChildrenIds = enableReordering ? orderedChildren.map(child => child.props.filterKey) : [];
handleDialogSave(e, selectionChangePayloadRef.current, orderedChildrenIds);
};
const handleClose = e => {
stopPropagation(e);
if (e.target !== e.currentTarget) {
return;
}
if (typeof handleDialogCancel === 'function') {
handleDialogCancel(true);
}
handleDialogClose('escPressed');
};
const handleCancel = () => {
if (typeof handleDialogCancel === 'function') {
handleDialogCancel(false);
}
handleDialogClose('cancelButtonPressed');
};
const handleRestore = () => {
setMessageBoxOpen(true);
};
const handleViewChange = e => {
const selectedItem = e.detail.selectedItems.at(0);
prevIsListView.current = isListView;
setIsListView(selectedItem.dataset.id === 'list');
};
const handleMessageBoxClose = action => {
if (action === 'OK') {
const initialChildren = visibleChildren();
const payload = {
source: 'dialog',
selectedFilterKeys: initialSelected.current,
previousSelectedFilterKeys: selectedFilters,
reorderedFilterKeys: enableReordering ? initialChildren.map(child => `${child.props.filterKey}`) : null
};
setSelectedFilters(initialSelected.current);
setOrderedChildren(initialChildren);
handleRestoreFilters(payload);
}
setMessageBoxOpen(false);
setTimeout(() => {
okBtnRef.current.focus();
}, 50);
};
useEffect(() => {
if (orderedChildren.length && wasReordered.current) {
if (typeof onReorder === 'function') {
onReorder({
reorderedFilterKeys: orderedChildren.map(item => `${item.props.filterKey}`)
});
}
wasReordered.current = false;
}
}, [orderedChildren, onReorder]);
// Reorder triggers setOrderedChildren; currentReorderedItem also provides context value
/* eslint-disable react-hooks/set-state-in-effect */
useEffect(() => {
if (currentReorderedItem?.index != null) {
const {
index,
direction
} = currentReorderedItem;
setOrderedChildren(prevChildren => {
const prev = [...prevChildren];
switch (direction) {
case 'up':
if (index > 0) {
setUpdatedIndex(index - 1);
const temp = prev[index];
prev[index] = prev[index - 1];
prev[index - 1] = temp;
}
break;
case 'down':
if (index < prev.length - 1) {
setUpdatedIndex(index + 1);
const temp = prev[index];
prev[index] = prev[index + 1];
prev[index + 1] = temp;
}
break;
case 'top':
if (index > 0) {
setUpdatedIndex(0);
const item = prev.splice(index, 1)[0];
prev.unshift(item);
}
break;
case 'bottom':
if (index < prev.length - 1) {
setUpdatedIndex(prev.length - 1);
const item = prev.splice(index, 1)[0];
prev.push(item);
}
break;
}
return prev;
});
void currentReorderedItem.target.focus();
}
}, [currentReorderedItem]);
/* eslint-enable react-hooks/set-state-in-effect */
useEffect(() => {
if (updatedIndex != null) {
prevRowKey.current = undefined;
}
}, [updatedIndex]);
const handleAttributeFilterChange = e => {
setFilteredAttribute(e.detail.selectedOption.dataset.id);
};
const fireOnFiltersDialogSelectionChange = (_selected, selectedKeys, prevSelected, prevent = false) => {
setSelectedFilters(Array.from(_selected));
if (!prevent && typeof onFiltersDialogSelectionChange === 'function') {
const payload = {
toggledFilterKeys: selectedKeys,
selected: selectedKeys.size === 1 ? _selected.has(selectedKeys.values().next().value) : undefined,
selectedFilterKeys: _selected,
previousSelectedFilterKeys: prevSelected
};
onFiltersDialogSelectionChange(payload);
}
};
useEffect(() => {
if (selectedFilters) {
selectionChangePayloadRef.current = {
selectedFilterKeys: selectedFilters
};
}
}, [selectedFilters]);
const handleCheckBoxChange = e => {
const selectionFeature = e.target;
if (selectionFeature.hasAttribute('ui5-table-selection-multi')) {
const _selected = selectionFeature.getSelectedAsSet();
const prevSelected = new Set(selectedFilters ?? []);
const alwaysSelected = Object.keys(requiredFilters).filter(key => requiredFilters[key]);
const selectedKeys = _selected.symmetricDifference(prevSelected);
// reset required filters to `true`
if (alwaysSelected.length) {
setForceRequired({
required: alwaysSelected,
target: selectionFeature,
selected: _selected,
prevSelected,
selectedKeys
});
return;
}
fireOnFiltersDialogSelectionChange(_selected, selectedKeys, prevSelected);
}
};
// Two-phase pattern: handler sets forceRequired, effect processes it after render and clears it
/* eslint-disable react-hooks/set-state-in-effect */
useEffect(() => {
if (forceRequired && forceRequired.target) {
const {
prevSelected,
selectedKeys,
selected: _selected,
required,
target
} = forceRequired;
required.forEach(requiredString => {
_selected.add(requiredString);
});
setTimeout(() => {
target.selected = Array.from(_selected).join(' ');
const prevent = selectedKeys.size === 1 && required.includes(selectedKeys.values().next().value);
fireOnFiltersDialogSelectionChange(_selected, selectedKeys, prevSelected, prevent);
});
setForceRequired(undefined);
}
// `forceRequired` triggers async DOM update; no extra deps needed
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [forceRequired]);
/* eslint-enable react-hooks/set-state-in-effect */
const renderGroups = () => {
const groups = {};
Children.forEach(renderChildren(), child => {
const childGroups = child.props.groupName ?? 'default';
if (groups[childGroups]) {
groups[childGroups].push(child);
} else {
groups[childGroups] = [child];
}
});
// filter groups
return Object.keys(groups).sort((x, y) => x === 'default' ? -1 : y === 'role' ? 1 : 0).map((item, index) => {
return /*#__PURE__*/_jsx(Panel, {
headerText: item === 'default' ? basicText : item,
className: classNames.groupPanel,
children: /*#__PURE__*/_jsx(Table, {
className: classNames.tableInGroup,
"data-component-name": "FilterBarDialogPanelTable",
"data-with-value": showValues,
features: /*#__PURE__*/_jsx(TableSelectionMulti, {
selected: selected,
onChange: handleCheckBoxChange
}),
headerRow: /*#__PURE__*/_jsxs(TableHeaderRow, {
className: classNames.groupedTableHeader,
children: [/*#__PURE__*/_jsx(TableHeaderCell, {
children: filterText
}), !showValues && /*#__PURE__*/_jsx(TableHeaderCell, {
className: classNames.tHactive,
children: activeText
})]
}),
children: groups[item]
})
}, `${item === 'default' ? basicText : item}${index}`);
});
};
// One-time lazy init, idempotent under Strict Mode
// eslint-disable-next-line react-hooks/refs
if (initialSelected.current === undefined && selected.length) {
initialSelected.current = selectedFilters;
}
return /*#__PURE__*/_jsxs(FilterBarDialogContext.Provider, {
value: {
isFilterInDialog: true,
enableReordering,
onReorder: handleReorder,
isListView,
withValues: showValues,
handleFocusFallback,
showBtnsOnHover,
setShowBtnsOnHover,
currentReorderedItemOrderId,
setSelectedKeys: setSelectedFilters,
setRequiredKeys,
prevIsListView
},
children: [/*#__PURE__*/_jsxs(Dialog, {
open: open,
ref: dialogRef,
"data-component-name": "FilterBarDialog",
"data-is-phone": isPhone,
onClose: handleClose,
accessibleNameRef: `${uniqueId}-fb-dialog-title`,
onOpen: onAfterFiltersDialogOpen,
resizable: true,
draggable: true,
className: classNames.dialogComponent,
preventFocusRestore: true,
initialFocus: `${uniqueId}-fb-dialog-search`,
header: /*#__PURE__*/_jsx(Bar, {
design: BarDesign.Header,
startContent: /*#__PURE__*/_jsx(Title, {
level: TitleLevel.H4,
title: filtersTitle,
id: `${uniqueId}-fb-dialog-title`,
children: filtersTitle
}),
endContent: showRestoreButton && /*#__PURE__*/_jsx(Button, {
design: ButtonDesign.Transparent,
onClick: handleRestore,
children: resetText
})
}),
footer: /*#__PURE__*/_jsx(Bar, {
design: BarDesign.Footer,
endContent: /*#__PURE__*/_jsxs(FlexBox, {
justifyContent: FlexBoxJustifyContent.End,
className: classNames.footer,
children: [/*#__PURE__*/_jsx(Button, {
ref: okBtnRef,
onClick: handleSave,
"data-component-name": "FilterBarDialogSaveBtn",
design: ButtonDesign.Emphasized,
children: okText
}), /*#__PURE__*/_jsx(Button, {
design: ButtonDesign.Transparent,
onClick: handleCancel,
"data-component-name": "FilterBarDialogCancelBtn",
children: cancelText
})]
})
}),
children: [/*#__PURE__*/_jsxs(FlexBox, {
direction: FlexBoxDirection.Column,
className: classNames.subheaderContainer,
children: [/*#__PURE__*/_jsxs(FlexBox, {
direction: FlexBoxDirection.Row,
children: [/*#__PURE__*/_jsxs(Select, {
onChange: handleAttributeFilterChange,
title: fieldsByAttributeText,
accessibleName: fieldsByAttributeText,
onClose: e => {
e.stopPropagation();
},
children: [/*#__PURE__*/_jsx(Option, {
selected: filteredAttribute === 'all',
"data-id": "all",
children: allText
}), /*#__PURE__*/_jsx(Option, {
selected: filteredAttribute === 'visible',
"data-id": "visible",
children: visibleText
}), /*#__PURE__*/_jsx(Option, {
selected: filteredAttribute === 'active',
"data-id": "active",
children: activeText
}), /*#__PURE__*/_jsx(Option, {
selected: filteredAttribute === 'visibleAndActive',
"data-id": "visibleAndActive",
children: visibleAndActiveText
}), /*#__PURE__*/_jsx(Option, {
selected: filteredAttribute === 'mandatory',
"data-id": "mandatory",
children: mandatoryText
})]
}), /*#__PURE__*/_jsx("span", {
className: classNames.spacer
}), /*#__PURE__*/_jsx(Button, {
design: ButtonDesign.Transparent,
onClick: toggleValues,
"aria-live": "polite",
className: classNames.showValuesBtn,
children: showValues ? hideValuesText : showValuesText
}), /*#__PURE__*/_jsxs(SegmentedButton, {
onSelectionChange: handleViewChange,
children: [/*#__PURE__*/_jsx(SegmentedButtonItem, {
icon: listIcon,
"data-id": "list",
selected: isListView,
accessibleName: listViewText,
tooltip: listViewText
}), /*#__PURE__*/_jsx(SegmentedButtonItem, {
icon: group2Icon,
"data-id": "group",
selected: !isListView,
accessibleName: groupViewText,
tooltip: groupViewText
})]
})]
}), /*#__PURE__*/_jsx(FlexBox, {
className: classNames.searchInputContainer,
children: /*#__PURE__*/_jsx(Input, {
id: `${uniqueId}-fb-dialog-search`,
noTypeahead: true,
placeholder: searchForFiltersText,
onInput: handleSearch,
showClearIcon: true,
icon: /*#__PURE__*/_jsx(Icon, {
name: searchIcon
}),
ref: dialogSearchRef,
className: classNames.searchInput,
"data-component-name": "FilterBarDialogSearchInput"
})
})]
}), /*#__PURE__*/_jsx(Table, {
ref: tableRef,
className: !isListView && classNames.inactiveTable,
"data-component-name": "FilterBarDialogTable",
"data-is-grouped": !isListView ? 'true' : 'false',
"data-with-value": `${showValues}`,
noData: !isListView ? /*#__PURE__*/_jsx("span", {}) : undefined,
tabIndex: !isListView ? -1 : undefined,
features: /*#__PURE__*/_jsx(TableSelectionMulti, {
onChange: handleCheckBoxChange,
selected: selected,
headerSelector: "ClearAll"
}),
headerRow: /*#__PURE__*/_jsxs(TableHeaderRow, {
"data-component-name": !isListView ? 'FilterBarDialogGroupTableHeaderRow' : 'FilterBarDialogTableHeaderRow',
children: [/*#__PURE__*/_jsxs(TableHeaderCell, {
children: [!isListView && /*#__PURE__*/_jsx("div", {
className: classNames.checkBoxSpacer
}), filterText]
}), !showValues && /*#__PURE__*/_jsx(TableHeaderCell, {
className: classNames.tHactive,
children: activeText
})]
}),
children: isListView && renderChildren()
}), !isListView && renderGroups()]
}), showRestoreButton && messageBoxOpen && /*#__PURE__*/_jsx(MessageBox, {
open: true,
type: MessageBoxType.Warning,
actions: [MessageBoxAction.OK, MessageBoxAction.Cancel],
onClose: handleMessageBoxClose,
"data-component-name": "FilterBarDialogResetMessageBox",
children: i18nBundle.getText(FILTER_DIALOG_RESET_WARNING)
})]
});
};