mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
📱 fix: Don't Connect the Favorites Drag Source on Touch Pointers (#14312)
#14272 gated the hover-revealed "..." button on hover capability, but pinned agents still take two taps on iOS. That fix was aimed at the wrong mechanism for this list. Every favorite row is wrapped by DraggableFavoriteItem, and react-dnd's HTML5Backend stamps `draggable="true"` on that wrapper unconditionally (connectDragSource, HTML5BackendImpl.js:101 — `canDrag: false` does not suppress it, react-dnd#2909). iOS Safari hands a touch on a draggable element to the drag recognizer rather than synthesizing a click, so the row underneath only selects on the second tap. The draggable wrapper is what separates favorites from every other sidebar row. Conversation rows are more hover-dependent than favorites ever were (ungated `opacity-0 group-hover:opacity-100` plus an onMouseEnter that mounts ConvoOptions) and select on the first tap. Connect the drag source only under `(hover: hover)`. Nothing is lost on touch: HTML5Backend has no touch support, so drag-to-reorder never worked there. Passing null to the connector unsubscribes cleanly and resets the attribute, so a hybrid pointer flipping the query re-arms drag.
This commit is contained in:
parent
bd1df30b7d
commit
b04ff2648e
2 changed files with 50 additions and 2 deletions
|
|
@ -2,9 +2,9 @@ import React, { useRef, useCallback, useMemo, useEffect, memo } from 'react';
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { Skeleton } from '@librechat/client';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueries } from '@tanstack/react-query';
|
||||
import { Skeleton, useMediaQuery } from '@librechat/client';
|
||||
import { QueryKeys, EModelEndpoint, dataService } from 'librechat-data-provider';
|
||||
import type { Agent, TEndpointsConfig, TModelSpec } from 'librechat-data-provider';
|
||||
import {
|
||||
|
|
@ -63,6 +63,13 @@ const DraggableFavoriteItem = ({
|
|||
children,
|
||||
}: DraggableFavoriteItemProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
/**
|
||||
* HTML5 drag needs a hover-capable pointer. Connecting the drag source on touch would
|
||||
* stamp `draggable="true"` on this wrapper, and iOS Safari hands a touch on a draggable
|
||||
* element to the drag recognizer instead of synthesizing a click, so the row underneath
|
||||
* only selects on the second tap.
|
||||
*/
|
||||
const canDrag = useMediaQuery('(hover: hover)');
|
||||
const [{ handlerId }, drop] = useDrop<{ index: number; id: string }, unknown, { handlerId: any }>(
|
||||
{
|
||||
accept: 'favorite-item',
|
||||
|
|
@ -118,7 +125,8 @@ const DraggableFavoriteItem = ({
|
|||
});
|
||||
|
||||
const opacity = isDragging ? 0 : 1;
|
||||
drag(drop(ref));
|
||||
drop(ref);
|
||||
drag(canDrag ? ref : null);
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
|
||||
|
|
|
|||
|
|
@ -526,4 +526,44 @@ describe('FavoritesList', () => {
|
|||
expect(types).toEqual(['agent', 'model', 'spec']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('drag source by pointer capability', () => {
|
||||
const mockHover = (hasHover: boolean) => {
|
||||
(window.matchMedia as jest.Mock).mockImplementation((query: string) => ({
|
||||
matches: query === '(hover: hover)' ? hasHover : false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
}));
|
||||
};
|
||||
|
||||
const renderFavoriteWrapper = async () => {
|
||||
mockFavorites.push({ model: 'gpt-5', endpoint: 'openai' });
|
||||
const { findByTestId } = renderWithProviders(<FavoritesList />);
|
||||
const item = await findByTestId('favorite-item');
|
||||
return item.closest('[data-handler-id]');
|
||||
};
|
||||
|
||||
it('connects the drag source on a hover-capable pointer', async () => {
|
||||
mockHover(true);
|
||||
|
||||
const wrapper = await renderFavoriteWrapper();
|
||||
|
||||
await waitFor(() => expect(wrapper).toHaveAttribute('draggable', 'true'));
|
||||
});
|
||||
|
||||
it('leaves the row undraggable on touch so the first tap selects it', async () => {
|
||||
mockHover(false);
|
||||
|
||||
const wrapper = await renderFavoriteWrapper();
|
||||
|
||||
/** iOS Safari gives a touch on a draggable element to the drag recognizer
|
||||
* instead of synthesizing a click, costing the row its first tap. */
|
||||
expect(wrapper).not.toHaveAttribute('draggable', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue