-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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] Add support for edit components that use portal #1772
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't seem to be working, the edit mode is not excited when clicking another cell: https://deploy-preview-1772--material-ui-x.netlify.app/components/data-grid/editing/#cell-editing
const cellCommitParams = apiRef.current.getEditCellPropsParams(params.id, params.field); | ||
if (!cellCommitParams.props.error) { | ||
// We commit the change when there are no error | ||
apiRef.current.publishEvent(GRID_CELL_EDIT_PROPS_CHANGE_COMMITTED, cellCommitParams); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't pass an event because publishEvent
requires a SyntheticEvent
(because of the isPropagationStopped
) and I only have a native event. That's the reason for the BC label.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we make this generic? Otherwise, there would be portal fixes in a lot of features, and each hook will attach and detach events...
If we add an event listener on the doc, and route it to publish some kind of CLICK_OUTSIDE_ROOT
And isolate that into a usePortal hook, it would be really nice.
Then each feature, can hook themselves to some kind of click on portal event...
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The click we're interested may occur inside the root as well. What we need to check is if it happened outside the cell that is currently being edited. Since it's a very specific situation, I don't think we need to abstract it. This new event would have to be named like GRID_EDIT_CELL_CLICK_OUTSIDE
and it would be attached only to the edit cell, which is the same thing I doing here but without exposing an event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if we add a renderCell that use portals? Ie a select component in the grid view mode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if we add a renderCell that use portals? Ie a select component in the grid view mode
We wouldn't have problems because clicking outside the cell in view mode doesn't do anything besides moving the focus.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unless there is a select dropdown within it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a CodeSandbox with a Select in renderCell
: https://codesandbox.io/s/material-demo-forked-kc7id?file=/demo.js
We only have a bug if a portaled component is used in renderEditCell
.
@@ -84,13 +101,18 @@ export function useGridEditRows(apiRef: GridApiRef) { | |||
api: apiRef.current, | |||
}); | |||
|
|||
const doc = ownerDocument(apiRef.current.rootElementRef!.current as HTMLElement); | |||
if (mode === 'view') { | |||
doc.removeEventListener('click', handleDocumentClick); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also attach the listener here when changing to the edit mode but by doing that it broke the regression tests. The click done to switch between tests is incorrectly caught as a click to exit from the edit mode.
); | ||
const handleCellMouseUp = React.useCallback((params: GridCellParams, event: React.MouseEvent) => { | ||
if (event.isPropagationStopped() || params.cellMode === 'edit') { | ||
insideCell.current = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is the magic. If a mouseup
came from the cell, then the next click should be treated as it was inside the cell too.
810f288
to
1404a43
Compare
packages/grid/_modules_/grid/hooks/features/keyboard/useGridKeyboard.ts
Outdated
Show resolved
Hide resolved
733b18f
to
97a028f
Compare
I changed the way I detect if the click is outside the cell. For that, I created a new |
@m4theushw Awesome! I have been waiting for this for some time. Can we expect this to go live and be available in coming few days? |
// See https://github.com/mui-org/material-ui/issues/10534 | ||
if ( | ||
(event.target as any).nodeType === 1 && | ||
!event.currentTarget.contains(event.target as Element) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we wrap that into a function such as isTargetWithinElement
or isTargetFromSubTree
or ?
Make the code more readable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will trigger the discussion about when to create helpers or not. 😂 I would prefer to keep it as it is because it's only used in two places now. But if we start using more I will definitely create a helper for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That logic is pretty complex so I would definitely abstract it.
Normally you can use a rule of 3, here it's repeated twice already, + complexity and readability benefits => clean code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If @oliviertassinari and @DanailH agree we can move this to a helper. I'm against it for the following reasons:
- This conditional is only used in two places
- We had past discussions about helpers and we didn't reach an agreement
- 100% of times I see a helper I go to its definition to see what it does, because I don't trust the name
- It makes the code cleaner but the developer has to navigate to more files to understand
- By the name of the helper I don't know if
currentTarget
ortarget
is used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that particular case, I'm more in favour of keeping it like it is. If you name the helper isTargetWithinElement
I would still need to check its implementation to understand what it does. I'm ok with helpers like isSpaceKey
for example as it is clear what the helper does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm all 💯 for https://github.com/mui-org/material-ui-x/pull/1772/files#r649207549. Some of the arguments are biased, some are not, but the arguments resonate with me. I also never trust a module I don't know well (nor I think anyone should ever, curiosity is important), having to check the content of small helpers is mentally draining. Two repetitions are not a pattern.
// See https://github.com/mui-org/material-ui/issues/10534 | ||
if ( | ||
(event.target as any).nodeType === 1 && | ||
!event.currentTarget.contains(event.target as Element) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reuse it here, remove comments and put it in the helper function
if (!isGridCellRoot(event.target as HTMLElement)) { | ||
// The target is not an element when triggered by a Select inside the cell | ||
// See https://github.com/mui-org/material-ui/issues/10534 | ||
if ((event.target as any).nodeType === 1 && !isGridCellRoot(event.target as Element)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
@@ -28,6 +32,7 @@ export const useGridFocus = (apiRef: GridApiRef): void => { | |||
focus: { cell: { id, field }, columnHeader: null }, | |||
}; | |||
}); | |||
// TODO replace with constant | |||
apiRef.current.publishEvent('cellFocusChange'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we have that event already defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's only a GRID_CELL_FOCUS
but it's mapped to the focus
event.
return; | ||
} | ||
|
||
if (params.id === cell.id && params.field === cell.field) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have a mismatch with the cell if we don't check both the id
and the field
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, because this handler is called when any cell is clicked. I just want insideFocusedCell.current
to be true when the currently focused cell is clicked.
const isInsideFocusedCell = insideFocusedCell.current; | ||
insideFocusedCell.current = false; | ||
|
||
const { cell } = apiRef.current.getState().focus; | ||
if (!cell || isInsideFocusedCell) { | ||
return; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this work?
const isInsideFocusedCell = insideFocusedCell.current; | |
insideFocusedCell.current = false; | |
const { cell } = apiRef.current.getState().focus; | |
if (!cell || isInsideFocusedCell) { | |
return; | |
} | |
const { cell } = apiRef.current.getState().focus; | |
if (!cell || insideFocusedCell.current) { | |
return; | |
} | |
insideFocusedCell.current = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a problem with setting insideFocusedCell.current
to false after checking if there's a cell focused. If there's no cell focused it will stay true, which can prevent the next click from working properly. I borrowed this logic from ClickAwayListener:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This demo behaves differently https://deploy-preview-1772--material-ui-x.netlify.app/components/data-grid/editing/#edit-using-external-button. Clicking twice on a cell exit the edit mode
// See https://github.com/mui-org/material-ui/issues/10534 | ||
if ( | ||
(event.target as any).nodeType === 1 && | ||
!event.currentTarget.contains(event.target as Element) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm all 💯 for https://github.com/mui-org/material-ui-x/pull/1772/files#r649207549. Some of the arguments are biased, some are not, but the arguments resonate with me. I also never trust a module I don't know well (nor I think anyone should ever, curiosity is important), having to check the content of small helpers is mentally draining. Two repetitions are not a pattern.
packages/grid/_modules_/grid/hooks/features/rows/useGridEditRows.ts
Outdated
Show resolved
Hide resolved
} | ||
|
||
if (params.id === cell.id && params.field === cell.field) { | ||
insideFocusedCell.current = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you copy the solution I was proposing for fixing ClickAwayListener with the Select? 😁
97370f8
to
a6df5e6
Compare
@m4theushw On https://deploy-preview-1772--material-ui-x.netlify.app/components/data-grid/editing/#edit-using-external-button. We were previously able to turn an unlimited amount of cells in edit mode. It's no longer the case. The first cell exits as soon as we click another. |
#1772 (comment) Ok, here we go: #1403, point 2. The click event is coming from the document, not managed by React. I guess this resolves the discussion once for all, |
@oliviertassinari We can't edit multiple cells at the same time anymore because only one cell can have focus. |
Co-authored-by: Olivier Tassinari <olivier.tassinari@gmail.com>
I have fixed the regression on https://deploy-preview-1772--material-ui-x.netlify.app/components/data-grid/editing/#edit-using-external-button and added a regression test case. The fix is relatively simple but there is quite a lot of boilerplate that makes reading the diff a bit more challenging. I guess it's the cost of scaling with an event system, so fair enough.
|
@oliviertassinari Looks good to me. Some points we could bring to #1824:
|
Why? My assumption was that for row edit, we need to have all the cells of the row in edit mode. It was designed with this constraint in mind so far (I believe). |
docs/src/pages/components/data-grid/editing/StartEditButtonGrid.tsx
Outdated
Show resolved
Hide resolved
@@ -31,6 +31,10 @@ import { GridColumnResizeParams } from './params/gridColumnResizeParams'; | |||
import { GridColumnVisibilityChangeParams } from './params/gridColumnVisibilityChangeParams'; | |||
import { GridClasses } from './gridClasses'; | |||
|
|||
export type MuiEvent<E> = (E | React.SyntheticEvent) & { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dtassone I added the E
type variable so we can reuse it with different native events if necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, but why not do an interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interfaces can't extend union types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because you shouldn't do a union type...
I would have done
interface MuiEvent<E> extends React.SyntheticEvent<E> {
...
}
But while typing, I noticed your E | SyntheticEvent
. E is any, but yeah it could work anywhere 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but this event when triggered by the document
is a native event. The union needs to be there.
@oliviertassinari Probably we'll have problems implementing the row editing, because clicking outside a cell triggers |
@m4theushw True, but I wouldn't worry too much about this. It was also the case when relying on the native blur event. In the core, there are a couple of places where we solve this by waiting with a setTimeout() to see if there isn't another focus event, before handling the blur. Great work! |
@Troy96 Yes, it will be included in the next release which might be this week or next. |
Breaking changes
onEditCellChangeCommitted
prop won't called with an event when committing changes by clicking outside the cell.Fixes #1439
Due to the fact that we relay on the
blur
event to detect if the user clicked outside the cell, we need to ignore when the focus changes from the root cell element to one of its children. With components that use a portal, this logic doesn't work because there're in a different part of the DOM. This PR changes this behavior to listen for allclick
events in the document. To filter out events that were inside the cell, we check if before the click amouseup
event was fired. If it does, then the click was inside the cell or inside a portaled element and we shouldn't commit the changes yet. This is the same logic that ClickAwayListener uses. The reason to usemouseup
instead ofclick
is to be able to use the Select component.https://deploy-preview-1772--material-ui-x.netlify.app/components/data-grid/editing/
https://codesandbox.io/s/material-demo-forked-tprys?file=/demo.js