Skip to content

Commit 69afe38

Browse files
committed
🐛(frontend) fix drag-and-drop move disabled by viewport breakpoint
The docs grid and the doc tree both gated the "move document" drag-and-drop on a screen-size breakpoint (isLargeScreen / isDesktop), derived from window.innerWidth. Browser zoom shrinks window.innerWidth the same way a narrower window does, so zooming in on a desktop browser silently disabled drag-and-drop even though the user was still on a mouse - matching the reported "zoomed in, can't move; zoomed out, can move" behaviour, in both the docs grid and moving a subdoc in the tree. Replace the width-based gate with a new hasCoarsePointer flag on useResponsiveStore, backed by the `(pointer: coarse)` media query. That feature reflects the actual input device rather than viewport size, so it stays correct across zoom levels/widths for mouse users while still disabling drag-and-drop for genuine touch-primary devices. Verified locally against a running dev server: before the fix the docs grid rendered zero draggable items below the 768px breakpoint; after the fix it stays draggable at any width for a mouse-driven context, and a touch-emulated context still gets the non-draggable fallback. The doc-tree canDrag/canDrop gate was verified via matchMedia('(pointer: coarse)') staying false across widths 1280-500 in a mouse context and true in a touch-emulated context. Closes #2599
1 parent 6e5dee3 commit 69afe38

3 files changed

Lines changed: 27 additions & 17 deletions

File tree

‎src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeSubpages.tsx‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
TreeDataItem,
44
TreeView,
55
TreeViewMoveResult,
6-
useResponsive,
76
useTreeContext,
87
} from '@gouvfr-lasuite/ui-components';
98
import { useRouter } from 'next/router';
@@ -13,6 +12,7 @@ import { NodeApi } from 'react-arborist';
1312
import { Overlayer } from '@/components';
1413
import { CLASS_DOC_TITLE } from '@/docs/doc-header';
1514
import { Doc, useMoveDoc } from '@/docs/doc-management';
15+
import { useResponsiveStore } from '@/stores/useResponsiveStore';
1616

1717
import { isDocNode, isWithinTreeItemActions } from '../utils';
1818

@@ -33,7 +33,7 @@ export const DocTreeSubpages = memo(function DocTreeSubpages({
3333
rootNodeId,
3434
rootItemRef,
3535
}: DocTreeSubPagesProps) {
36-
const { isDesktop } = useResponsive();
36+
const { hasCoarsePointer } = useResponsiveStore();
3737
const treeContext = useTreeContext<Doc | null>();
3838
const { mutateAsync: moveDoc } = useMoveDoc();
3939
const { query } = useRouter();
@@ -70,21 +70,21 @@ export const DocTreeSubpages = memo(function DocTreeSubpages({
7070
({ parentNode }: { parentNode: NodeApi<TreeDataItem<Doc>> | null }) => {
7171
const parentValue = parentNode?.data.value;
7272
if (!parentValue || !isDocNode(parentValue)) {
73-
return doc.abilities.move && isDesktop;
73+
return doc.abilities.move && !hasCoarsePointer;
7474
}
75-
return parentValue.abilities.move && isDesktop;
75+
return parentValue.abilities.move && !hasCoarsePointer;
7676
},
77-
[doc.abilities.move, isDesktop],
77+
[doc.abilities.move, hasCoarsePointer],
7878
);
7979

8080
const canDrag = useCallback(
8181
(node: TreeDataItem<Doc>) => {
8282
if (!isDocNode(node.value)) {
8383
return false;
8484
}
85-
return node.value.abilities.move && isDesktop;
85+
return node.value.abilities.move && !hasCoarsePointer;
8686
},
87-
[isDesktop],
87+
[hasCoarsePointer],
8888
);
8989

9090
const handleRowKeyDown = useCallback(

‎src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
} from '@dnd-kit/core';
99
import { getEventCoordinates } from '@dnd-kit/utilities';
1010
import { TreeViewMoveModeEnum, useModal } from '@gouvfr-lasuite/ui-components';
11-
import { useMediaQuery } from '@mantine/hooks';
1211
import dynamic from 'next/dynamic';
1312
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1413
import { createPortal } from 'react-dom';
@@ -17,6 +16,7 @@ import { css } from 'styled-components';
1716

1817
import { Card, Text } from '@/components';
1918
import { Doc, useMoveDoc, useTrans } from '@/docs/doc-management';
19+
import { useResponsiveStore } from '@/stores/useResponsiveStore';
2020

2121
import { DocDragEndData, useDragAndDrop } from '../hooks/useDragAndDrop';
2222

@@ -322,24 +322,18 @@ export const DraggableDocGridItem = ({
322322
);
323323
};
324324

325-
/**
326-
* Browser zoom shrinks the viewport below the mobile breakpoint on a desktop,
327-
* so dnD depends on the pointer, not on the width. `any-pointer` also covers a
328-
* mouse plugged into a touch-first computer, where the primary pointer is coarse.
329-
*/
330-
const FINE_POINTER_QUERY = '(any-pointer: fine)';
331-
332325
export const DocGridContentList = ({ docs }: DocGridContentListProps) => {
333-
const hasFinePointer = useMediaQuery(FINE_POINTER_QUERY);
326+
const { hasCoarsePointer } = useResponsiveStore();
334327

335328
if (docs.length === 0) {
336329
return null;
337330
}
338331

339-
if (hasFinePointer) {
332+
if (!hasCoarsePointer) {
340333
return <DraggableDocGridContentList docs={docs} />;
341334
}
342335

336+
// Render a non-draggable list for touch-primary devices.
343337
return docs.map((doc) => (
344338
<DocsGridItem key={doc.id} dragMode={false} doc={doc} />
345339
));

‎src/frontend/apps/impress/src/stores/useResponsiveStore.tsx‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export interface UseResponsiveStore {
1919
screenWidth: number;
2020
setScreenSize: (size: ScreenSize) => void;
2121
isDesktop: boolean;
22+
/**
23+
* Indicates whether the primary input device has a coarse pointer (e.g., a touchscreen)
24+
* rather than a fine pointer (e.g., a mouse or trackpad).
25+
*/
26+
hasCoarsePointer: boolean;
2227
initializeResizeListener: () => () => void;
2328
}
2429

@@ -28,6 +33,7 @@ const initialState = {
2833
isSmallMobile: false,
2934
isTablet: false,
3035
isDesktop: false,
36+
hasCoarsePointer: false,
3137
screenSize: 'desktop' as ScreenSize,
3238
screenWidth: 0,
3339
};
@@ -38,6 +44,7 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
3844
isMobile: initialState.isMobile,
3945
isSmallMobile: initialState.isSmallMobile,
4046
isTablet: initialState.isTablet,
47+
hasCoarsePointer: initialState.hasCoarsePointer,
4148
screenSize: initialState.screenSize,
4249
screenWidth: initialState.screenWidth,
4350
setScreenSize: (size: ScreenSize) => set(() => ({ screenSize: size })),
@@ -107,9 +114,18 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
107114

108115
resizeHandler();
109116

117+
// Listen for changes to the primary input device's pointer type (coarse vs fine).
118+
const coarsePointerQuery = window.matchMedia('(pointer: coarse)');
119+
const pointerHandler = () => {
120+
set({ hasCoarsePointer: coarsePointerQuery.matches });
121+
};
122+
pointerHandler();
123+
coarsePointerQuery.addEventListener('change', pointerHandler);
124+
110125
return () => {
111126
clearTimeout(resizeTimeout);
112127
window.removeEventListener('resize', debouncedResizeHandler);
128+
coarsePointerQuery.removeEventListener('change', pointerHandler);
113129
};
114130
},
115131
}));

0 commit comments

Comments
 (0)