feature: add TaskDetails component

This commit is contained in:
Jordan Knott 2020-04-10 14:45:49 -05:00
parent a877cd9414
commit 90f22d322e
10 changed files with 487 additions and 2 deletions

View File

@ -58,7 +58,8 @@
"eject": "react-scripts eject", "eject": "react-scripts eject",
"storybook": "start-storybook -p 9009 -s public", "storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public", "build-storybook": "build-storybook -s public",
"generate": "graphql-codegen" "generate": "graphql-codegen",
"lint": "eslint --ext js,ts,tsx src"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"

View File

@ -11,6 +11,7 @@ type Task = {
name: string; name: string;
position: number; position: number;
labels: Label[]; labels: Label[];
description?: string;
}; };
type TaskGroup = { type TaskGroup = {

View File

@ -0,0 +1,33 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import styled from 'styled-components';
import Modal from './index';
export default {
component: Modal,
title: 'Modal',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#cdd3e1', default: true },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Modal
width={1040}
onClose={action('on close')}
renderContent={() => {
return <h1>Hello!</h1>;
}}
/>
</>
);
};

View File

@ -0,0 +1,33 @@
import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const ScrollOverlay = styled.div`
z-index: 1000000;
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
`;
export const ClickableOverlay = styled.div`
min-height: 100%;
background: rgba(9, 30, 66, 0.54);
display: flex;
justify-content: center;
align-items: center;
padding: 50px;
`;
export const StyledModal = styled.div<{ width: number }>`
display: inline-block;
position: relative;
width: 100%;
background: #fff;
max-width: ${props => props.width}px;
vertical-align: middle;
border-radius: 3px;
${mixin.boxShadowMedium}
`;

View File

@ -0,0 +1,36 @@
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { ScrollOverlay, ClickableOverlay, StyledModal } from './Styles';
const $root: HTMLElement = document.getElementById('root')!;
type ModalProps = {
width: number;
onClose: () => void;
renderContent: () => JSX.Element;
};
const Modal: React.FC<ModalProps> = ({ width, onClose: tellParentToClose, renderContent }) => {
const $modalRef = useRef<HTMLDivElement>(null);
const $clickableOverlayRef = useRef<HTMLDivElement>(null);
useOnOutsideClick($modalRef, true, tellParentToClose, $clickableOverlayRef);
useOnEscapeKeyDown(true, tellParentToClose);
return ReactDOM.createPortal(
<ScrollOverlay>
<ClickableOverlay ref={$clickableOverlayRef}>
<StyledModal width={width} ref={$modalRef}>
{renderContent()}
</StyledModal>
</ClickableOverlay>
</ScrollOverlay>,
$root,
);
};
export default Modal;

View File

@ -0,0 +1,181 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
export const TaskHeader = styled.div`
display: flex;
-webkit-box-pack: justify;
justify-content: space-between;
padding: 21px 18px 0px;
`;
export const TaskMeta = styled.div`
position: relative;
cursor: pointer;
font-size: 14px;
display: inline-block;
border-radius: 4px;
`;
export const TaskActions = styled.div`
display: flex;
align-items: center;
`;
export const TaskAction = styled.button`
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
vertical-align: middle;
line-height: 1;
white-space: nowrap;
cursor: pointer;
user-select: none;
font-size: 14.5px;
color: rgb(66, 82, 110);
font-family: CircularStdBook;
font-weight: normal;
padding: 0px 9px;
border-radius: 3px;
transition: all 0.1s ease 0s;
background: rgb(255, 255, 255);
`;
export const TaskDetailsWrapper = styled.div`
display: flex;
padding: 0px 30px 60px;
`;
export const TaskDetailsContent = styled.div`
width: 65%;
padding-right: 50px;
`;
export const TaskDetailsSidebar = styled.div`
width: 35%;
padding-top: 5px;
`;
export const TaskDetailsTitleWrapper = styled.div`
height: 44px;
width: 100%;
margin: 18px 0px 0px -8px;
display: inline-block;
`;
export const TaskDetailsTitle = styled(TextareaAutosize)`
line-height: 1.28;
resize: none;
box-shadow: transparent 0px 0px 0px 1px;
font-size: 24px;
font-family: 'Droid Sans';
font-weight: 700;
padding: 7px 7px 8px;
background: rgb(255, 255, 255);
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
transition: background 0.1s ease 0s;
overflow-y: hidden;
width: 100%;
color: rgb(23, 43, 77);
&:hover {
background: rgb(235, 236, 240);
}
&:focus {
box-shadow: rgb(76, 154, 255) 0px 0px 0px 1px;
background: rgb(255, 255, 255);
border-width: 1px;
border-style: solid;
border-color: rgb(76, 154, 255);
border-image: initial;
}
`;
export const TaskDetailsLabel = styled.div`
padding: 20px 0px 12px;
font-size: 15px;
font-weight: 600;
`;
export const TaskDetailsAddDetailsButton = styled.div`
background-color: rgba(9, 30, 66, 0.04);
box-shadow: none;
border: none;
border-radius: 3px;
display: block;
min-height: 56px;
padding: 8px 12px;
text-decoration: none;
font-size: 14px;
cursor: pointer;
&:hover {
background-color: rgba(9, 30, 66, 0.08);
box-shadow: none;
border: none;
}
`;
export const TaskDetailsEditorWrapper = styled.div`
display: block;
float: left;
padding-bottom: 9px;
z-index: 50;
width: 100%;
`;
export const TaskDetailsEditor = styled(TextareaAutosize)`
width: 100%;
min-height: 108px;
background: #fff;
box-shadow: none;
border-color: rgba(9, 30, 66, 0.13);
border-radius: 3px;
line-height: 20px;
padding: 8px 12px;
outline: none;
border: none;
&:focus {
background: #fff;
border: none;
box-shadow: inset 0 0 0 2px #0079bf;
}
`;
export const TaskDetailsMarkdown = styled.div`
width: 100%;
cursor: pointer;
`;
export const TaskDetailsControls = styled.div`
clear: both;
margin-top: 8px;
`;
export const ConfirmSave = styled.div`
background-color: #5aac44;
box-shadow: none;
border: none;
color: #fff;
float: left;
margin: 0 4px 0 0;
cursor: pointer;
display: inline-block;
font-weight: 400;
line-height: 20px;
padding: 6px 12px;
text-align: center;
border-radius: 3px;
font-size: 14px;
`;
export const CancelEdit = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
cursor: pointer;
`;

View File

@ -0,0 +1,47 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import styled from 'styled-components';
import Modal from 'shared/components/Modal';
import TaskDetails from './';
export default {
component: TaskDetails,
title: 'TaskDetails',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#cdd3e1', default: true },
],
},
};
export const Default = () => {
const [description, setDescription] = useState('');
return (
<>
<NormalizeStyles />
<BaseStyles />
<Modal
width={1040}
onClose={action('on close')}
renderContent={() => {
return (
<TaskDetails
task={{
taskID: '1',
taskGroupID: '1',
name: 'Hello, world',
position: 1,
labels: [],
description: description,
}}
onTaskDescriptionChange={(task, desc) => setDescription(desc)}
/>
);
}}
/>
</>
);
};

View File

@ -0,0 +1,130 @@
import React, { useState, useRef, useEffect } from 'react';
import { Bin, Cross } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import {
TaskActions,
TaskAction,
TaskMeta,
TaskHeader,
TaskDetailsContent,
TaskDetailsWrapper,
TaskDetailsSidebar,
TaskDetailsTitleWrapper,
TaskDetailsTitle,
TaskDetailsLabel,
TaskDetailsAddDetailsButton,
TaskDetailsEditor,
TaskDetailsEditorWrapper,
TaskDetailsMarkdown,
TaskDetailsControls,
ConfirmSave,
CancelEdit,
} from './Styles';
type TaskContentProps = {
onEditContent: () => void;
description: string;
};
const TaskContent: React.FC<TaskContentProps> = ({ description, onEditContent }) => {
return description === '' ? (
<TaskDetailsAddDetailsButton onClick={onEditContent}>Add a more detailed description</TaskDetailsAddDetailsButton>
) : (
<TaskDetailsMarkdown onClick={onEditContent}>{description}</TaskDetailsMarkdown>
);
};
type DetailsEditorProps = {
description: string;
onTaskDescriptionChange: (newDescription: string) => void;
onCancel: () => void;
};
const DetailsEditor: React.FC<DetailsEditorProps> = ({
description: initialDescription,
onTaskDescriptionChange,
onCancel,
}) => {
const [description, setDescription] = useState(initialDescription);
const $editorWrapperRef = useRef<HTMLDivElement>(null);
const $editorRef = useRef<HTMLTextAreaElement>(null);
const handleOutsideClick = () => {
onTaskDescriptionChange(description);
};
useEffect(() => {
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
$editorRef.current.select();
}
}, []);
useOnOutsideClick($editorWrapperRef, true, handleOutsideClick, null);
return (
<TaskDetailsEditorWrapper ref={$editorWrapperRef}>
<TaskDetailsEditor
ref={$editorRef}
value={description}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(e.currentTarget.value)}
/>
<TaskDetailsControls>
<ConfirmSave>Save</ConfirmSave>
<CancelEdit onClick={onCancel}>
<Cross size={16} />
</CancelEdit>
</TaskDetailsControls>
</TaskDetailsEditorWrapper>
);
};
type TaskDetailsProps = {
task: Task;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
};
const TaskDetails: React.FC<TaskDetailsProps> = ({ task, onTaskDescriptionChange }) => {
const [editorOpen, setEditorOpen] = useState(false);
const handleClick = () => {
setEditorOpen(!editorOpen);
};
return (
<>
<TaskHeader>
<TaskMeta />
<TaskActions>
<TaskAction>
<Bin size={20} />
</TaskAction>
<TaskAction>
<Cross size={20} />
</TaskAction>
</TaskActions>
</TaskHeader>
<TaskDetailsWrapper>
<TaskDetailsContent>
<TaskDetailsTitleWrapper>
<TaskDetailsTitle value="Hello darkness my old friend" />
</TaskDetailsTitleWrapper>
<TaskDetailsLabel>Description</TaskDetailsLabel>
{editorOpen ? (
<DetailsEditor
description={task.description ?? ''}
onTaskDescriptionChange={newDescription => {
setEditorOpen(false);
onTaskDescriptionChange(task, newDescription);
}}
onCancel={() => {
setEditorOpen(false);
}}
/>
) : (
<TaskContent description={task.description ?? ''} onEditContent={handleClick} />
)}
</TaskDetailsContent>
<TaskDetailsSidebar />
</TaskDetailsWrapper>
</>
);
};
export default TaskDetails;

View File

@ -0,0 +1,22 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const Bin = ({ size, color }: Props) => {
return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
<path d="M2 5v10c0 0.55 0.45 1 1 1h9c0.55 0 1-0.45 1-1v-10h-11zM5 14h-1v-7h1v7zM7 14h-1v-7h1v7zM9 14h-1v-7h1v7zM11 14h-1v-7h1v7z" />
<path d="M13.25 2h-3.25v-1.25c0-0.412-0.338-0.75-0.75-0.75h-3.5c-0.412 0-0.75 0.338-0.75 0.75v1.25h-3.25c-0.413 0-0.75 0.337-0.75 0.75v1.25h13v-1.25c0-0.413-0.338-0.75-0.75-0.75zM9 2h-3v-0.987h3v0.987z" />
</svg>
);
};
Bin.defaultProps = {
size: 16,
color: '#000',
};
export default Bin;

View File

@ -1,5 +1,6 @@
import Cross from './Cross'; import Cross from './Cross';
import Bell from './Bell'; import Bell from './Bell';
import Bin from './Bin';
import Pencil from './Pencil'; import Pencil from './Pencil';
import Checkmark from './Checkmark'; import Checkmark from './Checkmark';
import User from './User'; import User from './User';
@ -11,4 +12,4 @@ import Stack from './Stack';
import Question from './Question'; import Question from './Question';
import Exit from './Exit'; import Exit from './Exit';
export { Cross, Bell, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock }; export { Cross, Bell, Bin, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };