From 9d35dbfaf08a3b865a1890779a3947cca4019898 Mon Sep 17 00:00:00 2001 From: Tomas Engebretsen Date: Mon, 2 Oct 2023 08:15:46 +0200 Subject: [PATCH] Eliminate the need to provide index in DragAndDrop (#11252) --- .../DragAndDropList/DragAndDropList.test.tsx | 62 +++++ .../DragAndDropList/DragAndDropList.tsx | 7 +- .../DragAndDropListItem.test.tsx | 70 +++++ .../DragAndDropListItem.tsx | 28 +- .../DragAndDropListItemContext.ts | 4 +- .../DragAndDropProvider.tsx | 5 +- .../DragAndDropRootContext.ts | 1 + .../hooks/useDomSelectors.test.tsx | 50 ++++ .../dragAndDrop/hooks/useDomSelectors.ts | 37 +++ .../hooks/useIsParentDisabled.test.tsx | 13 +- .../dragAndDrop/hooks/useParentId.test.tsx | 2 +- .../dragAndDrop/utils/domUtils.test.tsx | 246 ++++++++++++++++++ .../components/dragAndDrop/utils/domUtils.ts | 39 +++ .../ux-editor/src/containers/DesignView.tsx | 3 +- 14 files changed, 539 insertions(+), 28 deletions(-) create mode 100644 frontend/packages/shared/src/components/dragAndDrop/DragAndDropList/DragAndDropList.test.tsx create mode 100644 frontend/packages/shared/src/components/dragAndDrop/DragAndDropListItem/DragAndDropListItem.test.tsx create mode 100644 frontend/packages/shared/src/components/dragAndDrop/hooks/useDomSelectors.test.tsx create mode 100644 frontend/packages/shared/src/components/dragAndDrop/hooks/useDomSelectors.ts create mode 100644 frontend/packages/shared/src/components/dragAndDrop/utils/domUtils.test.tsx create mode 100644 frontend/packages/shared/src/components/dragAndDrop/utils/domUtils.ts diff --git a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropList/DragAndDropList.test.tsx b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropList/DragAndDropList.test.tsx new file mode 100644 index 00000000000..8c430c03713 --- /dev/null +++ b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropList/DragAndDropList.test.tsx @@ -0,0 +1,62 @@ +import React, { ReactNode } from 'react'; +import { DragAndDropList } from './'; +import { + DragAndDropListItemContext, + DragAndDropListItemContextProps, +} from '../DragAndDropListItem/DragAndDropListItemContext'; +import { + DragAndDropRootContext, + DragAndDropRootContextProps, +} from '../DragAndDropProvider/DragAndDropRootContext'; +import { render as renderRtl } from '@testing-library/react'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { domListClass, domListId } from 'app-shared/components/dragAndDrop/utils/domUtils'; + +// +const itemId = 'id'; +const rootId = 'rootId'; +const uniqueDomId = ':r0:'; +const onDrop = jest.fn(); +const defaultlistItemContextProps: DragAndDropListItemContextProps = { + isDisabled: false, + itemId, +}; +const defaultRootContextProps: DragAndDropRootContextProps = { + onDrop, + rootId, + uniqueDomId, +}; + +/* eslint-disable testing-library/no-node-access */ +describe('DragAndDropList', () => { + it('Renders with correct id and class name', () => { + const { container } = render()(
); + const expectedId = domListId(uniqueDomId, itemId); + const expectedClass = domListClass(uniqueDomId); + expect(container.firstChild).toHaveAttribute('id', expectedId); + expect(container.firstChild).toHaveClass(expectedClass); + }); +}); + +interface RenderProps { + listItemContextProps?: Partial; + rootContextProps?: Partial>; +} + +function render({ listItemContextProps = {}, rootContextProps = {} }: RenderProps = {}) { + return (children: ReactNode) => + renderRtl( + + + + {children} + + + , + ); +} diff --git a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropList/DragAndDropList.tsx b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropList/DragAndDropList.tsx index a654e61ce71..76cc97c63e0 100644 --- a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropList/DragAndDropList.tsx +++ b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropList/DragAndDropList.tsx @@ -6,6 +6,7 @@ import * as testids from '../../../../../../testing/testids'; import { useIsParentDisabled } from '../hooks/useIsParentDisabled'; import { useParentId } from '../hooks/useParentId'; import { useOnDrop } from 'app-shared/components/dragAndDrop/hooks/useOnDrop'; +import { useDomSelectors } from 'app-shared/components/dragAndDrop/hooks/useDomSelectors'; export interface DragAndDropListProps { /** The list of existing items. */ @@ -19,10 +20,11 @@ export interface DragAndDropListCollectedProps { export function DragAndDropList({ children }: DragAndDropListProps) { const disabledDrop = useIsParentDisabled(); const parentId = useParentId(); + const domSelectors = useDomSelectors(parentId); const onDrop = useOnDrop(); const canDrop = useCallback( (monitor: DropTargetMonitor) => monitor.isOver({ shallow: true }) && !disabledDrop, - [disabledDrop] + [disabledDrop], ); const [{ canBeDropped }, drop] = useDrop, unknown, DragAndDropListCollectedProps>({ accept: Object.values(DraggableEditorItemType), @@ -38,8 +40,9 @@ export function DragAndDropList({ children }: DragAndDropListProps) { const backgroundColor = canBeDropped ? 'var(--list-empty-space-hover-color)' : 'transparent'; return (
diff --git a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropListItem/DragAndDropListItem.test.tsx b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropListItem/DragAndDropListItem.test.tsx new file mode 100644 index 00000000000..9c0abbc5e49 --- /dev/null +++ b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropListItem/DragAndDropListItem.test.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { DragAndDropListItem, DragAndDropListItemProps } from './DragAndDropListItem'; +import { + DragAndDropListItemContext, + DragAndDropListItemContextProps, +} from './DragAndDropListItemContext'; +import { + DragAndDropRootContext, + DragAndDropRootContextProps, +} from '../DragAndDropProvider/DragAndDropRootContext'; +import { render as renderRtl } from '@testing-library/react'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { domItemClass, domItemId } from 'app-shared/components/dragAndDrop/utils/domUtils'; + +// +const itemId = 'id'; +const parentId = 'parentId'; +const rootId = 'rootId'; +const uniqueDomId = ':r0:'; +const onDrop = jest.fn(); +const renderItem = () =>
test
; +const defaultlistItemProps: DragAndDropListItemProps = { + itemId, + renderItem, +}; +const defaultlistItemContextProps: DragAndDropListItemContextProps = { + isDisabled: false, + itemId: parentId, +}; +const defaultRootContextProps: DragAndDropRootContextProps = { + onDrop, + rootId, + uniqueDomId, +}; + +/* eslint-disable testing-library/no-node-access */ +describe('DragAndDropListItem', () => { + it('Renders with correct id and class name', () => { + const { container } = render(); + const expectedId = domItemId(uniqueDomId, itemId); + const expectedClass = domItemClass(uniqueDomId); + expect(container.firstChild).toHaveAttribute('id', expectedId); + expect(container.firstChild).toHaveClass(expectedClass); + }); +}); + +interface RenderProps { + listItemProps?: Partial; + listItemContextProps?: Partial; + rootContextProps?: Partial>; +} + +function render({ + listItemProps = {}, + listItemContextProps = {}, + rootContextProps = {}, +}: RenderProps = {}) { + return renderRtl( + + + + {...listItemProps} {...defaultlistItemProps} /> + + + , + ); +} diff --git a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropListItem/DragAndDropListItem.tsx b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropListItem/DragAndDropListItem.tsx index 5e9edcde742..bd6e62b25dd 100644 --- a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropListItem/DragAndDropListItem.tsx +++ b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropListItem/DragAndDropListItem.tsx @@ -4,7 +4,7 @@ import { DndItem, ExistingDndItem, } from '../../../types/dndTypes'; -import React, { ReactNode, useMemo, useRef, useState } from 'react'; +import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react'; import { ConnectDragSource, useDrag, useDrop } from 'react-dnd'; import { calculateNewPosition, getDragCursorPosition } from '../../../utils/dndUtils'; import classes from './DragAndDropListItem.module.css'; @@ -12,11 +12,10 @@ import { useIsParentDisabled } from '../hooks/useIsParentDisabled'; import { DragAndDropListItemContext } from '../DragAndDropListItem'; import { useParentId } from '../hooks/useParentId'; import { useOnDrop } from 'app-shared/components/dragAndDrop/hooks/useOnDrop'; +import { useDomSelectors } from 'app-shared/components/dragAndDrop/hooks/useDomSelectors'; +import { findPositionInList } from 'app-shared/components/dragAndDrop/utils/domUtils'; -export interface DragAndDropListItemProps { - /** The index of the item. */ - index: number; - +export interface DragAndDropListItemProps { /** The id of the item. */ itemId: string; @@ -28,14 +27,15 @@ interface DragCollectedProps { isDragging: boolean; } -export function DragAndDropListItem({ index, itemId, renderItem }: DragAndDropListItemProps) { +export function DragAndDropListItem({ itemId, renderItem }: DragAndDropListItemProps) { const wrapperRef = useRef(null); const [dragCursorPosition, setDragCursorPosition] = useState( - DragCursorPosition.Outside + DragCursorPosition.Outside, ); const isParentDisabled = useIsParentDisabled(); const parentId = useParentId(); const onDrop = useOnDrop(); + const domSelectors = useDomSelectors(itemId); const boxShadow = useMemo(() => { switch (dragCursorPosition) { @@ -48,13 +48,13 @@ export function DragAndDropListItem({ index, itemId, renderItem }: DragAndDro } }, [dragCursorPosition]); - const item: ExistingDndItem = useMemo( + const item: () => ExistingDndItem = useCallback( () => ({ isNew: false, id: itemId, - position: { index, parentId }, + position: { index: findPositionInList(domSelectors.baseId, itemId), parentId }, }), - [index, itemId, parentId] + [itemId, parentId, domSelectors.baseId], ); const [{ isDragging }, drag, dragPreview] = useDrag, unknown, DragCollectedProps>({ @@ -68,7 +68,7 @@ export function DragAndDropListItem({ index, itemId, renderItem }: DragAndDro const [, drop] = useDrop, unknown, void>({ accept: Object.values(DraggableEditorItemType), drop: (draggedItem) => { - const position = calculateNewPosition(draggedItem, item, dragCursorPosition); + const position = calculateNewPosition(draggedItem, item(), dragCursorPosition); if (position) onDrop(draggedItem, position); setDragCursorPosition(DragCursorPosition.Idle); }, @@ -77,9 +77,9 @@ export function DragAndDropListItem({ index, itemId, renderItem }: DragAndDro const currentDragPosition = getDragCursorPosition( monitor, draggedItem, - item, + item(), wrapperRef, - isParentDisabled + isParentDisabled, ); if (currentDragPosition !== dragCursorPosition) setDragCursorPosition(currentDragPosition); }, @@ -93,7 +93,7 @@ export function DragAndDropListItem({ index, itemId, renderItem }: DragAndDro const opacity = isDragging ? 0.25 : 1; return ( -
+
({ +export const DragAndDropListItemContext = createContext({ isDisabled: false, itemId: null, }); diff --git a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropProvider/DragAndDropProvider.tsx b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropProvider/DragAndDropProvider.tsx index dcc3ff5bc3a..3a31d39ad1d 100644 --- a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropProvider/DragAndDropProvider.tsx +++ b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropProvider/DragAndDropProvider.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useId } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { DragAndDropRootContext } from './DragAndDropRootContext'; @@ -19,9 +19,10 @@ export function DragAndDropProvider({ }: DragAndDropProviderProps) { const onDrop: HandleDrop = (item, position) => item.isNew === true ? onAdd(item.payload, position) : onMove(item.id, position); + const uniqueDomId = useId(); // Can not be the same as root id because root id might not be unique (if there are multiple drag and drop lists) return ( - + {children} diff --git a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropProvider/DragAndDropRootContext.ts b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropProvider/DragAndDropRootContext.ts index 3b4ad038e5e..ba52f427a25 100644 --- a/frontend/packages/shared/src/components/dragAndDrop/DragAndDropProvider/DragAndDropRootContext.ts +++ b/frontend/packages/shared/src/components/dragAndDrop/DragAndDropProvider/DragAndDropRootContext.ts @@ -4,6 +4,7 @@ import { HandleDrop } from 'app-shared/types/dndTypes'; export interface DragAndDropRootContextProps { rootId: string; onDrop: HandleDrop; + uniqueDomId: string; } export const DragAndDropRootContext = createContext>(null); diff --git a/frontend/packages/shared/src/components/dragAndDrop/hooks/useDomSelectors.test.tsx b/frontend/packages/shared/src/components/dragAndDrop/hooks/useDomSelectors.test.tsx new file mode 100644 index 00000000000..155b3f1cb22 --- /dev/null +++ b/frontend/packages/shared/src/components/dragAndDrop/hooks/useDomSelectors.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react'; +import { useDomSelectors } from 'app-shared/components/dragAndDrop/hooks/useDomSelectors'; +import { DragAndDropRootContext } from 'app-shared/components/dragAndDrop/DragAndDropProvider'; +import { + extractIdFromDomItemId, + extractIdFromDomListId, +} from 'app-shared/components/dragAndDrop/utils/domUtils'; + +// Test data: +const id = 'id'; +const uniqueDomId = 'baseId'; + +describe('useDomSelectors', () => { + afterEach(jest.clearAllMocks); + + it('Returns the base id and selector attributes for list and item components with the given id', () => { + const { result } = renderHook(() => useDomSelectors(id), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + expect(result.current).toEqual({ + baseId: uniqueDomId, + list: { + id: expect.any(String), + className: expect.any(String), + }, + item: { + id: expect.any(String), + className: expect.any(String), + }, + }); + const { list, item } = result.current; + expect(extractIdFromDomListId(uniqueDomId, list.id)).toEqual(id); + expect(extractIdFromDomItemId(uniqueDomId, item.id)).toEqual(id); + }); + + it('Throws an error if not wrapped by a DragAndDropProvider', () => { + jest.spyOn(console, 'error').mockImplementation(); + const renderFn = () => renderHook(() => useDomSelectors(id)); + expect(renderFn).toThrow( + new Error('useDomSelectors must be used within a DragAndDropRootContext provider.'), + ); + }); +}); diff --git a/frontend/packages/shared/src/components/dragAndDrop/hooks/useDomSelectors.ts b/frontend/packages/shared/src/components/dragAndDrop/hooks/useDomSelectors.ts new file mode 100644 index 00000000000..6651ee2869c --- /dev/null +++ b/frontend/packages/shared/src/components/dragAndDrop/hooks/useDomSelectors.ts @@ -0,0 +1,37 @@ +import { DragAndDropRootContext } from 'app-shared/components/dragAndDrop/DragAndDropProvider'; +import { useContext } from 'react'; +import { + domItemClass, + domItemId, + domListClass, + domListId, +} from 'app-shared/components/dragAndDrop/utils/domUtils'; + +interface Attributes { + id: string; + className: string; +} + +interface DomSelectors { + baseId: string; + item: Attributes; + list: Attributes; +} + +export function useDomSelectors(itemId: string): DomSelectors { + const context = useContext(DragAndDropRootContext); + if (!context) { + throw new Error('useDomSelectors must be used within a DragAndDropRootContext provider.'); + } + return { + baseId: context.uniqueDomId, + item: { + id: domItemId(context.uniqueDomId, itemId), + className: domItemClass(context.uniqueDomId), + }, + list: { + id: domListId(context.uniqueDomId, itemId), + className: domListClass(context.uniqueDomId), + }, + }; +} diff --git a/frontend/packages/shared/src/components/dragAndDrop/hooks/useIsParentDisabled.test.tsx b/frontend/packages/shared/src/components/dragAndDrop/hooks/useIsParentDisabled.test.tsx index f6f475dec8b..3c3e5d68b44 100644 --- a/frontend/packages/shared/src/components/dragAndDrop/hooks/useIsParentDisabled.test.tsx +++ b/frontend/packages/shared/src/components/dragAndDrop/hooks/useIsParentDisabled.test.tsx @@ -7,7 +7,11 @@ import { DragAndDrop } from '../'; const draggedItemId = 'draggedItemId'; jest.mock('react-dnd', () => ({ ...jest.requireActual('react-dnd'), - useDrag: (args) => [{ isDragging: args.item.id === draggedItemId }, jest.fn(), jest.fn()], + useDrag: (args) => [{ isDragging: args.item().id === draggedItemId }, jest.fn(), jest.fn()], +})); +jest.mock('../utils/domUtils', () => ({ + ...jest.requireActual('../utils/domUtils'), + findPositionInList: jest.fn().mockReturnValue(0), })); describe('useIsParentDisabled', () => { @@ -38,7 +42,7 @@ describe('useIsParentDisabled', () => { wrapper: ({ children }) => ( - children} /> + children} /> ), @@ -52,11 +56,10 @@ describe('useIsParentDisabled', () => { ( - children} /> + children} /> )} /> @@ -72,7 +75,7 @@ describe('useIsParentDisabled', () => { wrapper: ({ children }) => ( - children} /> + children} /> ), diff --git a/frontend/packages/shared/src/components/dragAndDrop/hooks/useParentId.test.tsx b/frontend/packages/shared/src/components/dragAndDrop/hooks/useParentId.test.tsx index 5ab0f782a14..685d19828ca 100644 --- a/frontend/packages/shared/src/components/dragAndDrop/hooks/useParentId.test.tsx +++ b/frontend/packages/shared/src/components/dragAndDrop/hooks/useParentId.test.tsx @@ -10,7 +10,7 @@ describe('useParentId', () => { wrapper: ({ children }) => ( - children} /> + children} /> ), diff --git a/frontend/packages/shared/src/components/dragAndDrop/utils/domUtils.test.tsx b/frontend/packages/shared/src/components/dragAndDrop/utils/domUtils.test.tsx new file mode 100644 index 00000000000..1f4f91e39cf --- /dev/null +++ b/frontend/packages/shared/src/components/dragAndDrop/utils/domUtils.test.tsx @@ -0,0 +1,246 @@ +import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { + domItemClass, + domItemId, + domListClass, + domListId, + extractIdFromDomItemId, + extractIdFromDomListId, + findAllItemIds, + findDirectChildDomIds, + findParentId, + findPositionInList, +} from 'app-shared/components/dragAndDrop/utils/domUtils'; + +// Test data: +const defaultBaseId = ':baseId:'; + +interface TestComponentProps { + baseId?: string; + id: string; + children?: ReactNode; +} + +const List = ({ baseId = defaultBaseId, id, children }: TestComponentProps) => ( +
+ {children} +
+); +const Item = ({ baseId = defaultBaseId, id, children }: TestComponentProps) => ( +
+ {children} +
+); + +describe('domUtils', () => { + describe('domListId', () => { + it('Returns an id with the expected format', () => { + const id = 'id'; + expect(domListId(defaultBaseId, id)).toEqual(`${defaultBaseId}-${id}-list`); + }); + }); + + describe('domItemId', () => { + it('Returns an id with the expected format', () => { + const id = 'id'; + expect(domItemId(defaultBaseId, id)).toEqual(`${defaultBaseId}-${id}-listitem`); + }); + }); + + describe('extractIdFromDomListId', () => { + it('Returns the id when it has been formatted by domListId', () => { + const id1 = 'test1'; + const id2 = 'test2'; + expect(extractIdFromDomListId(defaultBaseId, domListId(defaultBaseId, id1))).toEqual(id1); + expect(extractIdFromDomListId(defaultBaseId, domListId(defaultBaseId, id2))).toEqual(id2); + }); + }); + + describe('extractIdFromDomItemId', () => { + it('Returns the id when it has been formatted by domItemId', () => { + const id1 = 'test1'; + const id2 = 'test2'; + expect(extractIdFromDomItemId(defaultBaseId, domItemId(defaultBaseId, id1))).toEqual(id1); + expect(extractIdFromDomItemId(defaultBaseId, domItemId(defaultBaseId, id2))).toEqual(id2); + }); + }); + + describe('domListClass', () => { + it('Returns a class name with the expected format', () => { + expect(domListClass(':baseId:')).toEqual(`_baseId_-list`); + }); + }); + + describe('domItemClass', () => { + it('Returns a class name with the expected format', () => { + expect(domItemClass(':baseId:')).toEqual(`_baseId_-listitem`); + }); + }); + + describe('findParentId', () => { + it('Returns the id of the parent listitem element', () => { + const parentId = 'parentId'; + const childId = 'childId'; + render( + + + , + ); + expect(findParentId(defaultBaseId, childId)).toEqual(parentId); + }); + + it('Returns the correct id when there are multiple layers of dom elements between the parent and the child', () => { + const parentId = 'parentId'; + const childId = 'childId'; + render( + +
+
+
+ +
+
+
+
, + ); + expect(findParentId(defaultBaseId, childId)).toEqual(parentId); + }); + + it('Returns the ID of the closest parent listitem element', () => { + const parentId = 'parentId'; + const childId = 'childId'; + render( + + + + + + + , + ); + expect(findParentId(defaultBaseId, childId)).toEqual(parentId); + }); + }); + + describe('findAllItemIds', () => { + it('Returns a list of all list and/or item ids', () => { + const rootId = 'rootId'; + const parent1Id = 'parent1Id'; + const parent2Id = 'parent2Id'; + const child1AId = 'child1AId'; + const child1BId = 'child1BId'; + const child2AId = 'child2AId'; + const child2BId = 'child2BId'; + render( + + + + + + + + + + + + + + , + ); + expect(findAllItemIds(defaultBaseId)).toEqual([ + parent1Id, + child1AId, + child1BId, + parent2Id, + child2AId, + child2BId, + ]); + }); + + it('Ignores lists with other base ids', () => { + const rootId = 'rootId'; + const baseId1 = 'baseId1'; + const itemId1 = 'itemId1'; + const baseId2 = 'baseId2'; + const itemId2 = 'itemId2'; + render( + <> + + + + + + + , + ); + expect(findAllItemIds(baseId1)).toEqual([itemId1]); + expect(findAllItemIds(baseId2)).toEqual([itemId2]); + }); + }); + + describe('findDirectChildIds', () => { + it('Returns a list of all direct child ids', () => { + const rootId = 'rootId'; + const item1Id = 'item1Id'; + const item2Id = 'item2Id'; + const item1AId = 'item1AId'; + const item1BId = 'item1BId'; + const item1A1Id = 'item1A1Id'; + render( + + + + + + + + + + + + + , + ); + expect(findDirectChildDomIds(defaultBaseId, rootId)).toEqual([item1Id, item2Id]); + expect(findDirectChildDomIds(defaultBaseId, item1Id)).toEqual([item1AId, item1BId]); + expect(findDirectChildDomIds(defaultBaseId, item1AId)).toEqual([item1A1Id]); + }); + }); + + describe('findPositionInList', () => { + const rootId = 'rootId'; + const item1Id = 'item1Id'; + const item2Id = 'item2Id'; + const item3Id = 'item3Id'; + + it('Returns the position of the item in the list', () => { + render( + + + + + , + ); + expect(findPositionInList(defaultBaseId, item1Id)).toEqual(0); + expect(findPositionInList(defaultBaseId, item2Id)).toEqual(1); + expect(findPositionInList(defaultBaseId, item3Id)).toEqual(2); + }); + + it('Returns the correct position when run on a complex composition', () => { + render( + + +
Something in between
+ +
+ +
+
, + ); + expect(findPositionInList(defaultBaseId, item1Id)).toEqual(0); + expect(findPositionInList(defaultBaseId, item2Id)).toEqual(1); + expect(findPositionInList(defaultBaseId, item3Id)).toEqual(2); + }); + }); +}); diff --git a/frontend/packages/shared/src/components/dragAndDrop/utils/domUtils.ts b/frontend/packages/shared/src/components/dragAndDrop/utils/domUtils.ts new file mode 100644 index 00000000000..f9bbef27d32 --- /dev/null +++ b/frontend/packages/shared/src/components/dragAndDrop/utils/domUtils.ts @@ -0,0 +1,39 @@ +import { removeEnd, removeStart } from 'app-shared/utils/stringUtils'; + +export const domListId = (baseId: string, id: string) => `${baseId}-${id}-list`; +export const domItemId = (baseId: string, id: string) => `${baseId}-${id}-listitem`; + +export const extractIdFromDomItemId = (baseId: string, domItemId: string): string => + removeEnd(removeStart(domItemId, `${baseId}-`), '-listitem'); +export const extractIdFromDomListId = (baseId: string, domListId: string): string => + removeEnd(removeStart(domListId, `${baseId}-`), '-list'); + +const replaceColonsWithUnderscores = (id: string): string => id.replaceAll(':', '_'); // Used to convert the useId value to a valid class name +export const domItemClass = (baseId: string) => `${replaceColonsWithUnderscores(baseId)}-listitem`; +export const domListClass = (baseId: string) => `${replaceColonsWithUnderscores(baseId)}-list`; + +export const findParentId = (baseId: string, id: string): string => { + const listClassName = domListClass(baseId); + const itemDomId = domItemId(baseId, id); + const domItem = document.getElementById(itemDomId); + const parent = domItem.closest(`.${listClassName}`); + return extractIdFromDomListId(baseId, parent.id); +}; + +export const findAllItemIds = (baseId: string): string[] => { + const itemClassName = domItemClass(baseId); + const domItems = document.getElementsByClassName(itemClassName); + const domItemIds = Array.from(domItems).map((item) => item.id); + return domItemIds.map((domId) => extractIdFromDomItemId(baseId, domId)); +}; + +export const findDirectChildDomIds = (baseId: string, id: string): string[] => { + const allIds = findAllItemIds(baseId); + return allIds.filter((itemId) => findParentId(baseId, itemId) === id); +}; + +export const findPositionInList = (baseId: string, id: string): number => { + const parentId = findParentId(baseId, id); + const sameLevelDomIds = findDirectChildDomIds(baseId, parentId); + return sameLevelDomIds.indexOf(id); +}; diff --git a/frontend/packages/ux-editor/src/containers/DesignView.tsx b/frontend/packages/ux-editor/src/containers/DesignView.tsx index 9f0beee5c6c..43130061323 100644 --- a/frontend/packages/ux-editor/src/containers/DesignView.tsx +++ b/frontend/packages/ux-editor/src/containers/DesignView.tsx @@ -38,7 +38,7 @@ export const DesignView = ({ className }: DesignViewProps) => { const renderContainer = ( id: string, isBaseContainer: boolean, - dragHandleRef?: ConnectDragSource + dragHandleRef?: ConnectDragSource, ) => { if (!id) return null; @@ -60,7 +60,6 @@ export const DesignView = ({ className }: DesignViewProps) => { items.map((itemId: string, itemIndex: number) => ( key={itemId} - index={itemIndex} itemId={itemId} renderItem={(itemDragHandleRef) => { const component = components[itemId];