Merge branch 'main' of https://github.com/xyflow/xyflow into 5030-focus-nodes-when-user-selects-via-tab
This commit is contained in:
@@ -7,15 +7,12 @@
|
||||
</script>
|
||||
|
||||
<div id={`${ARIA_NODE_DESC_KEY}-${store.flowId}`} class="a11y-hidden">
|
||||
Press enter or space to select a node.
|
||||
{#if !store.disableKeyboardA11y}
|
||||
You can then use the arrow keys to move the node around.
|
||||
{/if}
|
||||
Press delete to remove it and escape to cancel.
|
||||
{store.disableKeyboardA11y
|
||||
? store.ariaLabelConfig['node.a11yDescription.default']
|
||||
: store.ariaLabelConfig['node.a11yDescription.keyboardDisabled']}
|
||||
</div>
|
||||
<div id={`${ARIA_EDGE_DESC_KEY}-${store.flowId}`} class="a11y-hidden">
|
||||
Press enter or space to select an edge. You can then press delete to remove it or escape to
|
||||
cancel.
|
||||
{store.ariaLabelConfig['edge.a11yDescription.default']}
|
||||
</div>
|
||||
|
||||
{#if !store.disableKeyboardA11y}
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
style:width={toPxString(width)}
|
||||
style:height={toPxString(height)}
|
||||
style:z-index={z}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
onclick={() => {
|
||||
if (selectEdgeOnClick && id) store.handleEdgeSelection(id);
|
||||
|
||||
@@ -137,7 +137,8 @@
|
||||
? ariaLabel
|
||||
: `Edge from ${source} to ${target}`}
|
||||
aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined}
|
||||
role={focusable ? 'button' : 'img'}
|
||||
role={edge.ariaRole ?? (focusable ? 'group' : 'img')}
|
||||
aria-roledescription={edge.ariaRoleDescription || 'edge'}
|
||||
onkeydown={focusable ? onkeydown : undefined}
|
||||
tabindex={focusable ? 0 : undefined}
|
||||
>
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
);
|
||||
|
||||
let store = useStore();
|
||||
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||
|
||||
|
||||
let prevConnections: Map<string, HandleConnection> | null = null;
|
||||
$effect.pre(() => {
|
||||
@@ -227,6 +229,7 @@ The Handle component is the part of a node that can be used to connect nodes.
|
||||
onkeypress={() => {}}
|
||||
{style}
|
||||
role="button"
|
||||
aria-label={ariaLabelConfig[`handle.ariaLabel`]}
|
||||
tabindex="-1"
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
let prevTargetPosition: Position | undefined = targetPosition;
|
||||
|
||||
let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode);
|
||||
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||
|
||||
let connectableContext: ConnectableContext = {
|
||||
get value() {
|
||||
@@ -188,11 +189,11 @@
|
||||
) {
|
||||
// prevent default scrolling behavior on arrow key press when node is moved
|
||||
event.preventDefault();
|
||||
|
||||
store.ariaLiveMessage = `Moved selected node ${event.key
|
||||
.replace('Arrow', '')
|
||||
.toLowerCase()}. New position, x: ${node.internals.positionAbsolute.x}, y: ${node.internals.positionAbsolute.y}`;
|
||||
|
||||
store.ariaLiveMessage = ariaLabelConfig['node.a11yDescription.ariaLiveMessage']({
|
||||
direction: event.key.replace('Arrow', '').toLowerCase(),
|
||||
x: ~~node.internals.positionAbsolute.x,
|
||||
y: ~~node.internals.positionAbsolute.y
|
||||
});
|
||||
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
|
||||
}
|
||||
}
|
||||
@@ -274,7 +275,8 @@
|
||||
onkeydown={focusable ? onKeyDown : undefined}
|
||||
onfocus={focusable ? onFocus : undefined}
|
||||
tabIndex={focusable ? 0 : undefined}
|
||||
role={focusable ? 'button' : undefined}
|
||||
role={node.ariaRole ?? (focusable ? 'group' : undefined)}
|
||||
aria-roledescription={node.ariaRoleDescription || 'node'}
|
||||
aria-describedby={store.disableKeyboardA11y
|
||||
? undefined
|
||||
: `${ARIA_NODE_DESC_KEY}-${store.flowId}`}
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
noDragClass,
|
||||
noPanClass,
|
||||
noWheelClass,
|
||||
ariaLabelConfig,
|
||||
...divAttributes
|
||||
} = $derived(rest);
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
|
||||
@@ -20,7 +20,8 @@ import type {
|
||||
OnConnectEnd,
|
||||
OnReconnect,
|
||||
OnReconnectStart,
|
||||
OnReconnectEnd
|
||||
OnReconnectEnd,
|
||||
AriaLabelConfig
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -471,4 +472,9 @@ export type SvelteFlowProps<
|
||||
onselectionstart?: (event: PointerEvent) => void;
|
||||
/** This event handler gets called when the user finishes dragging a selection box */
|
||||
onselectionend?: (event: PointerEvent) => void;
|
||||
/**
|
||||
* Configuration for customizable labels, descriptions, and UI text. Provided keys will override the corresponding defaults.
|
||||
* Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings.
|
||||
*/
|
||||
ariaLabelConfig?: Partial<AriaLabelConfig>;
|
||||
};
|
||||
|
||||
@@ -112,7 +112,8 @@ export {
|
||||
type ResizeParamsWithDirection,
|
||||
type ResizeDragEvent,
|
||||
type IsValidConnection,
|
||||
type NodeConnection
|
||||
type NodeConnection,
|
||||
type AriaLabelConfig
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
);
|
||||
let minZoomReached = $derived(store.viewport.zoom <= store.minZoom);
|
||||
let maxZoomReached = $derived(store.viewport.zoom >= store.maxZoom);
|
||||
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||
let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical');
|
||||
|
||||
const onZoomInHandler = () => {
|
||||
@@ -72,7 +73,7 @@
|
||||
class={['svelte-flow__controls', orientationClass, className]}
|
||||
{position}
|
||||
data-testid="svelte-flow__controls"
|
||||
aria-label={ariaLabel ?? 'Svelte Flow controls'}
|
||||
aria-label={ariaLabelConfig['controls.ariaLabel']}
|
||||
{style}
|
||||
{...rest}
|
||||
>
|
||||
@@ -83,8 +84,8 @@
|
||||
<ControlButton
|
||||
onclick={onZoomInHandler}
|
||||
class="svelte-flow__controls-zoomin"
|
||||
title="zoom in"
|
||||
aria-label="zoom in"
|
||||
title={ariaLabelConfig['controls.zoomIn.ariaLabel']}
|
||||
aria-label={ariaLabelConfig['controls.zoomIn.ariaLabel']}
|
||||
disabled={maxZoomReached}
|
||||
{...buttonProps}
|
||||
>
|
||||
@@ -93,8 +94,8 @@
|
||||
<ControlButton
|
||||
onclick={onZoomOutHandler}
|
||||
class="svelte-flow__controls-zoomout"
|
||||
title="zoom out"
|
||||
aria-label="zoom out"
|
||||
title={ariaLabelConfig['controls.zoomOut.ariaLabel']}
|
||||
aria-label={ariaLabelConfig['controls.zoomOut.ariaLabel']}
|
||||
disabled={minZoomReached}
|
||||
{...buttonProps}
|
||||
>
|
||||
@@ -105,8 +106,8 @@
|
||||
<ControlButton
|
||||
class="svelte-flow__controls-fitview"
|
||||
onclick={onFitViewHandler}
|
||||
title="fit view"
|
||||
aria-label="fit view"
|
||||
title={ariaLabelConfig['controls.fitView.ariaLabel']}
|
||||
aria-label={ariaLabelConfig['controls.fitView.ariaLabel']}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FitViewIcon />
|
||||
@@ -116,8 +117,8 @@
|
||||
<ControlButton
|
||||
class="svelte-flow__controls-interactive"
|
||||
onclick={onToggleInteractivity}
|
||||
title="toggle interactivity"
|
||||
aria-label="toggle interactivity"
|
||||
title={ariaLabelConfig['controls.interactive.ariaLabel']}
|
||||
aria-label={ariaLabelConfig['controls.interactive.ariaLabel']}
|
||||
{...buttonProps}
|
||||
>
|
||||
{#if isInteractive}<UnlockIcon />{:else}<LockIcon />{/if}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
let {
|
||||
position = 'bottom-right',
|
||||
ariaLabel = 'Mini map',
|
||||
ariaLabel,
|
||||
nodeStrokeColor = 'transparent',
|
||||
nodeColor,
|
||||
nodeClass = '',
|
||||
@@ -42,6 +42,7 @@
|
||||
}: MiniMapProps = $props();
|
||||
|
||||
let store = $derived(useStore());
|
||||
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||
|
||||
const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor);
|
||||
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
||||
@@ -113,7 +114,9 @@
|
||||
zoomable
|
||||
}}
|
||||
>
|
||||
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
|
||||
{#if ariaLabel ?? ariaLabelConfig['minimap.ariaLabel']}
|
||||
<title id={labelledBy}>{ariaLabel ?? ariaLabelConfig['minimap.ariaLabel']}</title>
|
||||
{/if}
|
||||
|
||||
{#each store.nodes as userNode (userNode.id)}
|
||||
{@const node = store.nodeLookup.get(userNode.id)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getViewportForBounds,
|
||||
updateConnectionLookup,
|
||||
initialConnection,
|
||||
mergeAriaLabelConfig,
|
||||
type SelectionRect,
|
||||
type SnapGrid,
|
||||
type MarkerProps,
|
||||
@@ -32,7 +33,8 @@ import {
|
||||
type Handle,
|
||||
type OnReconnect,
|
||||
type OnReconnectStart,
|
||||
type OnReconnectEnd
|
||||
type OnReconnectEnd,
|
||||
type AriaLabelConfig
|
||||
} from '@xyflow/system';
|
||||
|
||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||
@@ -289,6 +291,9 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
||||
noPanClass: string = $derived(signals.props.noPanClass ?? 'nopan');
|
||||
noDragClass: string = $derived(signals.props.noDragClass ?? 'nodrag');
|
||||
noWheelClass: string = $derived(signals.props.noWheelClass ?? 'nowheel');
|
||||
ariaLabelConfig: AriaLabelConfig = $derived(
|
||||
mergeAriaLabelConfig(signals.props.ariaLabelConfig)
|
||||
);
|
||||
|
||||
// _viewport is the internal viewport.
|
||||
// when binding to viewport, we operate on signals.viewport instead
|
||||
|
||||
@@ -25,6 +25,11 @@ export type Edge<
|
||||
style?: string;
|
||||
class?: ClassValue;
|
||||
focusable?: boolean;
|
||||
/**
|
||||
* The ARIA role attribute for the edge, used for accessibility.
|
||||
* @default "group"
|
||||
*/
|
||||
ariaRole?: HTMLAttributes<HTMLElement>['role'];
|
||||
};
|
||||
|
||||
export type BaseEdgeProps = Pick<
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Component } from 'svelte';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
|
||||
import type { InternalNodeBase, NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
|
||||
|
||||
/**
|
||||
@@ -21,6 +21,11 @@ export type Node<
|
||||
class?: ClassValue;
|
||||
style?: string;
|
||||
focusable?: boolean;
|
||||
/**
|
||||
* The ARIA role attribute for the node element, used for accessibility.
|
||||
* @default "group"
|
||||
*/
|
||||
ariaRole?: HTMLAttributes<HTMLElement>['role'];
|
||||
};
|
||||
|
||||
// @todo: currently generics for nodes are not really supported
|
||||
|
||||
Reference in New Issue
Block a user