diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index f43063d45a47c..f029c776e8452 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -45,6 +45,9 @@ .lnsDragDrop-isDropTarget { @include lnsDroppable; @include lnsDroppableActive; + > * { + pointer-events: none; + } } .lnsDragDrop-isActiveGroup { diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index f2a2fda730388..2fc5efaa28b83 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -14,7 +14,7 @@ import { ReorderProvider, DragDropIdentifier, DraggingIdentifier, - DropTargets, + DropIdentifier, } from './providers'; import { act } from 'react-dom/test-utils'; import { DropType } from '../types'; @@ -32,6 +32,7 @@ describe('DragDrop', () => { setDragging: jest.fn(), setActiveDropTarget: jest.fn(), activeDropTarget: undefined, + dropTargetsByOrder: undefined, keyboardMode: false, setKeyboardMode: () => {}, setA11yMessage: jest.fn(), @@ -255,11 +256,10 @@ describe('DragDrop', () => { dragging = { id: '1', humanData: { label: 'Label1' } }; }} setActiveDropTarget={setActiveDropTarget} - activeDropTarget={ - ({ activeDropTarget: value } as unknown) as DragContextState['activeDropTarget'] - } + activeDropTarget={value as DragContextState['activeDropTarget']} keyboardMode={false} setKeyboardMode={(keyboardMode) => true} + dropTargetsByOrder={undefined} registerDropTarget={jest.fn()} > { dragging: { ...items[0].value, ghost: { children:
, style: {} } }, setActiveDropTarget, setA11yMessage, - activeDropTarget: { - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - '2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, - }, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + '2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, }, keyboardMode: true, }} @@ -463,11 +461,9 @@ describe('DragDrop', () => { dragging: { ...items[0].value, ghost: { children:
Hello
, style: {} } }, setActiveDropTarget, setA11yMessage, - activeDropTarget: { - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - }, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, }, keyboardMode: true, }} @@ -525,11 +521,12 @@ describe('DragDrop', () => { keyboardMode = mode; }), setActiveDropTarget: (target?: DragDropIdentifier) => { - activeDropTarget = { activeDropTarget: target } as DropTargets; + activeDropTarget = target as DropIdentifier; }, activeDropTarget, setA11yMessage, registerDropTarget, + dropTargetsByOrder: undefined, }; const dragDropSharedProps = { @@ -665,13 +662,11 @@ describe('DragDrop', () => { const component = mountComponent({ dragging: { ...items[0] }, keyboardMode: true, - activeDropTarget: { - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + 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, @@ -693,15 +688,12 @@ describe('DragDrop', () => { test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { const component = mountComponent({ dragging: { ...items[0] }, - activeDropTarget: { - activeDropTarget: { ...items[2], dropType: 'reorder', onDrop }, - dropTargetsByOrder: { - '2,0,0': { ...items[0], onDrop, dropType: 'reorder' }, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: { ...items[2], dropType: 'reorder', onDrop }, + dropTargetsByOrder: { + '2,0,0': { ...items[0], onDrop, dropType: 'reorder' }, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, - keyboardMode: true, }); const keyboardHandler = component @@ -747,13 +739,11 @@ describe('DragDrop', () => { const component = mountComponent({ dragging: { ...items[0] }, keyboardMode: true, - activeDropTarget: { - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, setA11yMessage, }); @@ -799,15 +789,13 @@ describe('DragDrop', () => { {...defaultContext} keyboardMode={true} activeDropTarget={{ - activeDropTarget: { - ...items[1], - onDrop, - dropType: 'reorder', - }, - dropTargetsByOrder: { - '2,0,1,0': undefined, - '2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' }, - }, + ...items[1], + onDrop, + dropType: 'reorder', + }} + dropTargetsByOrder={{ + '2,0,1,0': undefined, + '2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' }, }} dragging={{ ...items[0] }} setActiveDropTarget={setActiveDropTarget} diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 6c6a65ab421b3..618a7accb9b2b 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -19,8 +19,8 @@ import { ReorderContext, ReorderState, DropHandler, + announce, } from './providers'; -import { announce } from './announcements'; import { trackUiEvent } from '../lens_ui_telemetry'; import { DropType } from '../types'; @@ -99,13 +99,15 @@ interface BaseProps { * The props for a draggable instance of that component. */ interface DragInnerProps extends BaseProps { - isDragging: boolean; - keyboardMode: boolean; setKeyboardMode: DragContextState['setKeyboardMode']; setDragging: DragContextState['setDragging']; setActiveDropTarget: DragContextState['setActiveDropTarget']; setA11yMessage: DragContextState['setA11yMessage']; - activeDropTarget: DragContextState['activeDropTarget']; + activeDraggingProps?: { + keyboardMode: DragContextState['keyboardMode']; + activeDropTarget: DragContextState['activeDropTarget']; + dropTargetsByOrder: DragContextState['dropTargetsByOrder']; + }; onDragStart?: ( target?: | DroppableEvent['currentTarget'] @@ -121,6 +123,7 @@ interface DragInnerProps extends BaseProps { */ interface DropInnerProps extends BaseProps { dragging: DragContextState['dragging']; + keyboardMode: DragContextState['keyboardMode']; setKeyboardMode: DragContextState['setKeyboardMode']; setDragging: DragContextState['setDragging']; setActiveDropTarget: DragContextState['setActiveDropTarget']; @@ -136,8 +139,9 @@ export const DragDrop = (props: BaseProps) => { const { dragging, setDragging, - registerDropTarget, keyboardMode, + registerDropTarget, + dropTargetsByOrder, setKeyboardMode, activeDropTarget, setActiveDropTarget, @@ -147,34 +151,31 @@ export const DragDrop = (props: BaseProps) => { const { value, draggable, dropType, reorderableGroup } = props; const isDragging = !!(draggable && value.id === dragging?.id); + const activeDraggingProps = isDragging + ? { + keyboardMode, + activeDropTarget, + dropTargetsByOrder, + } + : undefined; + if (draggable && !dropType) { const dragProps = { ...props, - isDragging, - keyboardMode: isDragging ? keyboardMode : false, // optimization to not rerender all dragging components - activeDropTarget: isDragging ? activeDropTarget : undefined, // optimization to not rerender all dragging components + activeDraggingProps, setKeyboardMode, setDragging, setActiveDropTarget, setA11yMessage, }; if (reorderableGroup && reorderableGroup.length > 1) { - return ( - - ); + return ; } else { - return ; + return ; } } - const isActiveDropTarget = Boolean( - activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id - ); + const isActiveDropTarget = Boolean(activeDropTarget?.id === value.id); const dropProps = { ...props, keyboardMode, @@ -210,9 +211,7 @@ const DragInner = memo(function DragInner({ setKeyboardMode, setActiveDropTarget, order, - keyboardMode, - isDragging, - activeDropTarget, + activeDraggingProps, dragType, onDragStart, onDragEnd, @@ -220,6 +219,10 @@ const DragInner = memo(function DragInner({ ariaDescribedBy, setA11yMessage, }: DragInnerProps) { + const keyboardMode = activeDraggingProps?.keyboardMode; + const activeDropTarget = activeDraggingProps?.activeDropTarget; + const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const dragStart = ( e: DroppableEvent | React.KeyboardEvent, keyboardModeOn?: boolean @@ -273,9 +276,9 @@ const DragInner = memo(function DragInner({ } }; const dropToActiveDropTarget = () => { - if (isDragging && activeDropTarget?.activeDropTarget) { + if (activeDropTarget) { trackUiEvent('drop_total'); - const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget.activeDropTarget; + const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget; setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType))); onTargetDrop(value, dropType); } @@ -287,6 +290,7 @@ const DragInner = memo(function DragInner({ } const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [order.join(',')], (el) => el?.dropType !== 'reorder', @@ -301,11 +305,10 @@ const DragInner = memo(function DragInner({ ); }; const shouldShowGhostImageInstead = - isDragging && dragType === 'move' && keyboardMode && - activeDropTarget?.activeDropTarget && - activeDropTarget?.activeDropTarget.dropType !== 'reorder'; + activeDropTarget && + activeDropTarget.dropType !== 'reorder'; return (
{ - if (isDragging) { + if (activeDraggingProps) { dragEnd(); } }} @@ -331,13 +334,13 @@ const DragInner = memo(function DragInner({ dropToActiveDropTarget(); } - if (isDragging) { + if (activeDraggingProps) { dragEnd(); } else { dragStart(e, true); } } else if (key === keys.ESCAPE) { - if (isDragging) { + if (activeDraggingProps) { e.stopPropagation(); e.preventDefault(); dragEnd(); @@ -357,7 +360,8 @@ const DragInner = memo(function DragInner({ 'data-test-subj': dataTestSubj || 'lnsDragDrop', className: classNames(children.props.className, 'lnsDragDrop', 'lnsDragDrop-isDraggable', { 'lnsDragDrop-isHidden': - (isDragging && dragType === 'move' && !keyboardMode) || shouldShowGhostImageInstead, + (activeDraggingProps && dragType === 'move' && !keyboardMode) || + shouldShowGhostImageInstead, }), draggable: true, onDragEnd: dragEnd, @@ -384,19 +388,20 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { isActiveDropTarget, registerDropTarget, setActiveDropTarget, + keyboardMode, setKeyboardMode, setDragging, setA11yMessage, } = props; useShallowCompareEffect(() => { - if (dropType && value && onDrop) { + if (dropType && onDrop && keyboardMode) { registerDropTarget(order, { ...value, onDrop, dropType }); return () => { registerDropTarget(order, undefined); }; } - }, [order, value, registerDropTarget, dropType]); + }, [order, value, registerDropTarget, dropType, keyboardMode]); const classesOnEnter = getAdditionalClassesOnEnter?.(dropType); const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType); @@ -481,17 +486,19 @@ const ReorderableDrag = memo(function ReorderableDrag( const { value, setActiveDropTarget, - keyboardMode, - isDragging, - activeDropTarget, + activeDraggingProps, reorderableGroup, setA11yMessage, } = props; + const keyboardMode = activeDraggingProps?.keyboardMode; + const activeDropTarget = activeDraggingProps?.activeDropTarget; + const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const isDragging = !!activeDraggingProps; + const isFocusInGroup = keyboardMode ? isDragging && - (!activeDropTarget?.activeDropTarget || - reorderableGroup.some((i) => i.id === activeDropTarget?.activeDropTarget?.id)) + (!activeDropTarget || reorderableGroup.some((i) => i.id === activeDropTarget?.id)) : isDragging; useEffect(() => { @@ -530,10 +537,8 @@ const ReorderableDrag = memo(function ReorderableDrag( e.stopPropagation(); e.preventDefault(); let activeDropTargetIndex = reorderableGroup.findIndex((i) => i.id === value.id); - if (activeDropTarget?.activeDropTarget) { - const index = reorderableGroup.findIndex( - (i) => i.id === activeDropTarget.activeDropTarget?.id - ); + if (activeDropTarget) { + const index = reorderableGroup.findIndex((i) => i.id === activeDropTarget?.id); if (index !== -1) activeDropTargetIndex = index; } if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) { @@ -542,6 +547,7 @@ const ReorderableDrag = memo(function ReorderableDrag( } else if (keys.ARROW_DOWN === e.key) { if (activeDropTargetIndex < reorderableGroup.length - 1) { const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [props.order.join(',')], (el) => el?.dropType === 'reorder' @@ -551,6 +557,7 @@ const ReorderableDrag = memo(function ReorderableDrag( } else if (keys.ARROW_UP === e.key) { if (activeDropTargetIndex > 0) { const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [props.order.join(',')], (el) => el?.dropType === 'reorder', diff --git a/x-pack/plugins/lens/public/drag_drop/announcements.tsx b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx similarity index 98% rename from x-pack/plugins/lens/public/drag_drop/announcements.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx index 3c65008f8f38b..3bd1d5693005c 100644 --- a/x-pack/plugins/lens/public/drag_drop/announcements.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx @@ -6,13 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { DropType } from '../types'; -export interface HumanData { - label: string; - groupLabel?: string; - position?: number; - nextLabel?: string; -} +import { DropType } from '../../types'; +import { HumanData } from '.'; type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string; diff --git a/x-pack/plugins/lens/public/drag_drop/providers/index.tsx b/x-pack/plugins/lens/public/drag_drop/providers/index.tsx new file mode 100644 index 0000000000000..4262b65c85887 --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/index.tsx @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export * from './providers'; +export * from './reorder_provider'; +export * from './types'; +export * from './announcements'; diff --git a/x-pack/plugins/lens/public/drag_drop/providers.test.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx similarity index 94% rename from x-pack/plugins/lens/public/drag_drop/providers.test.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx index a46b7f6f95314..a8312cc927451 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; import { mount } from 'enzyme'; -import { RootDragDropProvider, DragContext } from './providers'; +import { RootDragDropProvider, DragContext } from '.'; jest.useFakeTimers(); diff --git a/x-pack/plugins/lens/public/drag_drop/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx similarity index 53% rename from x-pack/plugins/lens/public/drag_drop/providers.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/providers.tsx index deb9bf6cb17ae..6a78bc1b46ddf 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx @@ -6,70 +6,15 @@ */ import React, { useState, useMemo } from 'react'; -import classNames from 'classnames'; import { EuiScreenReaderOnly, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { HumanData } from './announcements'; -import { DropType } from '../types'; - -/** - * A function that handles a drop event. - */ -export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void; - -export type DragDropIdentifier = Record & { - id: string; - /** - * The data for accessibility, consists of required label and not required groupLabel and position in group - */ - humanData: HumanData; -}; - -export type DraggingIdentifier = DragDropIdentifier & { - ghost?: { - children: React.ReactElement; - style: React.CSSProperties; - }; -}; - -export type DropIdentifier = DragDropIdentifier & { - dropType: DropType; - onDrop: DropHandler; -}; - -export interface DropTargets { - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; -} -/** - * The shape of the drag / drop context. - */ -export interface DragContextState { - /** - * The item being dragged or undefined. - */ - dragging?: DraggingIdentifier; - - /** - * keyboard mode - */ - keyboardMode: boolean; - /** - * keyboard mode - */ - setKeyboardMode: (mode: boolean) => void; - /** - * Set the item being dragged. - */ - setDragging: (dragging?: DraggingIdentifier) => void; - - activeDropTarget?: DropTargets; - - setActiveDropTarget: (newTarget?: DropIdentifier) => void; - - setA11yMessage: (message: string) => void; - registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; -} +import { + DropIdentifier, + DraggingIdentifier, + DragDropIdentifier, + RegisteredDropTargets, + DragContextState, +} from './types'; /** * The drag / drop context singleton, used like so: @@ -84,51 +29,18 @@ export const DragContext = React.createContext({ activeDropTarget: undefined, setActiveDropTarget: () => {}, setA11yMessage: () => {}, + dropTargetsByOrder: undefined, registerDropTarget: () => {}, }); /** * The argument to DragDropProvider. */ -export interface ProviderProps { - /** - * keyboard mode - */ - keyboardMode: boolean; - /** - * keyboard mode - */ - setKeyboardMode: (mode: boolean) => void; - /** - * Set the item being dragged. - */ - /** - * The item being dragged. If unspecified, the provider will - * behave as if it is the root provider. - */ - dragging?: DraggingIdentifier; - - /** - * Sets the item being dragged. If unspecified, the provider - * will behave as if it is the root provider. - */ - setDragging: (dragging?: DraggingIdentifier) => void; - - activeDropTarget?: { - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; - }; - - setActiveDropTarget: (newTarget?: DropIdentifier) => void; - - registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; - +export interface ProviderProps extends DragContextState { /** * The React children. */ children: React.ReactNode; - - setA11yMessage: (message: string) => void; } /** @@ -144,13 +56,11 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } }); const [keyboardModeState, setKeyboardModeState] = useState(false); const [a11yMessageState, setA11yMessageState] = useState(''); - const [activeDropTargetState, setActiveDropTargetState] = useState<{ - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; - }>({ - activeDropTarget: undefined, - dropTargetsByOrder: {}, - }); + const [activeDropTargetState, setActiveDropTargetState] = useState( + undefined + ); + + const [dropTargetsByOrderState, setDropTargetsByOrderState] = useState({}); const setDragging = useMemo( () => (dragging?: DraggingIdentifier) => setDraggingState({ dragging }), @@ -162,24 +72,20 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } ]); const setActiveDropTarget = useMemo( - () => (activeDropTarget?: DropIdentifier) => - setActiveDropTargetState((s) => ({ ...s, activeDropTarget })), + () => (activeDropTarget?: DropIdentifier) => setActiveDropTargetState(activeDropTarget), [setActiveDropTargetState] ); const registerDropTarget = useMemo( () => (order: number[], dropTarget?: DropIdentifier) => { - return setActiveDropTargetState((s) => { + return setDropTargetsByOrderState((s) => { return { ...s, - dropTargetsByOrder: { - ...s.dropTargetsByOrder, - [order.join(',')]: dropTarget, - }, + [order.join(',')]: dropTarget, }; }); }, - [setActiveDropTargetState] + [setDropTargetsByOrderState] ); return ( @@ -193,6 +99,7 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } activeDropTarget={activeDropTargetState} setActiveDropTarget={setActiveDropTarget} registerDropTarget={registerDropTarget} + dropTargetsByOrder={dropTargetsByOrderState} > {children} @@ -220,16 +127,17 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } } export function nextValidDropTarget( - activeDropTarget: DropTargets | undefined, + dropTargetsByOrder: RegisteredDropTargets, + activeDropTarget: DropIdentifier | undefined, draggingOrder: [string], filterElements: (el: DragDropIdentifier) => boolean = () => true, reverse = false ) { - if (!activeDropTarget) { + if (!dropTargetsByOrder) { return; } - const filteredTargets = [...Object.entries(activeDropTarget.dropTargetsByOrder)].filter( + const filteredTargets = Object.entries(dropTargetsByOrder).filter( ([, dropTarget]) => dropTarget && filterElements(dropTarget) ); @@ -242,7 +150,7 @@ export function nextValidDropTarget( }); let currentActiveDropIndex = nextDropTargets.findIndex( - ([_, dropTarget]) => dropTarget?.id === activeDropTarget?.activeDropTarget?.id + ([_, dropTarget]) => dropTarget?.id === activeDropTarget?.id ); if (currentActiveDropIndex === -1) { @@ -274,6 +182,7 @@ export function ChildDragDropProvider({ setActiveDropTarget, setA11yMessage, registerDropTarget, + dropTargetsByOrder, children, }: ProviderProps) { const value = useMemo( @@ -285,6 +194,7 @@ export function ChildDragDropProvider({ activeDropTarget, setActiveDropTarget, setA11yMessage, + dropTargetsByOrder, registerDropTarget, }), [ @@ -295,84 +205,9 @@ export function ChildDragDropProvider({ setKeyboardMode, keyboardMode, setA11yMessage, + dropTargetsByOrder, registerDropTarget, ] ); return {children}; } - -export interface ReorderState { - /** - * Ids of the elements that are translated up or down - */ - reorderedItems: Array<{ id: string; height?: number }>; - - /** - * Direction of the move of dragged element in the reordered list - */ - direction: '-' | '+'; - /** - * height of the dragged element - */ - draggingHeight: number; - /** - * 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; - -export interface ReorderContextState { - reorderState: ReorderState; - setReorderState: (dispatch: SetReorderStateDispatch) => void; -} - -export const ReorderContext = React.createContext({ - reorderState: { - reorderedItems: [], - direction: '-', - draggingHeight: 40, - isReorderOn: false, - groupId: '', - }, - setReorderState: () => () => {}, -}); - -export function ReorderProvider({ - id, - children, - className, -}: { - id: string; - children: React.ReactNode; - className?: string; -}) { - const [state, setState] = useState({ - reorderedItems: [], - direction: '-', - draggingHeight: 40, - isReorderOn: false, - groupId: id, - }); - - const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [ - setState, - ]); - return ( -
1, - })} - > - - {children} - -
- ); -} diff --git a/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx b/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx new file mode 100644 index 0000000000000..77620ea131513 --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx @@ -0,0 +1,85 @@ +/* + * 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 React, { useState, useMemo } from 'react'; +import classNames from 'classnames'; + +export interface ReorderState { + /** + * Ids of the elements that are translated up or down + */ + reorderedItems: Array<{ id: string; height?: number }>; + + /** + * Direction of the move of dragged element in the reordered list + */ + direction: '-' | '+'; + /** + * height of the dragged element + */ + draggingHeight: number; + /** + * 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; + +export interface ReorderContextState { + reorderState: ReorderState; + setReorderState: (dispatch: SetReorderStateDispatch) => void; +} + +export const ReorderContext = React.createContext({ + reorderState: { + reorderedItems: [], + direction: '-', + draggingHeight: 40, + isReorderOn: false, + groupId: '', + }, + setReorderState: () => () => {}, +}); + +export function ReorderProvider({ + id, + children, + className, +}: { + id: string; + children: React.ReactNode; + className?: string; +}) { + const [state, setState] = useState({ + reorderedItems: [], + direction: '-', + draggingHeight: 40, + isReorderOn: false, + groupId: id, + }); + + const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [ + setState, + ]); + return ( +
1, + })} + > + + {children} + +
+ ); +} diff --git a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx new file mode 100644 index 0000000000000..11f460a400dcd --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx @@ -0,0 +1,75 @@ +/* + * 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 { DropType } from '../../types'; + +export interface HumanData { + label: string; + groupLabel?: string; + position?: number; + nextLabel?: string; +} + +export type DragDropIdentifier = Record & { + id: string; + /** + * The data for accessibility, consists of required label and not required groupLabel and position in group + */ + humanData: HumanData; +}; + +export type DraggingIdentifier = DragDropIdentifier & { + ghost?: { + children: React.ReactElement; + style: React.CSSProperties; + }; +}; + +export type DropIdentifier = DragDropIdentifier & { + dropType: DropType; + onDrop: DropHandler; +}; + +/** + * A function that handles a drop event. + */ +export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void; + +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 + */ + setKeyboardMode: (mode: boolean) => void; + /** + * Set the item being dragged. + */ + setDragging: (dragging?: DraggingIdentifier) => void; + + activeDropTarget?: DropIdentifier; + + dropTargetsByOrder: RegisteredDropTargets; + + setActiveDropTarget: (newTarget?: DropIdentifier) => void; + + setA11yMessage: (message: string) => void; + registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx index 1cbd41fff2a8f..04ab1318a12e0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; -import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop'; +import React, { useMemo, useCallback, useContext } from 'react'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; + import { Datasource, VisualizationDimensionGroupConfig, @@ -41,12 +42,10 @@ export function DraggableDimensionButton({ group, onDrop, children, - dragDropContext, layerDatasourceDropProps, layerDatasource, registerNewButtonRef, }: { - dragDropContext: DragContextState; layerId: string; groupIndex: number; layerIndex: number; @@ -64,8 +63,11 @@ export function DraggableDimensionButton({ columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; }) { + const { dragging } = useContext(DragContext); + const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, + dragging, columnId, filterOperations: group.filterOperations, groupId: group.groupId, @@ -105,6 +107,11 @@ export function DraggableDimensionButton({ columnId, ]); + const handleOnDrop = React.useCallback( + (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + [value, onDrop] + ); + return (
1 ? reorderableGroup : undefined} value={value} - onDrop={(drag: DragDropIdentifier, selectedDropType?: DropType) => - onDrop(drag, value, selectedDropType) - } + onDrop={handleOnDrop} > {children} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx index c9d0a7b002870..664e24b989836 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect, useContext } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { generateId } from '../../../id_generator'; -import { DragDrop, DragDropIdentifier } from '../../../drag_drop'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; + import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../types'; import { LayerDatasourceDropProps } from './types'; @@ -47,6 +48,8 @@ export function EmptyDimensionButton({ layerDatasource: Datasource; layerDatasourceDropProps: LayerDatasourceDropProps; }) { + const { dragging } = useContext(DragContext); + const itemIndex = group.accessors.length; const [newColumnId, setNewColumnId] = useState(generateId()); @@ -56,6 +59,7 @@ export function EmptyDimensionButton({ const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, + dragging, columnId: newColumnId, filterOperations: group.filterOperations, groupId: group.groupId, @@ -81,14 +85,18 @@ export function EmptyDimensionButton({ [dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel] ); + const handleOnDrop = React.useCallback( + (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + [value, onDrop] + ); + return (
onDrop(droppedItem, value, selectedDropType)} + onDrop={handleOnDrop} dropType={dropType} >
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 619147987cdd5..52726afcffe8d 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 @@ -28,6 +28,7 @@ const defaultContext = { setDragging: jest.fn(), setActiveDropTarget: () => {}, activeDropTarget: undefined, + dropTargetsByOrder: undefined, keyboardMode: false, setKeyboardMode: () => {}, setA11yMessage: jest.fn(), @@ -464,9 +465,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), + dragging: draggingField, }) ); @@ -474,9 +473,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), + droppedItem: draggingField, }) ); }); @@ -582,9 +579,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + dragging: draggingOperation, }) ); @@ -593,9 +588,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'b', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + droppedItem: draggingOperation, }) ); @@ -604,9 +597,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'newid', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + droppedItem: draggingOperation, }) ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 5d84f826ab988..59b64de369745 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -7,17 +7,12 @@ import './layer_panel.scss'; -import React, { useContext, useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types'; -import { - DragContext, - DragDropIdentifier, - ChildDragDropProvider, - ReorderProvider, -} from '../../../drag_drop'; +import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; import { LayerPanelProps, ActiveDimensionState } from './types'; @@ -49,7 +44,6 @@ export function LayerPanel( registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; } ) { - const dragDropContext = useContext(DragContext); const [activeDimension, setActiveDimension] = useState( initialActiveDimensionState ); @@ -78,7 +72,6 @@ export function LayerPanel( const layerVisualizationConfigProps = { layerId, - dragDropContext, state: props.visualizationState, frame: props.framePublicAPI, dateRange: props.framePublicAPI.dateRange, @@ -91,13 +84,12 @@ export function LayerPanel( const layerDatasourceDropProps = useMemo( () => ({ layerId, - dragDropContext, state: layerDatasourceState, setState: (newState: unknown) => { updateDatasource(datasourceId, newState); }, }), - [layerId, dragDropContext, layerDatasourceState, datasourceId, updateDatasource] + [layerId, layerDatasourceState, datasourceId, updateDatasource] ); const layerDatasource = props.datasourceMap[datasourceId]; @@ -116,7 +108,6 @@ export function LayerPanel( const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state); const { setDimension, removeDimension } = activeVisualization; - const layerDatasourceOnDrop = layerDatasource.onDrop; const allAccessors = groups.flatMap((group) => group.accessors.map((accessor) => accessor.columnId) @@ -128,6 +119,8 @@ export function LayerPanel( registerNewRef: registerNewButtonRef, } = useFocusUpdate(allAccessors); + const layerDatasourceOnDrop = layerDatasource.onDrop; + const onDrop = useMemo(() => { return ( droppedItem: DragDropIdentifier, @@ -194,275 +187,272 @@ export function LayerPanel( ]); return ( - -
- - - - - +
+ + + + + - {layerDatasource && ( - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, + {layerDatasource && ( + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ layerId, + columnId, + prevState: nextVisState, }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); + }); - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> - - )} - - - + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + + )} + - {groups.map((group, groupIndex) => { - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - {group.groupLabel}
} - labelType="legend" - key={group.groupId} - isInvalid={isMissing} - error={ - isMissing ? ( -
- {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { - defaultMessage: 'Required dimension', - })} -
- ) : ( - [] - ) - } - > - <> - - {group.accessors.map((accessorConfig, accessorIndex) => { - const { columnId } = accessorConfig; + - return ( - -
- { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: id, - }); - }} - onRemoveClick={(id: string) => { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: id, - prevState: layerDatasourceState, - }), - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - }) - ); - removeButtonRef(id); - }} - > - - -
-
- ); + {groups.map((group, groupIndex) => { + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + {group.groupLabel}
} + labelType="legend" + key={group.groupId} + isInvalid={isMissing} + error={ + isMissing ? ( +
+ {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { + defaultMessage: 'Required dimension', })} - - {group.supportsMoreColumns ? ( - { - setActiveDimension({ - activeGroup: group, - activeId: id, - isNew: true, - }); - }} - onDrop={onDrop} - /> - ) : null} - - - ); - })} - { - if (layerDatasource.updateStateOnCloseDimension) { - const newState = layerDatasource.updateStateOnCloseDimension({ - state: layerDatasourceState, - layerId, - columnId: activeId!, - }); - if (newState) { - props.updateDatasource(datasourceId, newState); - } +
+ ) : ( + [] + ) } - setActiveDimension(initialActiveDimensionState); - }} - panel={ + > <> - {activeGroup && activeId && ( - { - if (shouldReplaceDimension || shouldRemoveDimension) { - props.updateAll( - datasourceId, - newState, - shouldRemoveDimension - ? activeVisualization.removeDimension({ + + {group.accessors.map((accessorConfig, accessorIndex) => { + const { columnId } = accessorConfig; + + return ( + +
+ { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: id, + }); + }} + onRemoveClick={(id: string) => { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ layerId, - columnId: activeId, - prevState: props.visualizationState, - }) - : activeVisualization.setDimension({ + columnId: id, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ layerId, - groupId: activeGroup.groupId, - columnId: activeId, + columnId: id, prevState: props.visualizationState, }) - ); - } else { - props.updateDatasource(datasourceId, newState); - } - setActiveDimension({ - ...activeDimension, - isNew: false, - }); - }, + ); + removeButtonRef(id); + }} + > + + +
+
+ ); + })} +
+ {group.supportsMoreColumns ? ( + { + setActiveDimension({ + activeGroup: group, + activeId: id, + isNew: true, + }); }} + onDrop={onDrop} /> - )} - {activeGroup && - activeId && - !activeDimension.isNew && - activeVisualization.renderDimensionEditor && - activeGroup?.enableDimensionEditor && ( -
- -
- )} + ) : null} + + ); + })} + { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + if (newState) { + props.updateDatasource(datasourceId, newState); + } } - /> + setActiveDimension(initialActiveDimensionState); + }} + panel={ + <> + {activeGroup && activeId && ( + { + if (shouldReplaceDimension || shouldRemoveDimension) { + props.updateAll( + datasourceId, + newState, + shouldRemoveDimension + ? activeVisualization.removeDimension({ + layerId, + columnId: activeId, + prevState: props.visualizationState, + }) + : activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) + ); + } else { + props.updateDatasource(datasourceId, newState); + } + setActiveDimension({ + ...activeDimension, + isNew: false, + }); + }, + }} + /> + )} + {activeGroup && + activeId && + !activeDimension.isNew && + activeVisualization.renderDimensionEditor && + activeGroup?.enableDimensionEditor && ( +
+ +
+ )} + + } + /> - + - - - - - - - - + + + + + + + ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index 22e28292b8da7..37b2198cfd51f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -13,7 +13,6 @@ import { DatasourceDimensionEditorProps, VisualizationDimensionGroupConfig, } from '../../../types'; -import { DragContextState } from '../../../drag_drop'; export interface ConfigPanelWrapperProps { activeDatasourceId: string; visualizationState: unknown; @@ -51,7 +50,6 @@ export interface LayerPanelProps { export interface LayerDatasourceDropProps { layerId: string; - dragDropContext: DragContextState; state: unknown; setState: (newState: unknown) => void; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 92a2f0c5d03fc..218ceb8206080 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -6,7 +6,7 @@ */ import './chart_switch.scss'; -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, memo } from 'react'; import { EuiIcon, EuiPopover, @@ -79,7 +79,7 @@ function VisualizationSummary(props: Props) { ); } -export function ChartSwitch(props: Props) { +export const ChartSwitch = memo(function ChartSwitch(props: Props) { const [flyoutOpen, setFlyoutOpen] = useState(false); const commitSelection = (selection: VisualizationSelection) => { @@ -305,7 +305,7 @@ export function ChartSwitch(props: Props) { ); return
{popover}
; -} +}); function getTopSuggestion( props: Props, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 48aa56efdb3cc..ab718a99843c8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -794,6 +794,7 @@ describe('workspace_panel', () => { setKeyboardMode={() => {}} setA11yMessage={() => {}} registerDropTarget={jest.fn()} + dropTargetsByOrder={undefined} > dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging), + [dragDropContext.dragging, getSuggestionForField] + ); + + return ( + + ); +}); + +// Exported for testing purposes only. +export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ activeDatasourceId, activeVisualizationId, visualizationMap, @@ -102,13 +118,10 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ExpressionRenderer: ExpressionRendererComponent, title, visualizeTriggerFieldContext, - getSuggestionForField, -}: WorkspacePanelProps) { - const dragDropContext = useContext(DragContext); - - const suggestionForDraggedField = - dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging); - + suggestionForDraggedField, +}: Omit & { + suggestionForDraggedField: Suggestion | undefined; +}) { const [localState, setLocalState] = useState({ expressionBuildError: undefined, expandError: false, @@ -173,6 +186,8 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ] ); + const expressionExists = Boolean(expression); + const onEvent = useCallback( (event: ExpressionRendererEvent) => { if (!plugins.uiActions) { @@ -202,23 +217,23 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ useEffect(() => { // reset expression error if component attempts to run it again - if (expression && localState.expressionBuildError) { + if (expressionExists && localState.expressionBuildError) { setLocalState((s) => ({ ...s, expressionBuildError: undefined, })); } - }, [expression, localState.expressionBuildError]); + }, [expressionExists, localState.expressionBuildError]); - function onDrop() { + const onDrop = useCallback(() => { if (suggestionForDraggedField) { trackUiEvent('drop_onto_workspace'); - trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty'); + trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty'); switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION'); } - } + }, [suggestionForDraggedField, expressionExists, dispatch]); - function renderEmptyWorkspace() { + const renderEmptyWorkspace = () => { return (

- {expression === null + {!expressionExists ? i18n.translate('xpack.lens.editorFrame.emptyWorkspace', { defaultMessage: 'Drop some fields here to start', }) @@ -239,7 +254,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({

- {expression === null && ( + {!expressionExists && ( <>

{i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', { @@ -263,9 +278,9 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ )} ); - } + }; - function renderVisualization() { + const renderVisualization = () => { // we don't want to render the emptyWorkspace on visualizing field from Discover // as it is specific for the drag and drop functionality and can confuse the users if (expression === null && !visualizeTriggerFieldContext) { @@ -283,7 +298,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ExpressionRendererComponent={ExpressionRendererComponent} /> ); - } + }; return ( { let state: IndexPatternPrivateState; let setState: jest.Mock; let defaultProps: IndexPatternDimensionEditorProps; - let dragDropContext: DragContextState; beforeEach(() => { state = { @@ -140,8 +137,6 @@ describe('IndexPatternDimensionEditorPanel', () => { setState = jest.fn(); - dragDropContext = createMockedDragDropContext(); - defaultProps = { state, setState, @@ -174,24 +169,28 @@ describe('IndexPatternDimensionEditorPanel', () => { }); const groupId = 'a'; + describe('getDropProps', () => { it('returns undefined if no drag is happening', () => { - expect(getDropProps({ ...defaultProps, groupId, dragDropContext })).toBe(undefined); + const dragging = { + name: 'bar', + id: 'bar', + humanData: { label: 'Label' }, + }; + expect(getDropProps({ ...defaultProps, groupId, dragging })).toBe(undefined); }); it('returns undefined if the dragged item has no field', () => { + const dragging = { + name: 'bar', + id: 'bar', + humanData: { label: 'Label' }, + }; expect( getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - name: 'bar', - id: 'bar', - humanData: { label: 'Label' }, - }, - }, + dragging, }) ).toBe(undefined); }); @@ -201,14 +200,11 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - indexPatternId: 'foo', - field: { type: 'string', name: 'mystring', aggregatable: true }, - id: 'mystring', - humanData: { label: 'Label' }, - }, + dragging: { + indexPatternId: 'foo', + field: { type: 'string', name: 'mystring', aggregatable: true }, + id: 'mystring', + humanData: { label: 'Label' }, }, filterOperations: () => false, }) @@ -220,10 +216,7 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, + dragging: draggingField, filterOperations: (op: OperationMetadata) => op.dataType === 'number', }) ).toEqual({ dropType: 'field_replace', nextLabel: 'Intervals' }); @@ -234,14 +227,11 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo2', - id: 'bar', - humanData: { label: 'Label' }, - }, + dragging: { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo2', + id: 'bar', + humanData: { label: 'Label' }, }, filterOperations: (op: OperationMetadata) => op.dataType === 'number', }) @@ -253,21 +243,18 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, + dragging: { + field: { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, }, + indexPatternId: 'foo', + id: 'bar', + humanData: { label: 'Label' }, }, }) ).toBe(undefined); @@ -278,15 +265,12 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, columnId: 'col2', }) @@ -321,16 +305,14 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, + columnId: 'col2', }) ).toEqual(undefined); @@ -360,15 +342,12 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed === false, @@ -380,10 +359,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('appends the dropped column when a field is dropped', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, dropType: 'field_replace', columnId: 'col2', @@ -412,10 +387,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('selects the specific operation that was valid on drop', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed, @@ -444,10 +415,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('updates a column when a field is dropped', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, filterOperations: (op: OperationMetadata) => op.dataType === 'number', dropType: 'field_replace', @@ -470,18 +437,8 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('keeps the operation when dropping a different compatible field', () => { - const dragging = { - field: { name: 'memory', type: 'number', aggregatable: true }, - indexPatternId: 'foo', - id: '1', - humanData: { label: 'Label' }, - }; onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, droppedItem: { field: { name: 'memory', type: 'number', aggregatable: true }, indexPatternId: 'foo', @@ -538,10 +495,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, droppedItem: dragging, columnId: 'col2', dropType: 'move_compatible', @@ -598,10 +551,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: defaultDragging, - }, droppedItem: defaultDragging, state: testState, dropType: 'replace_compatible', @@ -667,10 +616,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: metricDragging, - }, droppedItem: metricDragging, state: testState, dropType: 'duplicate_in_group', @@ -703,10 +648,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: bucketDragging, - }, droppedItem: bucketDragging, state: testState, dropType: 'duplicate_in_group', @@ -768,10 +709,7 @@ describe('IndexPatternDimensionEditorPanel', () => { const defaultReorderDropParams = { ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, + dragging, droppedItem: dragging, state: testState, filterOperations: (op: OperationMetadata) => op.dataType === 'number', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index be791b3c7f7ce..a7d4774d8aa3d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -23,6 +23,7 @@ import { mergeLayer } from '../state_helpers'; import { hasField, isDraggedField } from '../utils'; import { IndexPatternPrivateState, DraggedField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { DragContextState } from '../../drag_drop/providers'; type DropHandlerProps = DatasourceDimensionDropHandlerProps & { droppedItem: T; @@ -31,9 +32,12 @@ type DropHandlerProps = DatasourceDimensionDropHandlerProps & { groupId: string } + props: DatasourceDimensionDropProps & { + dragging: DragContextState['dragging']; + groupId: string; + } ): { dropType: DropType; nextLabel?: string } | undefined { - const { dragging } = props.dragDropContext; + const { dragging } = props; if (!dragging) { return; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index b8b5eb4c1e6f8..aa144c96dc7af 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -93,6 +93,21 @@ export function getIndexPatternDatasource({ const indexPatternsService = data.indexPatterns; + const handleChangeIndexPattern = ( + id: string, + state: IndexPatternPrivateState, + setState: StateSetter + ) => { + changeIndexPattern({ + id, + state, + setState, + onError: onIndexPatternLoadError, + storage, + indexPatternsService, + }); + }; + // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { id: 'indexpattern', @@ -171,20 +186,7 @@ export function getIndexPatternDatasource({ render( - ) => { - changeIndexPattern({ - id, - state, - setState, - onError: onIndexPatternLoadError, - storage, - indexPatternsService, - }); - }} + changeIndexPattern={handleChangeIndexPattern} data={data} charts={charts} {...props} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 06560bb0fa244..e71b26b9d4cd9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -253,6 +253,7 @@ export function createMockedDragDropContext(): jest.Mocked { keyboardMode: false, setKeyboardMode: jest.fn(), setA11yMessage: jest.fn(), + dropTargetsByOrder: undefined, registerDropTarget: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 419354117eda2..6ac2d98994be3 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -190,7 +190,10 @@ export interface Datasource { renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; getDropProps: ( - props: DatasourceDimensionDropProps & { groupId: string } + props: DatasourceDimensionDropProps & { + groupId: string; + dragging: DragContextState['dragging']; + } ) => { dropType: DropType; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; updateStateOnCloseDimension?: (props: { @@ -278,9 +281,7 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro dimensionGroups: VisualizationDimensionGroupConfig[]; }; -export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { - dragDropContext: DragContextState; -}; +export type DatasourceDimensionTriggerProps = DatasourceDimensionProps; export interface DatasourceLayerPanelProps { layerId: string; @@ -310,7 +311,6 @@ export type DatasourceDimensionDropProps = SharedDimensionProps & { columnId: string; state: T; setState: StateSetter; - dragDropContext: DragContextState; }; export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & {