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] An input element in a custom header filter loses focus after a first letter is typed #12326

Closed
layerok opened this issue Mar 4, 2024 · 8 comments · Fixed by #12328
Labels
component: data grid This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature feature: Filtering on header Related to the header filtering (Pro) feature

Comments

@layerok
Copy link
Contributor

layerok commented Mar 4, 2024

Steps to reproduce

Link to live example: https://codesandbox.io/p/sandbox/optimistic-boyd-xjx87z?file=%2Fsrc%2FDemo.tsx%3A16%2C37

Steps:

  1. Click an input element inside a header filter cell
  2. Type any letter

Current behavior

When I try typing the first letter in an input element, then the input immediately loses focus, preventing me from continuing to type

Expected behavior

An input element doesn't immediately lose focus after pressing a first letter

Context

I'm trying to use custom header filter instead of the default one.

Your environment

npx @mui/envinfo
  System:
    OS: macOS 13.5.1
    CPU: (8) arm64 Apple M1
    Memory: 95.81 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 16.14.0 - ~/.nvm/versions/node/v16.14.0/bin/node
    Yarn: 1.22.19 - /opt/homebrew/bin/yarn
    npm: 8.3.1 - ~/.nvm/versions/node/v16.14.0/bin/npm
    bun: 1.0.0 - ~/.bun/bin/bun
    Watchman: 2024.01.22.00 - /opt/homebrew/bin/watchman
  Managers:
    CocoaPods: 1.11.3 - /Users/layerok/.rbenv/shims/pod
    Composer: 2.2.7 - /usr/local/bin/composer
    Homebrew: 4.2.8 - /opt/homebrew/bin/brew
    Maven: 3.9.5 - /opt/homebrew/bin/mvn
    pip3: 23.3.1 - /opt/homebrew/bin/pip3
    RubyGems: 3.3.25 - /Users/layerok/.rbenv/shims/gem
  Utilities:
    CMake: 3.25.1 - /opt/homebrew/bin/cmake
    Make: 3.81 - /usr/bin/make
    GCC: 15.0.0 - /usr/bin/gcc
    Git: 2.39.1 - /opt/homebrew/bin/git
    Clang: 15.0.0 - /usr/bin/clang
    Ninja: 1.8.2 - /Users/layerok/depot_tools/ninja
    Curl: 8.1.2 - /usr/bin/curl
  Servers:
    Apache: 2.4.56 - /usr/sbin/apachectl
    Nginx: 1.25.2 - /opt/homebrew/bin/nginx
  Virtualization:
    Docker: 20.10.16 - /usr/local/bin/docker
  SDKs:
    iOS SDK:
      Platforms: DriverKit 23.2, iOS 17.2, macOS 14.2, tvOS 17.2, visionOS 1.0, watchOS 10.2
  IDEs:
    Android Studio: 2021.3 AI-213.7172.25.2113.9123335
    PhpStorm: 2023.3.2
    Vim: 9.0 - /usr/bin/vim
    WebStorm: 2023.2.5
    Xcode: 15.2/15C500b - /usr/bin/xcodebuild
  Languages:
    Bash: 3.2.57 - /bin/bash
    Java: 11.0.17 - /usr/bin/javac
    Perl: 5.30.3 - /usr/bin/perl
    PHP: 8.3.3 - /opt/homebrew/bin/php
    Python3: 3.11.7 - /opt/homebrew/bin/python3
    Ruby: 2.7.5 - /Users/layerok/.rbenv/shims/ruby
  Databases:
    MySQL: 15.2 (MariaDB) - /opt/homebrew/bin/mysql
    SQLite: 3.39.5 - /usr/bin/sqlite3
  Browsers:
    Chrome: 122.0.6261.94 (reproduced)
    Safari: 16.6 (reproduced)

Search keywords: header filter, renderHeaderFilter, DataGrid, data grid
Order ID: 74777

@layerok layerok added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Mar 4, 2024
@MBilalShafi MBilalShafi added component: data grid This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature feature: Filtering on header Related to the header filtering (Pro) feature and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Mar 4, 2024
@MBilalShafi
Copy link
Member

Hey @vovarudomanenko,

Thanks for reporting the issue, the reason why it's not working is the header filter edit state needs to be imperatively triggered, you can do that by adding an onFocus listener to your input element.

function CustomFilter(props: GridHeaderFilterCellProps) {
+ const apiRef = useGridApiContext();
  return <input 
    placeholder="Search..."
+  onFocus={() => apiRef.current.startHeaderFilterEditMode(props.colDef.field)}
  />;
}

It works on two clicks because the first click is intercepted by the cell itself, you can expose the inputRef to the renderHeaderFilter component and add it to the input (ref={props.inputRef}) to fix that since Grid internally uses this ref to properly provide the focus to the input element. This diff would do that:

diff --git a/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx b/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx
index e2d8d8de8..f11a9c1a7 100644
--- a/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx
+++ b/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx
@@ -30,6 +30,10 @@ import { DataGridProProcessedProps } from '../../models/dataGridProProps';
 import { GridHeaderFilterMenuContainer } from './GridHeaderFilterMenuContainer';
 import { GridHeaderFilterClearButton } from './GridHeaderFilterClearButton';
 
+export interface RenderHeaderFilterProps extends GridHeaderFilterCellProps {
+  inputRef: React.RefObject<unknown>;
+}
+
 export interface GridHeaderFilterCellProps extends Pick<GridStateColDef, 'headerClassName'> {
   colIndex: number;
   height: number;
@@ -133,7 +137,7 @@ const GridHeaderFilterCell = React.forwardRef<HTMLDivElement, GridHeaderFilterCe
 
     let headerFilterComponent: React.ReactNode;
     if (colDef.renderHeaderFilter) {
-      headerFilterComponent = colDef.renderHeaderFilter(props);
+      headerFilterComponent = colDef.renderHeaderFilter({...props, inputRef});
     }
 
     React.useLayoutEffect(() => {
diff --git a/packages/x-data-grid-pro/src/typeOverloads/modules.ts b/packages/x-data-grid-pro/src/typeOverloads/modules.ts
index a2fcde4f2..04d0d4ff0 100644
--- a/packages/x-data-grid-pro/src/typeOverloads/modules.ts
+++ b/packages/x-data-grid-pro/src/typeOverloads/modules.ts
@@ -4,18 +4,19 @@ import type {
   GridRowOrderChangeParams,
   GridFetchRowsParams,
 } from '../models';
-import type { GridHeaderFilterCellProps } from '../components/headerFiltering/GridHeaderFilterCell';
+import type { RenderHeaderFilterProps } from '../components/headerFiltering/GridHeaderFilterCell';
 import type { GridColumnPinningInternalCache } from '../hooks/features/columnPinning/gridColumnPinningInterface';
 import type { GridCanBeReorderedPreProcessingContext } from '../hooks/features/columnReorder/columnReorderInterfaces';
 import { GridRowPinningInternalCache } from '../hooks/features/rowPinning/gridRowPinningInterface';
 
+
 export interface GridColDefPro {
   /**
    * Allows to render a component in the column header filter cell.
-   * @param {GridHeaderFilterCellProps} params Object containing parameters for the renderer.
+   * @param {RenderHeaderFilterProps} params Object containing parameters for the renderer and `inputRef`.
    * @returns {React.ReactNode} The element to be rendered.
    */
-  renderHeaderFilter?: (params: GridHeaderFilterCellProps) => React.ReactNode;
+  renderHeaderFilter?: (params: RenderHeaderFilterProps) => React.ReactNode;
 }
 
 export interface GridControlledStateEventLookupPro {

Feel free to update the PR you have with this diff.

@layerok
Copy link
Contributor Author

layerok commented Mar 5, 2024

Thank you for the response @MBilalShafi.
Passing inputRef to renderHeaderFilter function will definetely help. I've updated PR.

Copy link

⚠️ This issue has been closed.
If you have a similar problem, please open a new issue and provide details about your specific problem.
If you can provide additional information related to this topic that could help future readers, please feel free to leave a comment.

How did we do @vovarudomanenko?
Your experience with our support team matters to us. If you have a moment, please share your thoughts through our brief survey.

@Shudhanshu-Appnox
Copy link

@MBilalShafi I don't think the issue has been resolved as currently I'm facing the same issue when trying to render a Textfield in the renderHeaderFilter. When I tried the solution that you have provided the input only focus on the second click.
Here is the implementation of the TextInput Component.

import { IconButton, Menu, MenuItem, SelectChangeEvent, TextField } from '@mui/material'
import { GridHeaderFilterCellProps, gridFilterModelSelector, useGridApiContext, useGridSelector } from '@mui/x-data-grid-premium';
import FilterListIcon from '@mui/icons-material/FilterList';
import CloseIcon from '@mui/icons-material/Close';
import React, { useState } from 'react'
const ITEM_HEIGHT = 48;
const getDefaultFilter = (field: string) => ({ field, operator: 'contains' });

const ColumnFilerTextInput = (props: GridHeaderFilterCellProps) => {
    const { colDef } = props;
    const apiRef = useGridApiContext();
    const filterModel = useGridSelector(apiRef, gridFilterModelSelector);

    const currentFieldFilters = React.useMemo(
        () => filterModel.items?.filter(({ field }) => field === colDef.field),
        [colDef.field, filterModel.items],
    );

    const [value, setValue] = useState(currentFieldFilters[0]?.value ?? '');

    const handleClear = () => {
        setValue("");
        apiRef.current.deleteFilterItem(currentFieldFilters[0]);
    }
    const handleChange = React.useCallback(
        (event: any) => {
            // event.preventDefault();
            console.log('====>');

            setValue(event.target.value);
            if (!event.target.value) {
                if (currentFieldFilters[0]) {
                    apiRef.current.deleteFilterItem(currentFieldFilters[0]);
                }
                return;
            }
            const list = [...filterModel?.items];
            const index = list?.findIndex((item) => item?.field === currentFieldFilters[0]?.field);
            if (index > -1) {
                list[index] = { ...currentFieldFilters[0], value: event.target.value };
                apiRef.current.upsertFilterItems([...list]);
            } else {
                apiRef.current.upsertFilterItems([...list, {
                    ...(currentFieldFilters[0] || getDefaultFilter(colDef.field)),
                    value: event.target.value,
                }])
            }

        },
        [apiRef, colDef.field, currentFieldFilters],
    );

    const EndEndorseButton = () => {
        const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
        const open = Boolean(anchorEl);
        const handleClick = (event: React.MouseEvent<HTMLElement>) => {
            setAnchorEl(event.currentTarget);
        };

        const handleMenuClose = () => {
            setAnchorEl(null);
        };

        const handleClose = (operator: any) => {
            handleOperatorChange(operator)
            setAnchorEl(null);
        };

        const handleOperatorChange = React.useCallback(
            (operator: any) => {
                console.log(operator, "operator");

                if (!operator) {
                    if (currentFieldFilters[0]) {
                        apiRef.current.deleteFilterItem(currentFieldFilters[0]);
                    }
                    return;
                }
                const list = [...filterModel?.items];
                const index = list?.findIndex((item) => item?.field === currentFieldFilters[0]?.field);
                if (index > -1) {
                    list[index] = { ...currentFieldFilters[0], operator };
                    apiRef.current.upsertFilterItems([...list]);
                } else {
                    apiRef.current.upsertFilterItems([...list, {
                        ...(currentFieldFilters[0] || getDefaultFilter(colDef.field)),
                        operator,
                    }])
                }

            },
            [apiRef, colDef.field, currentFieldFilters],
        );

        return (
            <div>
                <IconButton
                    aria-label="more"
                    id="long-button"
                    aria-controls={open ? 'long-menu' : undefined}
                    aria-expanded={open ? 'true' : undefined}
                    aria-haspopup="true"
                    onClick={handleClick}
                    size='small'
                >
                    <FilterListIcon fontSize='small' sx={{ color: "#475467" }} />
                </IconButton>
                <Menu
                    id="long-menu"
                    MenuListProps={{
                        'aria-labelledby': 'long-button',
                    }}
                    anchorEl={anchorEl}
                    open={open}
                    onClose={handleMenuClose}
                    onChange={handleOperatorChange}
                    PaperProps={{
                        style: {
                            maxHeight: ITEM_HEIGHT * 4.5,
                            width: '15ch',
                        },
                    }}
                >
                    {colDef?.filterOperators?.map((option) => (
                        <MenuItem key={option?.value} selected={props?.item?.operator === option?.value} onClick={() => handleClose(option?.value)}>
                            <p className='capitalize'>{option?.value}</p>
                        </MenuItem>
                    ))}
                </Menu>
            </div>
        )
    };

    return (
        <TextField
            tabIndex={props?.tabIndex}
            onClick={(e) => e.stopPropagation()}
            onChange={handleChange}
            value={value}
            fullWidth
            onFocus={() => apiRef.current.startHeaderFilterEditMode(props.colDef.field)}
            sx={{ "& > div": { borderRadius: "8px", height: 32 }, ":hover": { "& > div": { borderColor: "blue" } } }}
            size='small'
            variant="outlined"
            placeholder='Search'
            InputProps={{
                endAdornment: (
                    <>
                        {value ? <IconButton onClick={handleClear} size='small'><CloseIcon fontSize='small' /></IconButton> : null}
                        <EndEndorseButton />
                    </>

                )
            }}
        />
    )
}

export default React.memo(ColumnFilerTextInput)

@layerok
Copy link
Contributor Author

layerok commented Apr 6, 2024

@Shudhanshu-Appnox you forgot to pass down inputRef to the underlying input element i

const { colDef, inputRef } = props;
//...
<TextField 
  // ...
  InputProps={{
    // ...
    ref: inputRef
  }}
/>

@Shudhanshu-Appnox
Copy link

@vovarudomanenko You have extracted the input ref from the props but its not present in the passed props.

@Shudhanshu-Appnox
Copy link

@vovarudomanenko PFA
Screenshot 2024-04-06 at 8 18 13 PM

@layerok
Copy link
Contributor Author

layerok commented Apr 6, 2024

@Shudhanshu-Appnox It is not present because your props have old type GridHeaderFilterCellProps.
It should be GridRenderHeaderFilterProps. If this type doesn't exist, then ensure you are using the latest version of the data grid

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature feature: Filtering on header Related to the header filtering (Pro) feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants