From 5059d5e351803966ce597913cef6460462f0c943 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:05:11 -0800 Subject: [PATCH] fix(ui): unable to use text inputs within draggable --- .../useCanvasEntityListDnd.ts | 2 + .../web/src/features/dnd/DndImage.tsx | 43 ++++++------ .../frontend/web/src/features/dnd/util.ts | 65 +++++++++++++++++++ .../components/ImageGrid/GalleryImage.tsx | 2 + .../workflow/useLinearViewFieldDnd.ts | 2 + 5 files changed, 95 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd.ts b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd.ts index d47e3c0c781..a036448cd19 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd.ts @@ -4,6 +4,7 @@ import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag- import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { singleCanvasEntityDndSource } from 'features/dnd/dnd'; import { type DndListTargetState, idle } from 'features/dnd/types'; +import { firefoxDndFix } from 'features/dnd/util'; import type { RefObject } from 'react'; import { useEffect, useState } from 'react'; @@ -17,6 +18,7 @@ export const useCanvasEntityListDnd = (ref: RefObject, entityIdenti return; } return combine( + firefoxDndFix(element), draggable({ element, getInitialData() { diff --git a/invokeai/frontend/web/src/features/dnd/DndImage.tsx b/invokeai/frontend/web/src/features/dnd/DndImage.tsx index e8b5a0f48cc..2a2cbf6f648 100644 --- a/invokeai/frontend/web/src/features/dnd/DndImage.tsx +++ b/invokeai/frontend/web/src/features/dnd/DndImage.tsx @@ -1,3 +1,4 @@ +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import type { ImageProps, SystemStyleObject } from '@invoke-ai/ui-library'; import { Image } from '@invoke-ai/ui-library'; @@ -5,6 +6,7 @@ import { useAppStore } from 'app/store/nanostores/store'; import { singleImageDndSource } from 'features/dnd/dnd'; import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage'; import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage'; +import { firefoxDndFix } from 'features/dnd/util'; import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; import { memo, useEffect, useState } from 'react'; import type { ImageDTO } from 'services/api/types'; @@ -35,25 +37,28 @@ export const DndImage = memo(({ imageDTO, asThumbnail, ...rest }: Props) => { if (!element) { return; } - return draggable({ - element, - getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name), - onDragStart: () => { - setIsDragging(true); - }, - onDrop: () => { - setIsDragging(false); - }, - onGenerateDragPreview: (args) => { - if (singleImageDndSource.typeGuard(args.source.data)) { - setSingleImageDragPreview({ - singleImageDndData: args.source.data, - onGenerateDragPreviewArgs: args, - setDragPreviewState, - }); - } - }, - }); + return combine( + firefoxDndFix(element), + draggable({ + element, + getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name), + onDragStart: () => { + setIsDragging(true); + }, + onDrop: () => { + setIsDragging(false); + }, + onGenerateDragPreview: (args) => { + if (singleImageDndSource.typeGuard(args.source.data)) { + setSingleImageDragPreview({ + singleImageDndData: args.source.data, + onGenerateDragPreviewArgs: args, + setDragPreviewState, + }); + } + }, + }) + ); }, [imageDTO, element, store]); useImageContextMenu(imageDTO, element); diff --git a/invokeai/frontend/web/src/features/dnd/util.ts b/invokeai/frontend/web/src/features/dnd/util.ts index f20614ad773..ca2560eaa7e 100644 --- a/invokeai/frontend/web/src/features/dnd/util.ts +++ b/invokeai/frontend/web/src/features/dnd/util.ts @@ -1,6 +1,7 @@ import type { GetOffsetFn } from '@atlaskit/pragmatic-drag-and-drop/dist/types/public-utils/element/custom-native-drag-preview/types'; import type { Input } from '@atlaskit/pragmatic-drag-and-drop/types'; import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { noop } from 'lodash-es'; import type { CSSProperties } from 'react'; /** @@ -44,3 +45,67 @@ export function triggerPostMoveFlash(element: HTMLElement, backgroundColor: CSSP iterations: 1, }); } + +/** + * Firefox has a bug where input or textarea elements with draggable parents do not allow selection of their text. + * + * This helper function implements a workaround by setting the draggable attribute to false when the mouse is over a + * input or textarea child of the draggable. It reverts the attribute on mouse out. + * + * The fix is only applied for Firefox, and should be used in every `pragmatic-drag-and-drop` `draggable`. + * + * See: + * - https://github.com/atlassian/pragmatic-drag-and-drop/issues/111 + * - https://bugzilla.mozilla.org/show_bug.cgi?id=1853069 + * + * @example + * ```tsx + * useEffect(() => { + * const element = ref.current; + * if (!element) { + * return; + * } + * return combine( + * firefoxDndFix(element), + * // The rest of the draggable setup is the same + * draggable({ + * element, + * // ... + * }), + * ); + *``` + * @param element The draggable element + * @returns A cleanup function that removes the event listeners + */ +export const firefoxDndFix = (element: HTMLElement): (() => void) => { + if (!navigator.userAgent.includes('Firefox')) { + return noop; + } + + const abortController = new AbortController(); + + element.addEventListener( + 'mouseover', + (event) => { + if (event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLInputElement) { + element.setAttribute('draggable', 'false'); + } + }, + { signal: abortController.signal } + ); + + element.addEventListener( + 'mouseout', + (event) => { + if (event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLInputElement) { + element.setAttribute('draggable', 'true'); + } + }, + { signal: abortController.signal } + ); + + return () => { + element.setAttribute('draggable', 'true'); + abortController.abort(); + }; +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index 8efed872104..2e754357820 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -12,6 +12,7 @@ import type { DndDragPreviewMultipleImageState } from 'features/dnd/DndDragPrevi import { createMultipleImageDragPreview, setMultipleImageDragPreview } from 'features/dnd/DndDragPreviewMultipleImage'; import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage'; import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage'; +import { firefoxDndFix } from 'features/dnd/util'; import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons'; import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId'; @@ -115,6 +116,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { return; } return combine( + firefoxDndFix(element), draggable({ element, getInitialData: () => { diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd.ts index 745c8ecdd33..fbd6b5bbda1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd.ts +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd.ts @@ -4,6 +4,7 @@ import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag- import { singleWorkflowFieldDndSource } from 'features/dnd/dnd'; import type { DndListTargetState } from 'features/dnd/types'; import { idle } from 'features/dnd/types'; +import { firefoxDndFix } from 'features/dnd/util'; import type { FieldIdentifier } from 'features/nodes/types/field'; import type { RefObject } from 'react'; import { useEffect, useState } from 'react'; @@ -18,6 +19,7 @@ export const useLinearViewFieldDnd = (ref: RefObject, fieldIdentifi return; } return combine( + firefoxDndFix(element), draggable({ element, getInitialData() {