diff --git a/examples/unified_field_list_examples/public/example_drop_zone.tsx b/examples/unified_field_list_examples/public/example_drop_zone.tsx index e1122d82ee30b..ba7c0ea4d630b 100644 --- a/examples/unified_field_list_examples/public/example_drop_zone.tsx +++ b/examples/unified_field_list_examples/public/example_drop_zone.tsx @@ -14,8 +14,8 @@ * Side Public License, v 1. */ -import React, { useContext, useMemo } from 'react'; -import { DragContext, DragDrop, DropOverlayWrapper, DropType } from '@kbn/dom-drag-drop'; +import React, { useMemo } from 'react'; +import { DragDrop, DropOverlayWrapper, DropType, useDragDropContext } from '@kbn/dom-drag-drop'; import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; const DROP_PROPS = { @@ -34,8 +34,8 @@ export interface ExampleDropZoneProps { } export const ExampleDropZone: React.FC = ({ onDropField }) => { - const dragDropContext = useContext(DragContext); - const draggingFieldName = dragDropContext.dragging?.id; + const [{ dragging }] = useDragDropContext(); + const draggingFieldName = dragging?.id; const onDroppingField = useMemo(() => { if (!draggingFieldName) { diff --git a/examples/unified_field_list_examples/public/field_list_sidebar.tsx b/examples/unified_field_list_examples/public/field_list_sidebar.tsx index 6d2364a3a5613..121132e89b810 100644 --- a/examples/unified_field_list_examples/public/field_list_sidebar.tsx +++ b/examples/unified_field_list_examples/public/field_list_sidebar.tsx @@ -14,10 +14,10 @@ * Side Public License, v 1. */ -import React, { useCallback, useContext, useMemo, useRef } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; import { generateFilters } from '@kbn/data-plugin/public'; -import { ChildDragDropProvider, DragContext } from '@kbn/dom-drag-drop'; +import { ChildDragDropProvider, useDragDropContext } from '@kbn/dom-drag-drop'; import { UnifiedFieldListSidebarContainer, type UnifiedFieldListSidebarContainerProps, @@ -54,7 +54,7 @@ export const FieldListSidebar: React.FC = ({ onAddFieldToWorkspace, onRemoveFieldFromWorkspace, }) => { - const dragDropContext = useContext(DragContext); + const dragDropContext = useDragDropContext(); const unifiedFieldListContainerRef = useRef(null); const filterManager = services.data?.query?.filterManager; @@ -80,7 +80,7 @@ export const FieldListSidebar: React.FC = ({ }, [unifiedFieldListContainerRef]); return ( - + + ... your app here ... ``` @@ -17,13 +17,13 @@ First, place a RootDragDropProvider at the root of your application. If you have a child React application (e.g. a visualization), you will need to pass the drag / drop context down into it. This can be obtained like so: ```js -const context = useContext(DragContext); +const context = useDragDropContext(); ``` -In your child application, place a `ChildDragDropProvider` at the root of that, and spread the context into it: +In your child application, place a `ChildDragDropProvider` at the root of that, and assign the context into it: ```js -... your child app here ... +... your child app here ... ``` This enables your child application to share the same drag / drop context as the root application. @@ -49,7 +49,7 @@ To enable dragging an item, use `DragDrop` with both a `draggable` and a `value` To enable dropping, use `DragDrop` with both a `dropTypes` attribute that should be an array with at least one value and an `onDrop` handler attribute. `dropType` should only be truthy if is an item being dragged, and if a drop of the dragged item is supported. ```js -const { dragging } = useContext(DragContext); +const [ dndState ] = useDragDropContext() return ( ... elements from one group here ... +... elements from one group here ... ``` The children `DragDrop` components must have props defined as in the example: ```js - +
{fields.map((f) => ( { - const defaultContext = { - dataTestSubjPrefix: 'testDragDrop', + const defaultContextState = { dragging: undefined, - setDragging: jest.fn(), - setActiveDropTarget: jest.fn(), + dataTestSubjPrefix: 'testDragDrop', activeDropTarget: undefined, dropTargetsByOrder: undefined, keyboardMode: false, - setKeyboardMode: () => {}, - setA11yMessage: jest.fn(), - registerDropTarget: jest.fn(), }; const value = { @@ -104,16 +97,9 @@ describe('DragDrop', () => { }); test('dragstart sets dragging in the context and calls it with proper params', async () => { - const setDragging = jest.fn(); - - const setA11yMessage = jest.fn(); + const dndDispatch = jest.fn(); const component = mount( - + @@ -127,21 +113,24 @@ describe('DragDrop', () => { }); expect(dataTransfer.setData).toBeCalledWith('text', 'hello'); - expect(setDragging).toBeCalledWith({ ...value }); - expect(setA11yMessage).toBeCalledWith('Lifted hello'); + expect(dndDispatch).toBeCalledWith({ + type: 'startDragging', + payload: { dragging: value }, + }); }); test('drop resets all the things', async () => { const preventDefault = jest.fn(); const stopPropagation = jest.fn(); - const setDragging = jest.fn(); + const dndDispatch = jest.fn(); const onDrop = jest.fn(); const component = mount( @@ -151,25 +140,27 @@ describe('DragDrop', () => { const dragDrop = component.find('[data-test-subj="testDragDrop"]').at(0); dragDrop.simulate('dragOver'); + dndDispatch.mockClear(); dragDrop.simulate('drop', { preventDefault, stopPropagation }); expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); - expect(setDragging).toBeCalledWith(undefined); expect(onDrop).toBeCalledWith({ id: '2', humanData: { label: 'Label1' } }, 'field_add'); + expect(dndDispatch).toBeCalledWith({ type: 'resetState' }); }); test('drop function is not called on dropTypes undefined', async () => { const preventDefault = jest.fn(); const stopPropagation = jest.fn(); - const setDragging = jest.fn(); + const dndDispatch = jest.fn(); const onDrop = jest.fn(); const component = mount( @@ -183,7 +174,7 @@ describe('DragDrop', () => { expect(preventDefault).not.toHaveBeenCalled(); expect(stopPropagation).not.toHaveBeenCalled(); - expect(setDragging).not.toHaveBeenCalled(); + expect(dndDispatch).not.toHaveBeenCalled(); expect(onDrop).not.toHaveBeenCalled(); }); @@ -206,7 +197,7 @@ describe('DragDrop', () => { test('items that has dropTypes=undefined get special styling when another item is dragged', () => { const component = mount( - + @@ -228,17 +219,10 @@ describe('DragDrop', () => { let dragging: { id: '1'; humanData: { label: 'Label1' } } | undefined; const getAdditionalClassesOnEnter = jest.fn().mockReturnValue('additional'); const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable'); - const setA11yMessage = jest.fn(); + const dndDispatch = jest.fn(); const component = mount( - { - dragging = { id: '1', humanData: { label: 'Label1' } }; - }} - > + { act(() => { jest.runAllTimers(); }); - expect(setA11yMessage).toBeCalledWith('Lifted ignored'); const dragDrop = component.find('[data-test-subj="testDragDrop"]').at(1); dragDrop.simulate('dragOver'); @@ -278,22 +261,20 @@ describe('DragDrop', () => { let dragging: { id: '1'; humanData: { label: 'Label1' } } | undefined; const getAdditionalClasses = jest.fn().mockReturnValue('additional'); const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable'); - const setActiveDropTarget = jest.fn(); + const dndDispatch = jest.fn(() => { + dragging = { id: '1', humanData: { label: 'Label1' } }; + }); const component = mount( { - dragging = { id: '1', humanData: { label: 'Label1' } }; - }} - setActiveDropTarget={setActiveDropTarget} - activeDropTarget={value as DragContextState['activeDropTarget']} - keyboardMode={false} - setKeyboardMode={(keyboardMode) => true} - dropTargetsByOrder={undefined} - registerDropTarget={jest.fn()} + value={[ + { + ...defaultContextState, + dragging, + activeDropTarget: value as DragContextState['activeDropTarget'], + }, + dndDispatch, + ]} > { component.find('[data-test-subj="testDragDrop"]').at(1).simulate('dragover'); expect(component.find('.additional')).toHaveLength(2); component.find('[data-test-subj="testDragDrop"]').at(1).simulate('dragleave'); - expect(setActiveDropTarget).toBeCalledWith(undefined); + expect(dndDispatch).toBeCalledWith({ type: 'leaveDropTarget' }); }); describe('Keyboard navigation', () => { test('User receives proper drop Targets highlighted when pressing arrow keys', () => { const onDrop = jest.fn(); - const setActiveDropTarget = jest.fn(); - const setA11yMessage = jest.fn(); + const dndDispatch = jest.fn(); const items = [ { draggable: true, @@ -391,20 +371,21 @@ describe('DragDrop', () => { ]; const component = mount( , style: {} } }, - setActiveDropTarget, - setA11yMessage, - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - '2,0,2,0,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, - '2,0,1,0,1': { ...items[1].value, onDrop, dropType: 'duplicate_compatible' }, - '2,0,1,0,2': { ...items[1].value, onDrop, dropType: 'swap_compatible' }, + value={[ + { + ...defaultContextState, + dragging: { ...items[0].value, ghost: { children:
, style: {} } }, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + '2,0,2,0,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, + '2,0,1,0,1': { ...items[1].value, onDrop, dropType: 'duplicate_compatible' }, + '2,0,1,0,2': { ...items[1].value, onDrop, dropType: 'swap_compatible' }, + }, + keyboardMode: true, }, - keyboardMode: true, - }} + dndDispatch, + ]} > {items.map((props) => ( @@ -418,17 +399,28 @@ describe('DragDrop', () => { .at(1) .simulate('focus'); + dndDispatch.mockClear(); + keyboardHandler.simulate('keydown', { key: 'ArrowRight' }); - expect(setActiveDropTarget).toBeCalledWith({ - ...items[2].value, - onDrop, - dropType: items[2].dropTypes![0], + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging: items[0].value, + dropTarget: { + ...items[2].value, + onDrop, + dropType: items[2].dropTypes![0], + }, + }, }); + + dndDispatch.mockClear(); + keyboardHandler.simulate('keydown', { key: 'Enter' }); - expect(setA11yMessage).toBeCalledWith( - `You're dragging Label1 from at position 1 in layer 0 over label3 from Y group at position 1 in layer 0. Press space or enter to replace label3 with Label1. Hold alt or option to duplicate. Hold shift to swap.` - ); - expect(setActiveDropTarget).toBeCalledWith(undefined); + expect(dndDispatch).toBeCalledWith({ + type: 'endDragging', + payload: { dragging: items[0].value }, + }); expect(onDrop).toBeCalledWith( { humanData: { label: 'Label1', position: 1, layerNumber: 0 }, id: '1' }, 'move_compatible' @@ -436,16 +428,17 @@ describe('DragDrop', () => { }); test('dragstart sets dragging in the context and calls it with proper params', async () => { - const setDragging = jest.fn(); - - const setA11yMessage = jest.fn(); + const dndDispatch = jest.fn(); const component = mount( @@ -463,23 +456,26 @@ describe('DragDrop', () => { jest.runAllTimers(); }); - expect(setDragging).toBeCalledWith({ - ...value, - ghost: { - children: , - style: { - minHeight: 0, - width: 0, + expect(dndDispatch).toBeCalledWith({ + type: 'startDragging', + payload: { + keyboardMode: true, + dragging: { + ...value, + ghost: { + children: , + style: { + minHeight: 0, + width: 0, + }, + }, }, }, }); - expect(setA11yMessage).toBeCalledWith('Lifted hello'); }); test('ActiveDropTarget gets ghost image', () => { const onDrop = jest.fn(); - const setActiveDropTarget = jest.fn(); - const setA11yMessage = jest.fn(); const items = [ { draggable: true, @@ -504,19 +500,22 @@ describe('DragDrop', () => { order: [2, 0, 1, 0], }, ]; + const dndDispatch = jest.fn(); const component = mount( Hello
, style: {} } }, - setActiveDropTarget, - setA11yMessage, - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + value={[ + { + ...defaultContextState, + dragging: { ...items[0].value, ghost: { children:
Hello
, style: {} } }, + + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + }, + keyboardMode: true, }, - keyboardMode: true, - }} + dndDispatch, + ]} > {items.map((props) => ( @@ -532,27 +531,25 @@ describe('DragDrop', () => { describe('multiple drop targets', () => { let activeDropTarget: DragContextState['activeDropTarget']; + const dragging = { id: '1', humanData: { label: 'Label1', layerNumber: 0 } }; const onDrop = jest.fn(); - let setActiveDropTarget = jest.fn(); - const setA11yMessage = jest.fn(); + let dndDispatch = jest.fn(); let component: ReactWrapper; beforeEach(() => { activeDropTarget = undefined; - setActiveDropTarget = jest.fn((val) => { + dndDispatch = jest.fn((val) => { activeDropTarget = value as DragContextState['activeDropTarget']; }); component = mount( true} - dropTargetsByOrder={undefined} - registerDropTarget={jest.fn()} + value={[ + { + ...defaultContextState, + dragging, + activeDropTarget, + }, + dndDispatch, + ]} > { .find('[data-test-subj="testDragDrop"]') .first() .simulate('dragstart', { dataTransfer }); - + dndDispatch.mockClear(); component.find('SingleDropInner').at(0).simulate('dragover'); - expect(setActiveDropTarget).toBeCalledWith({ - ...value, - dropType: 'move_compatible', - onDrop, + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging, + dropTarget: { + ...value, + dropType: 'move_compatible', + onDrop, + }, + }, }); + dndDispatch.mockClear(); component.find('SingleDropInner').at(1).simulate('dragover'); - - expect(setActiveDropTarget).toBeCalledWith({ - ...value, - dropType: 'duplicate_compatible', - onDrop, + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging, + dropTarget: { + ...value, + dropType: 'duplicate_compatible', + onDrop, + }, + }, }); + dndDispatch.mockClear(); component.find('SingleDropInner').at(2).simulate('dragover'); - expect(setActiveDropTarget).toBeCalledWith({ - ...value, - dropType: 'swap_compatible', - onDrop, + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging, + dropTarget: { + ...value, + dropType: 'swap_compatible', + onDrop, + }, + }, }); + dndDispatch.mockClear(); component.find('SingleDropInner').at(2).simulate('dragleave'); - expect(setActiveDropTarget).toBeCalledWith(undefined); + expect(dndDispatch).toBeCalledWith({ type: 'leaveDropTarget' }); }); test('drop on extra drop target passes correct dropType to onDrop', () => { @@ -673,10 +690,16 @@ describe('DragDrop', () => { .at(0) .simulate('dragover', { altKey: true }) .simulate('dragover', { altKey: true }); - expect(setActiveDropTarget).toBeCalledWith({ - ...value, - dropType: 'duplicate_compatible', - onDrop, + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging, + dropTarget: { + ...value, + dropType: 'duplicate_compatible', + onDrop, + }, + }, }); component @@ -684,10 +707,17 @@ describe('DragDrop', () => { .at(0) .simulate('dragover', { shiftKey: true }) .simulate('dragover', { shiftKey: true }); - expect(setActiveDropTarget).toBeCalledWith({ - ...value, - dropType: 'swap_compatible', - onDrop, + + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging, + dropTarget: { + ...value, + dropType: 'swap_compatible', + onDrop, + }, + }, }); }); @@ -702,8 +732,10 @@ describe('DragDrop', () => { extraDrop.simulate('dragover', { shiftKey: true }); extraDrop.simulate('dragover'); expect( - setActiveDropTarget.mock.calls.every((call) => call[0].dropType === 'duplicate_compatible') - ); + dndDispatch.mock.calls.every((call) => { + return call[0].payload.dropTarget.dropType === 'duplicate_compatible'; + }) + ).toBe(true); }); describe('keyboard navigation', () => { const items = [ @@ -781,26 +813,26 @@ describe('DragDrop', () => { }; test('when pressing enter key, context receives the proper dropTargetsByOrder', () => { let dropTargetsByOrder: DragContextState['dropTargetsByOrder'] = {}; - const setKeyboardMode = jest.fn(); + component = mount( , style: {} } }, - setDragging: jest.fn(), - setActiveDropTarget, - setA11yMessage, - activeDropTarget, - dropTargetsByOrder, - keyboardMode: true, - setKeyboardMode, - registerDropTarget: jest.fn((order, dropTarget) => { - dropTargetsByOrder = { - ...dropTargetsByOrder, - [order.join(',')]: dropTarget, - }; + value={[ + { + ...defaultContextState, + dragging: { ...items[0].value, ghost: { children:
, style: {} } }, + activeDropTarget, + dropTargetsByOrder, + keyboardMode: true, + }, + jest.fn((action) => { + if (action.type === 'registerDropTargets') { + dropTargetsByOrder = { + ...dropTargetsByOrder, + ...action.payload, + }; + } }), - }} + ]} > {items.map((props) => ( @@ -819,18 +851,16 @@ describe('DragDrop', () => { test('when pressing ArrowRight key with modifier key pressed in, the extra drop target is selected', () => { component = mount( , style: {} } }, - setDragging: jest.fn(), - setActiveDropTarget, - setA11yMessage, - activeDropTarget: undefined, - dropTargetsByOrder: assignedDropTargetsByOrder, - keyboardMode: true, - setKeyboardMode: jest.fn(), - registerDropTarget: jest.fn(), - }} + value={[ + { + ...defaultContextState, + dragging: { ...dragging, ghost: { children:
, style: {} } }, + activeDropTarget: undefined, + keyboardMode: true, + dropTargetsByOrder: assignedDropTargetsByOrder, + }, + dndDispatch, + ]} > {items.map((props) => ( @@ -839,44 +869,56 @@ describe('DragDrop', () => { ))} ); + dndDispatch.mockClear(); act(() => { component .find('[data-test-subj="testDragDrop-keyboardHandler"]') .at(1) .simulate('keydown', { key: 'ArrowRight', altKey: true }); }); - expect(setActiveDropTarget).toBeCalledWith({ - ...items[1].value, - onDrop, - dropType: 'duplicate_compatible', + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging: items[0].value, + dropTarget: { + ...items[1].value, + onDrop, + dropType: 'duplicate_compatible', + }, + }, }); + dndDispatch.mockClear(); act(() => { component .find('[data-test-subj="testDragDrop-keyboardHandler"]') .at(1) .simulate('keydown', { key: 'ArrowRight', shiftKey: true }); }); - expect(setActiveDropTarget).toBeCalledWith({ - ...items[1].value, - onDrop, - dropType: 'swap_compatible', + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging: items[0].value, + dropTarget: { + ...items[1].value, + onDrop, + dropType: 'swap_compatible', + }, + }, }); }); test('when having a main target selected and pressing alt, the first extra drop target is selected', () => { component = mount( , style: {} } }, - setDragging: jest.fn(), - setActiveDropTarget, - setA11yMessage, - activeDropTarget: assignedDropTargetsByOrder['2,0,1,0,0'], - dropTargetsByOrder: assignedDropTargetsByOrder, - keyboardMode: true, - setKeyboardMode: jest.fn(), - registerDropTarget: jest.fn(), - }} + value={[ + { + ...defaultContextState, + dragging: { ...items[0].value, ghost: { children:
, style: {} } }, + activeDropTarget: assignedDropTargetsByOrder['2,0,1,0,0'], + dropTargetsByOrder: assignedDropTargetsByOrder, + keyboardMode: true, + }, + dndDispatch, + ]} > {items.map((props) => ( @@ -885,44 +927,56 @@ describe('DragDrop', () => { ))} ); + dndDispatch.mockClear(); act(() => { component .find('[data-test-subj="testDragDrop-keyboardHandler"]') .at(1) .simulate('keydown', { key: 'Alt' }); }); - expect(setActiveDropTarget).toBeCalledWith({ - ...items[1].value, - onDrop, - dropType: 'duplicate_compatible', + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging: items[0].value, + dropTarget: { + ...items[1].value, + onDrop, + dropType: 'duplicate_compatible', + }, + }, }); + dndDispatch.mockClear(); act(() => { component .find('[data-test-subj="testDragDrop-keyboardHandler"]') .at(1) .simulate('keyup', { key: 'Alt' }); }); - expect(setActiveDropTarget).toBeCalledWith({ - ...items[1].value, - onDrop, - dropType: 'move_compatible', + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging: items[0].value, + dropTarget: { + ...items[1].value, + onDrop, + dropType: 'move_compatible', + }, + }, }); }); test('when having a main target selected and pressing shift, the second extra drop target is selected', () => { component = mount( , style: {} } }, - setDragging: jest.fn(), - setActiveDropTarget, - setA11yMessage, - activeDropTarget: assignedDropTargetsByOrder['2,0,1,0,0'], - dropTargetsByOrder: assignedDropTargetsByOrder, - keyboardMode: true, - setKeyboardMode: jest.fn(), - registerDropTarget: jest.fn(), - }} + value={[ + { + ...defaultContextState, + dragging: { ...items[0].value, ghost: { children:
, style: {} } }, + activeDropTarget: assignedDropTargetsByOrder['2,0,1,0,0'], + dropTargetsByOrder: assignedDropTargetsByOrder, + keyboardMode: true, + }, + dndDispatch, + ]} > {items.map((props) => ( @@ -931,6 +985,7 @@ describe('DragDrop', () => { ))} ); + dndDispatch.mockClear(); act(() => { component .find('[data-test-subj="testDragDrop-keyboardHandler"]') @@ -938,21 +993,34 @@ describe('DragDrop', () => { .simulate('keydown', { key: 'Shift' }); }); - expect(setActiveDropTarget).toBeCalledWith({ - ...items[1].value, - onDrop, - dropType: 'swap_compatible', + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging: items[0].value, + dropTarget: { + ...items[1].value, + onDrop, + dropType: 'swap_compatible', + }, + }, }); + dndDispatch.mockClear(); act(() => { component .find('[data-test-subj="testDragDrop-keyboardHandler"]') .at(1) .simulate('keyup', { key: 'Shift' }); }); - expect(setActiveDropTarget).toBeCalledWith({ - ...items[1].value, - onDrop, - dropType: 'move_compatible', + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging: items[0].value, + dropTarget: { + ...items[1].value, + onDrop, + dropType: 'move_compatible', + }, + }, }); }); }); @@ -979,34 +1047,10 @@ describe('DragDrop', () => { }, ]; const mountComponent = ( - dragContext: Partial | undefined, + dndContextState?: Partial, + dndDispatch?: Dispatch, onDropHandler?: () => void ) => { - let dragging = dragContext?.dragging; - let keyboardMode = !!dragContext?.keyboardMode; - let activeDropTarget = dragContext?.activeDropTarget; - - const setA11yMessage = jest.fn(); - const registerDropTarget = jest.fn(); - const baseContext = { - dataTestSubjPrefix: defaultContext.dataTestSubjPrefix, - dragging, - setDragging: (val?: DraggingIdentifier) => { - dragging = val; - }, - keyboardMode, - setKeyboardMode: jest.fn((mode) => { - keyboardMode = mode; - }), - setActiveDropTarget: (target?: DragDropIdentifier) => { - activeDropTarget = target as DropIdentifier; - }, - activeDropTarget, - setA11yMessage, - registerDropTarget, - dropTargetsByOrder: undefined, - }; - const dragDropSharedProps = { draggable: true, dragType: 'move' as 'copy' | 'move', @@ -1015,8 +1059,16 @@ describe('DragDrop', () => { }; return mount( - - + + { ); }; test(`Inactive group renders properly`, () => { - const component = mountComponent(undefined); + const component = mountComponent(); act(() => { jest.runAllTimers(); }); @@ -1054,13 +1106,8 @@ describe('DragDrop', () => { }); test(`Reorderable group with lifted element renders properly`, () => { - const setA11yMessage = jest.fn(); - const setDragging = jest.fn(); - const component = mountComponent({ - dragging: { ...items[0] }, - setDragging, - setA11yMessage, - }); + const dndDispatch = jest.fn(); + const component = mountComponent({ dragging: { ...items[0] } }, dndDispatch); act(() => { jest.runAllTimers(); @@ -1074,12 +1121,15 @@ describe('DragDrop', () => { jest.runAllTimers(); }); - expect(setDragging).toBeCalledWith({ ...items[0] }); - expect(setA11yMessage).toBeCalledWith('Lifted Label1'); + expect(dndDispatch).toBeCalledWith({ + type: 'startDragging', + payload: { dragging: { ...items[0] } }, + }); }); test(`Reordered elements get extra styles to show the reorder effect when dragging`, () => { - const component = mountComponent({ dragging: { ...items[0] } }); + const dndDispatch = jest.fn(); + const component = mountComponent({ dragging: { ...items[0] } }, dndDispatch); component .find('[data-test-subj="testDragDrop"]') @@ -1123,62 +1173,68 @@ describe('DragDrop', () => { test(`Dropping an item runs onDrop function`, () => { const preventDefault = jest.fn(); const stopPropagation = jest.fn(); - - const setA11yMessage = jest.fn(); - const setDragging = jest.fn(); - - const component = mountComponent({ - dragging: { ...items[0] }, - setDragging, - setA11yMessage, - }); + const dndDispatch = jest.fn(); + const component = mountComponent({ dragging: { ...items[0] } }, dndDispatch); const dragDrop = component.find('[data-test-subj="testDragDrop-reorderableDropLayer"]').at(1); dragDrop.simulate('dragOver'); + dndDispatch.mockClear(); dragDrop.simulate('drop', { preventDefault, stopPropagation }); act(() => { jest.runAllTimers(); }); - expect(setA11yMessage).toBeCalledWith( - 'Reordered Label1 in X group from position 1 to position 3' - ); + expect(dndDispatch).toBeCalledWith({ + type: 'dropToTarget', + payload: { + dragging: items[0], + dropTarget: { ...items[2], dropType: 'reorder' }, + }, + }); + expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); expect(onDrop).toBeCalledWith({ ...items[0] }, 'reorder'); }); test(`Keyboard Navigation: User cannot move an element outside of the group`, () => { - const setA11yMessage = jest.fn(); - const setActiveDropTarget = jest.fn(); - const component = mountComponent({ - dragging: { ...items[0] }, - keyboardMode: true, - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, + const dndDispatch = jest.fn(); + const component = mountComponent( + { + dragging: { ...items[0] }, + keyboardMode: true, + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, + }, }, - setActiveDropTarget, - setA11yMessage, - }); + dndDispatch + ); const keyboardHandler = component .find('[data-test-subj="testDragDrop-keyboardHandler"]') .at(1); + dndDispatch.mockClear(); keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); - expect(setActiveDropTarget).not.toHaveBeenCalled(); + expect(dndDispatch).not.toHaveBeenCalled(); keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); - expect(setActiveDropTarget).toBeCalledWith({ ...items[1], dropType: 'reorder' }); - expect(setA11yMessage).toBeCalledWith( - 'Reorder Label1 in X group from position 1 to position 2. Press space or enter to reorder' - ); + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dropTarget: { + ...items[1], + dropType: 'reorder', + }, + dragging: items[0], + }, + }); }); test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { @@ -1206,12 +1262,9 @@ describe('DragDrop', () => { }); test(`Keyboard Navigation: Doesn't call onDrop when movement is cancelled`, () => { - const setA11yMessage = jest.fn(); const onDropHandler = jest.fn(); - const component = mountComponent( - { dragging: { ...items[0] }, setA11yMessage }, - onDropHandler - ); + const dndDispatch = jest.fn(); + const component = mountComponent({ dragging: { ...items[0] } }, dndDispatch, onDropHandler); const keyboardHandler = component .find('[data-test-subj="testDragDrop-keyboardHandler"]') .at(1); @@ -1222,37 +1275,50 @@ describe('DragDrop', () => { }); expect(onDropHandler).not.toHaveBeenCalled(); - expect(setA11yMessage).toBeCalledWith( - 'Movement cancelled. Label1 returned to X group at position 1' - ); + + expect(dndDispatch).toBeCalledWith({ + type: 'endDragging', + payload: { + dragging: items[0], + }, + }); keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); + dndDispatch.mockClear(); keyboardHandler.simulate('blur'); expect(onDropHandler).not.toHaveBeenCalled(); - expect(setA11yMessage).toBeCalledWith( - 'Movement cancelled. Label1 returned to X group at position 1' - ); + + expect(dndDispatch).toBeCalledWith({ + type: 'endDragging', + payload: { + dragging: items[0], + }, + }); }); test(`Keyboard Navigation: Reordered elements get extra styles to show the reorder effect`, () => { - const setA11yMessage = jest.fn(); - const component = mountComponent({ - dragging: { ...items[0] }, - keyboardMode: true, - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, + const dndDispatch = jest.fn(); + const component = mountComponent( + { + dragging: { ...items[0] }, + keyboardMode: true, + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, + }, }, - setA11yMessage, - }); + dndDispatch + ); const keyboardHandler = component .find('[data-test-subj="testDragDrop-keyboardHandler"]') .at(1); + keyboardHandler.simulate('keydown', { key: 'Space' }); + dndDispatch.mockClear(); keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); expect( @@ -1268,9 +1334,14 @@ describe('DragDrop', () => { expect( component.find('[data-test-subj="testDragDrop-translatableDrop"]').at(1).prop('style') ).toEqual(undefined); - expect(setA11yMessage).toBeCalledWith( - 'Reorder Label1 in X group from position 1 to position 2. Press space or enter to reorder' - ); + + expect(dndDispatch).toBeCalledWith({ + type: 'selectDropTarget', + payload: { + dragging: items[0], + dropTarget: { ...items[1], onDrop, dropType: 'reorder' }, + }, + }); component .find('[data-test-subj="testDragDrop-reorderableDropLayer"]') @@ -1285,26 +1356,24 @@ describe('DragDrop', () => { }); test(`Keyboard Navigation: User cannot drop element to itself`, () => { - const setA11yMessage = jest.fn(); - const setActiveDropTarget = jest.fn(); + const dndDispatch = jest.fn(); + const contextState = { + ...defaultContextState, + keyboardMode: true, + activeDropTarget: { + ...items[1], + onDrop, + dropType: 'reorder' as const, + }, + dropTargetsByOrder: { + '2,0,1,0': undefined, + '2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' as const }, + }, + dragging: { ...items[0] }, + }; const component = mount( - - + + { keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); - expect(setActiveDropTarget).toBeCalledWith(undefined); - expect(setA11yMessage).toBeCalledWith('Label1 returned to its initial position 1'); + + expect(dndDispatch).toBeCalledWith({ + type: 'leaveDropTarget', + }); }); }); }); diff --git a/packages/kbn-dom-drag-drop/src/drag_drop.tsx b/packages/kbn-dom-drag-drop/src/drag_drop.tsx index ce3d6147d813f..66cb11bc15ad4 100644 --- a/packages/kbn-dom-drag-drop/src/drag_drop.tsx +++ b/packages/kbn-dom-drag-drop/src/drag_drop.tsx @@ -14,14 +14,14 @@ import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; import { DragDropIdentifier, DropIdentifier, - DragContext, - DragContextState, nextValidDropTarget, ReorderContext, - ReorderState, DropHandler, - announce, Ghost, + RegisteredDropTargets, + DragDropAction, + DragContextState, + useDragDropContext, } from './providers'; import { DropType } from './types'; import { REORDER_ITEM_MARGIN } from './constants'; @@ -63,11 +63,6 @@ interface BaseProps { */ value: DragDropIdentifier; - /** - * Optional comparison function to check whether a value is the dragged one - */ - isValueEqual?: (value1: unknown, value2: unknown) => boolean; - /** * The React element which will be passed the draggable handlers */ @@ -125,17 +120,13 @@ interface BaseProps { * The props for a draggable instance of that component. */ interface DragInnerProps extends BaseProps { - setKeyboardMode: DragContextState['setKeyboardMode']; - setDragging: DragContextState['setDragging']; - setActiveDropTarget: DragContextState['setActiveDropTarget']; - setA11yMessage: DragContextState['setA11yMessage']; + dndDispatch: React.Dispatch; + dataTestSubjPrefix?: string; activeDraggingProps?: { - keyboardMode: DragContextState['keyboardMode']; - activeDropTarget: DragContextState['activeDropTarget']; - dropTargetsByOrder: DragContextState['dropTargetsByOrder']; + keyboardMode: boolean; + activeDropTarget?: DropIdentifier; + dropTargetsByOrder: RegisteredDropTargets; }; - dataTestSubjPrefix: DragContextState['dataTestSubjPrefix']; - onTrackUICounterEvent: DragContextState['onTrackUICounterEvent'] | undefined; extraKeyboardHandler?: (e: KeyboardEvent) => void; ariaDescribedBy?: string; } @@ -144,16 +135,8 @@ interface DragInnerProps extends BaseProps { * The props for a non-draggable instance of that component. */ interface DropsInnerProps extends BaseProps { - dragging: DragContextState['dragging']; - keyboardMode: DragContextState['keyboardMode']; - setKeyboardMode: DragContextState['setKeyboardMode']; - setDragging: DragContextState['setDragging']; - setActiveDropTarget: DragContextState['setActiveDropTarget']; - setA11yMessage: DragContextState['setA11yMessage']; - registerDropTarget: DragContextState['registerDropTarget']; - activeDropTarget: DragContextState['activeDropTarget']; - dataTestSubjPrefix: DragContextState['dataTestSubjPrefix']; - onTrackUICounterEvent: DragContextState['onTrackUICounterEvent'] | undefined; + dndState: DragContextState; + dndDispatch: React.Dispatch; isNotDroppable: boolean; } @@ -165,19 +148,9 @@ const REORDER_OFFSET = REORDER_ITEM_MARGIN / 2; * @constructor */ export const DragDrop = (props: BaseProps) => { - const { - dragging, - setDragging, - keyboardMode, - registerDropTarget, - dropTargetsByOrder, - setKeyboardMode, - activeDropTarget, - setActiveDropTarget, - setA11yMessage, - dataTestSubjPrefix, - onTrackUICounterEvent, - } = useContext(DragContext); + const [dndState, dndDispatch] = useDragDropContext(); + + const { dragging, dropTargetsByOrder } = dndState; if (props.isDisabled) { return props.children; @@ -188,8 +161,8 @@ export const DragDrop = (props: BaseProps) => { const activeDraggingProps = isDragging ? { - keyboardMode, - activeDropTarget, + keyboardMode: dndState.keyboardMode, + activeDropTarget: dndState.activeDropTarget, dropTargetsByOrder, } : undefined; @@ -198,12 +171,8 @@ export const DragDrop = (props: BaseProps) => { const dragProps = { ...props, activeDraggingProps, - setKeyboardMode, - setDragging, - setActiveDropTarget, - setA11yMessage, - dataTestSubjPrefix, - onTrackUICounterEvent, + dataTestSubjPrefix: dndState.dataTestSubjPrefix, + dndDispatch, }; if (reorderableGroup && reorderableGroup.length > 1) { return ; @@ -214,16 +183,8 @@ export const DragDrop = (props: BaseProps) => { const dropProps = { ...props, - keyboardMode, - setKeyboardMode, - dragging, - setDragging, - activeDropTarget, - setActiveDropTarget, - registerDropTarget, - setA11yMessage, - dataTestSubjPrefix, - onTrackUICounterEvent, + dndState, + dndDispatch, isNotDroppable: // If the configuration has provided a droppable flag, but this particular item is not // droppable, then it should be less prominent. Ignores items that are both @@ -253,39 +214,35 @@ const DragInner = memo(function DragInner({ className, value, children, - setDragging, - setKeyboardMode, - setActiveDropTarget, + dndDispatch, order, activeDraggingProps, + dataTestSubjPrefix, dragType, onDragStart, onDragEnd, extraKeyboardHandler, ariaDescribedBy, - setA11yMessage, - dataTestSubjPrefix, - onTrackUICounterEvent, }: DragInnerProps) { - const keyboardMode = activeDraggingProps?.keyboardMode; - const activeDropTarget = activeDraggingProps?.activeDropTarget; - const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const { keyboardMode, activeDropTarget, dropTargetsByOrder } = activeDraggingProps || {}; const setTarget = useCallback( - (target?: DropIdentifier, announceModifierKeys = false) => { - setActiveDropTarget(target); - setA11yMessage( - target - ? announce.selectedTarget( - value.humanData, - target?.humanData, - target?.dropType, - announceModifierKeys - ) - : announce.noTarget() - ); + (target?: DropIdentifier) => { + if (!target) { + dndDispatch({ + type: 'leaveDropTarget', + }); + } else { + dndDispatch({ + type: 'selectDropTarget', + payload: { + dropTarget: target, + dragging: value, + }, + }); + } }, - [setActiveDropTarget, setA11yMessage, value.humanData] + [dndDispatch, value] ); const setTargetOfIndex = useCallback( @@ -293,11 +250,7 @@ const DragInner = memo(function DragInner({ const dropTargetsForActiveId = dropTargetsByOrder && Object.values(dropTargetsByOrder).filter((dropTarget) => dropTarget?.id === id); - if (index > 0 && dropTargetsForActiveId?.[index]) { - setTarget(dropTargetsForActiveId[index]); - } else { - setTarget(dropTargetsForActiveId?.[0], true); - } + setTarget(dropTargetsForActiveId?.[index]); }, [dropTargetsByOrder, setTarget] ); @@ -339,58 +292,64 @@ const DragInner = memo(function DragInner({ return { onKeyDown, onKeyUp }; }, [activeDropTarget, setTargetOfIndex]); - const dragStart = ( - e: DroppableEvent | KeyboardEvent, - keyboardModeOn?: boolean - ) => { - // Setting stopPropgagation causes Chrome failures, so - // we are manually checking if we've already handled this - // in a nested child, and doing nothing if so... - if (e && 'dataTransfer' in e && e.dataTransfer.getData('text')) { - return; - } + const dragStart = useCallback( + (e: DroppableEvent | KeyboardEvent, keyboardModeOn?: boolean) => { + // Setting stopPropgagation causes Chrome failures, so + // we are manually checking if we've already handled this + // in a nested child, and doing nothing if so... + if (e && 'dataTransfer' in e && e.dataTransfer.getData('text')) { + return; + } - // We only can reach the dragStart method if the element is draggable, - // so we know we have DraggableProps if we reach this code. - if (e && 'dataTransfer' in e) { - e.dataTransfer.setData('text', value.humanData.label); - } + // We only can reach the dragStart method if the element is draggable, + // so we know we have DraggableProps if we reach this code. + if (e && 'dataTransfer' in e) { + e.dataTransfer.setData('text', value.humanData.label); + } - // Chrome causes issues if you try to render from within a - // dragStart event, so we drop a setTimeout to avoid that. + // Chrome causes issues if you try to render from within a + // dragStart event, so we drop a setTimeout to avoid that. + + const currentTarget = e?.currentTarget; + + setTimeout(() => { + dndDispatch({ + type: 'startDragging', + payload: { + ...(keyboardModeOn ? { keyboardMode: true } : {}), + dragging: { + ...value, + ghost: keyboardModeOn + ? { + children, + style: { + width: currentTarget.offsetWidth, + minHeight: currentTarget?.offsetHeight, + }, + } + : undefined, + }, + }, + }); + onDragStart?.(currentTarget); + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [dndDispatch, value, onDragStart] + ); - const currentTarget = e?.currentTarget; + const dragEnd = useCallback( + (e?: DroppableEvent) => { + e?.stopPropagation(); - setTimeout(() => { - setDragging({ - ...value, - ghost: keyboardModeOn - ? { - children, - style: { width: currentTarget.offsetWidth, minHeight: currentTarget?.offsetHeight }, - } - : undefined, + dndDispatch({ + type: 'endDragging', + payload: { dragging: value }, }); - setA11yMessage(announce.lifted(value.humanData)); - if (keyboardModeOn) { - setKeyboardMode(true); - } - if (onDragStart) { - onDragStart(currentTarget); - } - }); - }; - - const dragEnd = (e?: DroppableEvent) => { - e?.stopPropagation(); - setDragging(undefined); - setActiveDropTarget(undefined); - setKeyboardMode(false); - setA11yMessage(announce.cancelled(value.humanData)); - if (onDragEnd) { - onDragEnd(); - } - }; + onDragEnd?.(); + }, + [dndDispatch, value, onDragEnd] + ); const setNextTarget = (e: KeyboardEvent, reversed = false) => { const nextTarget = nextValidDropTarget( @@ -408,16 +367,23 @@ const DragInner = memo(function DragInner({ } else if (e.ctrlKey && nextTarget?.id) { setTargetOfIndex(nextTarget.id, 3); } else { - setTarget(nextTarget, true); + setTarget(nextTarget); } }; const dropToActiveDropTarget = () => { if (activeDropTarget) { - onTrackUICounterEvent?.('drop_total'); - const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget; - setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType))); - onTargetDrop(value, dropType); + const { dropType, onDrop } = activeDropTarget; + setTimeout(() => { + dndDispatch({ + type: 'dropToTarget', + payload: { + dragging: value, + dropTarget: activeDropTarget, + }, + }); + }); + onDrop(value, dropType); } }; @@ -501,38 +467,35 @@ const DropsInner = memo(function DropsInner(props: DropsInnerProps) { value, children, draggable, - dragging, + dndState, + dndDispatch, isNotDroppable, dropTypes, order, getAdditionalClassesOnEnter, getAdditionalClassesOnDroppable, - activeDropTarget, - registerDropTarget, - setActiveDropTarget, - keyboardMode, - setKeyboardMode, - setDragging, - setA11yMessage, getCustomDropTarget, - dataTestSubjPrefix, } = props; + const { dragging, activeDropTarget, dataTestSubjPrefix, keyboardMode } = dndState; + const [isInZone, setIsInZone] = useState(false); const mainTargetRef = useRef(null); useShallowCompareEffect(() => { if (dropTypes && dropTypes?.[0] && onDrop && keyboardMode) { - dropTypes.forEach((dropType, index) => { - registerDropTarget([...order, index], { ...value, onDrop, dropType }); + dndDispatch({ + type: 'registerDropTargets', + payload: dropTypes.reduce( + (acc, dropType, index) => ({ + ...acc, + [[...props.order, index].join(',')]: { ...value, onDrop, dropType }, + }), + {} + ), }); - return () => { - dropTypes.forEach((_, index) => { - registerDropTarget([...order, index], undefined); - }); - }; } - }, [order, registerDropTarget, dropTypes, keyboardMode]); + }, [order, dndDispatch, dropTypes, keyboardMode]); useEffect(() => { let isMounted = true; @@ -586,15 +549,18 @@ const DropsInner = memo(function DropsInner(props: DropsInnerProps) { ); // An optimization to prevent a bunch of React churn. if (!isActiveDropTarget) { - setActiveDropTarget({ ...value, dropType: modifiedDropType, onDrop }); - setA11yMessage( - announce.selectedTarget(dragging.humanData, value.humanData, modifiedDropType) - ); + dndDispatch({ + type: 'selectDropTarget', + payload: { + dropTarget: { ...value, dropType: modifiedDropType, onDrop }, + dragging, + }, + }); } }; const dragLeave = () => { - setActiveDropTarget(undefined); + dndDispatch({ type: 'leaveDropTarget' }); }; const drop = (e: DroppableEvent, dropType: DropType) => { @@ -604,14 +570,17 @@ const DropsInner = memo(function DropsInner(props: DropsInnerProps) { if (onDrop && dragging) { const modifiedDropType = getModifiedDropType(e, dropType); onDrop(dragging, modifiedDropType); - setTimeout(() => - setA11yMessage(announce.dropped(dragging.humanData, value.humanData, modifiedDropType)) - ); + setTimeout(() => { + dndDispatch({ + type: 'dropToTarget', + payload: { + dragging, + dropTarget: { ...value, dropType: modifiedDropType, onDrop }, + }, + }); + }); } - - setDragging(undefined); - setActiveDropTarget(undefined); - setKeyboardMode(false); + dndDispatch({ type: 'resetState' }); }; const getProps = (dropType?: DropType, dropChildren?: ReactElement) => { @@ -746,23 +715,11 @@ const SingleDropInner = ({ const ReorderableDrag = memo(function ReorderableDrag( props: DragInnerProps & { reorderableGroup: Array<{ id: string }>; dragging?: DragDropIdentifier } ) { - const { - reorderState: { isReorderOn, reorderedItems, direction }, - setReorderState, - } = useContext(ReorderContext); + const [{ isReorderOn, reorderedItems, direction }, reorderDispatch] = useContext(ReorderContext); - const { - value, - setActiveDropTarget, - activeDraggingProps, - reorderableGroup, - setA11yMessage, - dataTestSubjPrefix, - } = props; + const { value, activeDraggingProps, reorderableGroup, dndDispatch, dataTestSubjPrefix } = props; - const keyboardMode = activeDraggingProps?.keyboardMode; - const activeDropTarget = activeDraggingProps?.activeDropTarget; - const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const { keyboardMode, activeDropTarget, dropTargetsByOrder } = activeDraggingProps || {}; const isDragging = !!activeDraggingProps; const isFocusInGroup = keyboardMode @@ -771,11 +728,11 @@ const ReorderableDrag = memo(function ReorderableDrag( : isDragging; useEffect(() => { - setReorderState((s: ReorderState) => ({ - ...s, - isReorderOn: isFocusInGroup, - })); - }, [setReorderState, isFocusInGroup]); + reorderDispatch({ + type: 'setIsReorderOn', + payload: isFocusInGroup, + }); + }, [reorderDispatch, isFocusInGroup]); const onReorderableDragStart = ( currentTarget?: @@ -783,24 +740,19 @@ const ReorderableDrag = memo(function ReorderableDrag( | KeyboardEvent['currentTarget'] ) => { if (currentTarget) { - const height = currentTarget.offsetHeight + REORDER_OFFSET; - setReorderState((s: ReorderState) => ({ - ...s, - draggingHeight: height, - })); + setTimeout(() => { + reorderDispatch({ + type: 'registerDraggingItemHeight', + payload: currentTarget.offsetHeight + REORDER_OFFSET, + }); + }); } }; const onReorderableDragEnd = () => { - resetReorderState(); + reorderDispatch({ type: 'reset' }); }; - const resetReorderState = () => - setReorderState((s: ReorderState) => ({ - ...s, - reorderedItems: [], - })); - const extraKeyboardHandler = (e: KeyboardEvent) => { if (isReorderOn && keyboardMode) { e.stopPropagation(); @@ -811,8 +763,7 @@ const ReorderableDrag = memo(function ReorderableDrag( if (index !== -1) activeDropTargetIndex = index; } if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) { - resetReorderState(); - setActiveDropTarget(undefined); + reorderDispatch({ type: 'reset' }); } else if (keys.ARROW_DOWN === e.key) { if (activeDropTargetIndex < reorderableGroup.length - 1) { const nextTarget = nextValidDropTarget( @@ -840,12 +791,8 @@ const ReorderableDrag = memo(function ReorderableDrag( const onReorderableDragOver = (target?: DropIdentifier) => { if (!target) { - setReorderState((s: ReorderState) => ({ - ...s, - reorderedItems: [], - })); - setA11yMessage(announce.selectedTarget(value.humanData, value.humanData, 'reorder')); - setActiveDropTarget(target); + reorderDispatch({ type: 'reset' }); + dndDispatch({ type: 'leaveDropTarget' }); return; } const droppingIndex = reorderableGroup.findIndex((i) => i.id === target.id); @@ -853,40 +800,34 @@ const ReorderableDrag = memo(function ReorderableDrag( if (draggingIndex === -1) { return; } - setActiveDropTarget(target); - - setA11yMessage(announce.selectedTarget(value.humanData, target.humanData, 'reorder')); - - setReorderState((s: ReorderState) => - draggingIndex < droppingIndex - ? { - ...s, - reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1), - direction: '-', - } - : { - ...s, - reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex), - direction: '+', - } - ); + + dndDispatch({ + type: 'selectDropTarget', + payload: { + dropTarget: target, + dragging: value, + }, + }); + reorderDispatch({ + type: 'setReorderedItems', + payload: { draggingIndex, droppingIndex, items: reorderableGroup }, + }); }; - const areItemsReordered = isDragging && keyboardMode && reorderedItems.length; + const areItemsReordered = keyboardMode && isDragging && reorderedItems.length; return (
acc + Number(cur.height || 0) + REORDER_OFFSET, + (acc, el) => acc + (el.height ?? 0) + REORDER_OFFSET, 0 )}px)`, } @@ -907,26 +848,13 @@ const ReorderableDrag = memo(function ReorderableDrag( const ReorderableDrop = memo(function ReorderableDrop( props: DropsInnerProps & { reorderableGroup: Array<{ id: string }> } ) { - const { - onDrop, - value, - dragging, - setDragging, - setKeyboardMode, - activeDropTarget, - setActiveDropTarget, - reorderableGroup, - setA11yMessage, - dataTestSubjPrefix, - onTrackUICounterEvent, - } = props; + const { onDrop, value, dndState, dndDispatch, reorderableGroup } = props; + const { dragging, dataTestSubjPrefix, activeDropTarget } = dndState; const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id); - const { - reorderState: { isReorderOn, reorderedItems, draggingHeight, direction }, - setReorderState, - } = useContext(ReorderContext); + const [{ isReorderOn, reorderedItems, draggingHeight, direction }, reorderDispatch] = + useContext(ReorderContext); const heightRef = useRef(null); @@ -935,52 +863,38 @@ const ReorderableDrop = memo(function ReorderableDrop( useEffect(() => { if (isReordered && heightRef.current?.clientHeight) { - setReorderState((s) => ({ - ...s, - reorderedItems: s.reorderedItems.map((el) => - el.id === value.id - ? { - ...el, - height: heightRef.current?.clientHeight, - } - : el - ), - })); + reorderDispatch({ + type: 'registerReorderedItemHeight', + payload: { id: value.id, height: heightRef.current.clientHeight }, + }); } - }, [isReordered, setReorderState, value.id]); + }, [isReordered, reorderDispatch, value.id]); const onReorderableDragOver = (e: DroppableEvent) => { e.preventDefault(); // An optimization to prevent a bunch of React churn. if (activeDropTarget?.id !== value?.id && onDrop) { - setActiveDropTarget({ ...value, dropType: 'reorder', onDrop }); - const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id); - if (!dragging || draggingIndex === -1) { return; } + const droppingIndex = currentIndex; if (draggingIndex === droppingIndex) { - setReorderState((s: ReorderState) => ({ - ...s, - reorderedItems: [], - })); + reorderDispatch({ type: 'reset' }); } - setReorderState((s: ReorderState) => - draggingIndex < droppingIndex - ? { - ...s, - reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1), - direction: '-', - } - : { - ...s, - reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex), - direction: '+', - } - ); + reorderDispatch({ + type: 'setReorderedItems', + payload: { draggingIndex, droppingIndex, items: reorderableGroup }, + }); + dndDispatch({ + type: 'selectDropTarget', + payload: { + dropTarget: { ...value, dropType: 'reorder', onDrop }, + dragging, + }, + }); } }; @@ -988,18 +902,20 @@ const ReorderableDrop = memo(function ReorderableDrop( e.preventDefault(); e.stopPropagation(); - setActiveDropTarget(undefined); - setDragging(undefined); - setKeyboardMode(false); - if (onDrop && dragging) { - onTrackUICounterEvent?.('drop_total'); onDrop(dragging, 'reorder'); // setTimeout ensures it will run after dragEnd messaging - setTimeout(() => - setA11yMessage(announce.dropped(dragging.humanData, value.humanData, 'reorder')) - ); + setTimeout(() => { + dndDispatch({ + type: 'dropToTarget', + payload: { + dragging, + dropTarget: { ...value, dropType: 'reorder', onDrop }, + }, + }); + }); } + dndDispatch({ type: 'resetState' }); }; return ( @@ -1027,11 +943,8 @@ const ReorderableDrop = memo(function ReorderableDrop( onDrop={onReorderableDrop} onDragOver={onReorderableDragOver} onDragLeave={() => { - setActiveDropTarget(undefined); - setReorderState((s: ReorderState) => ({ - ...s, - reorderedItems: [], - })); + dndDispatch({ type: 'leaveDropTarget' }); + reorderDispatch({ type: 'reset' }); }} />
diff --git a/packages/kbn-dom-drag-drop/src/providers/announcements.tsx b/packages/kbn-dom-drag-drop/src/providers/announcements.tsx index f3d5c97f57023..442aff7718780 100644 --- a/packages/kbn-dom-drag-drop/src/providers/announcements.tsx +++ b/packages/kbn-dom-drag-drop/src/providers/announcements.tsx @@ -10,11 +10,7 @@ import { i18n } from '@kbn/i18n'; import { DropType } from '../types'; import { HumanData } from '.'; -type AnnouncementFunction = ( - draggedElement: HumanData, - dropElement: HumanData, - announceModifierKeys?: boolean -) => string; +type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string; interface CustomAnnouncementsType { dropped: Partial<{ [dropType in DropType]: AnnouncementFunction }>; @@ -32,10 +28,9 @@ const replaceAnnouncement = { canDuplicate, canCombine, layerNumber: dropLayerNumber, - }: HumanData, - announceModifierKeys?: boolean + }: HumanData ) => { - if (announceModifierKeys && (canSwap || canDuplicate)) { + if (canSwap || canDuplicate) { return i18n.translate('domDragDrop.announce.selectedTarget.replaceMain', { defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to replace {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`, values: { @@ -168,10 +163,9 @@ const combineAnnouncement = { canDuplicate, canCombine, layerNumber: dropLayerNumber, - }: HumanData, - announceModifierKeys?: boolean + }: HumanData ) => { - if (announceModifierKeys && (canSwap || canDuplicate || canCombine)) { + if (canSwap || canDuplicate || canCombine) { return i18n.translate('domDragDrop.announce.selectedTarget.combineMain', { defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to combine {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`, values: { @@ -247,10 +241,9 @@ export const announcements: CustomAnnouncementsType = { canDuplicate, canCombine, layerNumber: dropLayerNumber, - }: HumanData, - announceModifierKeys?: boolean + }: HumanData ) => { - if (announceModifierKeys && (canSwap || canDuplicate || canCombine)) { + if (canSwap || canDuplicate || canCombine) { return i18n.translate('domDragDrop.announce.selectedTarget.replaceIncompatibleMain', { defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to convert {label} to {nextLabel} and replace {dropLabel}.{duplicateCopy}{swapCopy}{combineCopy}`, values: { @@ -290,10 +283,9 @@ export const announcements: CustomAnnouncementsType = { canSwap, canDuplicate, layerNumber: dropLayerNumber, - }: HumanData, - announceModifierKeys?: boolean + }: HumanData ) => { - if (announceModifierKeys && (canSwap || canDuplicate)) { + if (canSwap || canDuplicate) { return i18n.translate('domDragDrop.announce.selectedTarget.moveIncompatibleMain', { defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over position {dropPosition} in {dropGroupLabel} group in layer {dropLayerNumber}. Press space or enter to convert {label} to {nextLabel} and move.{duplicateCopy}`, values: { @@ -329,10 +321,9 @@ export const announcements: CustomAnnouncementsType = { canSwap, canDuplicate, layerNumber: dropLayerNumber, - }: HumanData, - announceModifierKeys?: boolean + }: HumanData ) => { - if (announceModifierKeys && (canSwap || canDuplicate)) { + if (canSwap || canDuplicate) { return i18n.translate('domDragDrop.announce.selectedTarget.moveCompatibleMain', { defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group in layer {dropLayerNumber}. Press space or enter to move.{duplicateCopy}`, values: { @@ -790,19 +781,9 @@ export const announce = { dropped: (draggedElement: HumanData, dropElement: HumanData, type?: DropType) => (type && announcements.dropped?.[type]?.(draggedElement, dropElement)) || defaultAnnouncements.dropped(draggedElement, dropElement), - selectedTarget: ( - draggedElement: HumanData, - dropElement: HumanData, - type?: DropType, - announceModifierKeys?: boolean - ) => { + selectedTarget: (draggedElement: HumanData, dropElement: HumanData, type?: DropType) => { return ( - (type && - announcements.selectedTarget?.[type]?.( - draggedElement, - dropElement, - announceModifierKeys - )) || + (type && announcements.selectedTarget?.[type]?.(draggedElement, dropElement)) || defaultAnnouncements.selectedTarget(draggedElement, dropElement) ); }, diff --git a/packages/kbn-dom-drag-drop/src/providers/providers.test.tsx b/packages/kbn-dom-drag-drop/src/providers/providers.test.tsx index 0bb255e9ac15d..06aceb603025a 100644 --- a/packages/kbn-dom-drag-drop/src/providers/providers.test.tsx +++ b/packages/kbn-dom-drag-drop/src/providers/providers.test.tsx @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import React, { useContext } from 'react'; +import React from 'react'; import { mount } from 'enzyme'; -import { RootDragDropProvider, DragContext } from '.'; +import { RootDragDropProvider, useDragDropContext } from '.'; jest.useFakeTimers({ legacyFakeTimers: true }); @@ -16,11 +16,11 @@ describe('RootDragDropProvider', () => { test('reuses contexts for each render', () => { const contexts: Array<{}> = []; const TestComponent = ({ name }: { name: string }) => { - const context = useContext(DragContext); + const context = useDragDropContext(); contexts.push(context); return (
- {name} {!!context.dragging} + {name} {!!context[0].dragging}
); }; diff --git a/packages/kbn-dom-drag-drop/src/providers/providers.tsx b/packages/kbn-dom-drag-drop/src/providers/providers.tsx index d1c4f5f23dd8f..a036d65d75bc2 100644 --- a/packages/kbn-dom-drag-drop/src/providers/providers.tsx +++ b/packages/kbn-dom-drag-drop/src/providers/providers.tsx @@ -6,45 +6,54 @@ * Side Public License, v 1. */ -import React, { useState, useMemo } from 'react'; +import React, { Reducer, useReducer } from 'react'; import { EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DropIdentifier, - DraggingIdentifier, DragDropIdentifier, RegisteredDropTargets, + DragContextValue, DragContextState, + CustomMiddleware, + DraggingIdentifier, } from './types'; import { DEFAULT_DATA_TEST_SUBJ } from '../constants'; +import { announce } from './announcements'; +const initialState = { + dragging: undefined, + activeDropTarget: undefined, + keyboardMode: false, + dropTargetsByOrder: {}, + dataTestSubjPrefix: DEFAULT_DATA_TEST_SUBJ, +}; /** * The drag / drop context singleton, used like so: * - * const { dragging, setDragging } = useContext(DragContext); + * const [ state, dispatch ] = useDragDropContext(); */ -export const DragContext = React.createContext({ - dragging: undefined, - setDragging: () => {}, - keyboardMode: false, - setKeyboardMode: () => {}, - activeDropTarget: undefined, - setActiveDropTarget: () => {}, - setA11yMessage: () => {}, - dropTargetsByOrder: undefined, - registerDropTarget: () => {}, - dataTestSubjPrefix: DEFAULT_DATA_TEST_SUBJ, - onTrackUICounterEvent: undefined, -}); +const DragContext = React.createContext([initialState, () => {}]); + +export function useDragDropContext() { + const context = React.useContext(DragContext); + if (context === undefined) { + throw new Error( + 'useDragDropContext must be used within a or ' + ); + } + return context; +} /** * The argument to DragDropProvider. */ -export interface ProviderProps extends DragContextState { +export interface ProviderProps { /** * The React children. */ children: React.ReactNode; + value: DragContextValue; } /** @@ -54,74 +63,193 @@ export interface ProviderProps extends DragContextState { * * @param props */ + +interface ResetStateAction { + type: 'resetState'; + payload?: string; +} + +interface EndDraggingAction { + type: 'endDragging'; + payload: { + dragging: DraggingIdentifier; + }; +} + +interface StartDraggingAction { + type: 'startDragging'; + payload: { + dragging: DraggingIdentifier; + keyboardMode?: boolean; + }; +} + +interface LeaveDropTargetAction { + type: 'leaveDropTarget'; +} + +interface SelectDropTargetAction { + type: 'selectDropTarget'; + payload: { + dropTarget: DropIdentifier; + dragging: DragDropIdentifier; + }; +} + +interface DragToTargetAction { + type: 'dropToTarget'; + payload: { + dragging: DragDropIdentifier; + dropTarget: DropIdentifier; + }; +} + +interface RegisterDropTargetAction { + type: 'registerDropTargets'; + payload: RegisteredDropTargets; +} + +export type DragDropAction = + | ResetStateAction + | RegisterDropTargetAction + | LeaveDropTargetAction + | SelectDropTargetAction + | DragToTargetAction + | StartDraggingAction + | EndDraggingAction; + +const dragDropReducer = (state: DragContextState, action: DragDropAction) => { + switch (action.type) { + case 'resetState': + case 'endDragging': + return { + ...state, + dropTargetsByOrder: undefined, + dragging: undefined, + keyboardMode: false, + activeDropTarget: undefined, + }; + case 'registerDropTargets': + return { + ...state, + dropTargetsByOrder: { + ...state.dropTargetsByOrder, + ...action.payload, + }, + }; + case 'dropToTarget': + return { + ...state, + dropTargetsByOrder: undefined, + dragging: undefined, + keyboardMode: false, + activeDropTarget: undefined, + }; + case 'leaveDropTarget': + return { + ...state, + activeDropTarget: undefined, + }; + case 'selectDropTarget': + return { + ...state, + activeDropTarget: action.payload.dropTarget, + }; + case 'startDragging': + return { + ...state, + ...action.payload, + }; + default: + return state; + } +}; + +const useReducerWithMiddleware = ( + reducer: Reducer, + initState: DragContextState, + middlewareFns?: Array<(action: DragDropAction) => void> +) => { + const [state, dispatch] = useReducer(reducer, initState); + + const dispatchWithMiddleware = React.useCallback( + (action: DragDropAction) => { + if (middlewareFns !== undefined && middlewareFns.length > 0) { + middlewareFns.forEach((middlewareFn) => middlewareFn(action)); + } + dispatch(action); + }, + [middlewareFns] + ); + + return [state, dispatchWithMiddleware] as const; +}; + +const useA11yMiddleware = () => { + const [a11yMessage, setA11yMessage] = React.useState(''); + const a11yMiddleware = React.useCallback((action: DragDropAction) => { + switch (action.type) { + case 'startDragging': + setA11yMessage(announce.lifted(action.payload.dragging.humanData)); + return; + case 'selectDropTarget': + setA11yMessage( + announce.selectedTarget( + action.payload.dragging.humanData, + action.payload.dropTarget.humanData, + action.payload.dropTarget.dropType + ) + ); + return; + case 'leaveDropTarget': + setA11yMessage(announce.noTarget()); + return; + case 'dropToTarget': + const { dragging, dropTarget } = action.payload; + setA11yMessage( + announce.dropped(dragging.humanData, dropTarget.humanData, dropTarget.dropType) + ); + return; + case 'endDragging': + setA11yMessage(announce.cancelled(action.payload.dragging.humanData)); + return; + default: + return; + } + }, []); + return { a11yMessage, a11yMiddleware }; +}; + export function RootDragDropProvider({ children, dataTestSubj = DEFAULT_DATA_TEST_SUBJ, - onTrackUICounterEvent, + customMiddleware, }: { children: React.ReactNode; dataTestSubj?: string; - onTrackUICounterEvent?: DragContextState['onTrackUICounterEvent']; + customMiddleware?: CustomMiddleware; }) { - const [draggingState, setDraggingState] = useState<{ dragging?: DraggingIdentifier }>({ - dragging: undefined, - }); - const [keyboardModeState, setKeyboardModeState] = useState(false); - const [a11yMessageState, setA11yMessageState] = useState(''); - const [activeDropTargetState, setActiveDropTargetState] = useState( - undefined - ); - - const [dropTargetsByOrderState, setDropTargetsByOrderState] = useState({}); - - const setDragging = useMemo( - () => (dragging?: DraggingIdentifier) => setDraggingState({ dragging }), - [setDraggingState] - ); + const { a11yMessage, a11yMiddleware } = useA11yMiddleware(); + const middlewareFns = React.useMemo(() => { + return customMiddleware ? [customMiddleware, a11yMiddleware] : [a11yMiddleware]; + }, [customMiddleware, a11yMiddleware]); - const setA11yMessage = useMemo( - () => (message: string) => setA11yMessageState(message), - [setA11yMessageState] - ); - - const setActiveDropTarget = useMemo( - () => (activeDropTarget?: DropIdentifier) => setActiveDropTargetState(activeDropTarget), - [setActiveDropTargetState] - ); - - const registerDropTarget = useMemo( - () => (order: number[], dropTarget?: DropIdentifier) => { - return setDropTargetsByOrderState((s) => { - return { - ...s, - [order.join(',')]: dropTarget, - }; - }); + const [state, dispatch] = useReducerWithMiddleware( + dragDropReducer, + { + ...initialState, + dataTestSubjPrefix: dataTestSubj, }, - [setDropTargetsByOrderState] + middlewareFns ); return ( <> - - {children} - + {children}

- {a11yMessageState} + {a11yMessage}

{i18n.translate('domDragDrop.keyboardInstructionsReorder', { @@ -206,47 +334,6 @@ export function nextValidDropTarget( * * @param props */ -export function ChildDragDropProvider({ - dragging, - setDragging, - setKeyboardMode, - keyboardMode, - activeDropTarget, - setActiveDropTarget, - setA11yMessage, - registerDropTarget, - dropTargetsByOrder, - dataTestSubjPrefix, - onTrackUICounterEvent, - children, -}: ProviderProps) { - const value = useMemo( - () => ({ - setKeyboardMode, - keyboardMode, - dragging, - setDragging, - activeDropTarget, - setActiveDropTarget, - setA11yMessage, - dropTargetsByOrder, - registerDropTarget, - dataTestSubjPrefix, - onTrackUICounterEvent, - }), - [ - setDragging, - dragging, - activeDropTarget, - setActiveDropTarget, - setKeyboardMode, - keyboardMode, - setA11yMessage, - dropTargetsByOrder, - registerDropTarget, - dataTestSubjPrefix, - onTrackUICounterEvent, - ] - ); +export function ChildDragDropProvider({ value, children }: ProviderProps) { return {children}; } diff --git a/packages/kbn-dom-drag-drop/src/providers/reorder_provider.tsx b/packages/kbn-dom-drag-drop/src/providers/reorder_provider.tsx index c2aa11eb8bc8d..814aecf4d08e1 100644 --- a/packages/kbn-dom-drag-drop/src/providers/reorder_provider.tsx +++ b/packages/kbn-dom-drag-drop/src/providers/reorder_provider.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useMemo } from 'react'; +import React, { useReducer, Reducer, Dispatch } from 'react'; import classNames from 'classnames'; import { DEFAULT_DATA_TEST_SUBJ, REORDER_ITEM_HEIGHT } from '../constants'; @@ -31,69 +31,116 @@ export interface ReorderState { * indicates that user is in keyboard mode */ isReorderOn: boolean; - /** - * reorder group needed for screen reader aria-described-by attribute - */ - groupId: string; } -type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState; +const initialState: ReorderState = { + reorderedItems: [], + direction: '-' as const, + draggingHeight: REORDER_ITEM_HEIGHT, + isReorderOn: false, +}; /** * Reorder context state */ -export interface ReorderContextState { - reorderState: ReorderState; - setReorderState: (dispatch: SetReorderStateDispatch) => void; -} +export type ReorderContextState = [ReorderState, Dispatch]; /** * Reorder context */ -export const ReorderContext = React.createContext({ - reorderState: { - reorderedItems: [], - direction: '-', - draggingHeight: REORDER_ITEM_HEIGHT, - isReorderOn: false, - groupId: '', - }, - setReorderState: () => () => {}, -}); +export const ReorderContext = React.createContext([ + initialState, + () => () => {}, +]); /** * To create a reordering group, surround the elements from the same group with a `ReorderProvider` - * @param id * @param children * @param className - * @param draggingHeight * @param dataTestSubj * @constructor */ + +interface ResetAction { + type: 'reset'; +} + +interface RegisterDraggingItemHeightAction { + type: 'registerDraggingItemHeight'; + payload: number; +} + +interface RegisterReorderedItemHeightAction { + type: 'registerReorderedItemHeight'; + payload: { id: string; height: number }; +} + +interface SetIsReorderOnAction { + type: 'setIsReorderOn'; + payload: boolean; +} + +interface SetReorderedItemsAction { + type: 'setReorderedItems'; + payload: { + items: ReorderState['reorderedItems']; + draggingIndex: number; + droppingIndex: number; + }; +} + +type ReorderAction = + | ResetAction + | RegisterDraggingItemHeightAction + | RegisterReorderedItemHeightAction + | SetIsReorderOnAction + | SetReorderedItemsAction; + +const reorderReducer = (state: ReorderState, action: ReorderAction) => { + switch (action.type) { + case 'reset': + return { ...state, reorderedItems: [] }; + case 'registerDraggingItemHeight': + return { ...state, draggingHeight: action.payload }; + case 'registerReorderedItemHeight': + return { + ...state, + reorderedItems: state.reorderedItems.map((i) => + i.id === action.payload.id ? { ...i, height: action.payload.height } : i + ), + }; + case 'setIsReorderOn': + return { ...state, isReorderOn: action.payload }; + case 'setReorderedItems': + const { items, draggingIndex, droppingIndex } = action.payload; + return draggingIndex < droppingIndex + ? { + ...state, + reorderedItems: items.slice(draggingIndex + 1, droppingIndex + 1), + direction: '-' as const, + } + : { + ...state, + reorderedItems: items.slice(droppingIndex, draggingIndex), + direction: '+' as const, + }; + default: + return state; + } +}; + export function ReorderProvider({ - id, children, className, - draggingHeight = REORDER_ITEM_HEIGHT, dataTestSubj = DEFAULT_DATA_TEST_SUBJ, }: { - id: string; children: React.ReactNode; className?: string; - draggingHeight?: number; dataTestSubj?: string; }) { - const [state, setState] = useState({ - reorderedItems: [], - direction: '-', - draggingHeight, - isReorderOn: false, - groupId: id, - }); - - const setReorderState = useMemo( - () => (dispatch: SetReorderStateDispatch) => setState(dispatch), - [setState] + const [state, dispatch] = useReducer>( + reorderReducer, + initialState ); return ( @@ -103,9 +150,7 @@ export function ReorderProvider({ 'domDragDrop-isActiveGroup': state.isReorderOn && React.Children.count(children) > 1, })} > - - {children} - + {children}

); } diff --git a/packages/kbn-dom-drag-drop/src/providers/types.tsx b/packages/kbn-dom-drag-drop/src/providers/types.tsx index 3c1523cb15e9b..e0a460569a70f 100644 --- a/packages/kbn-dom-drag-drop/src/providers/types.tsx +++ b/packages/kbn-dom-drag-drop/src/providers/types.tsx @@ -7,6 +7,7 @@ */ import { DropType } from '../types'; +import { DragDropAction } from './providers'; export interface HumanData { label: string; @@ -57,45 +58,34 @@ export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => export type RegisteredDropTargets = Record | undefined; -/** - * The shape of the drag / drop context. - */ export interface DragContextState { /** * The item being dragged or undefined. */ dragging?: DraggingIdentifier; - /** * keyboard mode */ keyboardMode: boolean; /** - * keyboard mode + * currently selected drop target */ - setKeyboardMode: (mode: boolean) => void; + activeDropTarget?: DropIdentifier; /** - * Set the item being dragged. + * currently registered drop targets */ - setDragging: (dragging?: DraggingIdentifier) => void; - - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: RegisteredDropTargets; - setActiveDropTarget: (newTarget?: DropIdentifier) => void; - - setA11yMessage: (message: string) => void; - registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; - /** * Customizable data-test-subj prefix */ dataTestSubjPrefix: string; - - /** - * A custom callback for telemetry - * @param event - */ - onTrackUICounterEvent?: (event: string) => void; } + +export type CustomMiddleware = (action: DragDropAction) => void; + +export type DragContextValue = [ + state: DragContextState, + dispatch: React.Dispatch, + customMiddleware?: CustomMiddleware +]; diff --git a/packages/kbn-unified-field-list/src/components/field_item_button/field_item_button.tsx b/packages/kbn-unified-field-list/src/components/field_item_button/field_item_button.tsx index 086db539487ec..13860a0e4f155 100644 --- a/packages/kbn-unified-field-list/src/components/field_item_button/field_item_button.tsx +++ b/packages/kbn-unified-field-list/src/components/field_item_button/field_item_button.tsx @@ -62,6 +62,7 @@ export interface FieldItemButtonProps { * @param otherProps * @constructor */ + export function FieldItemButton({ field, fieldSearchHighlight, diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index b31feed5813fe..de51aa80c92d4 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import './discover_layout.scss'; -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiButtonIcon, EuiFlexGroup, @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; import classNames from 'classnames'; import { generateFilters } from '@kbn/data-plugin/public'; -import { DragContext } from '@kbn/dom-drag-drop'; +import { useDragDropContext } from '@kbn/dom-drag-drop'; import { DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; import { useSavedSearchInitial } from '../../services/discover_state_provider'; import { DiscoverStateContainer } from '../../services/discover_state'; @@ -182,8 +182,8 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { const resizeRef = useRef(null); - const dragDropContext = useContext(DragContext); - const draggingFieldName = dragDropContext.dragging?.id; + const [{ dragging }] = useDragDropContext(); + const draggingFieldName = dragging?.id; const onDropFieldToTable = useMemo(() => { if (!draggingFieldName || currentColumns.includes(draggingFieldName)) { diff --git a/src/plugins/event_annotation/public/components/group_editor_controls/annotation_list.tsx b/src/plugins/event_annotation/public/components/group_editor_controls/annotation_list.tsx index f29638e202548..c81552100ccc6 100644 --- a/src/plugins/event_annotation/public/components/group_editor_controls/annotation_list.tsx +++ b/src/plugins/event_annotation/public/components/group_editor_controls/annotation_list.tsx @@ -7,13 +7,18 @@ */ import { css } from '@emotion/react'; -import { DragContext, DragDrop, DropTargetSwapDuplicateCombine } from '@kbn/dom-drag-drop'; +import { + DragDrop, + DropTargetSwapDuplicateCombine, + ReorderProvider, + useDragDropContext, +} from '@kbn/dom-drag-drop'; import { DimensionButton, DimensionTrigger, EmptyDimensionButton, } from '@kbn/visualization-ui-components/public'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { euiThemeVars } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; @@ -34,8 +39,6 @@ export const AnnotationList = ({ setNewAnnotationId(uuidv4()); }, [annotations.length]); - const { dragging } = useContext(DragContext); - const addAnnotationText = i18n.translate('eventAnnotation.annotationList.add', { defaultMessage: 'Add annotation', }); @@ -59,45 +62,78 @@ export const AnnotationList = ({ [annotations, newAnnotationId, selectAnnotation, updateAnnotations] ); + const reorderAnnotations = useCallback( + ( + sourceAnnotation: EventAnnotationConfig | undefined, + targetAnnotation: EventAnnotationConfig + ) => { + if (!sourceAnnotation || sourceAnnotation.id === targetAnnotation.id) { + return annotations; + } + const newAnnotations = annotations.filter((c) => c.id !== sourceAnnotation.id); + const targetPosition = newAnnotations.findIndex((c) => c.id === targetAnnotation.id); + const targetIndex = annotations.indexOf(sourceAnnotation); + const sourceIndex = annotations.indexOf(targetAnnotation); + newAnnotations.splice( + targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, + 0, + sourceAnnotation + ); + return updateAnnotations(newAnnotations); + }, + [annotations, updateAnnotations] + ); + + const [{ dragging }] = useDragDropContext(); + return (
- {annotations.map((annotation, index) => ( -
- + {annotations.map((annotation, index) => ( +
- selectAnnotation(annotation)} - onRemoveClick={() => - updateAnnotations(annotations.filter(({ id }) => id !== annotation.id)) - } - accessorConfig={getAnnotationAccessor(annotation)} - label={annotation.label} + { + const sourceAnnotation = source + ? annotations.find(({ id }) => id === source.id) + : undefined; + reorderAnnotations(sourceAnnotation, annotation); + }} > - - - -
- ))} + selectAnnotation(annotation)} + onRemoveClick={() => + updateAnnotations(annotations.filter(({ id }) => id !== annotation.id)) + } + accessorConfig={getAnnotationAccessor(annotation)} + label={annotation.label} + > + + +
+
+ ))} +
{ fieldFormats: fieldFormatsServiceMock.createStartContract(), indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), onIndexPatternRefresh: jest.fn(), - dragDropContext: createMockedDragDropContext(), currentIndexPatternId: '1', core, dateRange: { @@ -387,10 +386,6 @@ describe('FormBased Data Panel', () => { currentIndexPatternId: '', }} setState={jest.fn()} - dragDropContext={{ - ...createMockedDragDropContext(), - dragging: { id: '1', humanData: { label: 'Label' } }, - }} frame={createMockFramePublicAPI()} /> ); @@ -413,10 +408,9 @@ describe('FormBased Data Panel', () => { dataViews, }), setState: jest.fn(), - dragDropContext: { - ...createMockedDragDropContext(), + dragDropContext: createMockedDragDropContext({ dragging: { id: '1', humanData: { label: 'Label' } }, - }, + }), dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, frame: getFrameAPIMock({ indexPatterns: indexPatterns as unknown as DataViewsState['indexPatterns'], diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx index 3fddb3bedd9a9..82758d83b56c9 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx @@ -27,7 +27,6 @@ import { useExistingFieldsFetcher, useGroupedFields, } from '@kbn/unified-field-list'; -import { ChildDragDropProvider, DragContextState } from '@kbn/dom-drag-drop'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DatasourceDataPanelProps, @@ -41,7 +40,7 @@ import { FieldItem } from '../common/field_item'; export type Props = Omit< DatasourceDataPanelProps, - 'core' | 'onChangeIndexPattern' + 'core' | 'onChangeIndexPattern' | 'dragDropContext' > & { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; @@ -77,7 +76,6 @@ function onSupportedFieldFilter(field: IndexPatternField): boolean { export function FormBasedDataPanel({ state, - dragDropContext, core, data, dataViews, @@ -144,7 +142,6 @@ export function FormBasedDataPanel({ query={query} dateRange={dateRange} filters={filters} - dragDropContext={dragDropContext} core={core} data={data} dataViews={dataViews} @@ -171,7 +168,6 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ query, dateRange, filters, - dragDropContext, core, data, dataViews, @@ -187,14 +183,13 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ activeIndexPatterns, }: Omit< DatasourceDataPanelProps, - 'state' | 'setState' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns' + 'state' | 'setState' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns' | 'dragDropContext' > & { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; core: CoreStart; currentIndexPatternId: string; - dragDropContext: DragContextState; charts: ChartsPluginSetup; frame: FramePublicAPI; indexPatternFieldEditor: IndexPatternFieldEditorStart; @@ -398,20 +393,18 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ ); return ( - - } - > - - {...fieldListGroupedProps} - renderFieldItem={renderFieldItem} - data-test-subj="lnsIndexPattern" - localStorageKeyPrefix="lens" - /> - - + } + > + + {...fieldListGroupedProps} + renderFieldItem={renderFieldItem} + data-test-subj="lnsIndexPattern" + localStorageKeyPrefix="lens" + /> + ); }; diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index f650fe36b4426..533cdb179f64a 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -25,8 +25,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { EuiButton } from '@elastic/eui'; import type { SharePluginStart } from '@kbn/share-plugin/public'; -import type { DraggingIdentifier } from '@kbn/dom-drag-drop'; +import { ChildDragDropProvider, type DraggingIdentifier } from '@kbn/dom-drag-drop'; import { DimensionTrigger } from '@kbn/visualization-ui-components/public'; +import memoizeOne from 'memoize-one'; import type { DatasourceDimensionEditorProps, DatasourceDimensionTriggerProps, @@ -72,7 +73,7 @@ import { cloneLayer, getNotifiableFeatures, } from './utils'; -import { getUniqueLabelGenerator, isDraggedDataViewField } from '../../utils'; +import { getUniqueLabelGenerator, isDraggedDataViewField, nonNullable } from '../../utils'; import { hasField, normalizeOperationDataType } from './pure_utils'; import { LayerPanel } from './layerpanel'; import { @@ -112,6 +113,20 @@ function wrapOnDot(str?: string) { return str ? str.replace(/\./g, '.\u200B') : ''; } +const getSelectedFieldsFromColumns = memoizeOne( + (columns: GenericIndexPatternColumn[]) => + columns + .flatMap((c) => { + if (operationDefinitionMap[c.operationType]?.getCurrentFields) { + return operationDefinitionMap[c.operationType]?.getCurrentFields?.(c) || []; + } else if ('sourceField' in c) { + return c.sourceField; + } + }) + .filter(nonNullable), + isEqual +); + function getSortingHint(column: GenericIndexPatternColumn, dataView?: IndexPattern | DataView) { if (column.dataType === 'string') { const fieldTypes = @@ -421,18 +436,9 @@ export function getFormBasedDatasource({ }, getSelectedFields(state) { - const fields: string[] = []; - Object.values(state?.layers)?.forEach((l) => { - const { columns } = l; - Object.values(columns).forEach((c) => { - if (operationDefinitionMap[c.operationType]?.getCurrentFields) { - fields.push(...(operationDefinitionMap[c.operationType]?.getCurrentFields?.(c) || [])); - } else if ('sourceField' in c) { - fields.push(c.sourceField); - } - }); - }); - return fields; + return getSelectedFieldsFromColumns( + Object.values(state?.layers)?.flatMap((l) => Object.values(l.columns)) + ); }, toExpression: (state, layerId, indexPatterns, dateRange, nowInstant, searchSessionId) => @@ -470,7 +476,7 @@ export function getFormBasedDatasource({ }, renderDataPanel(domElement: Element, props: DatasourceDataPanelProps) { - const { onChangeIndexPattern, ...otherProps } = props; + const { onChangeIndexPattern, dragDropContext, ...otherProps } = props; const layerFields = formBasedDatasource?.getSelectedFields?.(props.state); render( @@ -487,18 +493,20 @@ export function getFormBasedDatasource({ share, }} > - + + + , diff --git a/x-pack/plugins/lens/public/datasources/form_based/mocks.ts b/x-pack/plugins/lens/public/datasources/form_based/mocks.ts index c148ebc9f9829..c01495f1993e4 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/mocks.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/mocks.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { DragContextState } from '@kbn/dom-drag-drop'; import { getFieldByNameFactory } from './pure_helpers'; import type { IndexPattern, IndexPatternField } from '../../types'; @@ -217,18 +216,3 @@ export const createMockedIndexPatternWithoutType = ( getFieldByName: getFieldByNameFactory(filteredFields), }; }; - -export function createMockedDragDropContext(): jest.Mocked { - return { - dataTestSubjPrefix: 'lnsDragDrop', - dragging: undefined, - setDragging: jest.fn(), - activeDropTarget: undefined, - setActiveDropTarget: jest.fn(), - keyboardMode: false, - setKeyboardMode: jest.fn(), - setA11yMessage: jest.fn(), - dropTargetsByOrder: undefined, - registerDropTarget: jest.fn(), - }; -} diff --git a/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx b/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx index abc84813ce9a2..210678fa909f3 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx @@ -28,8 +28,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock'; -import { createMockFramePublicAPI } from '../../mocks'; -import { createMockedDragDropContext } from './mocks'; +import { createMockFramePublicAPI, createMockedDragDropContext } from '../../mocks'; import { DataViewsState } from '../../state_management'; const fieldsFromQuery = [ diff --git a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx index 1a5bfeaa41be9..8477b94d951bf 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx @@ -24,7 +24,6 @@ import { GetCustomFieldType, useGroupedFields, } from '@kbn/unified-field-list'; -import { ChildDragDropProvider } from '@kbn/dom-drag-drop'; import type { DatasourceDataPanelProps } from '../../types'; import type { TextBasedPrivateState } from './types'; import { getStateFromAggregateQuery } from './utils'; @@ -42,7 +41,6 @@ export type TextBasedDataPanelProps = DatasourceDataPanelProps) { const prevQuery = usePrevious(query); const [dataHasLoaded, setDataHasLoaded] = useState(false); useEffect(() => { @@ -138,22 +136,20 @@ export function TextBasedDataPanel({ ...core, }} > - - - } - > - - {...fieldListGroupedProps} - renderFieldItem={renderFieldItem} - data-test-subj="lnsTextBasedLanguages" - localStorageKeyPrefix="lens" - /> - - + + } + > + + {...fieldListGroupedProps} + renderFieldItem={renderFieldItem} + data-test-subj="lnsTextBasedLanguages" + localStorageKeyPrefix="lens" + /> + ); } diff --git a/x-pack/plugins/lens/public/datasources/text_based/mocks.ts b/x-pack/plugins/lens/public/datasources/text_based/mocks.ts deleted file mode 100644 index 870b43c7b0a84..0000000000000 --- a/x-pack/plugins/lens/public/datasources/text_based/mocks.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DragContextState } from '@kbn/dom-drag-drop'; - -export function createMockedDragDropContext(): jest.Mocked { - return { - dataTestSubjPrefix: 'lnsDragDrop', - dragging: undefined, - setDragging: jest.fn(), - activeDropTarget: undefined, - setActiveDropTarget: jest.fn(), - keyboardMode: false, - setKeyboardMode: jest.fn(), - setA11yMessage: jest.fn(), - dropTargetsByOrder: undefined, - registerDropTarget: jest.fn(), - }; -} diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx index 653a3f30e1b44..f93efd9835ee2 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx @@ -20,6 +20,9 @@ import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugi import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { euiThemeVars } from '@kbn/ui-theme'; import { DimensionTrigger } from '@kbn/visualization-ui-components/public'; +import memoizeOne from 'memoize-one'; +import { isEqual } from 'lodash'; +import { ChildDragDropProvider } from '@kbn/dom-drag-drop'; import { DatasourceDimensionEditorProps, DatasourceDataPanelProps, @@ -43,12 +46,24 @@ import type { import { FieldSelect } from './field_select'; import type { Datasource, IndexPatternMap } from '../../types'; import { LayerPanel } from './layerpanel'; -import { getUniqueLabelGenerator } from '../../utils'; +import { getUniqueLabelGenerator, nonNullable } from '../../utils'; function getLayerReferenceName(layerId: string) { return `textBasedLanguages-datasource-layer-${layerId}`; } +const getSelectedFieldsFromColumns = memoizeOne( + (columns: TextBasedLayerColumn[]) => + columns + .map((c) => { + if ('fieldName' in c) { + return c.fieldName; + } + }) + .filter(nonNullable), + isEqual +); + export function getTextBasedDatasource({ core, storage, @@ -344,30 +359,26 @@ export function getTextBasedDatasource({ return toExpression(state, layerId); }, getSelectedFields(state) { - const fields: string[] = []; - Object.values(state?.layers)?.forEach((l) => { - const { columns } = l; - Object.values(columns).forEach((c) => { - if ('fieldName' in c) { - fields.push(c.fieldName); - } - }); - }); - return fields; + return getSelectedFieldsFromColumns( + Object.values(state?.layers)?.flatMap((l) => Object.values(l.columns)) + ); }, renderDataPanel(domElement: Element, props: DatasourceDataPanelProps) { const layerFields = TextBasedDatasource?.getSelectedFields?.(props.state); + const { dragDropContext, ...otherProps } = props; render( - + + + , domElement diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx index 910f97fb446e3..ae226521799eb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React, { useMemo, useCallback, useContext, ReactElement } from 'react'; +import React, { useMemo, useCallback, ReactElement } from 'react'; import { DragDrop, DragDropIdentifier, - DragContext, + useDragDropContext, DropType, DropTargetSwapDuplicateCombine, } from '@kbn/dom-drag-drop'; @@ -61,7 +61,7 @@ export function DraggableDimensionButton({ registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; indexPatterns: IndexPatternMap; }) { - const { dragging } = useContext(DragContext); + const [{ dragging }] = useDragDropContext(); let getDropProps; @@ -139,20 +139,20 @@ export function DraggableDimensionButton({ data-test-subj={group.dataTestSubj} > 1 ? reorderableGroup : undefined} value={value} onDrop={handleOnDrop} - onDragStart={() => onDragStart()} - onDragEnd={() => onDragEnd()} + onDragStart={onDragStart} + onDragEnd={onDragEnd} > {children} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index 1e2b3ff4b4c05..051ba4876eb35 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import React, { useMemo, useState, useEffect, useContext } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { DragDrop, DragDropIdentifier, - DragContext, + useDragDropContext, DropType, DropTargetSwapDuplicateCombine, } from '@kbn/dom-drag-drop'; @@ -116,7 +116,7 @@ export function EmptyDimensionButton({ }; }; }) { - const { dragging } = useContext(DragContext); + const [{ dragging }] = useDragDropContext(); let getDropProps; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 1555445d482d8..9db931efeda26 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -20,6 +20,7 @@ import { createMockDatasource, DatasourceMock, mountWithProvider, + createMockedDragDropContext, } from '../../../mocks'; import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock'; import { DimensionButton } from '@kbn/visualization-ui-components/public'; @@ -52,19 +53,6 @@ afterEach(() => { container = undefined; }); -const defaultContext = { - dataTestSubjPrefix: 'lnsDragDrop', - dragging: undefined, - setDragging: jest.fn(), - setActiveDropTarget: () => {}, - activeDropTarget: undefined, - dropTargetsByOrder: undefined, - keyboardMode: false, - setKeyboardMode: () => {}, - setA11yMessage: jest.fn(), - registerDropTarget: jest.fn(), -}; - const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', @@ -773,7 +761,7 @@ describe('LayerPanel', () => { }); const { instance } = await mountWithProvider( - + ); @@ -817,7 +805,7 @@ describe('LayerPanel', () => { ); const { instance } = await mountWithProvider( - + ); @@ -886,7 +874,7 @@ describe('LayerPanel', () => { }; const { instance } = await mountWithProvider( - + ); @@ -956,7 +944,7 @@ describe('LayerPanel', () => { }; const { instance } = await mountWithProvider( - + , undefined, @@ -1009,7 +997,7 @@ describe('LayerPanel', () => { }; const { instance } = await mountWithProvider( - + ); @@ -1064,7 +1052,7 @@ describe('LayerPanel', () => { const mockOnRemoveDimension = jest.fn(); const { instance } = await mountWithProvider( - + { const mockOnRemoveDimension = jest.fn(); const { instance } = await mountWithProvider( - + { const mockOnRemoveDimension = jest.fn(); const { instance } = await mountWithProvider( - + { const mockOnRemoveDimension = jest.fn(); const { instance } = await mountWithProvider( - + <> {group.accessors.length ? ( - + {group.accessors.map((accessorConfig, accessorIndex) => { const { columnId } = accessorConfig; @@ -644,7 +640,6 @@ export function LayerPanel( {group.fakeFinalAccessor && (
arg, isEqual); + export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { const externalContext = useLensSelector(selectExecutionContext); const activeDatasourceId = useLensSelector(selectActiveDatasourceId); @@ -158,7 +162,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { const datasourceProps: DatasourceDataPanelProps = { ...externalContext, - dragDropContext: useContext(DragContext), + dragDropContext: useDragDropContext(), state: activeDatasourceId ? datasourceStates[activeDatasourceId].state : null, setState: setDatasourceState, core: props.core, @@ -170,7 +174,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { indexPatternService: props.indexPatternService, frame: props.frame, // Visualization can handle dataViews, so need to pass to the data panel the full list of used dataViews - usedIndexPatterns: [ + usedIndexPatterns: memoizeStrictlyEqual([ ...((activeDatasourceId && props.datasourceMap[activeDatasourceId]?.getUsedDataViews( datasourceStates[activeDatasourceId].state @@ -181,7 +185,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { visualizationState.state )) || []), - ], + ]), }; return ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 01ac257dbcb2d..8571b2ca347b0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -820,11 +820,16 @@ describe('editor_frame', () => { getDatasourceSuggestionsForField: () => [generateSuggestion()], getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], - renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { + renderDataPanel: (_element, { dragDropContext: [{ dragging }, dndDispatch] }) => { if (!dragging || dragging.id !== 'draggedField') { - setDragging({ - id: 'draggedField', - humanData: { label: 'draggedField' }, + dndDispatch({ + type: 'startDragging', + payload: { + dragging: { + id: 'draggedField', + humanData: { label: 'draggedField' }, + }, + }, }); } }, @@ -922,11 +927,16 @@ describe('editor_frame', () => { getDatasourceSuggestionsForField: () => [generateSuggestion()], getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], - renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { + renderDataPanel: (_element, { dragDropContext: [{ dragging }, dndDispatch] }) => { if (!dragging || dragging.id !== 'draggedField') { - setDragging({ - id: 'draggedField', - humanData: { label: '1' }, + dndDispatch({ + type: 'startDragging', + payload: { + dragging: { + id: 'draggedField', + humanData: { label: '1' }, + }, + }, }); } }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index a7e1e1449ad2f..24d0d3bba780c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useRef } from 'react'; import { CoreStart } from '@kbn/core/public'; import { ReactExpressionRendererType } from '@kbn/expressions-plugin/public'; -import { DragDropIdentifier, RootDragDropProvider } from '@kbn/dom-drag-drop'; +import { type DragDropAction, DragDropIdentifier, RootDragDropProvider } from '@kbn/dom-drag-drop'; import { trackUiCounterEvents } from '../../lens_ui_telemetry'; import { DatasourceMap, @@ -108,8 +108,14 @@ export function EditorFrame(props: EditorFrameProps) { const bannerMessages = props.getUserMessages('banner', { severity: 'warning' }); + const telemetryMiddleware = useCallback((action: DragDropAction) => { + if (action.type === 'dropToTarget') { + trackUiCounterEvents('drop_total'); + } + }, []); + return ( - + { @@ -934,18 +935,7 @@ describe('workspace_panel', () => { async function initComponent(draggingContext = draggedField) { const mounted = await mountWithProvider( - {}} - setActiveDropTarget={() => {}} - activeDropTarget={undefined} - keyboardMode={false} - setKeyboardMode={() => {}} - setA11yMessage={() => {}} - registerDropTarget={jest.fn()} - dropTargetsByOrder={undefined} - > + dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging), - [dragDropContext.dragging, getSuggestionForField] + () => dragging && getSuggestionForField(dragging), + [dragging, getSuggestionForField] ); return ( @@ -573,15 +573,15 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ ); }; - const dragDropContext = useContext(DragContext); + const [{ dragging }] = useDragDropContext(); const renderWorkspace = () => { const customWorkspaceRenderer = activeDatasourceId && datasourceMap[activeDatasourceId]?.getCustomWorkspaceRenderer && - dragDropContext.dragging + dragging ? datasourceMap[activeDatasourceId].getCustomWorkspaceRenderer!( datasourceStates[activeDatasourceId].state, - dragDropContext.dragging, + dragging, dataViews.indexPatterns ) : undefined; diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts index 5cd62b5427cb4..c71da33fda3c9 100644 --- a/x-pack/plugins/lens/public/mocks/index.ts +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DragContextState, DragContextValue } from '@kbn/dom-drag-drop'; import { createMockDataViewsState } from '../data_views_service/mocks'; import { FramePublicAPI, FrameDatasourceAPI } from '../types'; export { mockDataPlugin } from './data_plugin_mock'; @@ -65,3 +66,20 @@ export const createMockFrameDatasourceAPI = ({ filters: filters ?? [], dataViews: createMockDataViewsState(dataViews), }); + +export function createMockedDragDropContext( + partialState?: Partial, + setState?: jest.Mocked[1] +): jest.Mocked { + return [ + { + dataTestSubjPrefix: 'lnsDragDrop', + dragging: undefined, + keyboardMode: false, + activeDropTarget: undefined, + dropTargetsByOrder: undefined, + ...partialState, + }, + setState ? setState : jest.fn(), + ]; +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 607b46f9e98f2..042bdef118984 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -43,7 +43,7 @@ import { EventAnnotationGroupConfig } from '@kbn/event-annotation-plugin/common' import type { DraggingIdentifier, DragDropIdentifier, - DragContextState, + DragContextValue, DropType, } from '@kbn/dom-drag-drop'; import type { AccessorConfig } from '@kbn/visualization-ui-components/public'; @@ -588,7 +588,7 @@ export interface DatasourceLayerSettingsProps { export interface DatasourceDataPanelProps { state: T; - dragDropContext: DragContextState; + dragDropContext: DragContextValue; setState: StateSetter; showNoDataPopover: () => void; core: Pick<