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 {
isOpen: boolean;
left: number;
top: number;
width: number;
target: React.RefObject<HTMLElement> | 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<HTMLElement>, 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 && (
<QuickCardEditor
task={currentQuickTask}
onCloseEditor={() => 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}
/>
)}
<Route

View File

@ -38,7 +38,7 @@ type Props = {
taskID: string;
taskGroupID: string;
complete?: boolean;
onContextMenu?: (e: ContextMenuEvent) => void;
onContextMenu?: ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => 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) => {

View File

@ -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<HTMLElement>, taskID: string, taskGroupID: string) => void;
onCreateTaskGroup: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
onCardMemberClick: OnCardMemberClick;

View File

@ -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;

View File

@ -31,24 +31,17 @@ type PopupContainerProps = {
top: number;
left: number;
invert: boolean;
invertY: boolean;
onClose: () => void;
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 [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 (
<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}
</Container>
);
@ -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<HTMLElement>, 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(
<PopupContainer
invertY={currentState.invertY}
invert={currentState.invert}
top={currentState.top}
left={currentState.left}
@ -168,7 +172,7 @@ export const PopupProvider: React.FC = ({ children }) => {
width={currentState.width ?? 316}
>
{currentState.content}
<ContainerDiamond invert={currentState.invert} />
<ContainerDiamond invertY={currentState.invertY} invert={currentState.invert} />
</PopupContainer>,
portalTarget,
)}
@ -192,7 +196,7 @@ const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader
useOnOutsideClick($containerRef, true, onClose, null);
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>
{onPrevious && (
<PreviousButton onClick={onPrevious}>

View File

@ -47,11 +47,12 @@ export const Default = () => {
},
};
const [isEditorOpen, setEditorOpen] = useState(false);
const [target, setTarget] = useState<null | React.RefObject<HTMLElement>>(null);
const [top, setTop] = useState(0);
const [left, setLeft] = useState(0);
return (
<>
{isEditorOpen && (
{isEditorOpen && target && (
<QuickCardEditor
task={task}
onCloseEditor={() => 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}
/>
)}
<List
@ -82,8 +82,7 @@ export const Default = () => {
title={task.name}
onClick={action('on click')}
onContextMenu={e => {
setTop(e.top);
setLeft(e.left);
setTarget($cardRef);
setEditorOpen(true);
}}
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 { 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`

View File

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