Feat: basic keyboard controls and better WAI-ARIA defaults (#2333)
* feat(nodes): focusable and moveable with keys * refactor(key-handling): cleanup, handle selections * feat(edges): selectable with keys * refactor(nodes-edges): cleanup keyboard controls and aria- attrs * refactor(nodes): node needs to be selected for arrow keys * refactor(minimap): create const for labelledby closes #1033
This commit is contained in:
@@ -29,6 +29,7 @@ const initBgColor = '#1A192B';
|
|||||||
|
|
||||||
const connectionLineStyle = { stroke: '#fff' };
|
const connectionLineStyle = { stroke: '#fff' };
|
||||||
const snapGrid: SnapGrid = [16, 16];
|
const snapGrid: SnapGrid = [16, 16];
|
||||||
|
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
selectorNode: ColorSelectorNode,
|
selectorNode: ColorSelectorNode,
|
||||||
@@ -141,7 +142,7 @@ const CustomNodeFlow = () => {
|
|||||||
connectionLineStyle={connectionLineStyle}
|
connectionLineStyle={connectionLineStyle}
|
||||||
snapToGrid={true}
|
snapToGrid={true}
|
||||||
snapGrid={snapGrid}
|
snapGrid={snapGrid}
|
||||||
defaultZoom={1.5}
|
defaultViewport={defaultViewport}
|
||||||
fitView
|
fitView
|
||||||
>
|
>
|
||||||
<MiniMap
|
<MiniMap
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ const EdgesFlow = () => {
|
|||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
(params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)),
|
(params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)),
|
||||||
[]
|
[setEdges]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -158,8 +158,7 @@ const NestedFlow = () => {
|
|||||||
onEdgeClick={onEdgeClick}
|
onEdgeClick={onEdgeClick}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onNodeDragStop={onNodeDragStop}
|
onNodeDragStop={onNodeDragStop}
|
||||||
className='react-flow-basic-example'
|
className="react-flow-basic-example"
|
||||||
defaultZoom={1.5}
|
|
||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
maxZoom={4}
|
maxZoom={4}
|
||||||
onlyRenderVisibleElements={false}
|
onlyRenderVisibleElements={false}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) =>
|
|||||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||||
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
|
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
|
||||||
|
|
||||||
|
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -206,8 +208,8 @@ const Subflow = () => {
|
|||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onNodeDrag={onNodeDrag}
|
onNodeDrag={onNodeDrag}
|
||||||
onNodeDragStop={onNodeDragStop}
|
onNodeDragStop={onNodeDragStop}
|
||||||
className='react-flow-basic-example'
|
className="react-flow-basic-example"
|
||||||
defaultZoom={1.5}
|
defaultViewport={defaultViewport}
|
||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
maxZoom={4}
|
maxZoom={4}
|
||||||
onlyRenderVisibleElements={false}
|
onlyRenderVisibleElements={false}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { MouseEvent } from 'react';
|
import React, { MouseEvent, useCallback } from 'react';
|
||||||
import ReactFlow, {
|
import ReactFlow, {
|
||||||
addEdge,
|
addEdge,
|
||||||
Node,
|
Node,
|
||||||
@@ -19,12 +19,14 @@ const nodesA: Node[] = [
|
|||||||
data: { label: 'Node 1' },
|
data: { label: 'Node 1' },
|
||||||
position: { x: 250, y: 5 },
|
position: { x: 250, y: 5 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
ariaLabel: 'Input Node 1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2a',
|
id: '2a',
|
||||||
data: { label: 'Node 2' },
|
data: { label: 'Node 2' },
|
||||||
position: { x: 100, y: 100 },
|
position: { x: 100, y: 100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
ariaLabel: 'Default Node 2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3a',
|
id: '3a',
|
||||||
@@ -41,7 +43,7 @@ const nodesA: Node[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const edgesA: Edge[] = [
|
const edgesA: Edge[] = [
|
||||||
{ id: 'e1-2', source: '1a', target: '2a' },
|
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: null },
|
||||||
{ id: 'e1-3', source: '1a', target: '3a' },
|
{ id: 'e1-3', source: '1a', target: '3a' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -52,18 +54,21 @@ const nodesB: Node[] = [
|
|||||||
data: { label: 'Input' },
|
data: { label: 'Input' },
|
||||||
position: { x: 300, y: 5 },
|
position: { x: 300, y: 5 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
ariaLabel: 'Input Node',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '1b',
|
id: '1b',
|
||||||
data: { label: 'Node 1' },
|
data: { label: 'Node 1' },
|
||||||
position: { x: 0, y: 100 },
|
position: { x: 0, y: 100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
ariaLabel: 'Node with id 1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2b',
|
id: '2b',
|
||||||
data: { label: 'Node 2' },
|
data: { label: 'Node 2' },
|
||||||
position: { x: 200, y: 100 },
|
position: { x: 200, y: 100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
ariaLabel: 'Node with id 2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3b',
|
id: '3b',
|
||||||
@@ -80,7 +85,7 @@ const nodesB: Node[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const edgesB: Edge[] = [
|
const edgesB: Edge[] = [
|
||||||
{ id: 'e1b', source: 'inputb', target: '1b' },
|
{ id: 'e1b', source: 'inputb', target: '1b', ariaLabel: 'edge to connect' },
|
||||||
{ id: 'e2b', source: 'inputb', target: '2b' },
|
{ id: 'e2b', source: 'inputb', target: '2b' },
|
||||||
{ id: 'e3b', source: 'inputb', target: '3b' },
|
{ id: 'e3b', source: 'inputb', target: '3b' },
|
||||||
{ id: 'e4b', source: 'inputb', target: '4b' },
|
{ id: 'e4b', source: 'inputb', target: '4b' },
|
||||||
@@ -90,8 +95,10 @@ const BasicFlow = () => {
|
|||||||
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
|
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA);
|
||||||
|
|
||||||
const onConnect = (params: Connection | Edge) =>
|
const onConnect = useCallback(
|
||||||
setEdges((eds) => addEdge(params, eds));
|
(params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)),
|
||||||
|
[setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
@@ -102,6 +109,7 @@ const BasicFlow = () => {
|
|||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onNodeDragStop={onNodeDragStop}
|
onNodeDragStop={onNodeDragStop}
|
||||||
|
disableKeyboardA11y
|
||||||
>
|
>
|
||||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ const UpdateNode = () => {
|
|||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
defaultZoom={1.5}
|
|
||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
maxZoom={4}
|
maxZoom={4}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
@@ -85,10 +84,10 @@ const UpdateNode = () => {
|
|||||||
<label className={styles.bgLabel}>background:</label>
|
<label className={styles.bgLabel}>background:</label>
|
||||||
<input value={nodeBg} onChange={(evt) => setNodeBg(evt.target.value)} />
|
<input value={nodeBg} onChange={(evt) => setNodeBg(evt.target.value)} />
|
||||||
|
|
||||||
<div className='updatenode__checkboxwrapper'>
|
<div className="updatenode__checkboxwrapper">
|
||||||
<label>hidden:</label>
|
<label>hidden:</label>
|
||||||
<input
|
<input
|
||||||
type='checkbox'
|
type="checkbox"
|
||||||
checked={nodeHidden}
|
checked={nodeHidden}
|
||||||
onChange={(evt) => setNodeHidden(evt.target.checked)}
|
onChange={(evt) => setNodeHidden(evt.target.checked)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { memo, FC, useEffect, useState, useRef } from 'react';
|
import React, { memo, FC, useRef, useId } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import { useStore, ReactFlowState } from '@react-flow/core';
|
import { useStore, ReactFlowState } from '@react-flow/core';
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ const defaultSize = {
|
|||||||
|
|
||||||
const transformSelector = (s: ReactFlowState) => s.transform;
|
const transformSelector = (s: ReactFlowState) => s.transform;
|
||||||
|
|
||||||
const Background: FC<BackgroundProps> = ({
|
function Background({
|
||||||
variant = BackgroundVariant.Dots,
|
variant = BackgroundVariant.Dots,
|
||||||
gap = 25,
|
gap = 25,
|
||||||
// only used for dots and cross
|
// only used for dots and cross
|
||||||
@@ -29,18 +29,11 @@ const Background: FC<BackgroundProps> = ({
|
|||||||
color,
|
color,
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}: BackgroundProps) {
|
||||||
const ref = useRef<SVGSVGElement>(null);
|
const ref = useRef<SVGSVGElement>(null);
|
||||||
const [patternId, setPatternId] = useState<string | null>(null);
|
const patternId = useId();
|
||||||
const [tX, tY, tScale] = useStore(transformSelector);
|
const [tX, tY, tScale] = useStore(transformSelector);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
|
|
||||||
const bgs = document.querySelectorAll('.react-flow__background');
|
|
||||||
const index = Array.from(bgs).findIndex((bg) => bg === ref.current);
|
|
||||||
setPatternId(`pattern-${index}`);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const patternColor = color ? color : defaultColors[variant];
|
const patternColor = color ? color : defaultColors[variant];
|
||||||
const patternSize = size ? size : defaultSize[variant];
|
const patternSize = size ? size : defaultSize[variant];
|
||||||
const isDots = variant === BackgroundVariant.Dots;
|
const isDots = variant === BackgroundVariant.Dots;
|
||||||
@@ -74,39 +67,35 @@ const Background: FC<BackgroundProps> = ({
|
|||||||
}}
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{patternId && (
|
<pattern
|
||||||
<>
|
id={patternId}
|
||||||
<pattern
|
x={tX % scaledGap[0]}
|
||||||
id={patternId}
|
y={tY % scaledGap[1]}
|
||||||
x={tX % scaledGap[0]}
|
width={scaledGap[0]}
|
||||||
y={tY % scaledGap[1]}
|
height={scaledGap[1]}
|
||||||
width={scaledGap[0]}
|
patternUnits="userSpaceOnUse"
|
||||||
height={scaledGap[1]}
|
patternTransform={`translate(-${patternOffset[0]},-${patternOffset[1]})`}
|
||||||
patternUnits="userSpaceOnUse"
|
>
|
||||||
patternTransform={`translate(-${patternOffset[0]},-${patternOffset[1]})`}
|
{isDots ? (
|
||||||
>
|
<DotPattern color={patternColor} radius={scaledSize / 2} />
|
||||||
{isDots ? (
|
) : (
|
||||||
<DotPattern color={patternColor} radius={scaledSize / 2} />
|
<LinePattern
|
||||||
) : (
|
dimensions={patternDimensions}
|
||||||
<LinePattern
|
color={patternColor}
|
||||||
dimensions={patternDimensions}
|
lineWidth={lineWidth}
|
||||||
color={patternColor}
|
|
||||||
lineWidth={lineWidth}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</pattern>
|
|
||||||
<rect
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
fill={`url(#${patternId})`}
|
|
||||||
/>
|
/>
|
||||||
</>
|
)}
|
||||||
)}
|
</pattern>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
fill={`url(#${patternId})`}
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
Background.displayName = 'Background';
|
Background.displayName = 'Background';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const style = { display: 'none' };
|
||||||
|
|
||||||
|
export const ARIA_NODE_DESC_KEY = 'react-flow__node-desc';
|
||||||
|
export const ARIA_EDGE_DESC_KEY = 'react-flow__edge-desc';
|
||||||
|
|
||||||
|
function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) {
|
||||||
|
if (disableKeyboardA11y) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id={`${ARIA_NODE_DESC_KEY}-${rfId}`} style={style}>
|
||||||
|
Press enter or space to select a node. You can then use the arrow keys to move the node around, press delete to
|
||||||
|
remove it and press escape to cancel.
|
||||||
|
</div>
|
||||||
|
<div id={`${ARIA_EDGE_DESC_KEY}-${rfId}`} style={style}>
|
||||||
|
Press enter or space to select an edge. You can then press delete to remove it or press escape to cancel.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default A11yDescriptions;
|
||||||
@@ -22,7 +22,7 @@ function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps
|
|||||||
className={cc(['react-flow__attribution', ...positionClasses])}
|
className={cc(['react-flow__attribution', ...positionClasses])}
|
||||||
data-message="Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev/pricing"
|
data-message="Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev/pricing"
|
||||||
>
|
>
|
||||||
<a href="https://reactflow.dev" target="_blank" rel="noopener noreferrer">
|
<a href="https://reactflow.dev" target="_blank" rel="noopener noreferrer" aria-label="React Flow attribution">
|
||||||
React Flow
|
React Flow
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React, { memo, ComponentType, useState, useMemo } from 'react';
|
import React, { memo, ComponentType, useState, useMemo, KeyboardEvent, useRef } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
|
|
||||||
import { useStoreApi } from '../../store';
|
import { useStoreApi } from '../../store';
|
||||||
import { EdgeProps, WrapEdgeProps, Connection } from '../../types';
|
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
|
||||||
import { handleMouseDown } from '../../components/Handle/handler';
|
import { handleMouseDown } from '../Handle/handler';
|
||||||
import { EdgeAnchor } from './EdgeAnchor';
|
import { EdgeAnchor } from './EdgeAnchor';
|
||||||
import { getMarkerId } from '../../utils/graph';
|
import { getMarkerId } from '../../utils/graph';
|
||||||
import { getMouseHandler } from './utils';
|
import { getMouseHandler } from './utils';
|
||||||
|
import { EdgeProps, WrapEdgeProps, Connection } from '../../types';
|
||||||
|
import { elementSelectionKeys } from '../../utils';
|
||||||
|
|
||||||
export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||||
const EdgeWrapper = ({
|
const EdgeWrapper = ({
|
||||||
@@ -48,10 +50,20 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
|||||||
markerEnd,
|
markerEnd,
|
||||||
markerStart,
|
markerStart,
|
||||||
rfId,
|
rfId,
|
||||||
|
ariaLabel,
|
||||||
|
disableKeyboardA11y,
|
||||||
}: WrapEdgeProps): JSX.Element | null => {
|
}: WrapEdgeProps): JSX.Element | null => {
|
||||||
|
const edgeRef = useRef<SVGGElement>(null);
|
||||||
const [updating, setUpdating] = useState<boolean>(false);
|
const [updating, setUpdating] = useState<boolean>(false);
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
|
|
||||||
|
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
|
||||||
|
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||||
const { edges, addSelectedEdges } = store.getState();
|
const { edges, addSelectedEdges } = store.getState();
|
||||||
const edge = edges.find((e) => e.id === id)!;
|
const edge = edges.find((e) => e.id === id)!;
|
||||||
@@ -107,12 +119,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
|||||||
|
|
||||||
const onEdgeUpdaterMouseEnter = () => setUpdating(true);
|
const onEdgeUpdaterMouseEnter = () => setUpdating(true);
|
||||||
const onEdgeUpdaterMouseOut = () => setUpdating(false);
|
const onEdgeUpdaterMouseOut = () => setUpdating(false);
|
||||||
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
|
|
||||||
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
|
|
||||||
|
|
||||||
if (hidden) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inactive = !elementsSelectable && !onClick;
|
const inactive = !elementsSelectable && !onClick;
|
||||||
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
|
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
|
||||||
@@ -123,6 +129,20 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
|||||||
{ selected, animated, inactive, updating },
|
{ selected, animated, inactive, updating },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (elementSelectionKeys.includes(event.key) && elementsSelectable) {
|
||||||
|
const { unselectNodesAndEdges, addSelectedEdges, edges } = store.getState();
|
||||||
|
const unselect = event.key === 'Escape';
|
||||||
|
|
||||||
|
if (unselect) {
|
||||||
|
edgeRef.current?.blur();
|
||||||
|
unselectNodesAndEdges({ edges: [edges.find((e) => e.id === id)!] });
|
||||||
|
} else {
|
||||||
|
addSelectedEdges([id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
className={edgeClasses}
|
className={edgeClasses}
|
||||||
@@ -132,6 +152,12 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
|||||||
onMouseEnter={onEdgeMouseEnter}
|
onMouseEnter={onEdgeMouseEnter}
|
||||||
onMouseMove={onEdgeMouseMove}
|
onMouseMove={onEdgeMouseMove}
|
||||||
onMouseLeave={onEdgeMouseLeave}
|
onMouseLeave={onEdgeMouseLeave}
|
||||||
|
onKeyDown={disableKeyboardA11y ? undefined : onKeyDown}
|
||||||
|
tabIndex={disableKeyboardA11y ? undefined : 0}
|
||||||
|
role={disableKeyboardA11y ? undefined : 'button'}
|
||||||
|
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`}
|
||||||
|
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_EDGE_DESC_KEY}-${rfId}`}
|
||||||
|
ref={edgeRef}
|
||||||
>
|
>
|
||||||
<EdgeComponent
|
<EdgeComponent
|
||||||
id={id}
|
id={id}
|
||||||
|
|||||||
@@ -51,12 +51,14 @@ export function getMouseHandler(
|
|||||||
export function handleNodeClick({
|
export function handleNodeClick({
|
||||||
id,
|
id,
|
||||||
store,
|
store,
|
||||||
|
unselect = false,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
store: {
|
store: {
|
||||||
getState: GetState<ReactFlowState>;
|
getState: GetState<ReactFlowState>;
|
||||||
setState: SetState<ReactFlowState>;
|
setState: SetState<ReactFlowState>;
|
||||||
};
|
};
|
||||||
|
unselect?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeInternals } = store.getState();
|
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeInternals } = store.getState();
|
||||||
const node = nodeInternals.get(id)!;
|
const node = nodeInternals.get(id)!;
|
||||||
@@ -65,7 +67,7 @@ export function handleNodeClick({
|
|||||||
|
|
||||||
if (!node.selected) {
|
if (!node.selected) {
|
||||||
addSelectedNodes([id]);
|
addSelectedNodes([id]);
|
||||||
} else if (node.selected && multiSelectionActive) {
|
} else if (unselect || (node.selected && multiSelectionActive)) {
|
||||||
unselectNodesAndEdges({ nodes: [node] });
|
unselectNodesAndEdges({ nodes: [node] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import React, { useEffect, useRef, memo, ComponentType, MouseEvent } from 'react';
|
import React, { useEffect, useRef, memo, ComponentType, MouseEvent, KeyboardEvent } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
|
|
||||||
import { useStoreApi } from '../../store';
|
import { useStoreApi } from '../../store';
|
||||||
import { Provider } from '../../contexts/NodeIdContext';
|
import { Provider } from '../../contexts/NodeIdContext';
|
||||||
import { NodeProps, WrapNodeProps } from '../../types';
|
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
|
||||||
import useDrag from '../../hooks/useDrag';
|
import useDrag from '../../hooks/useDrag';
|
||||||
|
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
|
||||||
import { getMouseHandler, handleNodeClick } from './utils';
|
import { getMouseHandler, handleNodeClick } from './utils';
|
||||||
|
import { NodeProps, WrapNodeProps, XYPosition } from '../../types';
|
||||||
|
import { elementSelectionKeys } from '../../utils';
|
||||||
|
|
||||||
|
export const arrowKeyDiffs: Record<string, XYPosition> = {
|
||||||
|
ArrowUp: { x: 0, y: -10 },
|
||||||
|
ArrowDown: { x: 0, y: 10 },
|
||||||
|
ArrowLeft: { x: -10, y: 0 },
|
||||||
|
ArrowRight: { x: 10, y: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
export default (NodeComponent: ComponentType<NodeProps>) => {
|
export default (NodeComponent: ComponentType<NodeProps>) => {
|
||||||
const NodeWrapper = ({
|
const NodeWrapper = ({
|
||||||
@@ -37,6 +47,9 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
noPanClassName,
|
noPanClassName,
|
||||||
noDragClassName,
|
noDragClassName,
|
||||||
initialized,
|
initialized,
|
||||||
|
disableKeyboardA11y,
|
||||||
|
ariaLabel,
|
||||||
|
rfId,
|
||||||
}: WrapNodeProps) => {
|
}: WrapNodeProps) => {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const nodeRef = useRef<HTMLDivElement>(null);
|
const nodeRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -44,6 +57,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
const prevTargetPosition = useRef(targetPosition);
|
const prevTargetPosition = useRef(targetPosition);
|
||||||
const prevType = useRef(type);
|
const prevType = useRef(type);
|
||||||
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
||||||
|
const updatePositions = useUpdateNodePositions();
|
||||||
|
|
||||||
const onMouseEnterHandler = getMouseHandler(id, store.getState, onMouseEnter);
|
const onMouseEnterHandler = getMouseHandler(id, store.getState, onMouseEnter);
|
||||||
const onMouseMoveHandler = getMouseHandler(id, store.getState, onMouseMove);
|
const onMouseMoveHandler = getMouseHandler(id, store.getState, onMouseMove);
|
||||||
@@ -65,6 +79,22 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (elementSelectionKeys.includes(event.key) && isSelectable) {
|
||||||
|
const unselect = event.key === 'Escape';
|
||||||
|
if (unselect) {
|
||||||
|
nodeRef.current?.blur();
|
||||||
|
}
|
||||||
|
handleNodeClick({
|
||||||
|
id,
|
||||||
|
store,
|
||||||
|
unselect,
|
||||||
|
});
|
||||||
|
} else if (selected && arrowKeyDiffs.hasOwnProperty(event.key)) {
|
||||||
|
updatePositions(arrowKeyDiffs[event.key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nodeRef.current && !hidden) {
|
if (nodeRef.current && !hidden) {
|
||||||
const currNode = nodeRef.current;
|
const currNode = nodeRef.current;
|
||||||
@@ -129,13 +159,18 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
visibility: initialized ? 'visible' : 'hidden',
|
visibility: initialized ? 'visible' : 'hidden',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
|
data-id={id}
|
||||||
onMouseEnter={onMouseEnterHandler}
|
onMouseEnter={onMouseEnterHandler}
|
||||||
onMouseMove={onMouseMoveHandler}
|
onMouseMove={onMouseMoveHandler}
|
||||||
onMouseLeave={onMouseLeaveHandler}
|
onMouseLeave={onMouseLeaveHandler}
|
||||||
onContextMenu={onContextMenuHandler}
|
onContextMenu={onContextMenuHandler}
|
||||||
onClick={onSelectNodeHandler}
|
onClick={onSelectNodeHandler}
|
||||||
onDoubleClick={onDoubleClickHandler}
|
onDoubleClick={onDoubleClickHandler}
|
||||||
data-id={id}
|
onKeyDown={disableKeyboardA11y ? undefined : onKeyDown}
|
||||||
|
tabIndex={disableKeyboardA11y ? undefined : 0}
|
||||||
|
role={disableKeyboardA11y ? undefined : 'button'}
|
||||||
|
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
|
||||||
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
<Provider value={id}>
|
<Provider value={id}>
|
||||||
<NodeComponent
|
<NodeComponent
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* made a selection with on or several nodes
|
* made a selection with on or several nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { memo, useRef, MouseEvent } from 'react';
|
import React, { memo, useRef, MouseEvent, KeyboardEvent, useEffect } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import shallow from 'zustand/shallow';
|
import shallow from 'zustand/shallow';
|
||||||
|
|
||||||
@@ -11,10 +11,13 @@ import { useStore, useStoreApi } from '../../store';
|
|||||||
import { Node, ReactFlowState } from '../../types';
|
import { Node, ReactFlowState } from '../../types';
|
||||||
import { getRectOfNodes } from '../../utils/graph';
|
import { getRectOfNodes } from '../../utils/graph';
|
||||||
import useDrag from '../../hooks/useDrag';
|
import useDrag from '../../hooks/useDrag';
|
||||||
|
import { arrowKeyDiffs } from '../Nodes/wrapNode';
|
||||||
|
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
|
||||||
|
|
||||||
export interface NodesSelectionProps {
|
export interface NodesSelectionProps {
|
||||||
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
|
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
|
||||||
noPanClassName?: string;
|
noPanClassName?: string;
|
||||||
|
disableKeyboardA11y: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
@@ -28,12 +31,19 @@ const bboxSelector = (s: ReactFlowState) => {
|
|||||||
return getRectOfNodes(selectedNodes);
|
return getRectOfNodes(selectedNodes);
|
||||||
};
|
};
|
||||||
|
|
||||||
function NodesSelection({ onSelectionContextMenu, noPanClassName }: NodesSelectionProps) {
|
function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboardA11y }: NodesSelectionProps) {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { transformString, userSelectionActive } = useStore(selector, shallow);
|
const { transformString, userSelectionActive } = useStore(selector, shallow);
|
||||||
const { width, height, x: left, y: top } = useStore(bboxSelector, shallow);
|
const { width, height, x: left, y: top } = useStore(bboxSelector, shallow);
|
||||||
|
const updatePositions = useUpdateNodePositions();
|
||||||
|
|
||||||
const nodeRef = useRef(null);
|
const nodeRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!disableKeyboardA11y) {
|
||||||
|
nodeRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [disableKeyboardA11y]);
|
||||||
|
|
||||||
useDrag({
|
useDrag({
|
||||||
nodeRef,
|
nodeRef,
|
||||||
@@ -50,6 +60,12 @@ function NodesSelection({ onSelectionContextMenu, noPanClassName }: NodesSelecti
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (arrowKeyDiffs.hasOwnProperty(event.key)) {
|
||||||
|
updatePositions(arrowKeyDiffs[event.key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])}
|
className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])}
|
||||||
@@ -61,6 +77,8 @@ function NodesSelection({ onSelectionContextMenu, noPanClassName }: NodesSelecti
|
|||||||
ref={nodeRef}
|
ref={nodeRef}
|
||||||
className="react-flow__nodesselection-rect"
|
className="react-flow__nodesselection-rect"
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
tabIndex={disableKeyboardA11y ? undefined : -1}
|
||||||
|
onKeyDown={disableKeyboardA11y ? undefined : onKeyDown}
|
||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ interface EdgeRendererProps {
|
|||||||
edgeUpdaterRadius?: number;
|
edgeUpdaterRadius?: number;
|
||||||
noPanClassName?: string;
|
noPanClassName?: string;
|
||||||
elevateEdgesOnSelect: boolean;
|
elevateEdgesOnSelect: boolean;
|
||||||
rfId?: string;
|
rfId: string;
|
||||||
|
disableKeyboardA11y: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
@@ -182,6 +183,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
|
|||||||
onEdgeUpdateStart={props.onEdgeUpdateStart}
|
onEdgeUpdateStart={props.onEdgeUpdateStart}
|
||||||
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
|
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
|
||||||
rfId={props.rfId}
|
rfId={props.rfId}
|
||||||
|
ariaLabel={edge.ariaLabel}
|
||||||
|
disableKeyboardA11y={props.disableKeyboardA11y}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type FlowRendererProps = Omit<
|
|||||||
| 'onlyRenderVisibleElements'
|
| 'onlyRenderVisibleElements'
|
||||||
| 'selectNodesOnDrag'
|
| 'selectNodesOnDrag'
|
||||||
| 'defaultMarkerColor'
|
| 'defaultMarkerColor'
|
||||||
|
| 'rfId'
|
||||||
> & {
|
> & {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
@@ -56,6 +57,7 @@ const FlowRenderer = ({
|
|||||||
onSelectionContextMenu,
|
onSelectionContextMenu,
|
||||||
noWheelClassName,
|
noWheelClassName,
|
||||||
noPanClassName,
|
noPanClassName,
|
||||||
|
disableKeyboardA11y,
|
||||||
}: FlowRendererProps) => {
|
}: FlowRendererProps) => {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const nodesSelectionActive = useStore(selector);
|
const nodesSelectionActive = useStore(selector);
|
||||||
@@ -95,7 +97,11 @@ const FlowRenderer = ({
|
|||||||
{children}
|
{children}
|
||||||
<UserSelection selectionKeyPressed={selectionKeyPressed} />
|
<UserSelection selectionKeyPressed={selectionKeyPressed} />
|
||||||
{nodesSelectionActive && (
|
{nodesSelectionActive && (
|
||||||
<NodesSelection onSelectionContextMenu={onSelectionContextMenu} noPanClassName={noPanClassName} />
|
<NodesSelection
|
||||||
|
onSelectionContextMenu={onSelectionContextMenu}
|
||||||
|
noPanClassName={noPanClassName}
|
||||||
|
disableKeyboardA11y={disableKeyboardA11y}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="react-flow__pane react-flow__container"
|
className="react-flow__pane react-flow__container"
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export interface GraphViewProps
|
|||||||
noWheelClassName: string;
|
noWheelClassName: string;
|
||||||
noPanClassName: string;
|
noPanClassName: string;
|
||||||
defaultViewport: Viewport;
|
defaultViewport: Viewport;
|
||||||
|
rfId: string;
|
||||||
|
disableKeyboardA11y: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GraphView = ({
|
const GraphView = ({
|
||||||
@@ -79,7 +81,8 @@ const GraphView = ({
|
|||||||
noWheelClassName,
|
noWheelClassName,
|
||||||
noPanClassName,
|
noPanClassName,
|
||||||
elevateEdgesOnSelect,
|
elevateEdgesOnSelect,
|
||||||
id,
|
disableKeyboardA11y,
|
||||||
|
rfId,
|
||||||
}: GraphViewProps) => {
|
}: GraphViewProps) => {
|
||||||
useOnInitHandler(onInit);
|
useOnInitHandler(onInit);
|
||||||
|
|
||||||
@@ -112,6 +115,7 @@ const GraphView = ({
|
|||||||
noDragClassName={noDragClassName}
|
noDragClassName={noDragClassName}
|
||||||
noWheelClassName={noWheelClassName}
|
noWheelClassName={noWheelClassName}
|
||||||
noPanClassName={noPanClassName}
|
noPanClassName={noPanClassName}
|
||||||
|
disableKeyboardA11y={disableKeyboardA11y}
|
||||||
>
|
>
|
||||||
<ViewportWrapper>
|
<ViewportWrapper>
|
||||||
<EdgeRenderer
|
<EdgeRenderer
|
||||||
@@ -134,7 +138,8 @@ const GraphView = ({
|
|||||||
defaultMarkerColor={defaultMarkerColor}
|
defaultMarkerColor={defaultMarkerColor}
|
||||||
noPanClassName={noPanClassName}
|
noPanClassName={noPanClassName}
|
||||||
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
|
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
|
||||||
rfId={id}
|
disableKeyboardA11y={disableKeyboardA11y}
|
||||||
|
rfId={rfId}
|
||||||
/>
|
/>
|
||||||
<NodeRenderer
|
<NodeRenderer
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
@@ -148,6 +153,8 @@ const GraphView = ({
|
|||||||
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
||||||
noPanClassName={noPanClassName}
|
noPanClassName={noPanClassName}
|
||||||
noDragClassName={noDragClassName}
|
noDragClassName={noDragClassName}
|
||||||
|
disableKeyboardA11y={disableKeyboardA11y}
|
||||||
|
rfId={rfId}
|
||||||
/>
|
/>
|
||||||
</ViewportWrapper>
|
</ViewportWrapper>
|
||||||
</FlowRenderer>
|
</FlowRenderer>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ interface NodeRendererProps {
|
|||||||
onlyRenderVisibleElements: boolean;
|
onlyRenderVisibleElements: boolean;
|
||||||
noPanClassName: string;
|
noPanClassName: string;
|
||||||
noDragClassName: string;
|
noDragClassName: string;
|
||||||
|
rfId: string;
|
||||||
|
disableKeyboardA11y: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
@@ -110,6 +112,9 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
|||||||
noDragClassName={props.noDragClassName}
|
noDragClassName={props.noDragClassName}
|
||||||
noPanClassName={props.noPanClassName}
|
noPanClassName={props.noPanClassName}
|
||||||
initialized={!!node.width && !!node.height}
|
initialized={!!node.width && !!node.height}
|
||||||
|
rfId={props.rfId}
|
||||||
|
disableKeyboardA11y={props.disableKeyboardA11y}
|
||||||
|
ariaLabel={node.ariaLabel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef, useId } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
|
|
||||||
import Attribution from '../../components/Attribution';
|
import Attribution from '../../components/Attribution';
|
||||||
@@ -29,6 +29,7 @@ import GraphView from '../GraphView';
|
|||||||
import { createNodeTypes } from '../NodeRenderer/utils';
|
import { createNodeTypes } from '../NodeRenderer/utils';
|
||||||
import { useNodeOrEdgeTypes } from './utils';
|
import { useNodeOrEdgeTypes } from './utils';
|
||||||
import Wrapper from './Wrapper';
|
import Wrapper from './Wrapper';
|
||||||
|
import A11yDescriptions from '../../components/A11yDescriptions';
|
||||||
|
|
||||||
const defaultNodeTypes: NodeTypes = {
|
const defaultNodeTypes: NodeTypes = {
|
||||||
input: InputNode,
|
input: InputNode,
|
||||||
@@ -141,12 +142,14 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
proOptions,
|
proOptions,
|
||||||
defaultEdgeOptions,
|
defaultEdgeOptions,
|
||||||
elevateEdgesOnSelect = false,
|
elevateEdgesOnSelect = false,
|
||||||
|
disableKeyboardA11y = false,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes) as NodeTypesWrapped;
|
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes) as NodeTypesWrapped;
|
||||||
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes) as EdgeTypesWrapped;
|
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes) as EdgeTypesWrapped;
|
||||||
|
const rfId = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...rest} ref={ref} className={cc(['react-flow', className])}>
|
<div {...rest} ref={ref} className={cc(['react-flow', className])}>
|
||||||
@@ -205,7 +208,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
noWheelClassName={noWheelClassName}
|
noWheelClassName={noWheelClassName}
|
||||||
noPanClassName={noPanClassName}
|
noPanClassName={noPanClassName}
|
||||||
elevateEdgesOnSelect={elevateEdgesOnSelect}
|
elevateEdgesOnSelect={elevateEdgesOnSelect}
|
||||||
id={rest?.id}
|
rfId={rfId}
|
||||||
|
disableKeyboardA11y={disableKeyboardA11y}
|
||||||
/>
|
/>
|
||||||
<StoreUpdater
|
<StoreUpdater
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -245,6 +249,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
|
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
|
||||||
{children}
|
{children}
|
||||||
<Attribution proOptions={proOptions} position={attributionPosition} />
|
<Attribution proOptions={proOptions} position={attributionPosition} />
|
||||||
|
<A11yDescriptions rfId={rfId} disableKeyboardA11y={disableKeyboardA11y} />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { FlowRendererProps } from '../FlowRenderer';
|
|||||||
|
|
||||||
type ZoomPaneProps = Omit<
|
type ZoomPaneProps = Omit<
|
||||||
FlowRendererProps,
|
FlowRendererProps,
|
||||||
'deleteKeyCode' | 'selectionKeyCode' | 'multiSelectionKeyCode' | 'noDragClassName'
|
'deleteKeyCode' | 'selectionKeyCode' | 'multiSelectionKeyCode' | 'noDragClassName' | 'disableKeyboardA11y'
|
||||||
> & { selectionKeyPressed: boolean };
|
> & { selectionKeyPressed: boolean };
|
||||||
|
|
||||||
const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
|
const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { select } from 'd3-selection';
|
|||||||
import { useStoreApi } from '../../store';
|
import { useStoreApi } from '../../store';
|
||||||
import { pointToRendererPoint } from '../../utils/graph';
|
import { pointToRendererPoint } from '../../utils/graph';
|
||||||
import { NodeDragItem, Node, SelectionDragHandler } from '../../types';
|
import { NodeDragItem, Node, SelectionDragHandler } from '../../types';
|
||||||
import { getDragItems, getEventHandlerParams, hasSelector, updatePosition } from './utils';
|
import { getDragItems, getEventHandlerParams, hasSelector, calcNextPosition } from './utils';
|
||||||
import { handleNodeClick } from '../../components/Nodes/utils';
|
import { handleNodeClick } from '../../components/Nodes/utils';
|
||||||
|
|
||||||
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
|
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
|
||||||
@@ -102,9 +102,19 @@ function useDrag({
|
|||||||
// skip events without movement
|
// skip events without movement
|
||||||
if ((lastPos.current.x !== pointerPos.x || lastPos.current.y !== pointerPos.y) && dragItems.current) {
|
if ((lastPos.current.x !== pointerPos.x || lastPos.current.y !== pointerPos.y) && dragItems.current) {
|
||||||
lastPos.current = pointerPos;
|
lastPos.current = pointerPos;
|
||||||
dragItems.current = dragItems.current.map((n) =>
|
dragItems.current = dragItems.current.map((n) => {
|
||||||
updatePosition(n, pointerPos, nodeInternals, nodeExtent)
|
const updatedPos = calcNextPosition(
|
||||||
);
|
n,
|
||||||
|
{ x: pointerPos.x - n.distance.x, y: pointerPos.y - n.distance.y },
|
||||||
|
nodeInternals,
|
||||||
|
nodeExtent
|
||||||
|
);
|
||||||
|
|
||||||
|
n.position = updatedPos.position;
|
||||||
|
n.positionAbsolute = updatedPos.positionAbsolute;
|
||||||
|
|
||||||
|
return n;
|
||||||
|
});
|
||||||
|
|
||||||
const onDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
|
const onDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
|
||||||
|
|
||||||
|
|||||||
@@ -56,25 +56,24 @@ export function getDragItems(nodeInternals: NodeInternals, mousePos: XYPosition,
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePosition(
|
export function calcNextPosition(
|
||||||
dragItem: NodeDragItem,
|
node: NodeDragItem | Node,
|
||||||
mousePos: XYPosition,
|
nextPosition: XYPosition,
|
||||||
nodeInternals: NodeInternals,
|
nodeInternals: NodeInternals,
|
||||||
nodeExtent?: CoordinateExtent
|
nodeExtent?: CoordinateExtent
|
||||||
): NodeDragItem {
|
): { position: XYPosition; positionAbsolute: XYPosition } {
|
||||||
let currentExtent = dragItem.extent || nodeExtent;
|
let currentExtent = node.extent || nodeExtent;
|
||||||
const nextPosition = { x: mousePos.x - dragItem.distance.x, y: mousePos.y - dragItem.distance.y };
|
|
||||||
|
|
||||||
if (dragItem.extent === 'parent') {
|
if (node.extent === 'parent') {
|
||||||
if (dragItem.parentNode && dragItem.width && dragItem.height) {
|
if (node.parentNode && node.width && node.height) {
|
||||||
const parent = nodeInternals.get(dragItem.parentNode);
|
const parent = nodeInternals.get(node.parentNode);
|
||||||
currentExtent =
|
currentExtent =
|
||||||
parent?.positionAbsolute && parent?.width && parent?.height
|
parent?.positionAbsolute && parent?.width && parent?.height
|
||||||
? [
|
? [
|
||||||
[parent.positionAbsolute.x, parent.positionAbsolute.y],
|
[parent.positionAbsolute.x, parent.positionAbsolute.y],
|
||||||
[
|
[
|
||||||
parent.positionAbsolute.x + parent.width - dragItem.width,
|
parent.positionAbsolute.x + parent.width - node.width,
|
||||||
parent.positionAbsolute.y + parent.height - dragItem.height,
|
parent.positionAbsolute.y + parent.height - node.height,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
: currentExtent;
|
: currentExtent;
|
||||||
@@ -85,33 +84,34 @@ export function updatePosition(
|
|||||||
}
|
}
|
||||||
currentExtent = nodeExtent;
|
currentExtent = nodeExtent;
|
||||||
}
|
}
|
||||||
} else if (dragItem.extent && dragItem.parentNode) {
|
} else if (node.extent && node.parentNode) {
|
||||||
const parent = nodeInternals.get(dragItem.parentNode);
|
const parent = nodeInternals.get(node.parentNode);
|
||||||
const parentX = parent?.positionAbsolute?.x ?? 0;
|
const parentX = parent?.positionAbsolute?.x ?? 0;
|
||||||
const parentY = parent?.positionAbsolute?.y ?? 0;
|
const parentY = parent?.positionAbsolute?.y ?? 0;
|
||||||
currentExtent = [
|
currentExtent = [
|
||||||
[dragItem.extent[0][0] + parentX, dragItem.extent[0][1] + parentY],
|
[node.extent[0][0] + parentX, node.extent[0][1] + parentY],
|
||||||
[dragItem.extent[1][0] + parentX, dragItem.extent[1][1] + parentY],
|
[node.extent[1][0] + parentX, node.extent[1][1] + parentY],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
let parentPosition = { x: 0, y: 0 };
|
let parentPosition = { x: 0, y: 0 };
|
||||||
|
|
||||||
if (dragItem.parentNode) {
|
if (node.parentNode) {
|
||||||
const parentNode = nodeInternals.get(dragItem.parentNode);
|
const parentNode = nodeInternals.get(node.parentNode);
|
||||||
parentPosition = { x: parentNode?.positionAbsolute?.x ?? 0, y: parentNode?.positionAbsolute?.y ?? 0 };
|
parentPosition = { x: parentNode?.positionAbsolute?.x ?? 0, y: parentNode?.positionAbsolute?.y ?? 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
dragItem.positionAbsolute = currentExtent
|
const positionAbsolute = currentExtent
|
||||||
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
|
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
|
||||||
: nextPosition;
|
: nextPosition;
|
||||||
|
|
||||||
dragItem.position = {
|
return {
|
||||||
x: dragItem.positionAbsolute.x - parentPosition.x,
|
position: {
|
||||||
y: dragItem.positionAbsolute.y - parentPosition.y,
|
x: positionAbsolute.x - parentPosition.x,
|
||||||
|
y: positionAbsolute.y - parentPosition.y,
|
||||||
|
},
|
||||||
|
positionAbsolute,
|
||||||
};
|
};
|
||||||
|
|
||||||
return dragItem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns two params:
|
// returns two params:
|
||||||
|
|||||||
@@ -1,35 +1,40 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import shallow from 'zustand/shallow';
|
|
||||||
|
|
||||||
import { useStore, useStoreApi } from '../store';
|
import { useStoreApi } from '../store';
|
||||||
import useKeyPress from './useKeyPress';
|
import useKeyPress from './useKeyPress';
|
||||||
import { getConnectedEdges } from '../utils/graph';
|
import { getConnectedEdges } from '../utils/graph';
|
||||||
import { EdgeChange, KeyCode, NodeChange, Node, ReactFlowState } from '../types';
|
import { KeyCode, NodeChange, Node } from '../types';
|
||||||
|
|
||||||
interface HookParams {
|
interface HookParams {
|
||||||
deleteKeyCode: KeyCode | null;
|
deleteKeyCode: KeyCode | null;
|
||||||
multiSelectionKeyCode: KeyCode | null;
|
multiSelectionKeyCode: KeyCode | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
|
||||||
onNodesChange: s.onNodesChange,
|
|
||||||
onEdgesChange: s.onEdgesChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { onNodesChange, onEdgesChange } = useStore(selector, shallow);
|
|
||||||
|
|
||||||
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
||||||
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
|
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { nodeInternals, edges, hasDefaultNodes, hasDefaultEdges, onNodesDelete, onEdgesDelete } = store.getState();
|
if (!deleteKeyPressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
nodeInternals,
|
||||||
|
edges,
|
||||||
|
hasDefaultNodes,
|
||||||
|
hasDefaultEdges,
|
||||||
|
onNodesDelete,
|
||||||
|
onEdgesDelete,
|
||||||
|
onNodesChange,
|
||||||
|
onEdgesChange,
|
||||||
|
} = store.getState();
|
||||||
const nodes = Array.from(nodeInternals.values());
|
const nodes = Array.from(nodeInternals.values());
|
||||||
const nodesToRemove = nodes.reduce<Node[]>((res, node) => {
|
const nodesToRemove = nodes.reduce<Node[]>((res, node) => {
|
||||||
if (!node.selected && node.parentNode && res.find((n) => n.id === node.parentNode)) {
|
const parentSelected = !node.selected && node.parentNode && res.find((n) => n.id === node.parentNode);
|
||||||
res.push(node);
|
if (node.selected || parentSelected) {
|
||||||
} else if (node.selected) {
|
|
||||||
res.push(node);
|
res.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +42,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
|||||||
}, []);
|
}, []);
|
||||||
const selectedEdges = edges.filter((e) => e.selected);
|
const selectedEdges = edges.filter((e) => e.selected);
|
||||||
|
|
||||||
if (deleteKeyPressed && (nodesToRemove || selectedEdges)) {
|
if (nodesToRemove || selectedEdges) {
|
||||||
const connectedEdges = getConnectedEdges(nodesToRemove, edges);
|
const connectedEdges = getConnectedEdges(nodesToRemove, edges);
|
||||||
const edgesToRemove = [...selectedEdges, ...connectedEdges];
|
const edgesToRemove = [...selectedEdges, ...connectedEdges];
|
||||||
const edgeIdsToRemove = edgesToRemove.reduce<string[]>((res, edge) => {
|
const edgeIdsToRemove = edgesToRemove.reduce<string[]>((res, edge) => {
|
||||||
@@ -69,11 +74,12 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
|||||||
onEdgesDelete?.(edgesToRemove);
|
onEdgesDelete?.(edgesToRemove);
|
||||||
|
|
||||||
if (onEdgesChange) {
|
if (onEdgesChange) {
|
||||||
const edgeChanges: EdgeChange[] = edgeIdsToRemove.map((id) => ({
|
onEdgesChange(
|
||||||
id,
|
edgeIdsToRemove.map((id) => ({
|
||||||
type: 'remove',
|
id,
|
||||||
}));
|
type: 'remove',
|
||||||
onEdgesChange(edgeChanges);
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +94,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
|||||||
|
|
||||||
store.setState({ nodesSelectionActive: false });
|
store.setState({ nodesSelectionActive: false });
|
||||||
}
|
}
|
||||||
}, [deleteKeyPressed, onNodesChange, onEdgesChange]);
|
}, [deleteKeyPressed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
store.setState({ multiSelectionActive: multiSelectionKeyPressed });
|
store.setState({ multiSelectionActive: multiSelectionKeyPressed });
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { useStoreApi } from '../store';
|
||||||
|
import { calcNextPosition } from './useDrag/utils';
|
||||||
|
|
||||||
|
import { XYPosition } from '../types';
|
||||||
|
|
||||||
|
function useUpdateNodePositions() {
|
||||||
|
const store = useStoreApi();
|
||||||
|
|
||||||
|
const updatePositions = useCallback((positionDiff: XYPosition) => {
|
||||||
|
const { nodeInternals, nodeExtent, updateNodePositions } = store.getState();
|
||||||
|
const selectedNodes = Array.from(nodeInternals.values()).filter((n) => n.selected);
|
||||||
|
|
||||||
|
const nodeUpdates = selectedNodes.map((n) => {
|
||||||
|
if (n.positionAbsolute) {
|
||||||
|
const updatedPos = calcNextPosition(
|
||||||
|
n,
|
||||||
|
{ x: n.positionAbsolute.x + positionDiff.x, y: n.positionAbsolute.y + positionDiff.y },
|
||||||
|
nodeInternals,
|
||||||
|
nodeExtent
|
||||||
|
);
|
||||||
|
|
||||||
|
n.position = updatedPos.position;
|
||||||
|
n.positionAbsolute = updatedPos.positionAbsolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateNodePositions(nodeUpdates, true, true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return updatePositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useUpdateNodePositions;
|
||||||
@@ -97,7 +97,7 @@ const createStore = () =>
|
|||||||
onNodesChange?.(changes);
|
onNodesChange?.(changes);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateNodePositions: (nodeDragItems: NodeDragItem[], positionChanged = true, dragging = false) => {
|
updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged = true, dragging = false) => {
|
||||||
const { onNodesChange, nodeInternals, hasDefaultNodes } = get();
|
const { onNodesChange, nodeInternals, hasDefaultNodes } = get();
|
||||||
|
|
||||||
if (hasDefaultNodes || onNodesChange) {
|
if (hasDefaultNodes || onNodesChange) {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
.react-flow__edge {
|
.react-flow__edge {
|
||||||
&.selected {
|
&.selected,
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
.react-flow__edge-path {
|
.react-flow__edge-path {
|
||||||
stroke: #555;
|
stroke: #555;
|
||||||
}
|
}
|
||||||
@@ -54,8 +57,11 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border-color: #1a192b;
|
border-color: #1a192b;
|
||||||
|
|
||||||
&.selected {
|
&.selected,
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
box-shadow: 0 0 0 0.5px #1a192b;
|
box-shadow: 0 0 0 0.5px #1a192b;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__handle {
|
.react-flow__handle {
|
||||||
@@ -71,7 +77,10 @@
|
|||||||
box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected,
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
box-shadow: 0 0 0 0.5px #1a192b;
|
box-shadow: 0 0 0 0.5px #1a192b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,6 +94,11 @@
|
|||||||
.react-flow__selection {
|
.react-flow__selection {
|
||||||
background: rgba(0, 89, 220, 0.08);
|
background: rgba(0, 89, 220, 0.08);
|
||||||
border: 1px dotted rgba(0, 89, 220, 0.8);
|
border: 1px dotted rgba(0, 89, 220, 0.8);
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__handle {
|
.react-flow__handle {
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
attributionPosition?: AttributionPosition;
|
attributionPosition?: AttributionPosition;
|
||||||
proOptions?: ProOptions;
|
proOptions?: ProOptions;
|
||||||
elevateEdgesOnSelect?: boolean;
|
elevateEdgesOnSelect?: boolean;
|
||||||
|
disableKeyboardA11y?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReactFlowRefType = HTMLDivElement;
|
export type ReactFlowRefType = HTMLDivElement;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export interface Edge<T = any> {
|
|||||||
markerStart?: EdgeMarkerType;
|
markerStart?: EdgeMarkerType;
|
||||||
markerEnd?: EdgeMarkerType;
|
markerEnd?: EdgeMarkerType;
|
||||||
zIndex?: number;
|
zIndex?: number;
|
||||||
|
ariaLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DefaultEdgeOptions = Omit<
|
export type DefaultEdgeOptions = Omit<
|
||||||
@@ -122,6 +123,8 @@ export interface WrapEdgeProps<T = any> {
|
|||||||
markerStart?: EdgeMarkerType;
|
markerStart?: EdgeMarkerType;
|
||||||
markerEnd?: EdgeMarkerType;
|
markerEnd?: EdgeMarkerType;
|
||||||
rfId?: string;
|
rfId?: string;
|
||||||
|
ariaLabel?: string | null;
|
||||||
|
disableKeyboardA11y: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EdgeSmoothStepProps<T = any> extends EdgeProps<T> {
|
export interface EdgeSmoothStepProps<T = any> extends EdgeProps<T> {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export type ReactFlowActions = {
|
|||||||
setEdges: (edges: Edge[]) => void;
|
setEdges: (edges: Edge[]) => void;
|
||||||
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
|
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
|
||||||
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
||||||
updateNodePositions: (nodeDragItems: NodeDragItem[], positionChanged: boolean, dragging: boolean) => void;
|
updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged: boolean, dragging: boolean) => void;
|
||||||
resetSelectedElements: () => void;
|
resetSelectedElements: () => void;
|
||||||
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;
|
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;
|
||||||
addSelectedNodes: (nodeIds: string[]) => void;
|
addSelectedNodes: (nodeIds: string[]) => void;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface Node<T = any> {
|
|||||||
extent?: 'parent' | CoordinateExtent;
|
extent?: 'parent' | CoordinateExtent;
|
||||||
expandParent?: boolean;
|
expandParent?: boolean;
|
||||||
positionAbsolute?: XYPosition;
|
positionAbsolute?: XYPosition;
|
||||||
|
ariaLabel?: string;
|
||||||
|
|
||||||
// only used internally
|
// only used internally
|
||||||
[internalsSymbol]?: {
|
[internalsSymbol]?: {
|
||||||
@@ -86,6 +87,9 @@ export interface WrapNodeProps<T = any> {
|
|||||||
isParent: boolean;
|
isParent: boolean;
|
||||||
noPanClassName: string;
|
noPanClassName: string;
|
||||||
noDragClassName: string;
|
noDragClassName: string;
|
||||||
|
rfId: string;
|
||||||
|
disableKeyboardA11y: boolean;
|
||||||
|
ariaLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NodeHandleBounds = {
|
export type NodeHandleBounds = {
|
||||||
|
|||||||
@@ -42,3 +42,6 @@ export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
|
|||||||
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
|
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
|
||||||
|
|
||||||
export const internalsSymbol = Symbol('internals');
|
export const internalsSymbol = Symbol('internals');
|
||||||
|
|
||||||
|
// used for a11y key board controls for nodes and edges
|
||||||
|
export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo, useId } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import shallow from 'zustand/shallow';
|
import shallow from 'zustand/shallow';
|
||||||
import {
|
import {
|
||||||
@@ -54,7 +54,9 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
|||||||
export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
|
export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
|
||||||
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
|
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
|
||||||
|
|
||||||
const MiniMap = ({
|
const ARIA_LABEL_KEY = 'react-flow__minimap-desc';
|
||||||
|
|
||||||
|
function MiniMap({
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
nodeStrokeColor = '#555',
|
nodeStrokeColor = '#555',
|
||||||
@@ -63,7 +65,8 @@ const MiniMap = ({
|
|||||||
nodeBorderRadius = 5,
|
nodeBorderRadius = 5,
|
||||||
nodeStrokeWidth = 2,
|
nodeStrokeWidth = 2,
|
||||||
maskColor = 'rgb(240, 242, 243, 0.7)',
|
maskColor = 'rgb(240, 242, 243, 0.7)',
|
||||||
}: MiniMapProps) => {
|
}: MiniMapProps) {
|
||||||
|
const minimapId = useId();
|
||||||
const {
|
const {
|
||||||
width: containerWidth,
|
width: containerWidth,
|
||||||
height: containerHeight,
|
height: containerHeight,
|
||||||
@@ -97,6 +100,7 @@ const MiniMap = ({
|
|||||||
typeof window === 'undefined' || !!window.chrome
|
typeof window === 'undefined' || !!window.chrome
|
||||||
? 'crispEdges'
|
? 'crispEdges'
|
||||||
: 'geometricPrecision';
|
: 'geometricPrecision';
|
||||||
|
const labelledBy = `${ARIA_LABEL_KEY}-${minimapId}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -105,7 +109,10 @@ const MiniMap = ({
|
|||||||
viewBox={`${x} ${y} ${width} ${height}`}
|
viewBox={`${x} ${y} ${width} ${height}`}
|
||||||
style={style}
|
style={style}
|
||||||
className={cc(['react-flow__minimap', className])}
|
className={cc(['react-flow__minimap', className])}
|
||||||
|
role="img"
|
||||||
|
aria-labelledby={labelledBy}
|
||||||
>
|
>
|
||||||
|
<title id={labelledBy}>React Flow mini map</title>
|
||||||
{nodes
|
{nodes
|
||||||
.filter((node) => !node.hidden && node.width && node.height)
|
.filter((node) => !node.hidden && node.width && node.height)
|
||||||
.map((node) => {
|
.map((node) => {
|
||||||
@@ -133,7 +140,7 @@ const MiniMap = ({
|
|||||||
height={viewBB.height}
|
height={viewBB.height}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
className='react-flow__minimap-mask'
|
className="react-flow__minimap-mask"
|
||||||
d={`M${x - offset},${y - offset}h${width + offset * 2}v${
|
d={`M${x - offset},${y - offset}h${width + offset * 2}v${
|
||||||
height + offset * 2
|
height + offset * 2
|
||||||
}h${-width - offset * 2}z
|
}h${-width - offset * 2}z
|
||||||
@@ -141,11 +148,11 @@ const MiniMap = ({
|
|||||||
viewBB.height
|
viewBB.height
|
||||||
}h${-viewBB.width}z`}
|
}h${-viewBB.width}z`}
|
||||||
fill={maskColor}
|
fill={maskColor}
|
||||||
fillRule='evenodd'
|
fillRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
MiniMap.displayName = 'MiniMap';
|
MiniMap.displayName = 'MiniMap';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user