From 7c11ca92f60ed2ee7fe2f2bb46c06398840e4309 Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Tue, 23 Jun 2020 20:08:48 -0500 Subject: [PATCH] bugfix: fix Popup and QuickCardEditor overflowing off screen when near viewport bottom --- web/src/Projects/Project/index.tsx | 30 ++++++++-------- web/src/shared/components/Card/index.tsx | 15 ++------ web/src/shared/components/Lists/index.tsx | 2 +- web/src/shared/components/PopupMenu/Styles.ts | 34 ++++++++++++++++--- web/src/shared/components/PopupMenu/index.tsx | 32 +++++++++-------- .../QuickCardEditor.stories.tsx | 9 +++-- .../components/QuickCardEditor/Styles.ts | 19 +++++++++-- .../components/QuickCardEditor/index.tsx | 29 +++++++++++----- 8 files changed, 106 insertions(+), 64 deletions(-) diff --git a/web/src/Projects/Project/index.tsx b/web/src/Projects/Project/index.tsx index ef22601..c90fbcc 100644 --- a/web/src/Projects/Project/index.tsx +++ b/web/src/Projects/Project/index.tsx @@ -75,9 +75,7 @@ type TaskRouteProps = { interface QuickCardEditorState { isOpen: boolean; - left: number; - top: number; - width: number; + target: React.RefObject | null; taskID: string | null; taskGroupID: string | null; } @@ -224,9 +222,7 @@ const initialQuickCardEditorState: QuickCardEditorState = { taskID: null, taskGroupID: null, isOpen: false, - top: 0, - left: 0, - width: 272, + target: null, }; const ProjectBar = styled.div` @@ -511,14 +507,18 @@ const Project = () => { } if (data) { console.log(data.findProject); - const onQuickEditorOpen = (e: ContextMenuEvent) => { - const taskGroup = data.findProject.taskGroups.find(t => t.id === e.taskGroupID); - const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === e.taskID) : null; + const onQuickEditorOpen = ($target: React.RefObject, taskID: string, taskGroupID: string) => { + if ($target && $target.current) { + const pos = $target.current.getBoundingClientRect(); + const height = 120; + if (window.innerHeight - pos.bottom < height) { + } + } + const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID); + const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === taskID) : null; if (currentTask) { setQuickCardEditor({ - top: e.top, - left: e.left, - width: e.width, + target: $target, isOpen: true, taskID: currentTask.id, taskGroupID: currentTask.taskGroup.id, @@ -675,7 +675,7 @@ const Project = () => { ); }} /> - {quickCardEditor.isOpen && currentQuickTask && ( + {quickCardEditor.isOpen && currentQuickTask && quickCardEditor.target && ( setQuickCardEditor(initialQuickCardEditorState)} @@ -780,9 +780,7 @@ const Project = () => { onToggleComplete={task => { setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } }); }} - top={quickCardEditor.top} - left={quickCardEditor.left} - width={quickCardEditor.width} + target={quickCardEditor.target} /> )} void; + onContextMenu?: ($target: React.RefObject, taskID: string, taskGroupID: string) => void; onClick?: (e: React.MouseEvent) => void; description?: null | string; dueDate?: DueDate; @@ -101,17 +101,8 @@ const Card = React.forwardRef( const [isActive, setActive] = useState(false); const $innerCardRef: any = useRef(null); const onOpenComposer = () => { - if (typeof $innerCardRef.current !== 'undefined') { - const pos = $innerCardRef.current.getBoundingClientRect(); - if (onContextMenu) { - onContextMenu({ - width: pos.width, - top: pos.top, - left: pos.left, - taskGroupID, - taskID, - }); - } + if (onContextMenu) { + onContextMenu($innerCardRef, taskID, taskGroupID); } }; const onTaskContext = (e: React.MouseEvent) => { diff --git a/web/src/shared/components/Lists/index.tsx b/web/src/shared/components/Lists/index.tsx index f3be576..1bdff5a 100644 --- a/web/src/shared/components/Lists/index.tsx +++ b/web/src/shared/components/Lists/index.tsx @@ -22,7 +22,7 @@ interface SimpleProps { onTaskClick: (task: Task) => void; onCreateTask: (taskGroupID: string, name: string) => void; onChangeTaskGroupName: (taskGroupID: string, name: string) => void; - onQuickEditorOpen: (e: ContextMenuEvent) => void; + onQuickEditorOpen: ($target: React.RefObject, taskID: string, taskGroupID: string) => void; onCreateTaskGroup: (listName: string) => void; onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject) => void; onCardMemberClick: OnCardMemberClick; diff --git a/web/src/shared/components/PopupMenu/Styles.ts b/web/src/shared/components/PopupMenu/Styles.ts index d971047..b4aa452 100644 --- a/web/src/shared/components/PopupMenu/Styles.ts +++ b/web/src/shared/components/PopupMenu/Styles.ts @@ -1,7 +1,14 @@ import styled, { css } from 'styled-components'; import { mixin } from 'shared/utils/styles'; -export const Container = styled.div<{ invert: boolean; top: number; left: number; ref: any; width: number | string }>` +export const Container = styled.div<{ + invertY: boolean; + invert: boolean; + top: number; + left: number; + ref: any; + width: number | string; +}>` left: ${props => props.left}px; top: ${props => props.top}px; display: block; @@ -15,6 +22,14 @@ export const Container = styled.div<{ invert: boolean; top: number; left: number css` transform: translate(-100%); `} + ${props => + props.invertY && + css` + top: auto; + padding-top: 0; + padding-bottom: 10px; + bottom: ${props.top}px; + `} `; export const Wrapper = styled.div` @@ -331,16 +346,25 @@ export const PreviousButton = styled.div` cursor: pointer; `; -export const ContainerDiamond = styled.div<{ invert: boolean }>` - top: 10px; +export const ContainerDiamond = styled.div<{ invert: boolean; invertY: boolean }>` ${props => (props.invert ? 'right: 10px; ' : 'left: 15px;')} position: absolute; width: 10px; height: 10px; display: block; + ${props => + props.invertY + ? css` + bottom: 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + border-right: 1px solid rgba(0, 0, 0, 0.1); + ` + : css` + top: 10px; + border-top: 1px solid rgba(0, 0, 0, 0.1); + border-left: 1px solid rgba(0, 0, 0, 0.1); + `} transform: rotate(45deg) translate(-7px); - border-top: 1px solid rgba(0, 0, 0, 0.1); - border-left: 1px solid rgba(0, 0, 0, 0.1); z-index: 10; background: #262c49; diff --git a/web/src/shared/components/PopupMenu/index.tsx b/web/src/shared/components/PopupMenu/index.tsx index 5793a91..60e6eb1 100644 --- a/web/src/shared/components/PopupMenu/index.tsx +++ b/web/src/shared/components/PopupMenu/index.tsx @@ -31,24 +31,17 @@ type PopupContainerProps = { top: number; left: number; invert: boolean; + invertY: boolean; onClose: () => void; width?: string | number; }; -const PopupContainer: React.FC = ({ width, top, left, onClose, children, invert }) => { +const PopupContainer: React.FC = ({ width, top, left, onClose, children, invert, invertY }) => { const $containerRef = useRef(null); const [currentTop, setCurrentTop] = useState(top); useOnOutsideClick($containerRef, true, onClose, null); - useEffect(() => { - if ($containerRef && $containerRef.current) { - const bounding = $containerRef.current.getBoundingClientRect(); - if (bounding.bottom > (window.innerHeight || document.documentElement.clientHeight)) { - setCurrentTop(44); - } - } - }, []); return ( - + {children} ); @@ -74,6 +67,7 @@ type PopupState = { isOpen: boolean; left: number; top: number; + invertY: boolean; invert: boolean; currentTab: number; previousTab: number; @@ -90,6 +84,7 @@ const defaultState = { left: 0, top: 0, invert: false, + invertY: false, currentTab: 0, previousTab: 0, content: null, @@ -100,12 +95,18 @@ export const PopupProvider: React.FC = ({ children }) => { const show = (target: RefObject, content: JSX.Element, width?: number | string) => { if (target && target.current) { const bounds = target.current.getBoundingClientRect(); - const top = bounds.top + bounds.height; + let top = bounds.top + bounds.height; + let invertY = false; + if (window.innerHeight / 2 < top) { + top = window.innerHeight - bounds.top; + invertY = true; + } if (bounds.left + 304 + 30 > window.innerWidth) { setState({ isOpen: true, left: bounds.left + bounds.width, top, + invertY, invert: true, currentTab: 0, previousTab: 0, @@ -118,6 +119,7 @@ export const PopupProvider: React.FC = ({ children }) => { left: bounds.left, top, invert: false, + invertY, currentTab: 0, previousTab: 0, content, @@ -132,6 +134,7 @@ export const PopupProvider: React.FC = ({ children }) => { left: 0, top: 0, invert: true, + invertY: false, currentTab: 0, previousTab: 0, content: null, @@ -140,7 +143,7 @@ export const PopupProvider: React.FC = ({ children }) => { const portalTarget = canUseDOM ? document.body : null; // appease flow const setTab = (newTab: number, width?: number | string) => { - let newWidth = width ?? currentState.width; + const newWidth = width ?? currentState.width; setState((prevState: PopupState) => { return { ...prevState, @@ -161,6 +164,7 @@ export const PopupProvider: React.FC = ({ children }) => { currentState.isOpen && createPortal( { width={currentState.width ?? 316} > {currentState.content} - + , portalTarget, )} @@ -192,7 +196,7 @@ const PopupMenu: React.FC = ({ width, title, top, left, onClose, noHeader useOnOutsideClick($containerRef, true, onClose, null); return ( - + {onPrevious && ( diff --git a/web/src/shared/components/QuickCardEditor/QuickCardEditor.stories.tsx b/web/src/shared/components/QuickCardEditor/QuickCardEditor.stories.tsx index a181245..bfc2b04 100644 --- a/web/src/shared/components/QuickCardEditor/QuickCardEditor.stories.tsx +++ b/web/src/shared/components/QuickCardEditor/QuickCardEditor.stories.tsx @@ -47,11 +47,12 @@ export const Default = () => { }, }; const [isEditorOpen, setEditorOpen] = useState(false); + const [target, setTarget] = useState>(null); const [top, setTop] = useState(0); const [left, setLeft] = useState(0); return ( <> - {isEditorOpen && ( + {isEditorOpen && target && ( setEditorOpen(false)} @@ -61,8 +62,7 @@ export const Default = () => { onOpenMembersPopup={action('open popup')} onToggleComplete={action('complete')} onArchiveCard={action('archive card')} - top={top} - left={left} + target={target} /> )} { title={task.name} onClick={action('on click')} onContextMenu={e => { - setTop(e.top); - setLeft(e.left); + setTarget($cardRef); setEditorOpen(true); }} watched diff --git a/web/src/shared/components/QuickCardEditor/Styles.ts b/web/src/shared/components/QuickCardEditor/Styles.ts index dae83a8..dd0fecf 100644 --- a/web/src/shared/components/QuickCardEditor/Styles.ts +++ b/web/src/shared/components/QuickCardEditor/Styles.ts @@ -1,4 +1,4 @@ -import styled, { keyframes } from 'styled-components'; +import styled, { keyframes, css } from 'styled-components'; import TextareaAutosize from 'react-autosize-textarea'; import { mixin } from 'shared/utils/styles'; @@ -14,11 +14,18 @@ export const Wrapper = styled.div<{ open: boolean }>` visibility: ${props => (props.open ? 'show' : 'hidden')}; `; -export const Container = styled.div<{ width: number; top: number; left: number }>` +export const Container = styled.div<{ fixed: boolean; width: number; top: number; left: number }>` position: absolute; width: ${props => props.width}px; top: ${props => props.top}px; left: ${props => props.left}px; + + ${props => + props.fixed && + css` + top: auto; + bottom: ${props.top}px; + `} `; export const SaveButton = styled.button` @@ -41,13 +48,19 @@ from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } `; -export const EditorButtons = styled.div` +export const EditorButtons = styled.div<{ fixed: boolean }>` left: 100%; position: absolute; top: 0; width: 240px; z-index: 0; animation: ${FadeInAnimation} 85ms ease-in 1; + ${props => + props.fixed && + css` + top: auto; + bottom: 8px; + `} `; export const EditorButton = styled.div` diff --git a/web/src/shared/components/QuickCardEditor/index.tsx b/web/src/shared/components/QuickCardEditor/index.tsx index ca145c0..e5e6561 100644 --- a/web/src/shared/components/QuickCardEditor/index.tsx +++ b/web/src/shared/components/QuickCardEditor/index.tsx @@ -20,9 +20,7 @@ type Props = { onOpenDueDatePopup: ($targetRef: React.RefObject, task: Task) => void; onArchiveCard: (taskGroupID: string, taskID: string) => void; onCardMemberClick?: OnCardMemberClick; - top: number; - left: number; - width?: number; + target: React.RefObject; }; const QuickCardEditor = ({ @@ -35,9 +33,7 @@ const QuickCardEditor = ({ onCardMemberClick, onArchiveCard, onEditCard, - width = 272, - top, - left, + target: $target, }: Props) => { const [currentCardTitle, setCardTitle] = useState(task.name); const $labelsRef: any = useRef(); @@ -49,12 +45,29 @@ const QuickCardEditor = ({ onCloseEditor(); }; + const height = 180; + const saveCardButtonBarHeight = 48; + let top = 0; + let left = 0; + let width = 272; + let fixed = false; + if ($target && $target.current) { + const pos = $target.current.getBoundingClientRect(); + top = pos.top; + left = pos.left; + width = pos.width; + if (window.innerHeight - pos.height > height) { + top = window.innerHeight - pos.bottom - saveCardButtonBarHeight; + fixed = true; + } + } + return ( - + l.projectLabel)} /> onEditCard(task.taskGroup.id, task.id, currentCardTitle)}>Save - + { e.stopPropagation();