diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js
index d74e4cfb91a..cd059d09dbc 100644
--- a/src-docs/src/routes.js
+++ b/src-docs/src/routes.js
@@ -149,6 +149,8 @@ import { IconExample } from './views/icon/icon_example';
import { ImageExample } from './views/image/image_example';
+import { InlineEditExample } from './views/inline_edit/inline_edit_example';
+
import { InnerTextExample } from './views/inner_text/inner_text_example';
import { KeyPadMenuExample } from './views/key_pad_menu/key_pad_menu_example';
@@ -557,6 +559,7 @@ const navigation = [
HealthExample,
IconExample,
ImageExample,
+ InlineEditExample,
ListGroupExample,
LoadingExample,
NotificationEventExample,
diff --git a/src-docs/src/views/inline_edit/inline_edit.tsx b/src-docs/src/views/inline_edit/inline_edit.tsx
new file mode 100644
index 00000000000..b26cf525ac2
--- /dev/null
+++ b/src-docs/src/views/inline_edit/inline_edit.tsx
@@ -0,0 +1,93 @@
+import React, { useState } from 'react';
+
+import {
+ EuiInlineEdit,
+ EuiFieldText,
+ EuiComboBox,
+ EuiTextArea,
+ EuiComboBoxProps,
+ EuiHorizontalRule,
+ EuiSpacer,
+} from '../../../../src/components';
+
+const optionsStatic = [
+ {
+ label: 'Titan',
+ 'data-test-subj': 'titanOption',
+ },
+ {
+ label: 'Enceladus is disabled',
+ disabled: true,
+ },
+ {
+ label: 'Mimas',
+ },
+ {
+ label: 'Dione',
+ },
+ {
+ label: 'Iapetus',
+ },
+ {
+ label: 'Phoebe',
+ },
+ {
+ label: 'Rhea',
+ },
+ {
+ label:
+ "Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
+ },
+ {
+ label: 'Tethys',
+ },
+ {
+ label: 'Hyperion',
+ },
+];
+
+export default () => {
+ const [options, setOptions] = useState(optionsStatic);
+ const [selectedOptions, setSelected] = useState([options[2], options[4]]);
+
+ const onChange = (selectedOptions: any) => {
+ setSelected(selectedOptions);
+ };
+
+ return (
+ <>
+ {/* Base components */}
+
EuiInlineEdit - Text
+
+
+
+ EuiInlineEdit - Textarea
+
+
+
+ EuiInlineEdit - Select
+
+
+ >
+ );
+};
diff --git a/src-docs/src/views/inline_edit/inline_edit_example.js b/src-docs/src/views/inline_edit/inline_edit_example.js
new file mode 100644
index 00000000000..0042ae171da
--- /dev/null
+++ b/src-docs/src/views/inline_edit/inline_edit_example.js
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import { GuideSectionTypes } from '../../components';
+
+import { EuiText, EuiInlineEdit } from '../../../../src';
+
+import InlineEdit from './inline_edit';
+const inlineEditSource = require('!!raw-loader!./inline_edit');
+
+export const InlineEditExample = {
+ title: 'Inline edit',
+ intro: (
+ <>
+
+ Hello! This is where the EuiInlineEdit documentation intro will go!
+
+ >
+ ),
+ sections: [
+ {
+ title: 'Inline edit',
+ text: (
+ <>
+
+ Description needed: how to use the EuiInlineEdit{' '}
+ component.
+
+ >
+ ),
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: inlineEditSource,
+ },
+ ],
+ demo: ,
+ props: { EuiInlineEdit },
+ },
+ ],
+};
diff --git a/src/components/common.ts b/src/components/common.ts
index 8e857e7e4c6..becbf4f4667 100644
--- a/src/components/common.ts
+++ b/src/components/common.ts
@@ -246,3 +246,18 @@ export type RecursivePartial = {
: RecursivePartial; // recurse for all non-array and non-primitive values
};
type NonAny = number | boolean | string | symbol | null;
+
+// `Defaultize` copied out of @types/react
+type Defaultize = P extends any
+ ? string extends keyof P
+ ? P
+ : Pick
> &
+ Partial>> &
+ Partial>>
+ : never;
+
+export type WithDefaultPropsApplied<
+ T extends keyof JSX.IntrinsicElements | JSXElementConstructor
+> = T extends { defaultProps: infer D }
+ ? Defaultize, D>
+ : ComponentProps;
diff --git a/src/components/index.ts b/src/components/index.ts
index cf1db2d2757..ff44efb68ed 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -89,6 +89,8 @@ export * from './icon';
export * from './image';
+export * from './inline_edit';
+
export * from './inner_text';
export * from './i18n';
diff --git a/src/components/inline_edit/index.ts b/src/components/inline_edit/index.ts
new file mode 100644
index 00000000000..4d6abf01576
--- /dev/null
+++ b/src/components/inline_edit/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { EuiInlineEdit } from './inline_edit';
diff --git a/src/components/inline_edit/inline_edit.tsx b/src/components/inline_edit/inline_edit.tsx
new file mode 100644
index 00000000000..e782a73103e
--- /dev/null
+++ b/src/components/inline_edit/inline_edit.tsx
@@ -0,0 +1,227 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { HTMLAttributes, useState } from 'react';
+import { CommonProps, WithDefaultPropsApplied } from '../common';
+import classNames from 'classnames';
+
+import { EuiButtonEmpty, EuiButtonIcon } from '../button';
+import { EuiFieldText, EuiTextArea, EuiFormRow } from '../form';
+import { EuiComboBox, EuiComboBoxOptionOption } from '../combo_box';
+import { EuiBadge } from '../badge';
+
+import { htmlIdGenerator } from '../../services/accessibility';
+
+type AcceptableComponents =
+ | typeof EuiTextArea
+ | typeof EuiFieldText
+ | typeof EuiComboBox;
+
+export type EuiInlineEditProps = HTMLAttributes<
+ HTMLDivElement
+> &
+ CommonProps & {
+ /**
+ * The type of form control that will be displayed when EuiInlineEdit
+ * is in editView
+ * @default text
+ */
+ editViewType?: T;
+ /**
+ * Props for the form control created by editViewType
+ */
+ editViewTypeProps?: WithDefaultPropsApplied;
+ /**
+ * Default string value for input in readView
+ */
+ defaultValue?: string;
+ /**
+ * Allow users to pass in a function when the confirm button is clicked
+ *
+ */
+ onConfirm?: () => void;
+ confirmButtonAriaLabel?: string;
+ cancelButtonAriaLabel?: string;
+ /**
+ * Start in editView
+ */
+ startWithEditOpen?: boolean;
+ /**
+ * Form label that appears above the form control
+ */
+ label?: String;
+ };
+
+export const EuiInlineEdit = ({
+ children,
+ className,
+ //@ts-ignore TypeScript sad :(
+ editViewType = EuiFieldText,
+ editViewTypeProps,
+ defaultValue = 'Click me to edit',
+ onConfirm,
+ confirmButtonAriaLabel,
+ cancelButtonAriaLabel,
+ startWithEditOpen,
+ label,
+ ...rest
+}: EuiInlineEditProps) => {
+ const classes = classNames('euiEuiInlineEdit', className);
+
+ const EditViewType = editViewType;
+ const [isInEdit, setIsInEdit] = useState(startWithEditOpen);
+ const inlineTextEditInputId = htmlIdGenerator('__inlineEditInput')();
+
+ /* Text Controls */
+ const [textEditViewValue, setTextEditViewValue] = useState(
+ defaultValue || ''
+ );
+ const [textReadViewValue, setTextReadViewValue] = useState(defaultValue);
+
+ /* ComboBox Control */
+ const [comboBoxSelectedOptions, setComboBoxSelectedOptions] = useState(
+ editViewTypeProps['selectedOptions'] || []
+ );
+
+ /* onConfirm / Save Functions */
+ const saveTextEditValue = () => {
+ const input = (document.getElementById(
+ inlineTextEditInputId
+ ) as HTMLInputElement).value;
+
+ // If there's no text, cancel the action, reset the input text, and return to readView
+ if (input) {
+ setTextReadViewValue(input);
+ setIsInEdit(!isInEdit);
+ onConfirm && onConfirm();
+ } else {
+ setTextEditViewValue(textReadViewValue);
+ setIsInEdit(!isInEdit);
+ }
+ };
+
+ const saveComboBoxEditValue = () => {
+ // If there are no selections, but the user tries to save, cancel the action and return to readView
+ if (editViewTypeProps['selectedOptions'].length !== 0) {
+ setComboBoxSelectedOptions(editViewTypeProps['selectedOptions']);
+ setIsInEdit(!isInEdit);
+ onConfirm && onConfirm();
+ } else {
+ editViewTypeProps['selectedOptions'] = comboBoxSelectedOptions;
+ setIsInEdit(!isInEdit);
+ }
+ };
+
+ /* Shared Elements & Functions (Text & ComboBox) */
+ const editViewButtons = (
+ <>
+
+ {
+ setIsInEdit(!isInEdit);
+ }}
+ />
+ >
+ );
+
+ /* Text Elements & Functions (Textarea and FieldText) */
+ const editTextViewOnChange = (e: any) => {
+ setTextEditViewValue(e.target.value);
+ };
+
+ const textEditViewElement = (
+ <>
+
+
+ {editViewButtons}
+ >
+ );
+
+ const textReadViewElement = (
+ {
+ setIsInEdit(!isInEdit);
+ }}
+ >
+ {textReadViewValue}
+
+ );
+
+ /* ComboBox Elements & Functions */
+
+ const comboBoxEditViewElement = (
+ <>
+
+
+ {editViewButtons}
+ >
+ );
+
+ const comboBoxReadViewElement = (
+ {
+ setIsInEdit(!isInEdit);
+ }}
+ >
+ {comboBoxSelectedOptions.map(
+ (option: EuiComboBoxOptionOption, index: number) => {
+ return (
+
+ {option.label}
+
+ );
+ }
+ )}
+
+ );
+
+ /* Current Form Control in View */
+ const currentFormControlInView =
+ EditViewType === EuiComboBox ? (
+
+ {isInEdit ? comboBoxEditViewElement : comboBoxReadViewElement}
+
+ ) : (
+
+ {isInEdit ? textEditViewElement : textReadViewElement}
+
+ );
+
+ return (
+
+ {currentFormControlInView}
+
+ );
+};