`;
-exports[`EuiInlineEditForm Read Mode renders small size 1`] = `
+exports[`EuiInlineEditForm Read Mode sizes 1`] = `
diff --git a/src/components/inline_edit/inline_edit_form.test.tsx b/src/components/inline_edit/inline_edit_form.test.tsx
index 1710f805942..a35f94b8d38 100644
--- a/src/components/inline_edit/inline_edit_form.test.tsx
+++ b/src/components/inline_edit/inline_edit_form.test.tsx
@@ -9,7 +9,7 @@
import React from 'react';
import { render } from '../../test/rtl';
import { requiredProps } from '../../test/required_props';
-import { fireEvent } from '@testing-library/dom';
+import { fireEvent, act, waitFor } from '@testing-library/react';
import {
EuiInlineEditForm,
@@ -36,7 +36,7 @@ describe('EuiInlineEditForm', () => {
expect(container.firstChild).toMatchSnapshot();
});
- it('renders readModeProps onto the button', () => {
+ test('readModeProps', () => {
const { container, getByTestSubject } = render(
{
expect(getByTestSubject('euiInlineReadModeButton')).toBeTruthy();
});
- it('renders small size', () => {
+ test('sizes', () => {
const { container } = render(
{
expect(container.firstChild).toMatchSnapshot();
});
- it('renders editModeProps.inputProps', () => {
+ test('editModeProps.inputProps', () => {
const { container, getByTestSubject } = render(
{
expect(getByTestSubject('customInput')).toBeTruthy();
});
- it('renders editModeProps.formRowProps', () => {
+ test('editModeProps.formRowProps', () => {
const { container, getByTestSubject } = render(
{
expect(getByTestSubject('customErrorText')).toBeTruthy();
});
- it('renders save button and cancel button aria-labels', () => {
+ test('editModeProps.saveButtonProps', () => {
const { container, getByLabelText } = render(
);
expect(container.firstChild).toMatchSnapshot();
expect(getByLabelText("Yes! Let's save.")).toBeTruthy();
- expect(getByLabelText('Uh no. Do not save.')).toBeTruthy();
});
- it('renders EuiSkeletonRectangles in place of editMode buttons when loading', () => {
+ test('editModeProps.cancelButtonProps', () => {
+ const { container, getByLabelText } = render(
+
+ );
+
+ expect(container.firstChild).toMatchSnapshot();
+ expect(getByLabelText('Uh no. Do not save.')).toBeDisabled();
+ });
+
+ test('isLoading', () => {
const { container, queryByTestSubject } = render(
{
);
expect(container.firstChild).toMatchSnapshot();
-
expect(container.querySelectorAll('.euiSkeletonRectangle')).toHaveLength(
2
);
-
expect(queryByTestSubject('euiInlineEditModeSaveButton')).toBeFalsy();
expect(queryByTestSubject('euiInlineEditModeCancelButton')).toBeFalsy();
});
- it('disables the save button when input is invalid ', () => {
+ test('isInvalid', () => {
const { container, getByTestSubject } = render(
{
);
expect(container.firstChild).toMatchSnapshot();
-
expect(
getByTestSubject('euiInlineEditModeInput').hasAttribute('aria-invalid')
).toBeTruthy();
-
- expect(getByTestSubject('euiInlineEditModeSaveButton')).toBeDisabled();
- expect(
- getByTestSubject('euiInlineEditModeCancelButton')
- ).not.toBeDisabled();
});
+ });
- it('returns the latest value within EuiFieldText upon saving', () => {
- const onSaveFunction = jest.fn();
+ describe('Toggling between readMode and editMode', () => {
+ const onClick = jest.fn();
+ const onSave = jest.fn();
+ beforeEach(() => jest.resetAllMocks());
- const { getByTestSubject } = render(
+ it('toggles to editMode when the readModeButton is clicked', () => {
+ const { getByTestSubject, queryByTestSubject } = render(
);
- fireEvent.change(getByTestSubject('euiInlineEditModeInput'), {
- target: { value: 'New message!' },
- });
- fireEvent.click(getByTestSubject('euiInlineEditModeSaveButton'));
-
- expect(onSaveFunction).toHaveBeenCalledWith('New message!');
- });
- });
-
- describe('Toggling between readMode and editMode', () => {
- it('clicking on the readModeButton takes us to editMode', () => {
- const { getByTestSubject, queryByTestSubject } = render(
-
- );
-
fireEvent.click(getByTestSubject('euiInlineReadModeButton'));
expect(getByTestSubject('euiInlineEditModeInput')).toBeTruthy();
expect(queryByTestSubject('euiInlineReadModeButton')).toBeFalsy();
+ expect(onClick).toHaveBeenCalledTimes(1);
});
it('saves text and returns to readMode', () => {
@@ -202,6 +204,8 @@ describe('EuiInlineEditForm', () => {
);
@@ -215,16 +219,17 @@ describe('EuiInlineEditForm', () => {
expect(getByTestSubject('euiInlineReadModeButton')).toBeTruthy();
expect(getByText('New message!')).toBeTruthy();
+ expect(onSave).toHaveBeenCalledWith('New message!');
+ expect(onClick).toHaveBeenCalledTimes(1);
});
it('cancels text and returns to readMode', () => {
- const onSave = jest.fn();
-
const { getByTestSubject, getByText } = render(
);
@@ -239,74 +244,93 @@ describe('EuiInlineEditForm', () => {
expect(getByTestSubject('euiInlineReadModeButton')).toBeTruthy();
expect(getByText('Hello World!')).toBeTruthy();
expect(onSave).not.toHaveBeenCalled();
+ expect(onClick).toHaveBeenCalledTimes(1);
});
- describe('onConfirm behavior on save', () => {
- it('returns to readMode with updated text when onConfirm returns true', () => {
+ describe('onSave validation', () => {
+ it('returns to readMode with updated text when onSave returns true', () => {
+ onSave.mockReturnValueOnce(true);
+
const { getByTestSubject, getByText } = render(
true}
+ onSave={onSave}
/>
);
fireEvent.change(getByTestSubject('euiInlineEditModeInput'), {
target: { value: 'New message!' },
});
- fireEvent.click(getByTestSubject('euiInlineEditModeSaveButton'));
+ act(() => {
+ fireEvent.click(getByTestSubject('euiInlineEditModeSaveButton'));
+ });
expect(getByTestSubject('euiInlineReadModeButton')).toBeTruthy();
expect(getByText('New message!')).toBeTruthy();
});
- it('stays in editMode when onConfirm returns false', () => {
- const onSave = jest.fn();
+ it('stays in editMode when onSave returns false', () => {
+ onSave.mockReturnValueOnce(false);
const { getByTestSubject, queryByTestSubject } = render(
false}
/>
);
fireEvent.change(getByTestSubject('euiInlineEditModeInput'), {
target: { value: 'New message!' },
});
- fireEvent.click(getByTestSubject('euiInlineEditModeSaveButton'));
+ act(() => {
+ fireEvent.click(getByTestSubject('euiInlineEditModeSaveButton'));
+ });
expect(queryByTestSubject('euiInlineReadModeButton')).toBeFalsy();
expect(getByTestSubject('euiInlineEditModeInput')).toBeTruthy();
- expect(onSave).not.toHaveBeenCalled();
});
- it('sends the editMode text to the onConfirm callback', () => {
- const { getByText, getByTestSubject } = render(
+ it('handles async promises', async () => {
+ onSave.mockImplementation(
+ (value) =>
+ new Promise((resolve) => {
+ setTimeout(resolve, 100);
+ return !!value; // returns false if empty string, true if not
+ })
+ );
+
+ const { getByTestSubject, queryByTestSubject, getByText } = render(
{
- return editModeValue === '' ? false : true;
- }}
+ onSave={onSave}
/>
);
+ // Should still be in edit mode after an empty string is submitted
fireEvent.change(getByTestSubject('euiInlineEditModeInput'), {
target: { value: '' },
});
- fireEvent.click(getByTestSubject('euiInlineEditModeSaveButton'));
-
+ await act(async () => {
+ fireEvent.click(getByTestSubject('euiInlineEditModeSaveButton'));
+ waitFor(() => setTimeout(() => {}, 100)); // Let the promise finish resolving
+ });
+ expect(queryByTestSubject('euiInlineReadModeButton')).toBeFalsy();
expect(getByTestSubject('euiInlineEditModeInput')).toBeTruthy();
+ // Should successfully save into read mode
fireEvent.change(getByTestSubject('euiInlineEditModeInput'), {
target: { value: 'hey there' },
});
- fireEvent.click(getByTestSubject('euiInlineEditModeSaveButton'));
-
- expect(getByTestSubject('euiInlineReadModeButton')).toBeTruthy();
- expect(getByText('hey there')).toBeTruthy();
+ await act(async () => {
+ fireEvent.click(getByTestSubject('euiInlineEditModeSaveButton'));
+ });
+ waitFor(() => {
+ expect(getByTestSubject('euiInlineReadModeButton')).toBeTruthy();
+ expect(getByText('hey there')).toBeTruthy();
+ });
});
});
});
diff --git a/src/components/inline_edit/inline_edit_form.tsx b/src/components/inline_edit/inline_edit_form.tsx
index 23b68582707..4bc3a88d007 100644
--- a/src/components/inline_edit/inline_edit_form.tsx
+++ b/src/components/inline_edit/inline_edit_form.tsx
@@ -11,6 +11,7 @@ import React, {
FunctionComponent,
useState,
HTMLAttributes,
+ MouseEvent,
} from 'react';
import classNames from 'classnames';
@@ -23,7 +24,8 @@ import {
EuiFieldTextProps,
} from '../form';
import { euiFormVariables } from '../form/form.styles';
-import { EuiButtonIcon, EuiButtonEmpty, EuiButtonEmptyProps } from '../button';
+import { EuiButtonIcon, EuiButtonEmpty } from '../button';
+import { EuiButtonIconPropsForButton } from '../button/button_icon';
import { EuiButtonEmptyPropsForButton } from '../button/button_empty/button_empty';
import { EuiFlexGroup, EuiFlexItem } from '../flex';
import { EuiSkeletonRectangle } from '../skeleton';
@@ -36,49 +38,45 @@ export type EuiInlineEditCommonProps = HTMLAttributes &
CommonProps & {
defaultValue: string;
/**
- * Callback that passes the updated value of the edited text when the save button is pressed,
- * and the `onConfirm` callback (if passed) returns true
+ * Callback that fires when a user clicks the save button.
+ * Passes the current edited text value as an argument.
+ *
+ * To validate the value of the edited text, pass back a boolean flag.
+ * If `false`, EuiInlineEdit will remain in edit mode, where loading or invalid states can be set.
+ * If `true`, EuiInlineEdit will return to read mode.
*/
- onSave?: (onSaveValue: string) => void;
+ onSave?: (value: string) => void | boolean | Promise;
/**
- * Callback that fires when users click the save button, but before the text actually saves. Passes the current edited
- * text value as an argument.
- */
- onConfirm?: (editModeValue: string) => boolean;
- /**
- * Form label that appears above the form control
- * This is required for accessibility because there is no visual label on the input
+ * Form label that appears above the form control.
+ * This is required for accessibility because there is no visual label on the input.
*/
inputAriaLabel: string;
/**
- * Aria-label for save button in editMode
- */
- saveButtonAriaLabel?: string;
- /**
- * Aria-label for cancel button in editMode
- */
- cancelButtonAriaLabel?: string;
- /**
- * Start in editMode
+ * Starts the component in edit mode
*/
startWithEditOpen?: boolean;
/**
- * Props that will be applied directly to the EuiEmptyButton displayed in readMode
+ * Props that will be applied directly to the `EuiEmptyButton` displayed in read mode
*/
- readModeProps?: Omit;
+ readModeProps?: Partial;
/**
- * Props that will be applied directly to the `EuiFormRow` and `EuiFieldText` input displayed in editMode
+ * Multiple props objects that can be applied directly to various child components displayed in edit mode.
+ * - `formRowProps` will be passed to `EuiFormRow`
+ * - `inputProps` will be passed to `EuiFieldText`
+ * - `saveButtonProps` & `cancelButtonProps` will be passed to their respective `EuiIconButton`s
*/
editModeProps?: {
formRowProps?: Partial;
inputProps?: Partial;
+ saveButtonProps?: Partial;
+ cancelButtonProps?: Partial;
};
/**
- * Loading state when changes are saved in editMode
+ * Loading state - only displayed in edit mode
*/
isLoading?: boolean;
/**
- * Validation for the form control used to edit text in editMode
+ * Invalid state - only displayed edit mode
*/
isInvalid?: boolean;
};
@@ -90,8 +88,8 @@ export type EuiInlineEditFormProps = EuiInlineEditCommonProps & {
*/
sizes: {
compressed: boolean;
- buttonSize: EuiButtonEmptyProps['size'];
- iconSize: EuiButtonEmptyProps['iconSize'];
+ buttonSize: EuiButtonEmptyPropsForButton['size'];
+ iconSize: EuiButtonEmptyPropsForButton['iconSize'];
};
/**
* Render prop that returns the read mode value as an arg
@@ -116,10 +114,7 @@ export const EuiInlineEditForm: FunctionComponent = ({
children,
sizes,
defaultValue,
- onConfirm,
inputAriaLabel,
- saveButtonAriaLabel,
- cancelButtonAriaLabel,
startWithEditOpen,
readModeProps,
editModeProps,
@@ -152,18 +147,20 @@ export const EuiInlineEditForm: FunctionComponent = ({
const cancelInlineEdit = () => {
setEditModeValue(readModeValue);
- setIsEditing(!isEditing);
+ setIsEditing(false);
};
- const saveInlineEditValue = () => {
- if (onConfirm && !onConfirm(editModeValue)) {
- // If an onConfirm method is present, and it has returned false, cancel the action
- return;
- } else {
- setReadModeValue(editModeValue);
- setIsEditing(!isEditing);
- onSave?.(editModeValue);
+ const saveInlineEditValue = async () => {
+ // If an onSave callback is present, and returns false, stay in edit mode
+ if (onSave) {
+ const onSaveReturn = onSave(editModeValue);
+ const awaitedReturn =
+ onSaveReturn instanceof Promise ? await onSaveReturn : onSaveReturn;
+ if (awaitedReturn === false) return;
}
+
+ setReadModeValue(editModeValue);
+ setIsEditing(false);
};
const editModeForm = (
@@ -202,14 +199,17 @@ export const EuiInlineEditForm: FunctionComponent = ({
>
) => {
+ saveInlineEditValue();
+ editModeProps?.saveButtonProps?.onClick?.(e);
+ }}
/>
@@ -225,15 +225,17 @@ export const EuiInlineEditForm: FunctionComponent = ({
>
) => {
+ cancelInlineEdit();
+ editModeProps?.cancelButtonProps?.onClick?.(e);
+ }}
/>
@@ -251,11 +253,12 @@ export const EuiInlineEditForm: FunctionComponent = ({
flush="both"
iconSize={sizes.iconSize}
size={sizes.buttonSize}
- onClick={() => {
- setIsEditing(!isEditing);
- }}
data-test-subj="euiInlineReadModeButton"
{...readModeProps}
+ onClick={(e) => {
+ setIsEditing(true);
+ readModeProps?.onClick?.(e);
+ }}
>
{children(readModeValue)}
diff --git a/src/components/inline_edit/inline_edit_text.tsx b/src/components/inline_edit/inline_edit_text.tsx
index 5a9451dba73..49921fd6d83 100644
--- a/src/components/inline_edit/inline_edit_text.tsx
+++ b/src/components/inline_edit/inline_edit_text.tsx
@@ -32,15 +32,12 @@ export const EuiInlineEditText: FunctionComponent = ({
className,
size = 'm',
defaultValue,
- onConfirm,
inputAriaLabel,
- saveButtonAriaLabel,
- cancelButtonAriaLabel,
startWithEditOpen,
readModeProps,
editModeProps,
isLoading,
- isInvalid = false,
+ isInvalid,
...rest
}) => {
const classes = classNames('euiInlineEditText', className);
@@ -55,10 +52,7 @@ export const EuiInlineEditText: FunctionComponent = ({
const formProps = {
sizes,
defaultValue,
- onConfirm,
inputAriaLabel,
- saveButtonAriaLabel,
- cancelButtonAriaLabel,
startWithEditOpen,
readModeProps,
editModeProps,
diff --git a/src/components/inline_edit/inline_edit_title.tsx b/src/components/inline_edit/inline_edit_title.tsx
index be663fa1c61..04de2ae2056 100644
--- a/src/components/inline_edit/inline_edit_title.tsx
+++ b/src/components/inline_edit/inline_edit_title.tsx
@@ -38,15 +38,12 @@ export const EuiInlineEditTitle: FunctionComponent = ({
size = 'm',
heading,
defaultValue,
- onConfirm,
inputAriaLabel,
- saveButtonAriaLabel,
- cancelButtonAriaLabel,
- startWithEditOpen = false,
+ startWithEditOpen,
readModeProps,
editModeProps,
isLoading,
- isInvalid = false,
+ isInvalid,
...rest
}) => {
const classes = classNames('euiInlineEditTitle', className);
@@ -63,10 +60,7 @@ export const EuiInlineEditTitle: FunctionComponent = ({
const formProps = {
sizes,
defaultValue,
- onConfirm,
inputAriaLabel,
- saveButtonAriaLabel,
- cancelButtonAriaLabel,
startWithEditOpen,
readModeProps,
editModeProps,