Fix html entities and newline handling (#77)

* Fix html entities and newline handling

This forces contenteditable to behave as single line input,
and properly handles html entities.

* Proposal without reacteditable

* Fix tests and re-add focus styleé

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Anders Borch 2023-04-26 11:58:40 +02:00 committed by GitHub
parent e19a85a5d0
commit 1c8a4058c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 1291 additions and 1511 deletions

2722
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,6 @@
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.26", "libphonenumber-js": "^1.10.26",
"react": "^18.2.0", "react": "^18.2.0",
"react-contenteditable": "^3.3.7",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.4.4", "react-router-dom": "^6.4.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"

View File

@ -1,6 +1,5 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import * as React from 'react'; import { ChangeEvent, useRef, useState } from 'react';
import ContentEditable, { ContentEditableEvent } from 'react-contenteditable';
type OwnProps = { type OwnProps = {
content: string; content: string;
@ -13,6 +12,11 @@ const StyledEditable = styled.div`
height: 32px; height: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%;
:hover::before {
display: block;
}
::before { ::before {
content: ''; content: '';
@ -28,37 +32,51 @@ const StyledEditable = styled.div`
display: none; display: none;
} }
:hover::before { &:has(input:focus-within)::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
width: calc(100% + 2px);
height: calc(100% + 2px);
border: 1px solid ${(props) => props.theme.blue};
border-radius: 4px;
pointer-events: none;
display: block; display: block;
} z-index: 1;
[contenteditable] {
outline: none;
} }
`; `;
const Container = styled.span` const StyledInplaceInput = styled.input`
width: 100%;
border: none;
outline: none;
`;
const Container = styled.div`
width: 100%;
padding-left: ${(props) => props.theme.spacing(2)}; padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
`; `;
function escapeHTML(unsafeText: string): string {
const div = document.createElement('div');
div.innerText = unsafeText;
return div.innerHTML;
}
function EditableCell({ content, changeHandler }: OwnProps) { function EditableCell({ content, changeHandler }: OwnProps) {
const ref = React.createRef<HTMLElement>(); const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(content);
return ( return (
<StyledEditable> <StyledEditable
onClick={() => {
inputRef.current?.focus();
}}
>
<Container> <Container>
<ContentEditable <StyledInplaceInput
innerRef={ref} ref={inputRef}
html={escapeHTML(content)} value={inputValue}
disabled={false} onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange={(e: ContentEditableEvent) => changeHandler(e.target.value)} setInputValue(event.target.value);
tagName="span" changeHandler(event.target.value);
}}
/> />
</Container> </Container>
</StyledEditable> </StyledEditable>

View File

@ -1,5 +1,4 @@
import { render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { RegularEditableCell } from '../__stories__/EditableCell.stories'; import { RegularEditableCell } from '../__stories__/EditableCell.stories';
@ -8,10 +7,12 @@ it('Checks the EditableCell editing event bubbles up', async () => {
const { getByTestId } = render(<RegularEditableCell changeHandler={func} />); const { getByTestId } = render(<RegularEditableCell changeHandler={func} />);
const parent = getByTestId('content-editable-parent'); const parent = getByTestId('content-editable-parent');
expect(parent).not.toBeNull(); const editableInput = parent.querySelector('input');
const editable = parent.querySelector('[contenteditable]');
expect(editable).not.toBeNull(); if (!editableInput) {
editable && userEvent.click(editable); throw new Error('Editable input not found');
userEvent.keyboard('a'); }
expect(func).toBeCalled();
fireEvent.change(editableInput, { target: { value: '23' } });
expect(func).toBeCalledWith('23');
}); });