Skip to content

Commit 21ebec6

Browse files
Ahtesham QuraishAhtesham Quraish
authored andcommitted
fix: Ensure double-click opens card instead of opening sidebar #2336
1 parent 2fb04d6 commit 21ebec6

File tree

2 files changed

+61
-19
lines changed

2 files changed

+61
-19
lines changed

src/library-authoring/collections/LibraryCollectionPage.test.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ describe('<LibraryCollectionPage />', () => {
5555
const mocks = initializeMocks();
5656
axiosMock = mocks.axiosMock;
5757
mockShowToast = mocks.mockShowToast;
58+
jest.useFakeTimers();
5859
fetchMock.mockReset();
5960

6061
// The Meilisearch client-side API uses fetch, not Axios.
@@ -89,6 +90,10 @@ describe('<LibraryCollectionPage />', () => {
8990
});
9091
});
9192

93+
afterEach(() => {
94+
jest.runOnlyPendingTimers();
95+
jest.useRealTimers();
96+
});
9297
const renderLibraryCollectionPage = async (collectionId?: string, libraryId?: string) => {
9398
const libId = libraryId || mockContentLibrary.libraryId;
9499
const colId = collectionId || mockCollection.collectionId;
@@ -354,28 +359,40 @@ describe('<LibraryCollectionPage />', () => {
354359
expect(screen.getByText(/no matching components/i)).toBeInTheDocument();
355360
});
356361

357-
it('should remove component from collection and hides sidebar', async () => {
362+
it('should remove unit from collection and hides sidebar', async () => {
358363
const url = getLibraryCollectionItemsApiUrl(
359364
mockContentLibrary.libraryId,
360365
mockCollection.collectionId,
361366
);
362367
axiosMock.onDelete(url).reply(204);
363-
const displayName = 'Introduction to Testing';
368+
const displayName = 'Test Unit';
364369
await renderLibraryCollectionPage();
365370

371+
// Wait for the unit cards to load
372+
await waitFor(() => expect(screen.getAllByTestId('container-card-menu-toggle').length).toBeGreaterThan(0));
373+
366374
// open sidebar
367375
fireEvent.click(await screen.findByText(displayName));
376+
377+
// advance timers so the sidebar opens
378+
jest.advanceTimersByTime(500);
379+
368380
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).toBeInTheDocument());
369381

370-
const menuBtns = await screen.findAllByRole('button', { name: 'Component actions menu' });
371-
// open menu
372-
fireEvent.click(menuBtns[0]);
382+
// Open menu
383+
fireEvent.click((await screen.findAllByTestId('container-card-menu-toggle'))[0]);
384+
385+
// Click remove to collection
386+
fireEvent.click(screen.getByRole('button', { name: 'Remove from collection' }));
373387

374-
fireEvent.click(await screen.findByText('Remove from collection'));
375388
await waitFor(() => {
376389
expect(axiosMock.history.delete.length).toEqual(1);
377390
});
378391
expect(mockShowToast).toHaveBeenCalledWith('Item successfully removed');
392+
393+
// advance timers so the sidebar close logic executes
394+
jest.advanceTimersByTime(500);
395+
379396
// Should close sidebar as component was removed
380397
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
381398
});
@@ -399,7 +416,7 @@ describe('<LibraryCollectionPage />', () => {
399416
expect(mockShowToast).toHaveBeenCalledWith('Failed to remove item');
400417
});
401418

402-
it('should remove unit from collection and hides sidebar', async () => {
419+
it.only('should remove unit from collection and hides sidebar', async () => {
403420
const url = getLibraryCollectionItemsApiUrl(
404421
mockContentLibrary.libraryId,
405422
mockCollection.collectionId,
@@ -413,6 +430,8 @@ describe('<LibraryCollectionPage />', () => {
413430

414431
// open sidebar
415432
fireEvent.click(await screen.findByText(displayName));
433+
// ⏩ let the 500ms pass in test-land
434+
jest.advanceTimersByTime(500);
416435
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).toBeInTheDocument());
417436

418437
// Open menu
@@ -425,6 +444,8 @@ describe('<LibraryCollectionPage />', () => {
425444
expect(axiosMock.history.delete.length).toEqual(1);
426445
});
427446
expect(mockShowToast).toHaveBeenCalledWith('Item successfully removed');
447+
// ⏩ let the 500ms pass in test-land
448+
jest.advanceTimersByTime(500);
428449
// Should close sidebar as component was removed
429450
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
430451
});

src/library-authoring/containers/ContainerCard.tsx

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { ReactNode, useCallback, useContext } from 'react';
1+
import {
2+
ReactNode, useCallback, useContext, useRef, useEffect,
3+
} from 'react';
24
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
35
import {
46
ActionRow,
@@ -26,6 +28,8 @@ import { useRunOnNextRender } from '../../utils';
2628
import BaseCard from '../components/BaseCard';
2729
import AddComponentWidget from '../components/AddComponentWidget';
2830

31+
const DOUBLE_CLICK_DELAY = 500; // ms
32+
2933
type ContainerMenuProps = {
3034
containerKey: string;
3135
displayName: string;
@@ -247,6 +251,8 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
247251
const { showOnlyPublished } = useLibraryContext();
248252
const { openContainerInfoSidebar, openItemSidebar, sidebarItemInfo } = useSidebarContext();
249253

254+
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
255+
250256
const {
251257
blockType: itemType,
252258
formatted,
@@ -269,18 +275,33 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
269275

270276
const { navigateTo } = useLibraryRoutes();
271277

272-
const selectContainer = useCallback((e?: React.MouseEvent) => {
273-
const doubleClicked = (e?.detail || 0) > 1;
274-
if (componentPickerMode) {
275-
// In component picker mode, we want to open the sidebar
276-
// without changing the URL
277-
openContainerInfoSidebar(containerKey);
278-
} else if (!doubleClicked) {
279-
openItemSidebar(containerKey, SidebarBodyItemId.ContainerInfo);
280-
} else {
281-
navigateTo({ containerId: containerKey });
278+
const selectContainer = useCallback(
279+
() => {
280+
if (clickTimerRef.current) {
281+
clearTimeout(clickTimerRef.current);
282+
clickTimerRef.current = null;
283+
284+
navigateTo({ containerId: containerKey });
285+
} else {
286+
clickTimerRef.current = setTimeout(() => {
287+
clickTimerRef.current = null;
288+
289+
if (componentPickerMode) {
290+
openContainerInfoSidebar(containerKey);
291+
} else {
292+
openItemSidebar(containerKey, SidebarBodyItemId.ContainerInfo);
293+
}
294+
}, DOUBLE_CLICK_DELAY);
295+
}
296+
},
297+
[containerKey, componentPickerMode, openContainerInfoSidebar, openItemSidebar, navigateTo],
298+
);
299+
300+
useEffect(() => () => {
301+
if (clickTimerRef.current) {
302+
clearTimeout(clickTimerRef.current);
282303
}
283-
}, [containerKey, openContainerInfoSidebar, openItemSidebar, navigateTo]);
304+
}, []);
284305

285306
return (
286307
<BaseCard

0 commit comments

Comments
 (0)