diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index acf9ce0393c..122ddea5b0a 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -151,6 +151,8 @@ import { ImageExample } from './views/image/image_example'; import { InnerTextExample } from './views/inner_text/inner_text_example'; +import { InlineEditExample } from './views/inline_edit/inline_edit_example'; + import { KeyPadMenuExample } from './views/key_pad_menu/key_pad_menu_example'; import { LinkExample } from './views/link/link_example'; @@ -561,6 +563,7 @@ const navigation = [ HealthExample, IconExample, ImageExample, + InlineEditExample, ListGroupExample, LoadingExample, NotificationEventExample, diff --git a/src-docs/src/views/inline_edit/inline_edit_confirm.tsx b/src-docs/src/views/inline_edit/inline_edit_confirm.tsx new file mode 100644 index 00000000000..2443db20b1e --- /dev/null +++ b/src-docs/src/views/inline_edit/inline_edit_confirm.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { EuiInlineEditText } from '../../../../src'; + +export default () => { + // TO DO: Convert this example to use something like a modal + const confirmInlineEditChanges = () => { + // eslint-disable-next-line no-restricted-globals + const flag = confirm('Are you sure you want to save?') ? true : false; + return flag; + }; + + return ( + <> + + + ); +}; 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..41587e0f19b --- /dev/null +++ b/src-docs/src/views/inline_edit/inline_edit_example.js @@ -0,0 +1,86 @@ +import React from 'react'; + +import { GuideSectionTypes } from '../../components'; + +import { + EuiText, + EuiInlineEditText, + EuiInlineEditTitle, +} from '../../../../src'; + +import InlineEditText from './inline_edit_text'; +const inlineEditTextSource = require('!!raw-loader!./inline_edit_text'); + +import InlineEditTitle from './inline_edit_title'; +const inlineEditTitleSource = require('!!raw-loader!./inline_edit_title'); + +import InlineEditConfirm from './inline_edit_confirm'; +const inlineEditConfirmSource = require('!!raw-loader!././inline_edit_confirm'); + +export const InlineEditExample = { + title: 'Inline edit', + intro: ( + <> + This is where the description will go + + ), + sections: [ + { + title: 'InlineEditText', + text: ( + <> +

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

+ + ), + source: [ + { + type: GuideSectionTypes.JS, + code: inlineEditTextSource, + }, + ], + demo: , + props: { EuiInlineEditText }, + }, + { + title: 'InlineEditTitle', + text: ( + <> +

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

+ + ), + source: [ + { + type: GuideSectionTypes.JS, + code: inlineEditTitleSource, + }, + ], + demo: , + props: { EuiInlineEditTitle }, + }, + { + title: 'Confirm inline edit', + text: ( + <> +

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

+ + ), + source: [ + { + type: GuideSectionTypes.JS, + code: inlineEditConfirmSource, + }, + ], + demo: , + props: { EuiInlineEditText }, + }, + ], +}; diff --git a/src-docs/src/views/inline_edit/inline_edit_text.tsx b/src-docs/src/views/inline_edit/inline_edit_text.tsx new file mode 100644 index 00000000000..df4657d17b2 --- /dev/null +++ b/src-docs/src/views/inline_edit/inline_edit_text.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; + +import { + EuiInlineEditText, + EuiSpacer, + EuiButtonGroup, + EuiInlineEditTextSizes, +} from '../../../../src'; + +export default () => { + const textSizeButtons = [ + { + id: 'xs', + label: 'Extra Small', + }, + { + id: 's', + label: 'Small', + }, + { + id: 'm', + label: 'Medium', + }, + ]; + + const [toggleTextButtonSize, setToggleTextButtonSize] = useState< + EuiInlineEditTextSizes + >('m'); + + const textSizeOnChange = (optionId: EuiInlineEditTextSizes) => { + setToggleTextButtonSize(optionId); + }; + + return ( + <> + textSizeOnChange(id as EuiInlineEditTextSizes)} + /> + + + + + + ); +}; diff --git a/src-docs/src/views/inline_edit/inline_edit_title.tsx b/src-docs/src/views/inline_edit/inline_edit_title.tsx new file mode 100644 index 00000000000..9484bd0a954 --- /dev/null +++ b/src-docs/src/views/inline_edit/inline_edit_title.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; + +import { + EuiInlineEditTitle, + EuiSpacer, + EuiButtonGroup, + EuiTitleSize, +} from '../../../../src'; + +export default () => { + const titleSizeButtons = [ + { + id: 'xxxs', + label: '3X Small', + }, + { + id: 'xxs', + label: '2X Small', + }, + { + id: 'xs', + label: 'Extra small', + }, + { + id: 's', + label: 'Small', + }, + { + id: 'm', + label: 'Medium', + }, + { + id: 'l', + label: 'Large', + }, + ]; + + const [toggleTitleButtonSize, setToggleTitleButtonSize] = useState< + EuiTitleSize + >('m'); + + const titleSizeOnChange = (optionId: EuiTitleSize) => { + setToggleTitleButtonSize(optionId); + }; + + return ( + <> + titleSizeOnChange(id as EuiTitleSize)} + /> + + + + + + ); +}; diff --git a/src/components/button/button_empty/button_empty.tsx b/src/components/button/button_empty/button_empty.tsx index 98d02064411..f172e61aac4 100644 --- a/src/components/button/button_empty/button_empty.tsx +++ b/src/components/button/button_empty/button_empty.tsx @@ -91,7 +91,9 @@ export interface CommonEuiButtonEmptyProps type EuiButtonEmptyPropsForAnchor = PropsForAnchor; -type EuiButtonEmptyPropsForButton = PropsForButton; +export type EuiButtonEmptyPropsForButton = PropsForButton< + CommonEuiButtonEmptyProps +>; export type EuiButtonEmptyProps = ExclusiveUnion< EuiButtonEmptyPropsForAnchor, diff --git a/src/components/index.ts b/src/components/index.ts index f11d7b79db8..671e45d7a43 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -91,6 +91,8 @@ export * from './image'; export * from './inner_text'; +export * from './inline_edit'; + export * from './i18n'; export * from './loading'; diff --git a/src/components/inline_edit/__snapshots__/inline_edit_text.test.tsx.snap b/src/components/inline_edit/__snapshots__/inline_edit_text.test.tsx.snap new file mode 100644 index 00000000000..e235f18852d --- /dev/null +++ b/src/components/inline_edit/__snapshots__/inline_edit_text.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiInlineEditText props renders as text 1`] = ` +
+ +
+`; diff --git a/src/components/inline_edit/__snapshots__/inline_edit_title.test.tsx.snap b/src/components/inline_edit/__snapshots__/inline_edit_title.test.tsx.snap new file mode 100644 index 00000000000..bf0cc76fdcc --- /dev/null +++ b/src/components/inline_edit/__snapshots__/inline_edit_title.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiInlineEditTitle props renders as title 1`] = ` +
+ +
+`; diff --git a/src/components/inline_edit/index.ts b/src/components/inline_edit/index.ts new file mode 100644 index 00000000000..fb491c0aa6b --- /dev/null +++ b/src/components/inline_edit/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { EuiInlineEditText } from './inline_edit_text'; + +export { EuiInlineEditTitle } from './inline_edit_title'; + +export type { EuiInlineEditTextSizes } from './inline_edit_text'; diff --git a/src/components/inline_edit/inline_edit.styles.ts b/src/components/inline_edit/inline_edit.styles.ts new file mode 100644 index 00000000000..61cc6824225 --- /dev/null +++ b/src/components/inline_edit/inline_edit.styles.ts @@ -0,0 +1,19 @@ +/* + * 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 { css } from '@emotion/react'; +import { UseEuiTheme } from '../../services'; + +export const euiInlineEditStyles = ({ euiTheme }: UseEuiTheme) => { + return { + euiInlineEdit: css` + // Always start the object with the first key being the name of the component + color: ${euiTheme.colors.primaryText}; + `, + }; +}; diff --git a/src/components/inline_edit/inline_edit_form.tsx b/src/components/inline_edit/inline_edit_form.tsx new file mode 100644 index 00000000000..7afcaa57c5f --- /dev/null +++ b/src/components/inline_edit/inline_edit_form.tsx @@ -0,0 +1,207 @@ +/* + * 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, { ReactNode, FunctionComponent, useState } from 'react'; +import classNames from 'classnames'; + +import { CommonProps } from '../common'; +import { EuiFormRow, EuiFieldText, EuiForm, EuiFieldTextProps } from '../form'; +import { EuiButtonIcon, EuiButtonEmpty, EuiButtonEmptyProps } from '../button'; +import { EuiButtonEmptyPropsForButton } from '../button/button_empty/button_empty'; +import { EuiFlexGroup, EuiFlexItem } from '../flex'; +import { useEuiI18n } from '../i18n'; +import { useGeneratedHtmlId } from '../../services/accessibility'; + +// Props shared between the internal form component as well as consumer-facing components +export type EuiInlineEditCommonProps = CommonProps & { + defaultValue: string; + /** + * Allow users to pass in a function that is called when the confirm button is clicked + * The function should return a boolean flag that will determine if the value will be saved. + * When the flag is true, the value will be saved. When the flag is false, the user will be + * returned to editMode. + */ + onConfirm?: () => boolean; + /** + * 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 + */ + startWithEditOpen?: boolean; + /** + * Props that will be applied directly to the EuiEmptyButton displayed in readMode + */ + readModeProps?: Omit; + /** + * Props that will be applied directly to the EuiFieldText displayed in editMode + */ + editModeProps?: EuiFieldTextProps; +}; + +// Internal-only props, passed by the consumer-facing components +export type EuiInlineEditFormProps = EuiInlineEditCommonProps & { + /** + * Form sizes + */ + sizes: { + compressed: boolean; + buttonSize: EuiButtonEmptyProps['size']; + iconSize: EuiButtonEmptyProps['iconSize']; + }; + /** + * Render prop that returns the read mode value as an arg + */ + children: (readModeValue: ReactNode) => ReactNode; +}; + +export const SMALL_SIZE_FORM = { + iconSize: 's', + compressed: true, + buttonSize: 's', +} as const; + +export const MEDIUM_SIZE_FORM = { + iconSize: 'm', + compressed: false, + buttonSize: 'm', +} as const; + +export const EuiInlineEditForm: FunctionComponent = ({ + className, + children, + sizes, + defaultValue, + onConfirm, + inputAriaLabel, + saveButtonAriaLabel, + cancelButtonAriaLabel, + startWithEditOpen, + readModeProps, + editModeProps, +}) => { + const classes = classNames('euiInlineEdit', className); + + // Styles to come later! (Styling editMode text to match the size of its readMode counterpart) + /*const theme = useEuiTheme(); + const styles = euiInlineEditStyles(theme); + const cssStyles = [styles.euiInlineEdit];*/ + + const defaultSaveButtonAriaLabel = useEuiI18n( + 'euiInlineEditForm.saveButtonAriaLabel', + 'Save edit' + ); + const defaultCancelButtonAriaLabel = useEuiI18n( + 'euiInlineEditForm.cancelButtonAriaLabel', + 'Cancel edit' + ); + + const [isEditing, setIsEditing] = useState(false || startWithEditOpen); + const inlineEditInputId = useGeneratedHtmlId({ prefix: '__inlineEditInput' }); + + const [editModeValue, setEditModeValue] = useState(defaultValue); + const [readModeValue, setReadModeValue] = useState(defaultValue); + + const cancelInlineEdit = () => { + setEditModeValue(readModeValue); + setIsEditing(!isEditing); + }; + + const saveInlineEditValue = () => { + if (editModeValue && onConfirm && !onConfirm()) { + // If there is text, an onConfirm method is present, and it has returned false, cancel the action + return; + } else if (editModeValue) { + setReadModeValue(editModeValue); + setIsEditing(!isEditing); + } else { + // If there's no text, cancel the action, reset the input text, and return to readMode + cancelInlineEdit(); + } + }; + + const editModeForm = ( + + + + { + setEditModeValue(e.target.value); + }} + aria-label={inputAriaLabel} + autoFocus + compressed={sizes.compressed} + {...editModeProps} + /> + + + + + + + + + + + + + + + + ); + + const readModeElement = ( + { + setIsEditing(!isEditing); + }} + {...readModeProps} + > + {children(readModeValue)} + + ); + + return ( +
{isEditing ? editModeForm : readModeElement}
+ ); +}; diff --git a/src/components/inline_edit/inline_edit_text.test.tsx b/src/components/inline_edit/inline_edit_text.test.tsx new file mode 100644 index 00000000000..eb47d25e5b8 --- /dev/null +++ b/src/components/inline_edit/inline_edit_text.test.tsx @@ -0,0 +1,29 @@ +/* + * 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 from 'react'; +import { render } from '../../test/rtl'; +import { requiredProps } from '../../test/required_props'; + +import { EuiInlineEditText } from './inline_edit_text'; + +describe('EuiInlineEditText', () => { + describe('props', () => { + test('renders as text', () => { + const { container } = render( + + ); + + expect(container.firstChild).toMatchSnapshot(); + }); + }); +}); diff --git a/src/components/inline_edit/inline_edit_text.tsx b/src/components/inline_edit/inline_edit_text.tsx new file mode 100644 index 00000000000..c761481e578 --- /dev/null +++ b/src/components/inline_edit/inline_edit_text.tsx @@ -0,0 +1,66 @@ +/* + * 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, { FunctionComponent } from 'react'; +import classNames from 'classnames'; +import { EuiText, EuiTextProps } from '../text'; +import { + EuiInlineEditCommonProps, + EuiInlineEditForm, + SMALL_SIZE_FORM, + MEDIUM_SIZE_FORM, +} from './inline_edit_form'; + +export type EuiInlineEditTextSizes = Exclude; + +export type EuiInlineEditTextProps = EuiInlineEditCommonProps & { + /** + * Text size level + */ + size?: EuiInlineEditTextSizes; +}; + +export const EuiInlineEditText: FunctionComponent = ({ + children, + className, + size = 'm', + defaultValue, + onConfirm, + inputAriaLabel, + saveButtonAriaLabel, + cancelButtonAriaLabel, + startWithEditOpen, + readModeProps, + editModeProps, + ...rest +}) => { + const classes = classNames('euiInlineEditText', className); + + const isSmallSize = ['xs', 's'].includes(size); + const sizes = isSmallSize ? SMALL_SIZE_FORM : MEDIUM_SIZE_FORM; + + const formProps = { + sizes, + defaultValue, + onConfirm, + inputAriaLabel, + saveButtonAriaLabel, + cancelButtonAriaLabel, + startWithEditOpen, + readModeProps, + editModeProps, + }; + + return ( + + {(textReadModeValue) => ( + {textReadModeValue} + )} + + ); +}; diff --git a/src/components/inline_edit/inline_edit_title.test.tsx b/src/components/inline_edit/inline_edit_title.test.tsx new file mode 100644 index 00000000000..9104347af45 --- /dev/null +++ b/src/components/inline_edit/inline_edit_title.test.tsx @@ -0,0 +1,30 @@ +/* + * 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 from 'react'; +import { render } from '../../test/rtl'; +import { requiredProps } from '../../test/required_props'; + +import { EuiInlineEditTitle } from './inline_edit_title'; + +describe('EuiInlineEditTitle', () => { + describe('props', () => { + test('renders as title', () => { + const { container } = render( + + ); + + expect(container.firstChild).toMatchSnapshot(); + }); + }); +}); diff --git a/src/components/inline_edit/inline_edit_title.tsx b/src/components/inline_edit/inline_edit_title.tsx new file mode 100644 index 00000000000..c612199d2b0 --- /dev/null +++ b/src/components/inline_edit/inline_edit_title.tsx @@ -0,0 +1,76 @@ +/* + * 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, { FunctionComponent } from 'react'; +import classNames from 'classnames'; +import { EuiTitle, EuiTitleSize } from '../title'; +import { + EuiInlineEditCommonProps, + EuiInlineEditForm, + SMALL_SIZE_FORM, + MEDIUM_SIZE_FORM, +} from './inline_edit_form'; + +export const HEADINGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const; +type Heading = typeof HEADINGS[number]; + +export type EuiInlineEditTitleProps = EuiInlineEditCommonProps & { + /** + * Title size level + */ + size?: EuiTitleSize; + /** + * Level of heading to be used for the title + */ + heading: Heading; +}; + +export const EuiInlineEditTitle: FunctionComponent = ({ + children, + className, + size = 'm', + heading, + defaultValue, + onConfirm, + inputAriaLabel, + saveButtonAriaLabel, + cancelButtonAriaLabel, + startWithEditOpen = false, + readModeProps, + editModeProps, + ...rest +}) => { + const classes = classNames('euiInlineEditTitle', className); + + const H: Heading = heading; + + const isSmallSize = ['xxxs', 'xxs', 'xs', 's'].includes(size); + const sizes = isSmallSize ? SMALL_SIZE_FORM : MEDIUM_SIZE_FORM; + + const formProps = { + sizes, + defaultValue, + onConfirm, + inputAriaLabel, + saveButtonAriaLabel, + cancelButtonAriaLabel, + startWithEditOpen, + readModeProps, + editModeProps, + }; + + return ( + + {(titleReadModeValue) => ( + + {titleReadModeValue} + + )} + + ); +};