371 lines
13 KiB
JavaScript
371 lines
13 KiB
JavaScript
'use client';
|
|
|
|
import _formatErrorMessage from "@mui/x-internals/formatErrorMessage";
|
|
import _extends from "@babel/runtime/helpers/esm/extends";
|
|
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
|
|
const _excluded = ["api", "colDef", "id", "hasFocus", "isEditable", "field", "value", "formattedValue", "row", "rowNode", "cellMode", "tabIndex", "position", "onMenuOpen", "onMenuClose", "children", "suppressChildrenValidation"];
|
|
import * as React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { useRtl } from '@mui/system/RtlProvider';
|
|
import useId from '@mui/utils/useId';
|
|
import { warnOnce } from '@mui/x-internals/warning';
|
|
import { gridClasses } from "../../constants/gridClasses.mjs";
|
|
import { GridMenu } from "../menu/GridMenu.mjs";
|
|
import { useGridRootProps } from "../../hooks/utils/useGridRootProps.mjs";
|
|
import { useGridApiContext } from "../../hooks/utils/useGridApiContext.mjs";
|
|
import { GridActionsCellItem } from "./GridActionsCellItem.mjs";
|
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
const hasActions = colDef => typeof colDef.getActions === 'function';
|
|
function GridActionsCell(props) {
|
|
const {
|
|
id,
|
|
hasFocus,
|
|
tabIndex,
|
|
position = 'bottom-end',
|
|
onMenuOpen,
|
|
onMenuClose,
|
|
children,
|
|
suppressChildrenValidation
|
|
} = props,
|
|
other = _objectWithoutPropertiesLoose(props, _excluded);
|
|
const [focusedButtonIndex, setFocusedButtonIndex] = React.useState(-1);
|
|
const [open, setOpen] = React.useState(false);
|
|
const apiRef = useGridApiContext();
|
|
const rootRef = React.useRef(null);
|
|
const buttonRef = React.useRef(null);
|
|
const ignoreCallToFocus = React.useRef(false);
|
|
const touchRippleRefs = React.useRef({});
|
|
const isRtl = useRtl();
|
|
const menuId = useId();
|
|
const buttonId = useId();
|
|
const rootProps = useGridRootProps();
|
|
const rowParams = apiRef.current.getRowParams(id);
|
|
const actions = [];
|
|
React.Children.forEach(children, child => {
|
|
// Unwrap React.Fragment
|
|
if (/*#__PURE__*/React.isValidElement(child)) {
|
|
if (child.type === React.Fragment) {
|
|
React.Children.forEach(child.props.children, fragChild => {
|
|
if (/*#__PURE__*/React.isValidElement(fragChild)) {
|
|
actions.push(fragChild);
|
|
}
|
|
});
|
|
} else if (child.type === GridActionsCellItem || suppressChildrenValidation) {
|
|
actions.push(child);
|
|
} else if (process.env.NODE_ENV !== 'production') {
|
|
const childType = typeof child.type === 'function' ? child.type.name : child.type;
|
|
warnOnce(`MUI X: Invalid child type in \`GridActionsCell\`. Expected \`GridActionsCellItem\` or \`React.Fragment\`, got \`${childType}\`.
|
|
If this is intentional, you can suppress this warning by passing the \`suppressChildrenValidation\` prop to \`GridActionsCell\`.`, 'error');
|
|
}
|
|
}
|
|
});
|
|
const iconButtons = actions.filter(option => !option.props.showInMenu);
|
|
const menuButtons = actions.filter(option => option.props.showInMenu);
|
|
const numberOfButtons = iconButtons.length + (menuButtons.length ? 1 : 0);
|
|
React.useLayoutEffect(() => {
|
|
if (!hasFocus) {
|
|
Object.entries(touchRippleRefs.current).forEach(([index, ref]) => {
|
|
ref?.stop({}, () => {
|
|
delete touchRippleRefs.current[index];
|
|
});
|
|
});
|
|
}
|
|
}, [hasFocus]);
|
|
React.useEffect(() => {
|
|
if (focusedButtonIndex < 0 || !rootRef.current) {
|
|
return;
|
|
}
|
|
if (focusedButtonIndex >= rootRef.current.children.length) {
|
|
return;
|
|
}
|
|
const child = rootRef.current.children[focusedButtonIndex];
|
|
child.focus({
|
|
preventScroll: true
|
|
});
|
|
}, [focusedButtonIndex]);
|
|
const firstFocusableButtonIndex = actions.findIndex(o => !o.props.disabled);
|
|
React.useEffect(() => {
|
|
if (hasFocus && focusedButtonIndex === -1) {
|
|
setFocusedButtonIndex(firstFocusableButtonIndex);
|
|
}
|
|
if (!hasFocus) {
|
|
setFocusedButtonIndex(-1);
|
|
ignoreCallToFocus.current = false;
|
|
}
|
|
}, [hasFocus, focusedButtonIndex, firstFocusableButtonIndex]);
|
|
React.useEffect(() => {
|
|
if (focusedButtonIndex >= numberOfButtons) {
|
|
setFocusedButtonIndex(numberOfButtons - 1);
|
|
}
|
|
}, [focusedButtonIndex, numberOfButtons]);
|
|
const showMenu = event => {
|
|
if (onMenuOpen && !onMenuOpen(rowParams, event)) {
|
|
return;
|
|
}
|
|
setOpen(true);
|
|
setFocusedButtonIndex(numberOfButtons - 1);
|
|
ignoreCallToFocus.current = true;
|
|
};
|
|
const hideMenu = event => {
|
|
if (onMenuClose && !onMenuClose(rowParams, event)) {
|
|
return;
|
|
}
|
|
setOpen(false);
|
|
};
|
|
const toggleMenu = event => {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
if (open) {
|
|
hideMenu(event);
|
|
} else {
|
|
showMenu(event);
|
|
}
|
|
};
|
|
const handleTouchRippleRef = index => instance => {
|
|
touchRippleRefs.current[index] = instance;
|
|
};
|
|
const handleButtonClick = (index, onClick) => event => {
|
|
setFocusedButtonIndex(index);
|
|
ignoreCallToFocus.current = true;
|
|
if (onClick) {
|
|
onClick(event);
|
|
}
|
|
};
|
|
const handleRootKeyDown = event => {
|
|
if (numberOfButtons <= 1) {
|
|
return;
|
|
}
|
|
const getNewIndex = (index, direction) => {
|
|
if (index < 0 || index > actions.length) {
|
|
return index;
|
|
}
|
|
|
|
// for rtl mode we need to reverse the direction
|
|
const rtlMod = isRtl ? -1 : 1;
|
|
const indexMod = (direction === 'left' ? -1 : 1) * rtlMod;
|
|
|
|
// if the button that should receive focus is disabled go one more step
|
|
return actions[index + indexMod]?.props.disabled ? getNewIndex(index + indexMod, direction) : index + indexMod;
|
|
};
|
|
let newIndex = focusedButtonIndex;
|
|
if (event.key === 'ArrowRight') {
|
|
newIndex = getNewIndex(focusedButtonIndex, 'right');
|
|
} else if (event.key === 'ArrowLeft') {
|
|
newIndex = getNewIndex(focusedButtonIndex, 'left');
|
|
}
|
|
if (newIndex < 0 || newIndex >= numberOfButtons) {
|
|
return; // We're already in the first or last item = do nothing and let the grid listen the event
|
|
}
|
|
if (newIndex !== focusedButtonIndex) {
|
|
event.preventDefault(); // Prevent scrolling
|
|
event.stopPropagation(); // Don't stop propagation for other keys, for example ArrowUp
|
|
setFocusedButtonIndex(newIndex);
|
|
}
|
|
};
|
|
|
|
// role="menu" requires at least one child element
|
|
const attributes = numberOfButtons > 0 ? {
|
|
role: 'menu',
|
|
onKeyDown: handleRootKeyDown
|
|
} : undefined;
|
|
return /*#__PURE__*/_jsxs("div", _extends({
|
|
ref: rootRef,
|
|
tabIndex: -1,
|
|
className: gridClasses.actionsCell
|
|
}, attributes, other, {
|
|
children: [iconButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
|
|
key: index,
|
|
touchRippleRef: handleTouchRippleRef(index),
|
|
onClick: handleButtonClick(index, button.props.onClick),
|
|
tabIndex: focusedButtonIndex === index ? tabIndex : -1
|
|
})), menuButtons.length > 0 && buttonId && /*#__PURE__*/_jsx(rootProps.slots.baseIconButton, _extends({
|
|
ref: buttonRef,
|
|
id: buttonId,
|
|
"aria-label": apiRef.current.getLocaleText('actionsCellMore'),
|
|
"aria-haspopup": "menu",
|
|
"aria-expanded": open,
|
|
"aria-controls": open ? menuId : undefined,
|
|
role: "menuitem",
|
|
size: "small",
|
|
onClick: toggleMenu,
|
|
touchRippleRef: handleTouchRippleRef(buttonId),
|
|
tabIndex: focusedButtonIndex === iconButtons.length ? tabIndex : -1
|
|
}, rootProps.slotProps?.baseIconButton, {
|
|
children: /*#__PURE__*/_jsx(rootProps.slots.moreActionsIcon, {
|
|
fontSize: "small"
|
|
})
|
|
})), menuButtons.length > 0 && /*#__PURE__*/_jsx(GridMenu, {
|
|
open: open,
|
|
target: buttonRef.current,
|
|
position: position,
|
|
onClose: hideMenu,
|
|
children: /*#__PURE__*/_jsx(rootProps.slots.baseMenuList, {
|
|
id: menuId,
|
|
className: gridClasses.menuList,
|
|
"aria-labelledby": buttonId,
|
|
autoFocusItem: true,
|
|
children: menuButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
|
|
key: index,
|
|
closeMenu: hideMenu
|
|
}))
|
|
})
|
|
})]
|
|
}));
|
|
}
|
|
process.env.NODE_ENV !== "production" ? GridActionsCell.propTypes /* remove-proptypes */ = {
|
|
// ----------------------------- Warning --------------------------------
|
|
// | These PropTypes are generated from the TypeScript type definitions |
|
|
// | To update them edit the TypeScript types and run "pnpm proptypes" |
|
|
// ----------------------------------------------------------------------
|
|
api: PropTypes.object,
|
|
/**
|
|
* The mode of the cell.
|
|
*/
|
|
cellMode: PropTypes.oneOf(['edit', 'view']).isRequired,
|
|
children: PropTypes.node,
|
|
/**
|
|
* 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,
|
|
/**
|
|
* Callback to fire before the menu gets closed.
|
|
* Use this callback to prevent the menu from closing.
|
|
*
|
|
* @param {GridRowParams<R>} params Row parameters.
|
|
* @param {React.MouseEvent<HTMLElement> | React.KeyboardEvent | MouseEvent | TouchEvent | undefined} event The event triggering this callback.
|
|
* @returns {boolean} if the menu should be closed.
|
|
*/
|
|
onMenuClose: PropTypes.func,
|
|
/**
|
|
* Callback to fire before the menu gets opened.
|
|
* Use this callback to prevent the menu from opening.
|
|
*
|
|
* @param {GridRowParams<R>} params Row parameters.
|
|
* @param {React.MouseEvent<HTMLElement>} event The event triggering this callback.
|
|
* @returns {boolean} if the menu should be opened.
|
|
*/
|
|
onMenuOpen: PropTypes.func,
|
|
position: PropTypes.oneOf(['bottom-end', 'bottom-start', 'bottom', 'left-end', 'left-start', 'left', 'right-end', 'right-start', 'right', 'top-end', 'top-start', 'top']),
|
|
/**
|
|
* The row model of the row that the current cell belongs to.
|
|
*/
|
|
row: PropTypes.object.isRequired,
|
|
/**
|
|
* The node of the row that the current cell belongs to.
|
|
*/
|
|
rowNode: PropTypes.object.isRequired,
|
|
/**
|
|
* If true, the children passed to the component will not be validated.
|
|
* If false, only `GridActionsCellItem` and `React.Fragment` are allowed as children.
|
|
* Only use this prop if you know what you are doing.
|
|
* @default false
|
|
*/
|
|
suppressChildrenValidation: PropTypes.bool,
|
|
/**
|
|
* 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 { GridActionsCell };
|
|
|
|
// Temporary wrapper for backward compatibility.
|
|
// Only used to support `getActions` method in `GridColDef`.
|
|
// TODO(v9): Remove this wrapper and the default `renderCell` in gridActionsColDef
|
|
function GridActionsCellWrapper(props) {
|
|
const {
|
|
colDef,
|
|
id
|
|
} = props;
|
|
const apiRef = useGridApiContext();
|
|
if (!hasActions(colDef)) {
|
|
throw new Error(process.env.NODE_ENV !== "production" ? 'MUI X: Missing the `getActions` property in the `GridColDef`.' : _formatErrorMessage(60));
|
|
}
|
|
const actions = colDef.getActions(apiRef.current.getRowParams(id));
|
|
return /*#__PURE__*/_jsx(GridActionsCell, _extends({
|
|
suppressChildrenValidation: true
|
|
}, props, {
|
|
children: actions
|
|
}));
|
|
}
|
|
process.env.NODE_ENV !== "production" ? GridActionsCellWrapper.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,
|
|
/**
|
|
* 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,
|
|
/**
|
|
* 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,
|
|
/**
|
|
* 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 const renderActionsCell = params => /*#__PURE__*/_jsx(GridActionsCellWrapper, _extends({}, params));
|
|
if (process.env.NODE_ENV !== "production") renderActionsCell.displayName = "renderActionsCell"; |