From 367ecec5672866e86099a05899c536ba5de3c5dc Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Wed, 18 Jan 2023 14:16:52 -0500 Subject: [PATCH 1/5] Create a directory for the new EuiInlineEdit component. Created the base component that accepts the EuiText, EuiTextarea, and EuiComboBox controls and provides both read and edit modes. --- src/components/index.ts | 2 + src/components/inline_edit/index.ts | 9 + src/components/inline_edit/inline_edit.tsx | 214 +++++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 src/components/inline_edit/index.ts create mode 100644 src/components/inline_edit/inline_edit.tsx 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..0ca7320b108 --- /dev/null +++ b/src/components/inline_edit/inline_edit.tsx @@ -0,0 +1,214 @@ +/* + * 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; + setTextReadViewValue(input); + setIsInEdit(!isInEdit); + onConfirm && onConfirm(); + }; + + const saveComboBoxEditValue = () => { + // we will need to do a check to see if the array is larger than 0 here. + setComboBoxSelectedOptions(editViewTypeProps['selectedOptions']); + setIsInEdit(!isInEdit); + onConfirm && onConfirm(); + }; + + /* 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} +
+ ); +}; From 40f6e4b0fef4f9fa21f974136f2f81f1156f320f Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Wed, 18 Jan 2023 14:17:43 -0500 Subject: [PATCH 2/5] Created the EuiInlineEdit docs page to interact with the component --- src-docs/src/routes.js | 3 + .../src/views/inline_edit/inline_edit.tsx | 93 +++++++++++++++++++ .../views/inline_edit/inline_edit_example.js | 40 ++++++++ 3 files changed, 136 insertions(+) create mode 100644 src-docs/src/views/inline_edit/inline_edit.tsx create mode 100644 src-docs/src/views/inline_edit/inline_edit_example.js 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..5b14c469ce6 --- /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: 'InlineEdit', + text: ( + <> +

+ Description needed: how to use the EuiInlineEdit{' '} + component. +

+ + ), + source: [ + { + type: GuideSectionTypes.JS, + code: inlineEditSource, + }, + ], + demo: , + props: { EuiInlineEdit }, + }, + ], +}; From 736209314f68db3029d76394708455cde90defb5 Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Wed, 18 Jan 2023 14:25:57 -0500 Subject: [PATCH 3/5] Created a common function that assists with typing within EuiInlineEdit. WithDefaultProps checks to ensure that the editViewTypeProps matches the editViewType being passed in. For exmaple, if the editViewType is EuiFieldText, WithDefaultProps ensure that editViewTypeProps matches the shape of the props for EuiFieldText. --- .../src/views/inline_edit/inline_edit_example.js | 2 +- src/components/common.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src-docs/src/views/inline_edit/inline_edit_example.js b/src-docs/src/views/inline_edit/inline_edit_example.js index 5b14c469ce6..0042ae171da 100644 --- a/src-docs/src/views/inline_edit/inline_edit_example.js +++ b/src-docs/src/views/inline_edit/inline_edit_example.js @@ -18,7 +18,7 @@ export const InlineEditExample = { ), sections: [ { - title: 'InlineEdit', + title: 'Inline edit', text: ( <>

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; From 5b4a0676bc82196b7714e24922ca3ae99ffc31ab Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Wed, 18 Jan 2023 15:33:46 -0500 Subject: [PATCH 4/5] Added in the basic validation for EuiInlineEdit to prevent the form controls from being saved when there is no text or combo box selections. --- src/components/inline_edit/inline_edit.tsx | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/components/inline_edit/inline_edit.tsx b/src/components/inline_edit/inline_edit.tsx index 0ca7320b108..cc38889d147 100644 --- a/src/components/inline_edit/inline_edit.tsx +++ b/src/components/inline_edit/inline_edit.tsx @@ -93,16 +93,27 @@ export const EuiInlineEdit = ({ const input = (document.getElementById( inlineTextEditInputId ) as HTMLInputElement).value; - setTextReadViewValue(input); - setIsInEdit(!isInEdit); - onConfirm && onConfirm(); + + // 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 = () => { - // we will need to do a check to see if the array is larger than 0 here. - setComboBoxSelectedOptions(editViewTypeProps['selectedOptions']); - setIsInEdit(!isInEdit); - onConfirm && onConfirm(); + // 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 { + setIsInEdit(!isInEdit); + } }; /* Shared Elements & Functions (Text & ComboBox) */ From 5c192aa99b391cb89b9304b74331fb717b66014c Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Thu, 19 Jan 2023 15:29:16 -0500 Subject: [PATCH 5/5] Fixed a small bug that caused the ComboBox input bar to appear empty when a save was attempted with no selections --- src/components/inline_edit/inline_edit.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/inline_edit/inline_edit.tsx b/src/components/inline_edit/inline_edit.tsx index cc38889d147..e782a73103e 100644 --- a/src/components/inline_edit/inline_edit.tsx +++ b/src/components/inline_edit/inline_edit.tsx @@ -112,6 +112,7 @@ export const EuiInlineEdit = ({ setIsInEdit(!isInEdit); onConfirm && onConfirm(); } else { + editViewTypeProps['selectedOptions'] = comboBoxSelectedOptions; setIsInEdit(!isInEdit); } }; @@ -173,6 +174,7 @@ export const EuiInlineEdit = ({ <>