UNPKG

@mui/x-data-grid-pro

Version:

The Pro plan edition of the MUI X Data Grid components.

367 lines (365 loc) 15 kB
'use client'; import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; const _excluded = ["colIndex", "height", "hasFocus", "width", "headerClassName", "colDef", "item", "headerFilterMenuRef", "InputComponentProps", "showClearIcon", "pinnedPosition", "pinnedOffset", "style", "showLeftBorder", "showRightBorder"]; import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { styled } from '@mui/material/styles'; import useForkRef from '@mui/utils/useForkRef'; import composeClasses from '@mui/utils/composeClasses'; import capitalize from '@mui/utils/capitalize'; import { fastMemo } from '@mui/x-internals/fastMemo'; import { gridVisibleColumnFieldsSelector, getDataGridUtilityClass, useGridSelector, GridFilterInputValue, GridFilterInputDate, GridFilterInputBoolean, GridFilterInputSingleSelect, gridFilterModelSelector, gridFilterableColumnLookupSelector, gridClasses } from '@mui/x-data-grid'; import { PinnedColumnPosition, useGridPrivateApiContext, gridHeaderFilteringEditFieldSelector, gridHeaderFilteringMenuSelector, isNavigationKey, attachPinnedStyle, vars } from '@mui/x-data-grid/internals'; import { useRtl } from '@mui/system/RtlProvider'; import { forwardRef } from '@mui/x-internals/forwardRef'; import { inputBaseClasses } from '@mui/material/InputBase'; import { useGridRootProps } from "../../hooks/utils/useGridRootProps.js"; import { GridHeaderFilterMenuContainer } from "./GridHeaderFilterMenuContainer.js"; import { GridHeaderFilterClearButton } from "./GridHeaderFilterClearButton.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const StyledInputComponent = styled(GridFilterInputValue, { name: 'MuiDataGrid', slot: 'ColumnHeaderFilterInput' })({ flex: 1, marginRight: vars.spacing(0.5), marginBottom: vars.spacing(-0.25), '& input[type="number"], & input[type="date"], & input[type="datetime-local"]': { '&[value=""]:not(:focus)': { color: 'transparent' } }, [`& .${inputBaseClasses.input}`]: { fontSize: '14px' }, [`.${gridClasses['root--densityCompact']} & .${inputBaseClasses.input}`]: { paddingTop: vars.spacing(0.5), paddingBottom: vars.spacing(0.5), height: 23 } }); const OperatorLabel = styled('span', { name: 'MuiDataGrid', slot: 'ColumnHeaderFilterOperatorLabel' })({ flex: 1, marginRight: vars.spacing(0.5), color: vars.colors.foreground.muted, whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }); const useUtilityClasses = ownerState => { const { colDef, classes, showRightBorder, showLeftBorder, pinnedPosition } = ownerState; const slots = { root: ['columnHeader', 'columnHeader--filter', colDef.headerAlign === 'left' && 'columnHeader--alignLeft', colDef.headerAlign === 'center' && 'columnHeader--alignCenter', colDef.headerAlign === 'right' && 'columnHeader--alignRight', 'withBorderColor', showRightBorder && 'columnHeader--withRightBorder', showLeftBorder && 'columnHeader--withLeftBorder', pinnedPosition === PinnedColumnPosition.LEFT && 'columnHeader--pinnedLeft', pinnedPosition === PinnedColumnPosition.RIGHT && 'columnHeader--pinnedRight'], input: ['columnHeaderFilterInput'], operatorLabel: ['columnHeaderFilterOperatorLabel'] }; return composeClasses(slots, getDataGridUtilityClass, classes); }; const DEFAULT_INPUT_COMPONENTS = { string: GridFilterInputValue, number: GridFilterInputValue, date: GridFilterInputDate, dateTime: GridFilterInputDate, boolean: GridFilterInputBoolean, singleSelect: GridFilterInputSingleSelect, actions: null, custom: null }; const GridHeaderFilterCell = forwardRef((props, ref) => { const { colIndex, height, hasFocus, width, headerClassName, colDef, item, headerFilterMenuRef, InputComponentProps, showClearIcon = false, pinnedPosition, pinnedOffset, style: styleProp, showLeftBorder, showRightBorder } = props, other = _objectWithoutPropertiesLoose(props, _excluded); const apiRef = useGridPrivateApiContext(); const isRtl = useRtl(); const columnFields = useGridSelector(apiRef, gridVisibleColumnFieldsSelector); const rootProps = useGridRootProps(); const cellRef = React.useRef(null); const handleRef = useForkRef(ref, cellRef); const inputRef = React.useRef(null); const buttonRef = React.useRef(null); const editingField = useGridSelector(apiRef, gridHeaderFilteringEditFieldSelector); const isEditing = editingField === colDef.field; const menuOpenField = useGridSelector(apiRef, gridHeaderFilteringMenuSelector); const isMenuOpen = menuOpenField === colDef.field; // TODO: Support for `isAnyOf` operator const filterOperators = React.useMemo(() => { if (!colDef.filterOperators) { return []; } return colDef.filterOperators.filter(operator => operator.value !== 'isAnyOf'); }, [colDef.filterOperators]); const filterModel = useGridSelector(apiRef, gridFilterModelSelector); const filterableColumnsLookup = useGridSelector(apiRef, gridFilterableColumnLookupSelector); const isFilterReadOnly = React.useMemo(() => { if (!filterModel?.items.length) { return false; } const filterModelItem = filterModel.items.find(it => it.field === colDef.field); return filterModelItem ? !filterableColumnsLookup[filterModelItem.field] : false; }, [colDef.field, filterModel, filterableColumnsLookup]); const currentOperator = React.useMemo(() => filterOperators.find(operator => operator.value === item.operator) ?? filterOperators[0], [item.operator, filterOperators]); const InputComponent = colDef.filterable || isFilterReadOnly ? currentOperator.InputComponent ?? DEFAULT_INPUT_COMPONENTS[colDef.type] : null; const clearFilterItem = React.useCallback(() => { apiRef.current.deleteFilterItem(item); }, [apiRef, item]); let headerFilterComponent; if (colDef.renderHeaderFilter) { headerFilterComponent = colDef.renderHeaderFilter(_extends({}, props, { inputRef })); } React.useLayoutEffect(() => { if (hasFocus && !isMenuOpen) { let focusableElement = cellRef.current.querySelector('[tabindex="0"]'); if (isEditing && InputComponent) { focusableElement = inputRef.current; } const elementToFocus = focusableElement || cellRef.current; elementToFocus?.focus(); if (apiRef.current.columnHeadersContainerRef.current) { apiRef.current.columnHeadersContainerRef.current.scrollLeft = 0; } } }, [InputComponent, apiRef, hasFocus, isEditing, isMenuOpen]); const onKeyDown = React.useCallback(event => { if (isMenuOpen || isNavigationKey(event.key) || isFilterReadOnly) { return; } switch (event.key) { case 'Escape': if (isEditing) { apiRef.current.stopHeaderFilterEditMode(); } break; case 'Enter': if (isEditing) { if (!event.defaultPrevented) { apiRef.current.stopHeaderFilterEditMode(); break; } } if (event.metaKey || event.ctrlKey) { headerFilterMenuRef.current = buttonRef.current; apiRef.current.showHeaderFilterMenu(colDef.field); break; } apiRef.current.startHeaderFilterEditMode(colDef.field); break; case 'Tab': { if (isEditing) { const fieldToFocus = columnFields[colIndex + (event.shiftKey ? -1 : 1)] ?? null; if (fieldToFocus) { apiRef.current.startHeaderFilterEditMode(fieldToFocus); apiRef.current.setColumnHeaderFilterFocus(fieldToFocus, event); } } break; } default: if (isEditing || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) { break; } apiRef.current.startHeaderFilterEditMode(colDef.field); break; } }, [apiRef, colDef.field, colIndex, columnFields, headerFilterMenuRef, isEditing, isFilterReadOnly, isMenuOpen]); const publish = React.useCallback((eventName, propHandler) => event => { apiRef.current.publishEvent(eventName, apiRef.current.getColumnHeaderParams(colDef.field), event); if (propHandler) { propHandler(event); } }, [apiRef, colDef.field]); const onMouseDown = React.useCallback(event => { if (!hasFocus) { if (inputRef.current?.contains?.(event.target)) { inputRef.current.focus(); } apiRef.current.setColumnHeaderFilterFocus(colDef.field, event); } }, [apiRef, colDef.field, hasFocus]); const mouseEventsHandlers = React.useMemo(() => ({ onKeyDown: publish('headerFilterKeyDown', onKeyDown), onClick: publish('headerFilterClick'), onMouseDown: publish('headerFilterMouseDown', onMouseDown), onBlur: publish('headerFilterBlur') }), [onMouseDown, onKeyDown, publish]); const ownerState = _extends({}, rootProps, { pinnedPosition, colDef, showLeftBorder, showRightBorder }); const classes = useUtilityClasses(ownerState); const label = currentOperator.headerLabel ?? apiRef.current.getLocaleText(`headerFilterOperator${capitalize(item.operator)}`); const isNoInputOperator = currentOperator.requiresFilterValue === false; const isApplied = item?.value !== undefined || isNoInputOperator; const isFilterActive = isApplied || hasFocus; const headerFilterMenu = /*#__PURE__*/_jsx(GridHeaderFilterMenuContainer, { operators: filterOperators, item: item, field: colDef.field, disabled: isFilterReadOnly, applyFilterChanges: apiRef.current.upsertFilterItem, headerFilterMenuRef: headerFilterMenuRef, buttonRef: buttonRef, showClearItem: !showClearIcon && isApplied, clearFilterItem: clearFilterItem }); const clearButton = showClearIcon && isApplied ? /*#__PURE__*/_jsx(GridHeaderFilterClearButton, { onClick: clearFilterItem, disabled: isFilterReadOnly }) : null; return /*#__PURE__*/_jsxs("div", _extends({ className: clsx(classes.root, headerClassName), style: attachPinnedStyle(_extends({ height, width }, styleProp), isRtl, pinnedPosition, pinnedOffset), role: "columnheader", "aria-colindex": colIndex + 1, "aria-label": headerFilterComponent == null ? colDef.headerName ?? colDef.field : undefined }, other, mouseEventsHandlers, { ref: handleRef, children: [headerFilterComponent, headerFilterComponent === undefined ? /*#__PURE__*/_jsxs(React.Fragment, { children: [isNoInputOperator ? /*#__PURE__*/_jsxs(React.Fragment, { children: [/*#__PURE__*/_jsx(OperatorLabel, { className: classes.operatorLabel, children: label }), clearButton, headerFilterMenu] }) : null, InputComponent && !isNoInputOperator ? /*#__PURE__*/_jsx(StyledInputComponent, _extends({ as: InputComponent, className: classes.input, apiRef: apiRef, item: item, inputRef: inputRef, applyValue: apiRef.current.upsertFilterItem, onFocus: () => apiRef.current.startHeaderFilterEditMode(colDef.field), onBlur: event => { apiRef.current.stopHeaderFilterEditMode(); // Blurring an input element should reset focus state only if `relatedTarget` is not the header filter cell if (!event.relatedTarget?.className.includes('columnHeader')) { apiRef.current.setState(state => _extends({}, state, { focus: { cell: null, columnHeader: null, columnHeaderFilter: null, columnGroupHeader: null } })); } }, isFilterActive: isFilterActive, headerFilterMenu: headerFilterMenu, clearButton: clearButton, disabled: isFilterReadOnly || isNoInputOperator, tabIndex: -1, slotProps: { root: { size: 'small', label: capitalize(label), placeholder: '' } } }, isNoInputOperator ? { value: '' } : {}, currentOperator?.InputComponentProps, InputComponentProps)) : null] }) : null] })); }); if (process.env.NODE_ENV !== "production") GridHeaderFilterCell.displayName = "GridHeaderFilterCell"; process.env.NODE_ENV !== "production" ? GridHeaderFilterCell.propTypes = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- colDef: PropTypes.object.isRequired, colIndex: PropTypes.number.isRequired, hasFocus: PropTypes.bool, /** * Class name added to the column header cell. */ headerClassName: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), headerFilterMenuRef: PropTypes.shape({ current: PropTypes.object }).isRequired, height: PropTypes.number.isRequired, InputComponentProps: PropTypes.shape({ apiRef: PropTypes.shape({ current: PropTypes.object.isRequired }), applyValue: PropTypes.func, className: PropTypes.string, clearButton: PropTypes.node, disabled: PropTypes.bool, focusElementRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any.isRequired })]), headerFilterMenu: PropTypes.node, inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: (props, propName) => { if (props[propName] == null) { return null; } if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) { return new Error(`Expected prop '${propName}' to be of type Element`); } return null; } })]), isFilterActive: PropTypes.bool, item: PropTypes.shape({ field: PropTypes.string.isRequired, id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), operator: PropTypes.string.isRequired, value: PropTypes.any }), onBlur: PropTypes.func, onFocus: PropTypes.func, slotProps: PropTypes.object, tabIndex: PropTypes.number }), item: PropTypes.shape({ field: PropTypes.string.isRequired, id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), operator: PropTypes.string.isRequired, value: PropTypes.any }).isRequired, pinnedOffset: PropTypes.number, pinnedPosition: PropTypes.oneOf([0, 1, 2, 3]), showClearIcon: PropTypes.bool, showLeftBorder: PropTypes.bool.isRequired, showRightBorder: PropTypes.bool.isRequired, sortIndex: PropTypes.number, style: PropTypes.object, tabIndex: PropTypes.oneOf([-1, 0]).isRequired, width: PropTypes.number.isRequired } : void 0; const Memoized = fastMemo(GridHeaderFilterCell); export { Memoized as GridHeaderFilterCell };