Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DataGrid] Refactor: create base Checkbox #16445

Merged
merged 12 commits into from
Feb 11, 2025
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import {
unstable_composeClasses as composeClasses,
unstable_useForkRef as useForkRef,
} from '@mui/utils';
import { unstable_composeClasses as composeClasses } from '@mui/utils';
import { forwardRef } from '@mui/x-internals/forwardRef';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
Expand All @@ -26,10 +23,6 @@ const useUtilityClasses = (ownerState: OwnerState) => {
return composeClasses(slots, getDataGridUtilityClass, classes);
};

interface TouchRippleActions {
stop: (event: any, callback?: () => void) => void;
}

const GridCellCheckboxForwardRef = forwardRef<HTMLInputElement, GridRenderCellParams>(
function GridCellCheckboxRenderer(props, ref) {
const {
Expand All @@ -50,10 +43,6 @@ const GridCellCheckboxForwardRef = forwardRef<HTMLInputElement, GridRenderCellPa
const rootProps = useGridRootProps();
const ownerState = { classes: rootProps.classes };
const classes = useUtilityClasses(ownerState);
const checkboxElement = React.useRef<HTMLElement>(null);

const rippleRef = React.useRef<TouchRippleActions>(null);
const handleRef = useForkRef(checkboxElement, ref);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const params: GridRowSelectionCheckboxParams = { value: event.target.checked, id };
Expand All @@ -69,16 +58,6 @@ const GridCellCheckboxForwardRef = forwardRef<HTMLInputElement, GridRenderCellPa
}
}, [apiRef, tabIndex, id, field]);

React.useEffect(() => {
if (hasFocus) {
const input = checkboxElement.current?.querySelector('input');
input?.focus({ preventScroll: true });
} else if (rippleRef.current) {
// Only available in @mui/material v5.4.1 or later
rippleRef.current.stop({});
}
}, [hasFocus]);

const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
if (event.key === ' ') {
// We call event.stopPropagation to avoid selecting the row and also scrolling to bottom
Expand Down Expand Up @@ -114,14 +93,15 @@ const GridCellCheckboxForwardRef = forwardRef<HTMLInputElement, GridRenderCellPa
checked={isChecked && !isIndeterminate}
onChange={handleChange}
className={classes.root}
inputProps={{ 'aria-label': label, name: 'select_row' }}
slotProps={{
htmlInput: { 'aria-label': label, name: 'select_row' },
}}
onKeyDown={handleKeyDown}
indeterminate={isIndeterminate}
disabled={!isSelectable}
touchRippleRef={rippleRef as any /* FIXME: typing error */}
{...rootProps.slotProps?.baseCheckbox}
{...other}
ref={handleRef}
ref={ref as any}
/>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ const GridHeaderCheckbox = forwardRef<HTMLButtonElement, GridColumnHeaderParams>
checked={isChecked && !isIndeterminate}
onChange={handleChange}
className={classes.root}
inputProps={{ 'aria-label': label, name: 'select_all_rows' }}
slotProps={{
htmlInput: { 'aria-label': label, name: 'select_all_rows' },
}}
tabIndex={tabIndex}
onKeyDown={handleKeyDown}
disabled={!isMultipleRowSelectionEnabled(rootProps)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import composeClasses from '@mui/utils/composeClasses';
import FormControlLabel from '@mui/material/FormControlLabel';
import { styled } from '@mui/material/styles';
import { inputBaseClasses } from '@mui/material/InputBase';
import { TextFieldProps } from '../../models/gridBaseSlots';
Expand Down Expand Up @@ -272,21 +271,19 @@ function GridColumnsManagement(props: GridColumnsManagementProps) {
</GridColumnsManagementHeader>
<GridColumnsManagementBody className={classes.root} ownerState={rootProps}>
{currentColumns.map((column) => (
<FormControlLabel
<rootProps.slots.baseCheckbox
key={column.field}
className={classes.row}
control={
<rootProps.slots.baseCheckbox
disabled={column.hideable === false}
checked={columnVisibilityModel[column.field] !== false}
onClick={toggleColumn}
name={column.field}
sx={{ p: 0.5 }}
inputRef={isFirstHideableColumn(column) ? firstSwitchRef : undefined}
{...rootProps.slotProps?.baseCheckbox}
/>
}
disabled={column.hideable === false}
checked={columnVisibilityModel[column.field] !== false}
onClick={toggleColumn}
name={column.field}
inputRef={isFirstHideableColumn(column) ? firstSwitchRef : undefined}
label={column.headerName || column.field}
size="medium"
density="compact"
fullWidth
{...rootProps.slotProps?.baseCheckbox}
/>
))}
{currentColumns.length === 0 && (
Expand All @@ -298,19 +295,14 @@ function GridColumnsManagement(props: GridColumnsManagementProps) {
{(!disableShowHideToggle || !disableResetButton) && currentColumns.length > 0 ? (
<GridColumnsManagementFooter ownerState={rootProps} className={classes.footer}>
{!disableShowHideToggle ? (
<FormControlLabel
control={
<rootProps.slots.baseCheckbox
disabled={hideableColumns.length === 0}
checked={allHideableColumnsVisible}
indeterminate={!allHideableColumnsVisible && !allHideableColumnsHidden}
onClick={() => toggleAllColumns(!allHideableColumnsVisible)}
name={apiRef.current.getLocaleText('columnsManagementShowHideAllText')}
sx={{ p: 0.5 }}
{...rootProps.slotProps?.baseCheckbox}
/>
}
<rootProps.slots.baseCheckbox
disabled={hideableColumns.length === 0}
checked={allHideableColumnsVisible}
indeterminate={!allHideableColumnsVisible && !allHideableColumnsHidden}
onClick={() => toggleAllColumns(!allHideableColumnsVisible)}
name={apiRef.current.getLocaleText('columnsManagementShowHideAllText')}
label={apiRef.current.getLocaleText('columnsManagementShowHideAllText')}
{...rootProps.slotProps?.baseCheckbox}
/>
) : (
<span />
Expand Down Expand Up @@ -427,9 +419,8 @@ GridColumnsManagement.propTypes = {
const GridColumnsManagementBody = styled('div', {
name: 'MuiDataGrid',
slot: 'ColumnsManagement',
overridesResolver: (props, styles) => styles.columnsManagement,
})<{ ownerState: OwnerState }>(({ theme }) => ({
padding: theme.spacing(0, 3, 1.5),
padding: theme.spacing(0, 2, 1.5),
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
Expand All @@ -441,15 +432,13 @@ const GridColumnsManagementBody = styled('div', {
const GridColumnsManagementHeader = styled('div', {
name: 'MuiDataGrid',
slot: 'ColumnsManagementHeader',
overridesResolver: (props, styles) => styles.columnsManagementHeader,
})<{ ownerState: OwnerState }>(({ theme }) => ({
padding: theme.spacing(1.5, 3),
}));

const SearchInput = styled(NotRendered<GridSlotProps['baseTextField']>, {
name: 'MuiDataGrid',
slot: 'ColumnsManagementSearchInput',
overridesResolver: (props, styles) => styles.columnsManagementSearchInput,
})<{ ownerState: OwnerState }>(({ theme }) => ({
[`& .${inputBaseClasses.root}`]: {
padding: theme.spacing(0, 1.5, 0, 1.5),
Expand All @@ -466,7 +455,6 @@ const SearchInput = styled(NotRendered<GridSlotProps['baseTextField']>, {
const GridColumnsManagementFooter = styled('div', {
name: 'MuiDataGrid',
slot: 'ColumnsManagementFooter',
overridesResolver: (props, styles) => styles.columnsManagementFooter,
})<{ ownerState: OwnerState }>(({ theme }) => ({
padding: theme.spacing(0.5, 1, 0.5, 3),
display: 'flex',
Expand Down
59 changes: 58 additions & 1 deletion packages/x-data-grid/src/material/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import useForkRef from '@mui/utils/useForkRef';
import MUIBadge from '@mui/material/Badge';
import MUICheckbox from '@mui/material/Checkbox';
import MUIChip from '@mui/material/Chip';
Expand All @@ -11,6 +12,7 @@ import MUIMenuList from '@mui/material/MenuList';
import MUIMenuItem from '@mui/material/MenuItem';
import MUITextField from '@mui/material/TextField';
import MUIFormControl from '@mui/material/FormControl';
import MUIFormControlLabel from '@mui/material/FormControlLabel';
import MUISelect from '@mui/material/Select';
import MUIButton from '@mui/material/Button';
import MUIIconButton from '@mui/material/IconButton';
Expand Down Expand Up @@ -52,6 +54,8 @@ import type { GridBaseSlots } from '../models/gridSlotsComponent';
import type { GridSlotProps } from '../models/gridSlotsComponentsProps';
import { useGridRootProps } from '../hooks/utils/useGridRootProps';

/* eslint-disable material-ui/disallow-react-api-in-server-components */

const iconSlots: GridIconSlotsComponent = {
booleanCellTrueIcon: GridCheckIcon,
booleanCellFalseIcon: GridCloseIcon,
Expand Down Expand Up @@ -93,7 +97,7 @@ const iconSlots: GridIconSlotsComponent = {

const baseSlots: GridBaseSlots = {
baseBadge: MUIBadge,
baseCheckbox: MUICheckbox,
baseCheckbox: React.forwardRef(BaseCheckbox),
baseCircularProgress: MUICircularProgress,
baseDivider: MUIDivider,
baseLinearProgress: MUILinearProgress,
Expand All @@ -120,6 +124,59 @@ const materialSlots: GridBaseSlots & GridIconSlotsComponent = {

export default materialSlots;

const CHECKBOX_COMPACT = { p: 0.5 };

function BaseCheckbox(props: GridSlotProps['baseCheckbox'], ref: React.Ref<HTMLButtonElement>) {
const { autoFocus, label, fullWidth, slotProps, className, density, ...other } = props;

const elementRef = React.useRef<HTMLButtonElement>(null);
const handleRef = useForkRef(elementRef, ref);
const rippleRef = React.useRef<any>(null);

const sx = density === 'compact' ? CHECKBOX_COMPACT : undefined;

React.useEffect(() => {
if (autoFocus) {
const input = elementRef.current?.querySelector('input');
input?.focus({ preventScroll: true });
} else if (autoFocus === false && rippleRef.current) {
// Only available in @mui/material v5.4.1 or later
// @ts-ignore
rippleRef.current.stop({});
}
}, [autoFocus]);

if (!label) {
return (
<MUICheckbox
{...other}
className={className}
inputProps={slotProps?.htmlInput}
ref={handleRef}
sx={sx}
touchRippleRef={rippleRef}
/>
);
}

return (
<MUIFormControlLabel
className={className}
control={
<MUICheckbox
{...other}
inputProps={slotProps?.htmlInput}
ref={handleRef}
sx={sx}
touchRippleRef={rippleRef}
/>
}
label={label}
sx={fullWidth ? { width: '100%', margin: 0 } : undefined}
/>
);
}

function BaseMenuList(props: GridSlotProps['baseMenuList']) {
return <MUIMenuList {...props} />;
}
Expand Down
25 changes: 25 additions & 0 deletions packages/x-data-grid/src/models/gridBaseSlots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,31 @@ export type ButtonProps = {
touchRippleRef?: any; // FIXME(v8:romgrk): find a way to remove
};

export type CheckboxProps = {
ref?: Ref<HTMLButtonElement>;
id?: string;
autoFocus?: boolean;
checked?: boolean;
className?: string;
disabled?: boolean;
fullWidth?: boolean;
indeterminate?: boolean;
inputRef?: React.Ref<HTMLInputElement>;
name?: string;
label?: React.ReactNode;
onClick?: React.MouseEventHandler;
onChange?: React.ChangeEventHandler;
onKeyDown?: React.KeyboardEventHandler;
size?: 'small' | 'medium';
density?: 'standard' | 'compact';
slotProps?: {
htmlInput?: React.InputHTMLAttributes<HTMLInputElement>;
};
style?: React.CSSProperties;
tabIndex?: number;
touchRippleRef?: any; // FIXME(v8:romgrk): find a way to remove
romgrk marked this conversation as resolved.
Show resolved Hide resolved
};

export type IconButtonProps = Omit<ButtonProps, 'startIcon'> & {
label?: string;
color?: 'default' | 'inherit' | 'primary';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
import type { BadgeProps as MUIBadgeProps } from '@mui/material/Badge';
import type { ButtonProps as MUIButtonProps } from '@mui/material/Button';
import type { CheckboxProps } from '@mui/material/Checkbox';
import type { CircularProgressProps as MUICircularProgressProps } from '@mui/material/CircularProgress';
import type { LinearProgressProps as MUILinearProgressProps } from '@mui/material/LinearProgress';
import type { MenuItemProps as MUIMenuItemProps } from '@mui/material/MenuItem';
Expand Down Expand Up @@ -35,6 +34,7 @@ import type { GridColumnHeaderSortIconProps } from '../components/columnHeaders/
import type {
BadgeProps,
ButtonProps,
CheckboxProps,
CircularProgressProps,
DividerProps,
IconButtonProps,
Expand Down