bugfix: fix Popup and QuickCardEditor overflowing off screen when near viewport bottom

This commit is contained in:
Jordan Knott 2020-06-23 20:08:48 -05:00
parent 596a4d904c
commit 7c11ca92f6
8 changed files with 106 additions and 64 deletions

View File

@ -75,9 +75,7 @@ type TaskRouteProps = {
interface QuickCardEditorState { interface QuickCardEditorState {
isOpen: boolean; isOpen: boolean;
left: number; target: React.RefObject<HTMLElement> | null;
top: number;
width: number;
taskID: string | null; taskID: string | null;
taskGroupID: string | null; taskGroupID: string | null;
} }
@ -224,9 +222,7 @@ const initialQuickCardEditorState: QuickCardEditorState = {
taskID: null, taskID: null,
taskGroupID: null, taskGroupID: null,
isOpen: false, isOpen: false,
top: 0, target: null,
left: 0,
width: 272,
}; };
const ProjectBar = styled.div` const ProjectBar = styled.div`
@ -511,14 +507,18 @@ const Project = () => {
} }
if (data) { if (data) {
console.log(data.findProject); console.log(data.findProject);
const onQuickEditorOpen = (e: ContextMenuEvent) => { const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => {
const taskGroup = data.findProject.taskGroups.find(t => t.id === e.taskGroupID); if ($target && $target.current) {
const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === e.taskID) : null; 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) { if (currentTask) {
setQuickCardEditor({ setQuickCardEditor({
top: e.top, target: $target,
left: e.left,
width: e.width,
isOpen: true, isOpen: true,
taskID: currentTask.id, taskID: currentTask.id,
taskGroupID: currentTask.taskGroup.id, taskGroupID: currentTask.taskGroup.id,
@ -675,7 +675,7 @@ const Project = () => {
); );
}} }}
/> />
{quickCardEditor.isOpen && currentQuickTask && ( {quickCardEditor.isOpen && currentQuickTask && quickCardEditor.target && (
<QuickCardEditor <QuickCardEditor
task={currentQuickTask} task={currentQuickTask}
onCloseEditor={() => setQuickCardEditor(initialQuickCardEditorState)} onCloseEditor={() => setQuickCardEditor(initialQuickCardEditorState)}
@ -780,9 +780,7 @@ const Project = () => {
onToggleComplete={task => { onToggleComplete={task => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } }); setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
}} }}
top={quickCardEditor.top} target={quickCardEditor.target}
left={quickCardEditor.left}
width={quickCardEditor.width}
/> />
)} )}
<Route <Route

View File

@ -38,7 +38,7 @@ type Props = {
taskID: string; taskID: string;
taskGroupID: string; taskGroupID: string;
complete?: boolean; complete?: boolean;
onContextMenu?: (e: ContextMenuEvent) => void; onContextMenu?: ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void; onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
description?: null | string; description?: null | string;
dueDate?: DueDate; dueDate?: DueDate;
@ -101,17 +101,8 @@ const Card = React.forwardRef(
const [isActive, setActive] = useState(false); const [isActive, setActive] = useState(false);
const $innerCardRef: any = useRef(null); const $innerCardRef: any = useRef(null);
const onOpenComposer = () => { const onOpenComposer = () => {
if (typeof $innerCardRef.current !== 'undefined') {
const pos = $innerCardRef.current.getBoundingClientRect();
if (onContextMenu) { if (onContextMenu) {
onContextMenu({ onContextMenu($innerCardRef, taskID, taskGroupID);
width: pos.width,
top: pos.top,
left: pos.left,
taskGroupID,
taskID,
});
}
} }
}; };
const onTaskContext = (e: React.MouseEvent) => { const onTaskContext = (e: React.MouseEvent) => {

View File

@ -22,7 +22,7 @@ interface SimpleProps {
onTaskClick: (task: Task) => void; onTaskClick: (task: Task) => void;
onCreateTask: (taskGroupID: string, name: string) => void; onCreateTask: (taskGroupID: string, name: string) => void;
onChangeTaskGroupName: (taskGroupID: string, name: string) => void; onChangeTaskGroupName: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void; onQuickEditorOpen: ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => void;
onCreateTaskGroup: (listName: string) => void; onCreateTaskGroup: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void; onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
onCardMemberClick: OnCardMemberClick; onCardMemberClick: OnCardMemberClick;

View File

@ -1,7 +1,14 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles'; 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; left: ${props => props.left}px;
top: ${props => props.top}px; top: ${props => props.top}px;
display: block; display: block;
@ -15,6 +22,14 @@ export const Container = styled.div<{ invert: boolean; top: number; left: number
css` css`
transform: translate(-100%); transform: translate(-100%);
`} `}
${props =>
props.invertY &&
css`
top: auto;
padding-top: 0;
padding-bottom: 10px;
bottom: ${props.top}px;
`}
`; `;
export const Wrapper = styled.div` export const Wrapper = styled.div`
@ -331,16 +346,25 @@ export const PreviousButton = styled.div`
cursor: pointer; cursor: pointer;
`; `;
export const ContainerDiamond = styled.div<{ invert: boolean }>` export const ContainerDiamond = styled.div<{ invert: boolean; invertY: boolean }>`
top: 10px;
${props => (props.invert ? 'right: 10px; ' : 'left: 15px;')} ${props => (props.invert ? 'right: 10px; ' : 'left: 15px;')}
position: absolute; position: absolute;
width: 10px; width: 10px;
height: 10px; height: 10px;
display: block; display: block;
transform: rotate(45deg) translate(-7px); ${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-top: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1); border-left: 1px solid rgba(0, 0, 0, 0.1);
`}
transform: rotate(45deg) translate(-7px);
z-index: 10; z-index: 10;
background: #262c49; background: #262c49;

View File

@ -31,24 +31,17 @@ type PopupContainerProps = {
top: number; top: number;
left: number; left: number;
invert: boolean; invert: boolean;
invertY: boolean;
onClose: () => void; onClose: () => void;
width?: string | number; width?: string | number;
}; };
const PopupContainer: React.FC<PopupContainerProps> = ({ width, top, left, onClose, children, invert }) => { const PopupContainer: React.FC<PopupContainerProps> = ({ width, top, left, onClose, children, invert, invertY }) => {
const $containerRef = useRef<HTMLDivElement>(null); const $containerRef = useRef<HTMLDivElement>(null);
const [currentTop, setCurrentTop] = useState(top); const [currentTop, setCurrentTop] = useState(top);
useOnOutsideClick($containerRef, true, onClose, null); 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 ( return (
<Container width={width ?? 316} left={left} top={currentTop} ref={$containerRef} invert={invert}> <Container width={width ?? 316} left={left} top={currentTop} ref={$containerRef} invert={invert} invertY={invertY}>
{children} {children}
</Container> </Container>
); );
@ -74,6 +67,7 @@ type PopupState = {
isOpen: boolean; isOpen: boolean;
left: number; left: number;
top: number; top: number;
invertY: boolean;
invert: boolean; invert: boolean;
currentTab: number; currentTab: number;
previousTab: number; previousTab: number;
@ -90,6 +84,7 @@ const defaultState = {
left: 0, left: 0,
top: 0, top: 0,
invert: false, invert: false,
invertY: false,
currentTab: 0, currentTab: 0,
previousTab: 0, previousTab: 0,
content: null, content: null,
@ -100,12 +95,18 @@ export const PopupProvider: React.FC = ({ children }) => {
const show = (target: RefObject<HTMLElement>, content: JSX.Element, width?: number | string) => { const show = (target: RefObject<HTMLElement>, content: JSX.Element, width?: number | string) => {
if (target && target.current) { if (target && target.current) {
const bounds = target.current.getBoundingClientRect(); 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) { if (bounds.left + 304 + 30 > window.innerWidth) {
setState({ setState({
isOpen: true, isOpen: true,
left: bounds.left + bounds.width, left: bounds.left + bounds.width,
top, top,
invertY,
invert: true, invert: true,
currentTab: 0, currentTab: 0,
previousTab: 0, previousTab: 0,
@ -118,6 +119,7 @@ export const PopupProvider: React.FC = ({ children }) => {
left: bounds.left, left: bounds.left,
top, top,
invert: false, invert: false,
invertY,
currentTab: 0, currentTab: 0,
previousTab: 0, previousTab: 0,
content, content,
@ -132,6 +134,7 @@ export const PopupProvider: React.FC = ({ children }) => {
left: 0, left: 0,
top: 0, top: 0,
invert: true, invert: true,
invertY: false,
currentTab: 0, currentTab: 0,
previousTab: 0, previousTab: 0,
content: null, content: null,
@ -140,7 +143,7 @@ export const PopupProvider: React.FC = ({ children }) => {
const portalTarget = canUseDOM ? document.body : null; // appease flow const portalTarget = canUseDOM ? document.body : null; // appease flow
const setTab = (newTab: number, width?: number | string) => { const setTab = (newTab: number, width?: number | string) => {
let newWidth = width ?? currentState.width; const newWidth = width ?? currentState.width;
setState((prevState: PopupState) => { setState((prevState: PopupState) => {
return { return {
...prevState, ...prevState,
@ -161,6 +164,7 @@ export const PopupProvider: React.FC = ({ children }) => {
currentState.isOpen && currentState.isOpen &&
createPortal( createPortal(
<PopupContainer <PopupContainer
invertY={currentState.invertY}
invert={currentState.invert} invert={currentState.invert}
top={currentState.top} top={currentState.top}
left={currentState.left} left={currentState.left}
@ -168,7 +172,7 @@ export const PopupProvider: React.FC = ({ children }) => {
width={currentState.width ?? 316} width={currentState.width ?? 316}
> >
{currentState.content} {currentState.content}
<ContainerDiamond invert={currentState.invert} /> <ContainerDiamond invertY={currentState.invertY} invert={currentState.invert} />
</PopupContainer>, </PopupContainer>,
portalTarget, portalTarget,
)} )}
@ -192,7 +196,7 @@ const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader
useOnOutsideClick($containerRef, true, onClose, null); useOnOutsideClick($containerRef, true, onClose, null);
return ( return (
<Container width={width ?? 316} invert={false} left={left} top={top} ref={$containerRef}> <Container invertY={false} width={width ?? 316} invert={false} left={left} top={top} ref={$containerRef}>
<Wrapper> <Wrapper>
{onPrevious && ( {onPrevious && (
<PreviousButton onClick={onPrevious}> <PreviousButton onClick={onPrevious}>

View File

@ -47,11 +47,12 @@ export const Default = () => {
}, },
}; };
const [isEditorOpen, setEditorOpen] = useState(false); const [isEditorOpen, setEditorOpen] = useState(false);
const [target, setTarget] = useState<null | React.RefObject<HTMLElement>>(null);
const [top, setTop] = useState(0); const [top, setTop] = useState(0);
const [left, setLeft] = useState(0); const [left, setLeft] = useState(0);
return ( return (
<> <>
{isEditorOpen && ( {isEditorOpen && target && (
<QuickCardEditor <QuickCardEditor
task={task} task={task}
onCloseEditor={() => setEditorOpen(false)} onCloseEditor={() => setEditorOpen(false)}
@ -61,8 +62,7 @@ export const Default = () => {
onOpenMembersPopup={action('open popup')} onOpenMembersPopup={action('open popup')}
onToggleComplete={action('complete')} onToggleComplete={action('complete')}
onArchiveCard={action('archive card')} onArchiveCard={action('archive card')}
top={top} target={target}
left={left}
/> />
)} )}
<List <List
@ -82,8 +82,7 @@ export const Default = () => {
title={task.name} title={task.name}
onClick={action('on click')} onClick={action('on click')}
onContextMenu={e => { onContextMenu={e => {
setTop(e.top); setTarget($cardRef);
setLeft(e.left);
setEditorOpen(true); setEditorOpen(true);
}} }}
watched watched

View File

@ -1,4 +1,4 @@
import styled, { keyframes } from 'styled-components'; import styled, { keyframes, css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
@ -14,11 +14,18 @@ export const Wrapper = styled.div<{ open: boolean }>`
visibility: ${props => (props.open ? 'show' : 'hidden')}; 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; position: absolute;
width: ${props => props.width}px; width: ${props => props.width}px;
top: ${props => props.top}px; top: ${props => props.top}px;
left: ${props => props.left}px; left: ${props => props.left}px;
${props =>
props.fixed &&
css`
top: auto;
bottom: ${props.top}px;
`}
`; `;
export const SaveButton = styled.button` export const SaveButton = styled.button`
@ -41,13 +48,19 @@ from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); } to { opacity: 1; transform: translateX(0); }
`; `;
export const EditorButtons = styled.div` export const EditorButtons = styled.div<{ fixed: boolean }>`
left: 100%; left: 100%;
position: absolute; position: absolute;
top: 0; top: 0;
width: 240px; width: 240px;
z-index: 0; z-index: 0;
animation: ${FadeInAnimation} 85ms ease-in 1; animation: ${FadeInAnimation} 85ms ease-in 1;
${props =>
props.fixed &&
css`
top: auto;
bottom: 8px;
`}
`; `;
export const EditorButton = styled.div` export const EditorButton = styled.div`

View File

@ -20,9 +20,7 @@ type Props = {
onOpenDueDatePopup: ($targetRef: React.RefObject<HTMLElement>, task: Task) => void; onOpenDueDatePopup: ($targetRef: React.RefObject<HTMLElement>, task: Task) => void;
onArchiveCard: (taskGroupID: string, taskID: string) => void; onArchiveCard: (taskGroupID: string, taskID: string) => void;
onCardMemberClick?: OnCardMemberClick; onCardMemberClick?: OnCardMemberClick;
top: number; target: React.RefObject<HTMLElement>;
left: number;
width?: number;
}; };
const QuickCardEditor = ({ const QuickCardEditor = ({
@ -35,9 +33,7 @@ const QuickCardEditor = ({
onCardMemberClick, onCardMemberClick,
onArchiveCard, onArchiveCard,
onEditCard, onEditCard,
width = 272, target: $target,
top,
left,
}: Props) => { }: Props) => {
const [currentCardTitle, setCardTitle] = useState(task.name); const [currentCardTitle, setCardTitle] = useState(task.name);
const $labelsRef: any = useRef(); const $labelsRef: any = useRef();
@ -49,12 +45,29 @@ const QuickCardEditor = ({
onCloseEditor(); 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 ( return (
<Wrapper onClick={handleCloseEditor} open> <Wrapper onClick={handleCloseEditor} open>
<CloseButton onClick={handleCloseEditor}> <CloseButton onClick={handleCloseEditor}>
<Cross width={16} height={16} /> <Cross width={16} height={16} />
</CloseButton> </CloseButton>
<Container width={width} left={left} top={top}> <Container fixed={fixed} width={width} left={left} top={top}>
<Card <Card
editable editable
onCardMemberClick={onCardMemberClick} onCardMemberClick={onCardMemberClick}
@ -70,7 +83,7 @@ const QuickCardEditor = ({
labels={task.labels.map(l => l.projectLabel)} labels={task.labels.map(l => l.projectLabel)}
/> />
<SaveButton onClick={() => onEditCard(task.taskGroup.id, task.id, currentCardTitle)}>Save</SaveButton> <SaveButton onClick={() => onEditCard(task.taskGroup.id, task.id, currentCardTitle)}>Save</SaveButton>
<EditorButtons> <EditorButtons fixed={fixed}>
<EditorButton <EditorButton
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();