= (
- uiState = { activeDescendentId: null },
+ uiState = { activeDescendantId: null, selectedDescendantId: null },
action
) => {
if (action.type === 'userFocusedOnResolverNode') {
return {
- activeDescendentId: action.payload.nodeId,
+ ...uiState,
+ activeDescendantId: action.payload.nodeId,
+ };
+ } else if (action.type === 'userSelectedResolverNode') {
+ return {
+ ...uiState,
+ selectedDescendantId: action.payload.nodeId,
+ };
+ } else if (action.type === 'userBroughtProcessIntoView') {
+ /**
+ * This action has a process payload (instead of a processId), so we use
+ * `uniquePidForProcess` and `resolverNodeIdGenerator` to resolve the determinant
+ * html id of the node being brought into view.
+ */
+ const processNodeId = resolverNodeIdGenerator(uniquePidForProcess(action.payload.process));
+ return {
+ ...uiState,
+ activeDescendantId: processNodeId,
};
} else {
return uiState;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts
index 37482916496e7..e8ae3d08e5cb6 100644
--- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts
@@ -6,6 +6,7 @@
import * as cameraSelectors from './camera/selectors';
import * as dataSelectors from './data/selectors';
+import * as uiSelectors from './ui/selectors';
import { ResolverState } from '../types';
/**
@@ -59,6 +60,22 @@ export const processAdjacencies = composeSelectors(
dataSelectors.processAdjacencies
);
+/**
+ * Returns the id of the "current" tree node (fake-focused)
+ */
+export const uiActiveDescendantId = composeSelectors(
+ uiStateSelector,
+ uiSelectors.activeDescendantId
+);
+
+/**
+ * Returns the id of the "selected" tree node (the node that is currently "pressed" and possibly controlling other popups / components)
+ */
+export const uiSelectedDescendantId = composeSelectors(
+ uiStateSelector,
+ uiSelectors.selectedDescendantId
+);
+
/**
* Returns the camera state from within ResolverState
*/
@@ -73,6 +90,13 @@ function dataStateSelector(state: ResolverState) {
return state.data;
}
+/**
+ * Returns the ui state from within ResolverState
+ */
+function uiStateSelector(state: ResolverState) {
+ return state.ui;
+}
+
/**
* Whether or not the resolver is pending fetching data
*/
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/ui/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/ui/selectors.ts
new file mode 100644
index 0000000000000..196e834c406b3
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/ui/selectors.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { createSelector } from 'reselect';
+import { ResolverUIState } from '../../types';
+
+/**
+ * id of the "current" tree node (fake-focused)
+ */
+export const activeDescendantId = createSelector(
+ (uiState: ResolverUIState) => uiState,
+ /* eslint-disable no-shadow */
+ ({ activeDescendantId }) => {
+ return activeDescendantId;
+ }
+);
+
+/**
+ * id of the currently "selected" tree node
+ */
+export const selectedDescendantId = createSelector(
+ (uiState: ResolverUIState) => uiState,
+ /* eslint-disable no-shadow */
+ ({ selectedDescendantId }) => {
+ return selectedDescendantId;
+ }
+);
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts
index 674553aba0937..d370bda0d1842 100644
--- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts
@@ -37,7 +37,11 @@ export interface ResolverUIState {
/**
* The ID attribute of the resolver's aria-activedescendent.
*/
- readonly activeDescendentId: string | null;
+ readonly activeDescendantId: string | null;
+ /**
+ * The ID attribute of the resolver's currently selected descendant.
+ */
+ readonly selectedDescendantId: string | null;
}
/**
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx
index 911cda1be6517..8ee9bfafc630e 100644
--- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx
@@ -193,6 +193,7 @@ export const SymbolIds = {
runningTriggerCube: idGenerator('runningTriggerCube'),
terminatedProcessCube: idGenerator('terminatedCube'),
terminatedTriggerCube: idGenerator('terminatedTriggerCube'),
+ processCubeActiveBacking: idGenerator('activeBacking'),
};
/**
@@ -393,6 +394,15 @@ const SymbolsAndShapes = memo(() => (
/>
+
+ resolver active backing
+
+
>
));
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx
index 58ce9b963de5d..36155ece57a9c 100644
--- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx
@@ -59,6 +59,7 @@ export const Resolver = styled(
const { projectionMatrix, ref, onMouseDown } = useCamera();
const isLoading = useSelector(selectors.isLoading);
+ const activeDescendantId = useSelector(selectors.uiActiveDescendantId);
useLayoutEffect(() => {
dispatch({
@@ -66,6 +67,7 @@ export const Resolver = styled(
payload: { selectedEvent },
});
}, [dispatch, selectedEvent]);
+
return (
{isLoading ? (
@@ -79,6 +81,7 @@ export const Resolver = styled(
ref={ref}
role="tree"
tabIndex={0}
+ aria-activedescendant={activeDescendantId || undefined}
>
{edgeLineSegments.map(([startPosition, endPosition], index) => (
({
left: `${left}px`,
@@ -143,6 +148,9 @@ export const ProcessEventDot = styled(
const labelId = useMemo(() => resolverNodeIdGenerator(), [resolverNodeIdGenerator]);
const descriptionId = useMemo(() => resolverNodeIdGenerator(), [resolverNodeIdGenerator]);
+ const isActiveDescendant = nodeId === activeDescendantId;
+ const isSelectedDescendant = nodeId === selectedDescendantId;
+
const dispatch = useResolverDispatch();
const handleFocus = useCallback(
@@ -153,16 +161,24 @@ export const ProcessEventDot = styled(
nodeId,
},
});
- focusEvent.currentTarget.setAttribute('aria-current', 'true');
},
[dispatch, nodeId]
);
- const handleClick = useCallback(() => {
- if (animationTarget.current !== null) {
- animationTarget.current.beginElement();
- }
- }, [animationTarget]);
+ const handleClick = useCallback(
+ (clickEvent: React.MouseEvent) => {
+ if (animationTarget.current !== null) {
+ (animationTarget.current as any).beginElement();
+ }
+ dispatch({
+ type: 'userSelectedResolverNode',
+ payload: {
+ nodeId,
+ },
+ });
+ },
+ [animationTarget, dispatch, nodeId]
+ );
return (
@@ -179,6 +195,8 @@ export const ProcessEventDot = styled(
aria-labelledby={labelId}
aria-describedby={descriptionId}
aria-haspopup={'true'}
+ aria-current={isActiveDescendant ? 'true' : undefined}
+ aria-selected={isSelectedDescendant ? 'true' : undefined}
style={nodeViewportStyle}
id={nodeId}
onClick={handleClick}
@@ -186,6 +204,15 @@ export const ProcessEventDot = styled(
tabIndex={-1}
>
+
+