Skip to content

Commit

Permalink
[DataGrid] Refactor: create base Checkbox (#16445)
Browse files Browse the repository at this point in the history
  • Loading branch information
romgrk authored Feb 11, 2025
1 parent 34c6b64 commit 2372cf8
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 58 deletions.
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 @@ -12,6 +13,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 @@ -57,6 +59,8 @@ import type { GridSlotProps } from '../models/gridSlotsComponentsProps';
import type { PopperProps } from '../models/gridBaseSlots';
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 @@ -98,7 +102,7 @@ const iconSlots: GridIconSlotsComponent = {

const baseSlots: GridBaseSlots = {
baseBadge: MUIBadge,
baseCheckbox: MUICheckbox,
baseCheckbox: React.forwardRef(BaseCheckbox),
baseCircularProgress: MUICircularProgress,
baseDivider: MUIDivider,
baseLinearProgress: MUILinearProgress,
Expand All @@ -125,6 +129,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
};

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 @@ -34,6 +33,7 @@ import type { GridColumnHeaderSortIconProps } from '../components/columnHeaders/
import type {
BadgeProps,
ButtonProps,
CheckboxProps,
CircularProgressProps,
DividerProps,
IconButtonProps,
Expand Down

0 comments on commit 2372cf8

Please sign in to comment.