[Cases] Replace EditableTitle component with EuiInlineEdit component (#162095)

Included in https://github.com/elastic/eui/issues/6778

Hi team! EUI recently released the EuiInlineEdit component and the Cases
page title was identified as a good candidate for the new component.
This PR is replaces the inner workings of the EditableTitle component
and replaces it with the new EuiInlineEdit component.

## Summary
Replace inner component within `EditableTitle` to use to the new
`EuiInlineEdit` component.


**Read Mode**
<img width="1116" alt="image"
src="c3aef300-7d7f-44cd-b0a2-da72ef8bedf9">

---

**Edit Mode**
<img width="1093" alt="image"
src="0567ea9b-29e4-4443-bcbe-7a45f09738ff">

---

**Insufficient Permissions**
<img width="1090" alt="image"
src="7a83ebc6-7319-415d-a4a8-015fd6f6893b">

---

**Error States**
<img width="1093" alt="image"
src="1e5360b0-4fe8-4a25-a5e2-d3b235fc6242">

<img width="1108" alt="image"
src="ecfdcdc5-9360-4d25-8a4b-7132ec2caa67">

---

**Release Phases**
<img width="1096" alt="image"
src="cb0ac70b-1ba2-4f3a-8966-25b38043c4a1">

<img width="1096" alt="image"
src="23597578-0a36-4190-8e84-804cecbbdf78">

---


### Checklist

Delete any items that are not applicable to this PR.

- [ ] ~Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)~
- [ ]
~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials~
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] ~If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Bree Hall 2023-08-01 04:07:25 -04:00 committed by GitHub
parent 8683621a65
commit 8c7c621205
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 124 additions and 159 deletions

View file

@ -35,14 +35,16 @@ describe('EditableTitle', () => {
expect(renderResult.getByText('Test title')).toBeInTheDocument();
});
it('does not show the edit icon when the user does not have edit permissions', () => {
it('inline edit defaults to readOnly when the user does not have the edit permissions', () => {
const wrapper = mount(
<TestProviders permissions={readCasesPermissions()}>
<EditableTitle {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').exists()).toBeFalsy();
expect(
wrapper.find('button[data-test-subj="editable-title-header-value"]').prop('disabled')
).toBe(true);
});
it('shows the edit title input field', () => {
@ -52,7 +54,7 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
expect(wrapper.find('[data-test-subj="editable-title-input-field"]').first().exists()).toBe(
@ -67,12 +69,12 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
expect(wrapper.find('[data-test-subj="editable-title-submit-btn"]').first().exists()).toBe(
true
);
expect(
wrapper.find('button[data-test-subj="editable-title-submit-btn"]').first().exists()
).toBe(true);
});
it('shows the cancel button', () => {
@ -82,27 +84,12 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
expect(wrapper.find('[data-test-subj="editable-title-cancel-btn"]').first().exists()).toBe(
true
);
});
it('DOES NOT shows the edit icon when in edit mode', () => {
const wrapper = mount(
<TestProviders>
<EditableTitle {...defaultProps} />
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.update();
expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe(
false
);
expect(
wrapper.find('button[data-test-subj="editable-title-cancel-btn"]').first().exists()
).toBe(true);
});
it('switch to non edit mode when canceled', () => {
@ -112,11 +99,13 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
wrapper.find('button[data-test-subj="editable-title-cancel-btn"]').simulate('click');
expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe(true);
expect(
wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists()
).toBe(true);
});
it('should change the title', () => {
@ -128,7 +117,7 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
wrapper
@ -152,18 +141,21 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
wrapper
.find('input[data-test-subj="editable-title-input-field"]')
.simulate('change', { target: { value: newTitle } });
wrapper.update();
wrapper.find('button[data-test-subj="editable-title-cancel-btn"]').simulate('click');
wrapper.update();
expect(wrapper.find('h1[data-test-subj="header-page-title"]').text()).toEqual(title);
expect(wrapper.find('button[data-test-subj="editable-title-header-value"]').text()).toEqual(
title
);
});
it('submits the title', () => {
@ -175,11 +167,12 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
wrapper
.find('input[data-test-subj="editable-title-input-field"]')
.last()
.simulate('change', { target: { value: newTitle } });
wrapper.find('button[data-test-subj="editable-title-submit-btn"]').simulate('click');
@ -187,7 +180,9 @@ describe('EditableTitle', () => {
expect(submitTitle).toHaveBeenCalled();
expect(submitTitle.mock.calls[0][0]).toEqual(newTitle);
expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe(true);
expect(
wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists()
).toBe(true);
});
it('does not submit the title when the length is longer than 160 characters', () => {
@ -199,7 +194,7 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
wrapper
@ -213,9 +208,9 @@ describe('EditableTitle', () => {
);
expect(submitTitle).not.toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe(
false
);
expect(
wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists()
).toBe(false);
});
it('does not submit the title is empty', () => {
@ -225,11 +220,12 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
wrapper
.find('input[data-test-subj="editable-title-input-field"]')
.simulate('change', { target: { value: '' } });
wrapper.find('button[data-test-subj="editable-title-submit-btn"]').simulate('click');
@ -237,9 +233,9 @@ describe('EditableTitle', () => {
expect(wrapper.find('.euiFormErrorText').text()).toBe('A name is required.');
expect(submitTitle).not.toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe(
false
);
expect(
wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists()
).toBe(false);
});
it('does not show an error after a previous edit error was displayed', () => {
@ -252,7 +248,7 @@ describe('EditableTitle', () => {
</TestProviders>
);
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
// simualte a long title
@ -269,6 +265,7 @@ describe('EditableTitle', () => {
// write a shorter one
wrapper
.find('input[data-test-subj="editable-title-input-field"]')
.simulate('change', { target: { value: shortTitle } });
wrapper.update();
@ -277,7 +274,7 @@ describe('EditableTitle', () => {
wrapper.update();
// edit again
wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click');
wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click');
wrapper.update();
// no error should appear

View file

@ -5,38 +5,15 @@
* 2.0.
*/
import type { ChangeEvent } from 'react';
import React, { useState, useCallback } from 'react';
import styled, { css } from 'styled-components';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFieldText,
EuiButtonIcon,
EuiLoadingSpinner,
EuiFormRow,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle } from '@elastic/eui';
import { MAX_TITLE_LENGTH } from '../../../common/constants';
import * as i18n from './translations';
import { Title } from './title';
import { TitleExperimentalBadge, TitleBetaBadge } from './title';
import { useCasesContext } from '../cases_context/use_cases_context';
const MyEuiButtonIcon = styled(EuiButtonIcon)`
${({ theme }) => css`
margin-left: ${theme.eui.euiSize};
`}
`;
const MySpinner = styled(EuiLoadingSpinner)`
${({ theme }) => css`
margin-left: ${theme.eui.euiSize};
`}
`;
export interface EditableTitleProps {
isLoading: boolean;
title: string;
@ -47,92 +24,81 @@ const EditableTitleComponent: React.FC<EditableTitleProps> = ({ onSubmit, isLoad
const { releasePhase, permissions } = useCasesContext();
const [editMode, setEditMode] = useState(false);
const [errors, setErrors] = useState<string[]>([]);
const [newTitle, setNewTitle] = useState<string>(title);
const onCancel = useCallback(() => {
const onClickSubmit = useCallback(
(newTitleValue: string): boolean => {
if (!newTitleValue.trim().length) {
setErrors([i18n.TITLE_REQUIRED]);
return false;
}
if (newTitleValue.trim().length > MAX_TITLE_LENGTH) {
setErrors([i18n.MAX_LENGTH_ERROR('title', MAX_TITLE_LENGTH)]);
return false;
}
if (newTitleValue !== title) {
onSubmit(newTitleValue.trim());
}
setEditMode(false);
setErrors([]);
return true;
},
[onSubmit, title]
);
const onCancel = () => {
setErrors([]);
setEditMode(false);
setErrors([]);
setNewTitle(title);
}, [title]);
const onClickEditIcon = useCallback(() => setEditMode(true), []);
const onClickSubmit = useCallback((): void => {
if (!newTitle.trim().length) {
setErrors([i18n.TITLE_REQUIRED]);
return;
}
if (newTitle.trim().length > MAX_TITLE_LENGTH) {
setErrors([i18n.MAX_LENGTH_ERROR('title', MAX_TITLE_LENGTH)]);
return;
}
if (newTitle !== title) {
onSubmit(newTitle);
}
setEditMode(false);
setErrors([]);
}, [newTitle, onSubmit, title]);
const handleOnChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setNewTitle(e.target.value);
setErrors([]);
}, []);
};
const hasErrors = errors.length > 0;
return editMode ? (
<EuiFormRow isInvalid={hasErrors} error={errors} fullWidth>
<EuiFlexGroup
alignItems="center"
responsive={true}
gutterSize="m"
justifyContent="spaceBetween"
>
<EuiFlexItem grow={true}>
<EuiFieldText
fullWidth={true}
onChange={handleOnChange}
value={`${newTitle}`}
data-test-subj="editable-title-input-field"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
color="success"
data-test-subj="editable-title-submit-btn"
fill
iconType="save"
onClick={onClickSubmit}
size="s"
>
{i18n.SAVE}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="editable-title-cancel-btn"
iconType="cross"
onClick={onCancel}
size="s"
>
{i18n.CANCEL}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
) : (
<Title title={title} releasePhase={releasePhase}>
{isLoading && <MySpinner data-test-subj="editable-title-loading" />}
{!isLoading && permissions.update && (
<MyEuiButtonIcon
aria-label={i18n.EDIT_TITLE_ARIA(title as string)}
iconType="pencil"
onClick={onClickEditIcon}
data-test-subj="editable-title-edit-icon"
return (
<EuiFlexGroup>
<EuiFlexItem grow={true} css={releasePhase && { overflow: 'hidden' }}>
<EuiInlineEditTitle
defaultValue={title}
readModeProps={{
onClick: () => setEditMode(true),
'data-test-subj': 'editable-title-header-value',
}}
editModeProps={{
formRowProps: { error: errors },
inputProps: {
'data-test-subj': 'editable-title-input-field',
onChange: () => {
setErrors([]);
},
},
saveButtonProps: {
'data-test-subj': 'editable-title-submit-btn',
isDisabled: hasErrors,
},
cancelButtonProps: {
onClick: () => onCancel(),
'data-test-subj': 'editable-title-cancel-btn',
},
}}
inputAriaLabel="Editable title input field"
heading="h1"
size="s"
isInvalid={hasErrors}
isLoading={isLoading}
isReadOnly={!permissions.update}
onSave={(value) => {
return onClickSubmit(value);
}}
startWithEditOpen={editMode}
data-test-subj="header-page-title"
/>
)}
</Title>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{releasePhase === 'experimental' && <TitleExperimentalBadge />}
{releasePhase === 'beta' && <TitleBetaBadge />}
</EuiFlexItem>
</EuiFlexGroup>
);
};
EditableTitleComponent.displayName = 'EditableTitle';

View file

@ -87,7 +87,7 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
return (
<Header border={border} data-test-subj={dataTestSubj}>
<EuiFlexGroup alignItems="center">
<FlexItem>
<FlexItem css={{ overflow: 'hidden' }}>
{showBackButton && (
<LinkBack>
<LinkIcon

View file

@ -28,12 +28,14 @@ const ExperimentalBadge: React.FC = () => (
);
ExperimentalBadge.displayName = 'ExperimentalBadge';
export const TitleExperimentalBadge = React.memo(ExperimentalBadge);
const BetaBadge: React.FC = () => (
<EuiBetaBadge label={i18n.BETA_LABEL} tooltipContent={i18n.BETA_DESC} tooltipPosition="bottom" />
);
BetaBadge.displayName = 'BetaBadge';
export const TitleBetaBadge = React.memo(BetaBadge);
const TitleComponent: React.FC<Props> = ({ title, releasePhase, children }) => (
<EuiFlexGroup alignItems="baseline" gutterSize="s" responsive={false}>

View file

@ -14,7 +14,7 @@ export const CASE_DELETE = '[data-test-subj="property-actions-trash"]';
export const CASE_DETAILS_DESCRIPTION =
'[data-test-subj="description"] [data-test-subj="scrollable-markdown"]';
export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="header-page-title"]';
export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="editable-title-header-value"]';
export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status-dropdown"]';

View file

@ -110,7 +110,7 @@ export function CasesSingleViewServiceProvider({ getService, getPageObject }: Ft
},
async assertCaseTitle(expectedTitle: string) {
const actionTitle = await testSubjects.getVisibleText('header-page-title');
const actionTitle = await testSubjects.getVisibleText('editable-title-header-value');
expect(actionTitle).to.eql(
expectedTitle,
`Expected case title to be '${expectedTitle}' (got '${actionTitle}')`
@ -138,7 +138,7 @@ export function CasesSingleViewServiceProvider({ getService, getPageObject }: Ft
async closeAssigneesPopover() {
await retry.try(async () => {
// Click somewhere outside the popover
await testSubjects.click('header-page-title');
await testSubjects.click('editable-title-header-value');
await header.waitUntilLoadingHasFinished();
await testSubjects.missingOrFail('euiSelectableList');
});

View file

@ -48,7 +48,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
});
// validate title
const title = await find.byCssSelector('[data-test-subj="header-page-title"]');
const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]');
expect(await title.getVisibleText()).equal(caseTitle);
// validate description

View file

@ -58,13 +58,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
it('edits a case title from the case view page', async () => {
const newTitle = `test-${uuidv4()}`;
await testSubjects.click('editable-title-edit-icon');
await testSubjects.click('editable-title-header-value');
await testSubjects.setValue('editable-title-input-field', newTitle);
await testSubjects.click('editable-title-submit-btn');
// wait for backend response
await retry.tryForTime(5000, async () => {
const title = await find.byCssSelector('[data-test-subj="header-page-title"]');
const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]');
expect(await title.getVisibleText()).equal(newTitle);
});
@ -75,7 +75,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
it('shows error message when title is more than 160 characters', async () => {
const longTitle = Array(161).fill('x').toString();
await testSubjects.click('editable-title-edit-icon');
await testSubjects.click('editable-title-header-value');
await testSubjects.setValue('editable-title-input-field', longTitle);
await testSubjects.click('editable-title-submit-btn');

View file

@ -385,7 +385,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await cases.common.expectToasterToContain(`${caseTitle} has been updated`);
await testSubjects.click('toaster-content-case-view-link');
const title = await find.byCssSelector('[data-test-subj="header-page-title"]');
const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]');
expect(await title.getVisibleText()).toEqual(caseTitle);
await testSubjects.existOrFail('comment-persistableState-.lens');
@ -412,7 +412,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await cases.common.expectToasterToContain(`${theCaseTitle} has been updated`);
await testSubjects.click('toaster-content-case-view-link');
const title = await find.byCssSelector('[data-test-subj="header-page-title"]');
const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]');
expect(await title.getVisibleText()).toEqual(theCaseTitle);
await testSubjects.existOrFail('comment-persistableState-.lens');

View file

@ -80,7 +80,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
});
it('shows the title correctly', async () => {
const title = await testSubjects.find('header-page-title');
const title = await testSubjects.find('editable-title-header-value');
expect(await title.getVisibleText()).equal('Upgrade test in Kibana');
});