@ui5/webcomponents-react
Version:
React Wrapper for UI5 Web Components and additional components
381 lines (377 loc) • 14.8 kB
JavaScript
'use client';
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import InputType from '@ui5/webcomponents/dist/types/InputType.js';
import ToolbarDesign from '@ui5/webcomponents/dist/types/ToolbarDesign.js';
import searchIcon from '@ui5/webcomponents-icons/dist/search.js';
import { debounce, Device, enrichEventWithDetails, useI18nBundle, useStylesheet } from '@ui5/webcomponents-react-base';
import { clsx } from 'clsx';
import { Children, cloneElement, forwardRef, isValidElement, useEffect, useId, useRef, useState } from 'react';
import { FlexBoxAlignItems } from '../../enums/FlexBoxAlignItems.js';
import { ADAPT_FILTERS, CLEAR, FILTERS, GO, HIDE_FILTER_BAR, RESTORE, SEARCH, SHOW_FILTER_BAR, SHOW_RESULTS } from '../../i18n/i18n-defaults.js';
import { Button } from '../../webComponents/Button/index.js';
import { Icon } from '../../webComponents/Icon/index.js';
import { Toolbar } from '../../webComponents/Toolbar/index.js';
import { ToolbarButton } from '../../webComponents/ToolbarButton/index.js';
import { ToolbarItem } from '../../webComponents/ToolbarItem/index.js';
import { FilterGroupItem } from '../FilterGroupItem/index.js';
import { FlexBox } from '../FlexBox/index.js';
import { classNames, styleData } from './FilterBar.module.css.js';
import { FilterDialog } from './FilterDialog.js';
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
const isPhone = Device.isPhone();
const isTablet = Device.isTablet();
const isDesktop = Device.isDesktop();
const resizeObserverEntryWidth = entry => {
if (entry.borderBoxSize.length) {
return entry.borderBoxSize[0]?.inlineSize;
}
};
/**
* The `FilterBar` displays filters in a user-friendly manner to populate values for a query. It consists of a row containing the `VariantManagement` or a title, the related buttons, and an area underneath displaying the filters. The filters are arranged in a logical row that is divided depending on the space available and the width of the filters. The area containing the filters can be hidden or shown using the "Hide FilterBar / Show FilterBar" button, the "Filters" button shows the filter dialog.
* In this dialog, the consumer has full control over the FilterBar. The filters in this dialog are displayed in one column and organized in groups. Each filter can be marked as visible in the FilterBar by selecting the respective row.
*
* __Responsiveness:__
* The name of the view or title is always visible. The filter area varies:
*
* * Desktop: Expanded or collapsed by default
* * Tablet: Collapsed by default
* * Phone: Not displayed. Accessible through filter dialog.
*
* __Note:__ When `hideToolbar` is set to `true`, filters are always visible.
*/
const FilterBar = /*#__PURE__*/forwardRef((props, ref) => {
const {
hideToolbar,
filterBarCollapsed,
considerGroupName,
filterContainerWidth = '13.125rem',
activeFiltersCount,
showClearOnFB,
showGoOnFB,
hideFilterConfiguration,
showRestoreOnFB,
showResetButton,
hideToggleFiltersButton,
enableReordering,
style,
className,
slot,
search,
header,
as = 'div',
onToggleFilters,
onFiltersDialogOpen,
onAfterFiltersDialogOpen,
onFiltersDialogCancel,
onFiltersDialogClose,
onFiltersDialogSave,
onClear,
onFiltersDialogSelectionChange,
onFiltersDialogSearch,
onGo,
onRestore,
onReorder,
...rest
} = props;
const children = props.children;
const initiallyShowFilters = (() => {
if (!hideToolbar) {
if (filterBarCollapsed !== undefined) {
return filterBarCollapsed;
}
// isTablet is true for some desktops with touch screens
return !(isTablet && !isDesktop);
}
return true;
})();
const [showFilters, setShowFilters] = useState(initiallyShowFilters);
const [dialogOpen, setDialogOpen] = useState(false);
const dialogRef = useRef(null);
const filterBarButtonsRef = useRef(null);
const filterAreaRef = useRef(null);
const filterBtnRef = useRef(null);
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
const uniqueId = useId();
const clearText = i18nBundle.getText(CLEAR);
const restoreText = i18nBundle.getText(RESTORE);
const showFilterBarText = i18nBundle.getText(SHOW_FILTER_BAR);
const hideFilterBarText = i18nBundle.getText(HIDE_FILTER_BAR);
const goText = i18nBundle.getText(GO);
const goDescription = i18nBundle.getText(SHOW_RESULTS);
const searchText = i18nBundle.getText(SEARCH);
const filtersText = !hideToolbar ? i18nBundle.getText(FILTERS) : i18nBundle.getText(ADAPT_FILTERS);
useEffect(() => {
if (filterBarCollapsed !== undefined) {
// syncing controlled prop to state
setShowFilters(!hideToolbar ? !filterBarCollapsed : true);
}
}, [hideToolbar, filterBarCollapsed]);
useStylesheet(styleData, FilterBar.displayName);
const filterAreaClasses = clsx(classNames.filterArea, showFilters && (!isPhone || isPhone && hideToolbar) ? classNames.filterAreaOpen : classNames.filterAreaClosed);
const handleToggle = e => {
if (typeof onToggleFilters === 'function') {
onToggleFilters(enrichEventWithDetails(e, {
visible: !showFilters
}));
}
setShowFilters(!showFilters);
};
const handleDialogSave = (e, selectionChangePayload, orderIds) => {
const details = {
...selectionChangePayload,
reorderedFilterKeys: orderIds
};
if (onFiltersDialogSave) {
onFiltersDialogSave(enrichEventWithDetails(e, details));
}
handleDialogClose('okButtonPressed');
};
const handleDialogOpen = e => {
if (typeof onFiltersDialogOpen === 'function') {
onFiltersDialogOpen(e);
}
if (e.defaultPrevented) {
setDialogOpen(false);
} else {
setDialogOpen(true);
}
};
const handleDialogClose = closeTrigger => {
if (typeof onFiltersDialogClose === 'function') {
onFiltersDialogClose(closeTrigger);
}
setDialogOpen(false);
void filterBtnRef.current?.focus();
};
const handleGoOnFb = e => {
if (typeof onGo === 'function') {
onGo(e);
}
};
const safeChildren = () => {
return Children.toArray(children);
};
const renderChildren = () => {
const childProps = {
considerGroupName,
['data-with-toolbar']: !hideToolbar
};
return Children.toArray(children).filter(item => {
if (! /*#__PURE__*/isValidElement(item)) {
return false;
}
return (typeof item.props.hidden === 'undefined' || item?.props?.hidden !== true) && (item.props?.required || item.props?.hiddenInFilterBar !== true);
}).map(child => {
if (filterContainerWidth) {
childProps.style = {
width: filterContainerWidth,
...child.props.style
};
}
return /*#__PURE__*/cloneElement(child, {
...childProps
});
});
};
const handleRestoreFilters = payload => {
if (onRestore) {
onRestore(payload);
}
};
const calculatedChildren = renderChildren();
const handleFBRestore = () => {
handleRestoreFilters({
source: 'filterBar',
selectedFilterKeys: calculatedChildren.map(child => `${child.props.filterKey}`),
previousSelectedFilterKeys: null,
reorderedFilterKeys: null
});
};
const handleClear = e => {
if (typeof onClear === 'function') {
onClear(e);
}
};
const cssClasses = clsx(classNames.outerContainer, className, !hideToolbar && classNames.outerContainerWithToolbar);
const FBButtonComponent = hideToolbar ? Button : ToolbarButton;
const filtersButtonText = `${filtersText}${activeFiltersCount && parseInt(activeFiltersCount, 10) ? ` (${activeFiltersCount})` : ''}`;
const GoButton = showGoOnFB && /*#__PURE__*/_jsx(Button, {
onClick: handleGoOnFb,
design: ButtonDesign.Emphasized,
accessibleDescription: goDescription,
children: goText
});
const FBButtons = /*#__PURE__*/_jsxs(_Fragment, {
children: [showGoOnFB && (hideToolbar ? GoButton : /*#__PURE__*/_jsx(ToolbarItem, {
children: GoButton
})), !hideToggleFiltersButton && !hideToolbar && !isPhone && /*#__PURE__*/_jsx(ToolbarButton, {
text: showFilters ? hideFilterBarText : showFilterBarText,
onClick: handleToggle,
design: ButtonDesign.Transparent,
className: classNames.showFiltersBtn,
"aria-live": "polite"
}), showClearOnFB && /*#__PURE__*/_jsx(FBButtonComponent, {
text: hideToolbar ? undefined : clearText,
onClick: handleClear,
design: ButtonDesign.Transparent,
children: hideToolbar ? clearText : undefined
}), showRestoreOnFB && /*#__PURE__*/_jsx(FBButtonComponent, {
text: hideToolbar ? undefined : restoreText,
onClick: handleFBRestore,
design: ButtonDesign.Transparent,
children: hideToolbar ? restoreText : undefined
}), !hideFilterConfiguration && /*#__PURE__*/_jsx(FBButtonComponent, {
text: hideToolbar ? undefined : filtersButtonText,
onClick: handleDialogOpen,
"aria-haspopup": "dialog",
design: ButtonDesign.Transparent
//@ts-expect-error: both types are allowed here
,
ref: filterBtnRef,
children: hideToolbar ? filtersButtonText : undefined
})]
});
const [filterBarButtonsWidth, setFilterBarButtonsWidth] = useState(undefined);
const [filterAreaWidth, setFilterAreaWidth] = useState(undefined);
const [firstChildWidth, setFirstChildWidth] = useState(undefined);
useEffect(() => {
const debouncedObserverFn = debounce(([area]) => {
const firstChild = area.target?.children?.[0];
if (firstChild) {
setFirstChildWidth(firstChild.getBoundingClientRect().width + 16 /*margin*/);
}
}, 100);
const filterAreaObserver = new ResizeObserver(debouncedObserverFn);
if (hideToolbar && filterAreaRef.current) {
filterAreaObserver.observe(filterAreaRef.current);
}
return () => {
debouncedObserverFn.cancel();
filterAreaObserver.disconnect();
};
}, [hideToolbar]);
useEffect(() => {
const debouncedObserverFn = debounce(([area]) => {
const filterWidth = resizeObserverEntryWidth(area);
setFilterAreaWidth(filterWidth);
}, 100);
const filterAreaObserver = new ResizeObserver(debouncedObserverFn);
if (hideToolbar && filterAreaRef.current) {
filterAreaObserver.observe(filterAreaRef.current);
}
return () => {
debouncedObserverFn.cancel();
filterAreaObserver.disconnect();
};
}, [hideToolbar]);
useEffect(() => {
const debouncedObserverFn = debounce(([buttons]) => {
const buttonsWidth = resizeObserverEntryWidth(buttons);
setFilterBarButtonsWidth(buttonsWidth);
}, 100);
const filterBarButtonsObserver = new ResizeObserver(debouncedObserverFn);
if (hideToolbar && filterBarButtonsRef.current) {
filterBarButtonsObserver.observe(filterBarButtonsRef.current);
}
return () => {
debouncedObserverFn.cancel();
filterBarButtonsObserver.disconnect();
};
}, [hideToolbar]);
// calculates the number of spacers depending on the available width inside the row
const renderSpacers = () => {
if (firstChildWidth && filterAreaWidth) {
const totalItems = calculatedChildren.length + (search ? 1 : 0);
const itemsPerRow = Math.round(filterAreaWidth / firstChildWidth);
if (totalItems <= itemsPerRow) {
return null;
}
const itemsInLastRow = totalItems % itemsPerRow || itemsPerRow;
// -1 because "lastSpacer" already occupies one flex slot
const numberOfSpacers = Math.max(0, itemsPerRow - itemsInLastRow - 1);
const spacers = [];
for (let i = 0; i < numberOfSpacers; i++) {
spacers.push(/*#__PURE__*/_jsx("div", {
className: classNames.spacer
}, `filter-spacer-${i}`));
}
return spacers;
}
return null;
};
const CustomTag = as;
return /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsxs(CustomTag, {
ref: ref,
className: cssClasses,
style: {
['--_ui5wcr_filter_group_item_flex_basis']: filterContainerWidth,
...style
},
slot: slot,
...rest,
children: [!hideToolbar && /*#__PURE__*/_jsxs(FlexBox, {
className: classNames.toolbar,
alignItems: FlexBoxAlignItems.Center,
children: [header, /*#__PURE__*/_jsx(Toolbar, {
className: classNames.wcToolbar,
design: ToolbarDesign.Transparent,
children: FBButtons
})]
}), /*#__PURE__*/_jsxs("div", {
className: filterAreaClasses,
style: {
position: 'relative'
},
ref: filterAreaRef,
"data-component-name": "FilterBarFilterArea",
children: [search && /*#__PURE__*/_jsx(FilterGroupItem, {
"data-with-toolbar": !hideToolbar,
filterKey: `${uniqueId}-search`,
children: /*#__PURE__*/_jsx("div", {
className: classNames.searchContainer,
children: /*#__PURE__*/cloneElement(search, {
placeholder: search.props.placeholder ?? searchText,
noTypeahead: search.props.noTypeahead ?? true,
showClearIcon: true,
type: InputType.Search,
icon: /*#__PURE__*/_jsx(Icon, {
name: searchIcon
})
})
})
}), calculatedChildren, hideToolbar && /*#__PURE__*/_jsxs(_Fragment, {
children: [renderSpacers(), /*#__PURE__*/_jsx("div", {
style: {
width: filterBarButtonsWidth ? `${filterBarButtonsWidth}px` : '120px',
minWidth: filterBarButtonsWidth ? `${filterBarButtonsWidth}px` : '120px'
},
className: classNames.lastSpacer,
children: /*#__PURE__*/_jsx("div", {
className: classNames.filterBarButtons,
ref: filterBarButtonsRef,
children: FBButtons
})
})]
})]
})]
}), dialogOpen && !hideFilterConfiguration && /*#__PURE__*/_jsx(FilterDialog, {
open: dialogOpen,
handleDialogClose: handleDialogClose,
handleRestoreFilters: handleRestoreFilters,
showRestoreButton: showResetButton,
onFiltersDialogSelectionChange: onFiltersDialogSelectionChange,
handleDialogSave: handleDialogSave,
handleDialogSearch: onFiltersDialogSearch,
handleDialogCancel: onFiltersDialogCancel,
onAfterFiltersDialogOpen: onAfterFiltersDialogOpen,
dialogRef: dialogRef,
enableReordering: enableReordering,
isPhone: isPhone,
onReorder: onReorder,
children: safeChildren()
})]
});
});
FilterBar.displayName = 'FilterBar';
export { FilterBar };