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} +
+ ); +};