bugfix: fix Popup and QuickCardEditor overflowing off screen when near viewport bottom
This commit is contained in:
parent
596a4d904c
commit
7c11ca92f6
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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}>
|
||||
|
@ -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
|
||||
|
@ -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`
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user