212 lines
6.4 KiB
TypeScript
212 lines
6.4 KiB
TypeScript
/*
|
|
* 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 { EuiScreenReaderOnly } from '@elastic/eui';
|
|
import { i18n } from '@kbn/i18n';
|
|
import {
|
|
DropIdentifier,
|
|
DraggingIdentifier,
|
|
DragDropIdentifier,
|
|
RegisteredDropTargets,
|
|
DragContextState,
|
|
} from './types';
|
|
|
|
/**
|
|
* The drag / drop context singleton, used like so:
|
|
*
|
|
* const { dragging, setDragging } = useContext(DragContext);
|
|
*/
|
|
export const DragContext = React.createContext<DragContextState>({
|
|
dragging: undefined,
|
|
setDragging: () => {},
|
|
keyboardMode: false,
|
|
setKeyboardMode: () => {},
|
|
activeDropTarget: undefined,
|
|
setActiveDropTarget: () => {},
|
|
setA11yMessage: () => {},
|
|
dropTargetsByOrder: undefined,
|
|
registerDropTarget: () => {},
|
|
});
|
|
|
|
/**
|
|
* The argument to DragDropProvider.
|
|
*/
|
|
export interface ProviderProps extends DragContextState {
|
|
/**
|
|
* The React children.
|
|
*/
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
/**
|
|
* A React provider that tracks the dragging state. This should
|
|
* be placed at the root of any React application that supports
|
|
* drag / drop.
|
|
*
|
|
* @param props
|
|
*/
|
|
export function RootDragDropProvider({ children }: { children: React.ReactNode }) {
|
|
const [draggingState, setDraggingState] = useState<{ dragging?: DraggingIdentifier }>({
|
|
dragging: undefined,
|
|
});
|
|
const [keyboardModeState, setKeyboardModeState] = useState(false);
|
|
const [a11yMessageState, setA11yMessageState] = useState('');
|
|
const [activeDropTargetState, setActiveDropTargetState] = useState<DropIdentifier | undefined>(
|
|
undefined
|
|
);
|
|
|
|
const [dropTargetsByOrderState, setDropTargetsByOrderState] = useState<RegisteredDropTargets>({});
|
|
|
|
const setDragging = useMemo(
|
|
() => (dragging?: DraggingIdentifier) => setDraggingState({ dragging }),
|
|
[setDraggingState]
|
|
);
|
|
|
|
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,
|
|
};
|
|
});
|
|
},
|
|
[setDropTargetsByOrderState]
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<ChildDragDropProvider
|
|
keyboardMode={keyboardModeState}
|
|
setKeyboardMode={setKeyboardModeState}
|
|
dragging={draggingState.dragging}
|
|
setA11yMessage={setA11yMessage}
|
|
setDragging={setDragging}
|
|
activeDropTarget={activeDropTargetState}
|
|
setActiveDropTarget={setActiveDropTarget}
|
|
registerDropTarget={registerDropTarget}
|
|
dropTargetsByOrder={dropTargetsByOrderState}
|
|
>
|
|
{children}
|
|
</ChildDragDropProvider>
|
|
<EuiScreenReaderOnly>
|
|
<div>
|
|
<p aria-live="assertive" aria-atomic={true}>
|
|
{a11yMessageState}
|
|
</p>
|
|
<p id={`lnsDragDrop-keyboardInstructionsWithReorder`}>
|
|
{i18n.translate('xpack.lens.dragDrop.keyboardInstructionsReorder', {
|
|
defaultMessage: `Press space or enter to start dragging. When dragging, use the up/down arrow keys to reorder items in the group and left/right arrow keys to choose drop targets outside of the group. Press space or enter again to finish.`,
|
|
})}
|
|
</p>
|
|
<p id={`lnsDragDrop-keyboardInstructions`}>
|
|
{i18n.translate('xpack.lens.dragDrop.keyboardInstructions', {
|
|
defaultMessage: `Press space or enter to start dragging. When dragging, use the left/right arrow keys to move between drop targets. Press space or enter again to finish.`,
|
|
})}
|
|
</p>
|
|
</div>
|
|
</EuiScreenReaderOnly>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function nextValidDropTarget(
|
|
dropTargetsByOrder: RegisteredDropTargets,
|
|
activeDropTarget: DropIdentifier | undefined,
|
|
draggingOrder: [string],
|
|
filterElements: (el: DragDropIdentifier) => boolean = () => true,
|
|
reverse = false
|
|
) {
|
|
if (!dropTargetsByOrder) {
|
|
return;
|
|
}
|
|
|
|
const filteredTargets = Object.entries(dropTargetsByOrder).filter(
|
|
([, dropTarget]) => dropTarget && filterElements(dropTarget)
|
|
);
|
|
|
|
const nextDropTargets = [...filteredTargets, draggingOrder].sort(([orderA], [orderB]) => {
|
|
const parsedOrderA = orderA.split(',').map((v) => Number(v));
|
|
const parsedOrderB = orderB.split(',').map((v) => Number(v));
|
|
|
|
const relevantLevel = parsedOrderA.findIndex((v, i) => parsedOrderA[i] !== parsedOrderB[i]);
|
|
return parsedOrderA[relevantLevel] - parsedOrderB[relevantLevel];
|
|
});
|
|
|
|
let currentActiveDropIndex = nextDropTargets.findIndex(
|
|
([_, dropTarget]) => dropTarget?.id === activeDropTarget?.id
|
|
);
|
|
|
|
if (currentActiveDropIndex === -1) {
|
|
currentActiveDropIndex = nextDropTargets.findIndex(
|
|
([targetOrder]) => targetOrder === draggingOrder[0]
|
|
);
|
|
}
|
|
|
|
const previousElement =
|
|
(nextDropTargets.length + currentActiveDropIndex - 1) % nextDropTargets.length;
|
|
const nextElement = (currentActiveDropIndex + 1) % nextDropTargets.length;
|
|
|
|
return nextDropTargets[reverse ? previousElement : nextElement][1];
|
|
}
|
|
|
|
/**
|
|
* A React drag / drop provider that derives its state from a RootDragDropProvider. If
|
|
* part of a React application is rendered separately from the root, this provider can
|
|
* be used to enable drag / drop functionality within the disconnected part.
|
|
*
|
|
* @param props
|
|
*/
|
|
export function ChildDragDropProvider({
|
|
dragging,
|
|
setDragging,
|
|
setKeyboardMode,
|
|
keyboardMode,
|
|
activeDropTarget,
|
|
setActiveDropTarget,
|
|
setA11yMessage,
|
|
registerDropTarget,
|
|
dropTargetsByOrder,
|
|
children,
|
|
}: ProviderProps) {
|
|
const value = useMemo(
|
|
() => ({
|
|
setKeyboardMode,
|
|
keyboardMode,
|
|
dragging,
|
|
setDragging,
|
|
activeDropTarget,
|
|
setActiveDropTarget,
|
|
setA11yMessage,
|
|
dropTargetsByOrder,
|
|
registerDropTarget,
|
|
}),
|
|
[
|
|
setDragging,
|
|
dragging,
|
|
activeDropTarget,
|
|
setActiveDropTarget,
|
|
setKeyboardMode,
|
|
keyboardMode,
|
|
setA11yMessage,
|
|
dropTargetsByOrder,
|
|
registerDropTarget,
|
|
]
|
|
);
|
|
return <DragContext.Provider value={value}>{children}</DragContext.Provider>;
|
|
}
|