UNPKG

@mui/x-data-grid-pro

Version:

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

509 lines (506 loc) 17.7 kB
'use client'; import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import { styled } from '@mui/material/styles'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import useEventCallback from '@mui/utils/useEventCallback'; import { getDataGridUtilityClass, useGridRootProps, useGridApiContext, useGridSelector, GridCellEditStopReasons } from '@mui/x-data-grid'; import { NotRendered, vars, isMultiSelectColDef, getValueOptions, gridRowHeightSelector } from '@mui/x-data-grid/internals'; import { autocompleteClasses } from '@mui/material/Autocomplete'; import { inputBaseClasses } from '@mui/material/InputBase'; import { useGridPrivateApiContext } from "../../hooks/utils/useGridPrivateApiContext.mjs"; import { GridMultiSelectChips } from "./GridMultiSelectChips.mjs"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const useUtilityClasses = ownerState => { const { classes } = ownerState; const slots = { root: ['editMultiSelectCell'], chip: ['editMultiSelectCellChip'], chipHidden: ['editMultiSelectCellChip--hidden'], overflow: ['editMultiSelectCellOverflow'], popup: ['editMultiSelectCellPopup'], popperContent: ['editMultiSelectCellPopperContent'] }; return composeClasses(slots, getDataGridUtilityClass, classes); }; const GridEditMultiSelectCellPopper = styled(NotRendered, { name: 'MuiDataGrid', slot: 'EditMultiSelectCellPopper' })(({ theme }) => ({ zIndex: vars.zIndex.modal, background: (theme.vars || theme).palette.background.paper, borderRadius: (theme.vars || theme).shape.borderRadius, '&[data-popper-reference-hidden]': { opacity: 0 } })); const GridEditMultiSelectCellPopperContent = styled('div', { name: 'MuiDataGrid', slot: 'EditMultiSelectCellPopperContent' })(({ theme }) => ({ width: 'var(--_width)', boxShadow: (theme.vars || theme).shadows[4], boxSizing: 'border-box' })); const GridEditMultiSelectCellAutocomplete = styled(NotRendered, { name: 'MuiDataGrid', slot: 'EditMultiSelectCellAutocomplete' })(({ theme }) => ({ [`& .${inputBaseClasses.root}.${inputBaseClasses.sizeSmall}`]: { minHeight: 'var(--_rowHeight, 52px)', paddingBlock: 4 }, [`& + .${autocompleteClasses.popper}`]: { [`& .${autocompleteClasses.option}`]: _extends({}, theme.typography.body2) } })); const GridEditMultiSelectCellAutocompletePopper = styled('div', { slot: 'internal', shouldForwardProp: prop => prop !== 'ownerState' && prop !== 'anchorEl' && prop !== 'open' && prop !== 'disablePortal' })({}); const GridEditMultiSelectChips = styled(GridMultiSelectChips)({ padding: '0 10px', '&:focus-visible': { outline: 'none' // let the grid cell handle the focus ring } }); function GridEditMultiSelectCell(props) { const rootProps = useGridRootProps(); const { id, value: valueProp, field, row, colDef, cellMode, hasFocus, slotProps } = props; const apiRef = useGridApiContext(); const privateApiRef = useGridPrivateApiContext(); const classes = useUtilityClasses(rootProps); const rowHeight = useGridSelector(apiRef, gridRowHeightSelector); const [anchorEl, setAnchorEl] = React.useState(null); const [open, setOpen] = React.useState(true); const isAutoHeight = privateApiRef.current.rowHasAutoHeight(id); const popupId = `${id}-${field}-multiselect-edit-popup`; const showPopup = hasFocus && Boolean(anchorEl) && open; const closePopup = reason => { if (rootProps.editMode === 'row') { setOpen(false); return; } const params = apiRef.current.getCellParams(id, field); apiRef.current.publishEvent('cellEditStop', _extends({}, params, { reason })); }; const handleModalClose = () => closePopup(GridCellEditStopReasons.cellFocusOut); const handleKeyDownCapture = event => { if (event.key === 'Escape') { closePopup(GridCellEditStopReasons.escapeKeyDown); } }; const handleReopen = () => { if (rootProps.editMode === 'row' && !open) { setOpen(true); } }; const handleChipsKeyDown = event => { if (event.key === 'Enter' && rootProps.editMode === 'row' && !open) { // Consume Enter so the grid's row-edit handler doesn't exit the row. event.preventDefault(); event.stopPropagation(); setOpen(true); } }; // Reset `open` on focus-loss so re-entering (Shift+Tab back) reopens the autocomplete // by default — matches the "every focus opens the autocomplete" expectation. Also // scrolls the cell into view on focus-gain because the Modal locks body scroll and // the autocomplete input is body-portaled, so the browser default scroll-into-view // text-input cells rely on doesn't fire here. Kept as a single effect to avoid a // per-cell `cellFocusIn` listener (EventManager warns past 20 in row-edit). const prevHasFocusRef = React.useRef(false); useEnhancedEffect(() => { if (rootProps.editMode !== 'row') { return; } if (!hasFocus) { setOpen(true); prevHasFocusRef.current = false; return; } if (!prevHasFocusRef.current) { apiRef.current.scrollToIndexes({ colIndex: apiRef.current.getColumnIndex(field, true), rowIndex: apiRef.current.getRowIndexRelativeToVisibleRows(id) }); prevHasFocusRef.current = true; } }, [hasFocus, rootProps.editMode, apiRef, field, id]); useEnhancedEffect(() => { if (rootProps.editMode !== 'row') { return; } if (hasFocus && !open) { anchorEl?.focus(); } }, [hasFocus, open, rootProps.editMode, anchorEl]); const getOptionValue = colDef.getOptionValue; const getOptionLabel = colDef.getOptionLabel; if (!isMultiSelectColDef(colDef)) { return null; } const valueOptions = getValueOptions(colDef, { id, row }); if (!valueOptions) { return null; } const currentValue = Array.isArray(valueProp) ? valueProp : []; const optionByValue = new Map(); for (const opt of valueOptions) { optionByValue.set(getOptionValue(opt), opt); } // Convert values to options for Autocomplete const selectedOptions = currentValue.map(val => optionByValue.get(val)).filter(option => option !== undefined); return /*#__PURE__*/_jsxs(React.Fragment, { children: [/*#__PURE__*/_jsx(GridEditMultiSelectChips, { ref: setAnchorEl, values: currentValue, field: field, columnWidth: colDef.computedWidth, autoWrap: isAutoHeight, optionByValue: optionByValue, getOptionLabel: getOptionLabel, classes: { root: classes.root, chip: classes.chip, chipHidden: classes.chipHidden, overflow: classes.overflow }, slotProps: { root: _extends({ tabIndex: cellMode === 'edit' && rootProps.editMode === 'row' ? 0 : undefined, 'aria-controls': showPopup ? popupId : undefined, 'aria-expanded': showPopup, onClick: handleReopen, onKeyDown: handleChipsKeyDown }, slotProps?.root), chip: slotProps?.chip // No `overflow` slotProps → `+N` renders as a non-interactive indicator; // clicks on the cell focus the autocomplete which opens the editor. } }), /*#__PURE__*/_jsx(rootProps.slots.baseModal, { open: showPopup, disableAutoFocus: true, disableEnforceFocus: true, disableRestoreFocus: true, onClose: handleModalClose, material: { slotProps: { root: { style: { zIndex: 'auto' } }, backdrop: { invisible: true } } }, children: /*#__PURE__*/_jsx(GridEditMultiSelectCellPopper, _extends({ as: rootProps.slots.basePopper, ownerState: rootProps, id: popupId, role: "dialog", "aria-label": colDef.headerName || field, open: showPopup, target: anchorEl, placement: "bottom-start", flip: true, material: { modifiers: [{ name: 'offset', options: { offset: [-1, -rowHeight + 1] } // 1px compensate for the editing cell padding. }] } }, slotProps?.popper, { className: clsx(classes.popup, slotProps?.popper?.className), children: /*#__PURE__*/_jsx(GridEditMultiSelectCellPopperContent, _extends({ onKeyDownCapture: handleKeyDownCapture }, slotProps?.popperContent, { className: clsx(classes.popperContent, slotProps?.popperContent?.className), style: { '--_width': `${colDef.computedWidth}px`, '--_rowHeight': `${rowHeight}px` }, children: /*#__PURE__*/_jsx(GridEditMultiSelectAutocomplete, _extends({}, props, { valueOptions: valueOptions, selectedOptions: selectedOptions, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, onDismiss: () => setOpen(false) })) })) })) })] }); } function GridEditMultiSelectAutocomplete(props) { const { id, field, hasFocus, onValueChange, slotProps, valueOptions, selectedOptions, getOptionValue, getOptionLabel, onDismiss } = props; const inputRef = React.useRef(null); const apiRef = useGridApiContext(); const rootProps = useGridRootProps(); useEnhancedEffect(() => { if (hasFocus && inputRef.current) { inputRef.current.focus(); } }, [hasFocus]); const handleClose = useEventCallback((_event, reason) => { if (rootProps.editMode === 'row') { onDismiss(); return; } const params = apiRef.current.getCellParams(id, field); apiRef.current.publishEvent('cellEditStop', _extends({}, params, { reason: reason === 'escape' ? GridCellEditStopReasons.escapeKeyDown : GridCellEditStopReasons.cellFocusOut })); }); const isOptionEqualToValue = React.useCallback((option, val) => getOptionValue(option) === getOptionValue(val), [getOptionValue]); const handleChange = useEventCallback(async (event, newValue) => { if (event.type === 'keydown') { const keyboardEvent = event.nativeEvent; if (keyboardEvent.key === 'Enter') { if (keyboardEvent.ctrlKey || keyboardEvent.metaKey) { // Ctrl/Cmd + Enter: stop propagation to prevent cell navigation event.stopPropagation(); } else if (rootProps.editMode === 'row') { // Row mode bare Enter: dismiss popup, keep cell focused, ignore this selection. onDismiss(); return; } else { // Cell mode bare Enter: exit edit mode, ignore this selection change. const params = apiRef.current.getCellParams(id, field); apiRef.current.publishEvent('cellEditStop', _extends({}, params, { reason: GridCellEditStopReasons.enterKeyDown })); return; } } } const newValues = newValue.map(option => getOptionValue(option)); if (onValueChange) { await onValueChange(event, newValues); } await apiRef.current.setEditCellValue({ id, field, value: newValues }, event); }); return /*#__PURE__*/_jsx(GridEditMultiSelectCellAutocomplete, _extends({ as: rootProps.slots.baseAutocomplete, multiple: true, open: true, options: valueOptions, value: selectedOptions, onClose: handleClose, onChange: handleChange, getOptionLabel: getOptionLabel, isOptionEqualToValue: isOptionEqualToValue, disableCloseOnSelect: true, openOnFocus: true, slotProps: { textField: { size: 'small', inputRef }, chip: slotProps?.chip } }, slotProps?.autocomplete, { material: _extends({}, slotProps?.autocomplete?.material, { slots: _extends({ // @ts-expect-error the types require import from Material UI package popper: GridEditMultiSelectCellAutocompletePopper }, slotProps?.autocomplete?.material?.slots) }) })); } process.env.NODE_ENV !== "production" ? GridEditMultiSelectAutocomplete.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- /** * GridApi that let you manipulate the grid. */ api: PropTypes.object.isRequired, /** * The mode of the cell. */ cellMode: PropTypes.oneOf(['edit', 'view']).isRequired, changeReason: PropTypes.oneOf(['debouncedSetEditCellValue', 'setEditCellValue']), /** * The column of the row that the current cell belongs to. */ colDef: PropTypes.object.isRequired, /** * The column field of the cell that triggered the event. */ field: PropTypes.string.isRequired, /** * The cell value formatted with the column valueFormatter. */ formattedValue: PropTypes.any, getOptionLabel: PropTypes.func.isRequired, getOptionValue: PropTypes.func.isRequired, /** * If true, the cell is the active element. */ hasFocus: PropTypes.bool.isRequired, /** * The grid row id. */ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, /** * If true, the cell is editable. */ isEditable: PropTypes.bool, isProcessingProps: PropTypes.bool, isValidating: PropTypes.bool, onDismiss: PropTypes.func.isRequired, /** * Callback called when the value is changed by the user. * @param {React.SyntheticEvent} event The event source of the callback. * @param {any[]} newValue The value that is going to be passed to `apiRef.current.setEditCellValue`. * @returns {Promise<void> | void} A promise to be awaited before calling `apiRef.current.setEditCellValue` */ onValueChange: PropTypes.func, /** * The row model of the row that the current cell belongs to. */ row: PropTypes.any.isRequired, /** * The node of the row that the current cell belongs to. */ rowNode: PropTypes.object.isRequired, selectedOptions: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.shape({ label: PropTypes.string.isRequired, value: PropTypes.any.isRequired }), PropTypes.string]).isRequired).isRequired, /** * Props passed to internal components. */ slotProps: PropTypes.object, /** * the tabIndex value. */ tabIndex: PropTypes.oneOf([-1, 0]).isRequired, /** * The cell value. * If the column has `valueGetter`, use `params.row` to directly access the fields. */ value: PropTypes.any, valueOptions: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.shape({ label: PropTypes.string.isRequired, value: PropTypes.any.isRequired }), PropTypes.string]).isRequired).isRequired } : void 0; process.env.NODE_ENV !== "production" ? GridEditMultiSelectCell.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- /** * GridApi that let you manipulate the grid. */ api: PropTypes.object.isRequired, /** * The mode of the cell. */ cellMode: PropTypes.oneOf(['edit', 'view']).isRequired, changeReason: PropTypes.oneOf(['debouncedSetEditCellValue', 'setEditCellValue']), /** * The column of the row that the current cell belongs to. */ colDef: PropTypes.object.isRequired, /** * The column field of the cell that triggered the event. */ field: PropTypes.string.isRequired, /** * The cell value formatted with the column valueFormatter. */ formattedValue: PropTypes.any, /** * If true, the cell is the active element. */ hasFocus: PropTypes.bool.isRequired, /** * The grid row id. */ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, /** * If true, the cell is editable. */ isEditable: PropTypes.bool, isProcessingProps: PropTypes.bool, isValidating: PropTypes.bool, /** * Callback called when the value is changed by the user. * @param {React.SyntheticEvent} event The event source of the callback. * @param {any[]} newValue The value that is going to be passed to `apiRef.current.setEditCellValue`. * @returns {Promise<void> | void} A promise to be awaited before calling `apiRef.current.setEditCellValue` */ onValueChange: PropTypes.func, /** * The row model of the row that the current cell belongs to. */ row: PropTypes.any.isRequired, /** * The node of the row that the current cell belongs to. */ rowNode: PropTypes.object.isRequired, /** * Props passed to internal components. */ slotProps: PropTypes.object, /** * the tabIndex value. */ tabIndex: PropTypes.oneOf([-1, 0]).isRequired, /** * The cell value. * If the column has `valueGetter`, use `params.row` to directly access the fields. */ value: PropTypes.any } : void 0; export { GridEditMultiSelectCell }; export const renderEditMultiSelectCell = params => /*#__PURE__*/_jsx(GridEditMultiSelectCell, _extends({}, params)); if (process.env.NODE_ENV !== "production") renderEditMultiSelectCell.displayName = "renderEditMultiSelectCell";