diff --git a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.spec.tsx b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.spec.tsx index b9bc471acdc..0a27fb6c480 100644 --- a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.spec.tsx +++ b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.spec.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; -import { Basic } from './InPlaceEditor.stories'; +import { Basic, WithSiblingControl } from './InPlaceEditor.stories'; describe('InPlaceEditor', () => { it('should render the field value on mount', async () => { @@ -54,4 +54,37 @@ describe('InPlaceEditor', () => { await screen.findByLabelText('Cancel'); }); }); + describe('blur', () => { + it('should save when focus moves to a control outside the editor', async () => { + render(); + const value = await screen.findByText('John Doe'); + value.click(); + const input = await screen.findByDisplayValue('John Doe'); + fireEvent.change(input, { target: { value: 'Jane Doe' } }); + const nextButton = screen.getByRole('button', { name: 'Next' }); + fireEvent.blur(input, { relatedTarget: nextButton }); + await screen.findByText('Jane Doe'); + }); + it('should cancel when focus moves to a control outside the editor and cancelOnBlur is set', async () => { + render(); + const value = await screen.findByText('John Doe'); + value.click(); + const input = await screen.findByDisplayValue('John Doe'); + fireEvent.change(input, { target: { value: 'Jane Doe' } }); + const nextButton = screen.getByRole('button', { name: 'Next' }); + fireEvent.blur(input, { relatedTarget: nextButton }); + await screen.findByText('John Doe'); + expect(screen.queryByDisplayValue('Jane Doe')).toBeNull(); + }); + it('should keep editing when focus moves to a button inside the editor', async () => { + render(); + const value = await screen.findByText('John Doe'); + value.click(); + const input = await screen.findByDisplayValue('John Doe'); + fireEvent.change(input, { target: { value: 'Jane Doe' } }); + const saveButton = screen.getByLabelText('Save'); + fireEvent.blur(input, { relatedTarget: saveButton }); + expect(screen.getByDisplayValue('Jane Doe')).not.toBeNull(); + }); + }); }); diff --git a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.stories.tsx b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.stories.tsx index 19ad352c879..ff2b420fb28 100644 --- a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.stories.tsx +++ b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.stories.tsx @@ -175,6 +175,21 @@ export const CancelOnBlur = () => ( ); +export const WithSiblingControl = ({ + cancelOnBlur, +}: { + cancelOnBlur?: boolean; +}) => ( + + + + +); + export const MutationMode = () => ( diff --git a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.tsx b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.tsx index d8756b61b12..4138e15c2a4 100644 --- a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.tsx +++ b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.tsx @@ -196,7 +196,13 @@ export const InPlaceEditor = < }; const handleBlur = (event: React.FocusEvent) => { - if (event.relatedTarget) { + // Keep editing when focus moves to another element inside the editor + // (e.g. the Save and Cancel buttons). Only save or cancel when focus + // leaves the editor entirely. + if ( + event.relatedTarget && + event.currentTarget.contains(event.relatedTarget as Node) + ) { return; } if (cancelOnBlur) {