forked from github/plane
[WEB-1204] fix: Kanban and calendar drag and drop in mobile (#4408)
* chore: don't show context menu on mobile devices * fix: drag and drop in mobile * chore: default show more options in mobile * fix: dnd in calendar layout
This commit is contained in:
parent
33079c826d
commit
26188f208b
@ -38,3 +38,5 @@ export const ControlLink = React.forwardRef<HTMLAnchorElement, TControlLink>((pr
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ControlLink.displayName = "ControlLink";
|
||||||
|
@ -6,6 +6,7 @@ import { ContextMenuItem } from "./item";
|
|||||||
import { cn } from "../../../helpers";
|
import { cn } from "../../../helpers";
|
||||||
// hooks
|
// hooks
|
||||||
import useOutsideClickDetector from "../../hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "../../hooks/use-outside-click-detector";
|
||||||
|
import { usePlatformOS } from "../../hooks/use-platform-os";
|
||||||
|
|
||||||
export type TContextMenuItem = {
|
export type TContextMenuItem = {
|
||||||
key: string;
|
key: string;
|
||||||
@ -38,6 +39,7 @@ const ContextMenuWithoutPortal: React.FC<ContextMenuProps> = (props) => {
|
|||||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||||
// derived values
|
// derived values
|
||||||
const renderedItems = items.filter((item) => item.shouldRender !== false);
|
const renderedItems = items.filter((item) => item.shouldRender !== false);
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@ -51,6 +53,8 @@ const ContextMenuWithoutPortal: React.FC<ContextMenuProps> = (props) => {
|
|||||||
if (!parentElement || !contextMenu) return;
|
if (!parentElement || !contextMenu) return;
|
||||||
|
|
||||||
const handleContextMenu = (e: MouseEvent) => {
|
const handleContextMenu = (e: MouseEvent) => {
|
||||||
|
if (isMobile) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
@ -83,7 +87,7 @@ const ContextMenuWithoutPortal: React.FC<ContextMenuProps> = (props) => {
|
|||||||
parentElement.removeEventListener("contextmenu", handleContextMenu);
|
parentElement.removeEventListener("contextmenu", handleContextMenu);
|
||||||
window.removeEventListener("keydown", hideContextMenu);
|
window.removeEventListener("keydown", hideContextMenu);
|
||||||
};
|
};
|
||||||
}, [contextMenuRef, isOpen, parentRef, setIsOpen, setPosition]);
|
}, [contextMenuRef, isMobile, isOpen, parentRef, setIsOpen, setPosition]);
|
||||||
|
|
||||||
// handle keyboard navigation
|
// handle keyboard navigation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
15
packages/ui/src/hooks/use-platform-os.ts
Normal file
15
packages/ui/src/hooks/use-platform-os.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const usePlatformOS = () => {
|
||||||
|
// states
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const userAgent = window.navigator.userAgent;
|
||||||
|
const isMobile = /iPhone|iPad|iPod|Android/i.test(userAgent);
|
||||||
|
|
||||||
|
if (isMobile) setIsMobile(isMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isMobile };
|
||||||
|
};
|
@ -71,7 +71,7 @@ export const CalendarIssueBlock = observer(
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
className="block w-full text-sm text-custom-text-100 rounded border-b md:border-[1px] border-custom-border-200 hover:border-custom-border-400"
|
className="block w-full text-sm text-custom-text-100 rounded border-b md:border-[1px] border-custom-border-200 hover:border-custom-border-400"
|
||||||
disabled={!!issue?.tempId}
|
disabled={!!issue?.tempId || isMobile}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
@ -105,9 +105,10 @@ export const CalendarIssueBlock = observer(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 md:hidden h-5 w-5 group-hover/calendar-block:block ${
|
className={cn("flex-shrink-0 size-5", {
|
||||||
isMenuActive ? "!block" : ""
|
"hidden group-hover/calendar-block:block": !isMobile,
|
||||||
}`}
|
block: isMenuActive,
|
||||||
|
})}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -63,7 +63,9 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
|||||||
{getProjectIdentifierById(issue.project_id)}-{issue.sequence_id}
|
{getProjectIdentifierById(issue.project_id)}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="absolute -top-1 right-0 hidden group-hover/kanban-block:block"
|
className={cn("absolute -top-1 right-0", {
|
||||||
|
"hidden group-hover/kanban-block:block": !isMobile,
|
||||||
|
})}
|
||||||
onClick={handleEventPropagation}
|
onClick={handleEventPropagation}
|
||||||
>
|
>
|
||||||
{quickActions({
|
{quickActions({
|
||||||
@ -116,6 +118,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
|||||||
// hooks
|
// hooks
|
||||||
const { workspaceSlug } = useAppRouter();
|
const { workspaceSlug } = useAppRouter();
|
||||||
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
const handleIssuePeekOverview = (issue: TIssue) =>
|
const handleIssuePeekOverview = (issue: TIssue) =>
|
||||||
workspaceSlug &&
|
workspaceSlug &&
|
||||||
@ -210,7 +213,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
disabled={!!issue?.tempId}
|
disabled={!!issue?.tempId || isMobile}
|
||||||
>
|
>
|
||||||
<RenderIfVisible
|
<RenderIfVisible
|
||||||
classNames="space-y-2 px-3 py-2"
|
classNames="space-y-2 px-3 py-2"
|
||||||
|
20
yarn.lock
20
yarn.lock
@ -7945,16 +7945,7 @@ streamx@^2.15.0, streamx@^2.16.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
bare-events "^2.2.0"
|
bare-events "^2.2.0"
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
||||||
version "4.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
||||||
dependencies:
|
|
||||||
emoji-regex "^8.0.0"
|
|
||||||
is-fullwidth-code-point "^3.0.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
|
|
||||||
string-width@^4.1.0:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@ -8034,14 +8025,7 @@ stringify-object@^3.3.0:
|
|||||||
is-obj "^1.0.1"
|
is-obj "^1.0.1"
|
||||||
is-regexp "^1.0.0"
|
is-regexp "^1.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
Loading…
Reference in New Issue
Block a user