Skip to content

[DataGrid] Refactor: create base Checkbox #16445

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

Merged
merged 12 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
/>
);
Comment on lines +149 to +177

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romgrk Hope it's okay to ask it directly here:
If I'm not mistaken I can't override the baseCheckbox slot in a good way anymore (without diving into the source code here).
I have to specifically recreate this conditional logic so that it works in the "Manage columns" overlay.
Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdym by override? What's the use-case?

Base slots are indeed expected to have a bit of friction to overwrite, these are meant for design-system authors to re-implement so they're more low-level than what the average end-user would use.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use-case is, that we have custom styling for most of the controls, which we also want in our DataGrid as good as possible.
When reading the docs I would expect this to work:

import { Checkbox } from '@mui/material';

slots={{
  baseCheckbox: Checkbox
}}

// or at least with some minor adjustments
slots={{
  baseCheckbox: ({ slotProps, ...props }) => <Checkbox {...props} />,
}}

In v7 it was more or less a direct mapping from slot to MUI component but in v8 there's a whole new layer of complexity because every base slot may or may not be composed from multiple components now.

From my perspective I would expect something like this:

slots={{
  baseCheckbox: MyCheckbox,
  baseFormControlLabel: MyFormControlLabel
}}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry there's not way around the added complexity. We want the datagrid to be usable with other design-systems, which means material-ui needs to go through a standardized intermediate layer, the base typings. If all you care about is styling, you can still pass material props via the material={{ ... }} prop, possibly via slotProps. Otherwise, forking /material/index.tsx is best, it should be considered like the reference to implement custom bindings, either to material or to shadcn/mantine/etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it works for you, you could also wrap over the base checkbox instead of wrapping over the material-ui checkbox.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get your reasoning behind it but for me it gives the impression that other design-systems are "overvalued" compared to the native one.
From a pure consumer-perspective it should be easy to adjust the native components and hard to integrate another design-system.
Rough thought: Is it worth to make it harder for 99% of the people when 1% actually use another design-system.

This is highly conceptual, so I guess we can leave it here. 😅
Thanks for the additional technical input, how to solve it. 👍

}

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