Update Edit Inplace behavior and style (#97)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Charles Bochet 2023-05-04 16:03:13 +02:00 committed by GitHub
parent 3605c0034c
commit e65fd3d6a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 178 additions and 126 deletions

View File

@ -1,87 +0,0 @@
import styled from '@emotion/styled';
import { ChangeEvent, useRef, useState } from 'react';
type OwnProps = {
content: string;
changeHandler: (updated: string) => void;
};
const StyledEditable = styled.div`
position: relative;
box-sizing: border-box;
height: 32px;
display: flex;
align-items: center;
width: 100%;
:hover::before {
display: block;
}
::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
width: calc(100% + 2px);
height: calc(100% + 2px);
border: 1px solid ${(props) => props.theme.text20};
box-sizing: border-box;
border-radius: 4px;
pointer-events: none;
display: none;
}
&: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.text20};
border-radius: 4px;
pointer-events: none;
display: block;
z-index: 1;
box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09);
}
`;
const StyledInplaceInput = styled.input`
width: 100%;
border: none;
outline: none;
`;
const Container = styled.div`
width: 100%;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
`;
function EditableCell({ content, changeHandler }: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(content);
return (
<StyledEditable
onClick={() => {
inputRef.current?.focus();
}}
>
<Container>
<StyledInplaceInput
ref={inputRef}
value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
changeHandler(event.target.value);
}}
/>
</Container>
</StyledEditable>
);
}
export default EditableCell;

View File

@ -1,31 +0,0 @@
import EditableCell from '../EditableCell';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
const component = {
title: 'EditableCell',
component: EditableCell,
};
type OwnProps = {
content: string;
changeHandler: (updated: string) => void;
};
export default component;
const Template: StoryFn<typeof EditableCell> = (args: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditableCell {...args} />
</div>
</ThemeProvider>
);
};
export const EditableCellStory = Template.bind({});
EditableCellStory.args = {
content: 'Test string',
};

View File

@ -0,0 +1,71 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { ReactElement, useRef, useState } from 'react';
import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter';
import { ThemeType } from '../../../layout/styles/themes';
type OwnProps = {
children: ReactElement;
onEditModeChange: (isEditMode: boolean) => void;
};
const StyledWrapper = styled.div`
position: relative;
box-sizing: border-box;
height: 32px;
display: flex;
align-items: center;
width: 100%;
`;
type styledEditModeWrapperProps = {
isEditMode: boolean;
};
const styledEditModeWrapper = (theme: ThemeType) =>
css`
position: absolute;
width: 260px;
height: 100%;
display: flex;
padding-left: ${theme.spacing(2)};
padding-right: ${theme.spacing(2)};
background: ${theme.primaryBackground};
border: 1px solid ${theme.primaryBorder};
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.16);
z-index: 1;
border-radius: 4px;
backdrop-filter: blur(20px);
`;
const Container = styled.div<styledEditModeWrapperProps>`
width: 100%;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
${(props) => props.isEditMode && styledEditModeWrapper(props.theme)}
`;
function EditableCellWrapper({ children, onEditModeChange }: OwnProps) {
const [isEditMode, setIsEditMode] = useState(false);
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef, () => {
setIsEditMode(false);
onEditModeChange(false);
});
return (
<StyledWrapper
ref={wrapperRef}
onClick={() => {
setIsEditMode(true);
onEditModeChange(true);
}}
>
<Container isEditMode={isEditMode}>{children}</Container>
</StyledWrapper>
);
}
export default EditableCellWrapper;

View File

@ -0,0 +1,55 @@
import styled from '@emotion/styled';
import { ChangeEvent, useRef, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper';
type OwnProps = {
placeholder?: string;
content: string;
changeHandler: (updated: string) => void;
};
type StyledEditModeProps = {
isEditMode: boolean;
};
const StyledInplaceInput = styled.input<StyledEditModeProps>`
width: 100%;
border: none;
outline: none;
&::placeholder {
font-weight: ${(props) => (props.isEditMode ? 'bold' : 'normal')};
color: ${(props) =>
props.isEditMode ? props.theme.text20 : 'transparent'};
}
`;
function EditableCell({ content, placeholder, changeHandler }: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(content);
const [isEditMode, setIsEditMode] = useState(false);
const onEditModeChange = (isEditMode: boolean) => {
setIsEditMode(isEditMode);
if (isEditMode) {
inputRef.current?.focus();
}
};
return (
<EditableCellWrapper onEditModeChange={onEditModeChange}>
<StyledInplaceInput
isEditMode={isEditMode}
placeholder={placeholder || ''}
ref={inputRef}
value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
changeHandler(event.target.value);
}}
/>
</EditableCellWrapper>
);
}
export default EditableCell;

View File

@ -0,0 +1,35 @@
import EditableText from '../EditableText';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
const component = {
title: 'EditableText',
component: EditableText,
};
type OwnProps = {
content: string;
changeHandler: (updated: string) => void;
};
export default component;
const Template: StoryFn<typeof EditableText> = (args: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditableText {...args} />
</div>
</ThemeProvider>
);
};
export const EditableTextStory = Template.bind({});
EditableTextStory.args = {
placeholder: 'Test placeholder',
content: 'Test string',
changeHandler: () => {
console.log('changed');
},
};

View File

@ -1,14 +1,22 @@
import { fireEvent, render } from '@testing-library/react';
import { EditableCellStory } from '../__stories__/EditableCell.stories';
import { EditableTextStory } from '../__stories__/EditableText.stories';
it('Checks the EditableCell editing event bubbles up', async () => {
const func = jest.fn(() => null);
const { getByTestId } = render(
<EditableCellStory content="test" changeHandler={func} />,
<EditableTextStory content="test" changeHandler={func} />,
);
const parent = getByTestId('content-editable-parent');
const wrapper = parent.querySelector('div');
if (!wrapper) {
throw new Error('Editable input not found');
}
fireEvent.click(wrapper);
const editableInput = parent.querySelector('input');
if (!editableInput) {

View File

@ -5,7 +5,7 @@ import ColumnHead from '../../components/table/ColumnHead';
import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer';
import Checkbox from '../../components/form/Checkbox';
import CompanyChip from '../../components/chips/CompanyChip';
import EditableCell from '../../components/table/EditableCell';
import EditableText from '../../components/table/editable-cell/EditableText';
import PipeChip from '../../components/chips/PipeChip';
import {
FaRegBuilding,
@ -53,7 +53,7 @@ export const companiesColumns = [
columnHelper.accessor('employees', {
header: () => <ColumnHead viewName="Employees" viewIcon={<FaUsers />} />,
cell: (props) => (
<EditableCell
<EditableText
content={props.row.original.employees.toFixed(0)}
changeHandler={(value) => {
const company = props.row.original;
@ -66,7 +66,7 @@ export const companiesColumns = [
columnHelper.accessor('domain_name', {
header: () => <ColumnHead viewName="URL" viewIcon={<FaLink />} />,
cell: (props) => (
<EditableCell
<EditableText
content={props.row.original.domain_name}
changeHandler={(value) => {
const company = props.row.original;
@ -79,7 +79,7 @@ export const companiesColumns = [
columnHelper.accessor('address', {
header: () => <ColumnHead viewName="Address" viewIcon={<FaMapPin />} />,
cell: (props) => (
<EditableCell
<EditableText
content={props.row.original.address}
changeHandler={(value) => {
const company = props.row.original;

View File

@ -19,7 +19,7 @@ import CompanyChip from '../../components/chips/CompanyChip';
import PersonChip from '../../components/chips/PersonChip';
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface';
import PipeChip from '../../components/chips/PipeChip';
import EditableCell from '../../components/table/EditableCell';
import EditableText from '../../components/table/editable-cell/EditableText';
import { OrderByFields, updatePerson } from '../../services/people';
import {
FilterType,
@ -152,7 +152,8 @@ export const peopleColumns = [
columnHelper.accessor('email', {
header: () => <ColumnHead viewName="Email" viewIcon={<FaEnvelope />} />,
cell: (props) => (
<EditableCell
<EditableText
placeholder="Email"
content={props.row.original.email}
changeHandler={(value) => {
const person = props.row.original;