Skip to content

Commit

Permalink
base changes for active/current node styling (#62007)
Browse files Browse the repository at this point in the history
* changes for active/current node styling
* Adjustment to reducer for selected node
*Fix spelling mistake
  • Loading branch information
bkimmel authored Apr 3, 2020
1 parent e6c23ea commit 0946376
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ interface AppRequestedResolverData {
}

/**
* When the user switches the active descendent of the Resolver.
* When the user switches the "active descendant" of the Resolver.
* The "active descendant" (from the point of view of the parent element)
* corresponds to the "current" child element. "active" or "current" here meaning
* the element that is focused on by the user's interactions with the UI, but
* not necessarily "selected" (see UserSelectedResolverNode below)
*/
interface UserFocusedOnResolverNode {
readonly type: 'userFocusedOnResolverNode';
Expand All @@ -57,10 +61,27 @@ interface UserFocusedOnResolverNode {
};
}

/**
* When the user "selects" a node in the Resolver
* "Selected" refers to the state of being the element that the
* user most recently "picked" (by e.g. pressing a button corresponding
* to the element in a list) as opposed to "active" or "current" (see UserFocusedOnResolverNode above).
*/
interface UserSelectedResolverNode {
readonly type: 'userSelectedResolverNode';
readonly payload: {
/**
* Used to identify the process node that the user selected
*/
readonly nodeId: string;
};
}

export type ResolverAction =
| CameraAction
| DataAction
| UserBroughtProcessIntoView
| UserChangedSelectedEvent
| AppRequestedResolverData
| UserFocusedOnResolverNode;
| UserFocusedOnResolverNode
| UserSelectedResolverNode;
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,44 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Reducer, combineReducers } from 'redux';
import { htmlIdGenerator } from '@elastic/eui';
import { animateProcessIntoView } from './methods';
import { cameraReducer } from './camera/reducer';
import { dataReducer } from './data/reducer';
import { ResolverState, ResolverAction, ResolverUIState } from '../types';
import { uniquePidForProcess } from '../models/process_event';

/**
* Despite the name "generator", this function is entirely determinant
* (i.e. it will return the same html id given the same prefix 'resolverNode'
* and nodeId)
*/
const resolverNodeIdGenerator = htmlIdGenerator('resolverNode');

const uiReducer: Reducer<ResolverUIState, ResolverAction> = (
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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
*/
Expand All @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
);
6 changes: 5 additions & 1 deletion x-pack/plugins/endpoint/public/embeddables/resolver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export const SymbolIds = {
runningTriggerCube: idGenerator('runningTriggerCube'),
terminatedProcessCube: idGenerator('terminatedCube'),
terminatedTriggerCube: idGenerator('terminatedTriggerCube'),
processCubeActiveBacking: idGenerator('activeBacking'),
};

/**
Expand Down Expand Up @@ -393,6 +394,15 @@ const SymbolsAndShapes = memo(() => (
/>
</g>
</symbol>
<symbol viewBox="0 -3 88 106" id={SymbolIds.processCubeActiveBacking}>
<title>resolver active backing</title>
<path
d="m87.521 25.064a3.795 3.795 0 0 0-1.4313-1.4717l-40.164-23.083a3.8338 3.8338 0 0 0-3.8191 0l-40.165 23.083a3.8634 3.8634 0 0 0-1.9097 3.2926v46.165a3.7986 3.7986 0 0 0 1.9097 3.2925l40.164 23.083a3.8342 3.8342 0 0 0 3.8191 0l40.164-23.083a3.7988 3.7988 0 0 0 1.9099-3.2925v-46.165a3.7775 3.7775 0 0 0-0.47857-1.8209z"
fill="transparent"
strokeWidth="2"
stroke="#7E839C"
/>
</symbol>
</>
));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ export const Resolver = styled(

const { projectionMatrix, ref, onMouseDown } = useCamera();
const isLoading = useSelector(selectors.isLoading);
const activeDescendantId = useSelector(selectors.uiActiveDescendantId);

useLayoutEffect(() => {
dispatch({
type: 'userChangedSelectedEvent',
payload: { selectedEvent },
});
}, [dispatch, selectedEvent]);

return (
<div data-test-subj="resolverEmbeddable" className={className}>
{isLoading ? (
Expand All @@ -79,6 +81,7 @@ export const Resolver = styled(
ref={ref}
role="tree"
tabIndex={0}
aria-activedescendant={activeDescendantId || undefined}
>
{edgeLineSegments.map(([startPosition, endPosition], index) => (
<EdgeLine
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { htmlIdGenerator, EuiKeyboardAccessible } from '@elastic/eui';
import { useSelector } from 'react-redux';
import { applyMatrix3 } from '../lib/vector2';
import { Vector2, Matrix3, AdjacentProcessMap, ResolverProcessType } from '../types';
import { SymbolIds, NamedColors, PaintServerIds } from './defs';
import { ResolverEvent } from '../../../../common/types';
import { useResolverDispatch } from './use_resolver_dispatch';
import * as eventModel from '../../../../common/models/event';
import * as processModel from '../models/process_event';
import * as selectors from '../store/selectors';

const nodeAssets = {
runningProcessCube: {
Expand Down Expand Up @@ -93,6 +95,9 @@ export const ProcessEventDot = styled(

const selfId = adjacentNodeMap.self;

const activeDescendantId = useSelector(selectors.uiActiveDescendantId);
const selectedDescendantId = useSelector(selectors.uiSelectedDescendantId);

const nodeViewportStyle = useMemo(
() => ({
left: `${left}px`,
Expand Down Expand Up @@ -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(
Expand All @@ -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<SVGSVGElement, MouseEvent>) => {
if (animationTarget.current !== null) {
(animationTarget.current as any).beginElement();
}
dispatch({
type: 'userSelectedResolverNode',
payload: {
nodeId,
},
});
},
[animationTarget, dispatch, nodeId]
);

return (
<EuiKeyboardAccessible>
Expand All @@ -179,13 +195,24 @@ 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}
onFocus={handleFocus}
tabIndex={-1}
>
<g>
<use
xlinkHref={`#${SymbolIds.processCubeActiveBacking}`}
x={-11.35}
y={-11.35}
width={markerSize * 1.5}
height={markerSize * 1.5}
className="backing"
/>
<rect x="7" y="-12.75" width="15" height="10" fill={NamedColors.resolverBackground} />
<use
role="presentation"
xlinkHref={cubeSymbol}
Expand Down Expand Up @@ -265,6 +292,20 @@ export const ProcessEventDot = styled(
white-space: nowrap;
will-change: left, top, width, height;
contain: strict;
//dasharray & dashoffset should be equal to "pull" the stroke back
//when it is transitioned.
//The value is tuned to look good when animated, but to preserve
//the effect, it should always be _at least_ the length of the stroke
& .backing {
stroke-dasharray: 500;
stroke-dashoffset: 500;
}
&[aria-current] .backing {
transition-property: stroke-dashoffset;
transition-duration: 1s;
stroke-dashoffset: 0;
}
`;

const processTypeToCube: Record<ResolverProcessType, keyof typeof nodeAssets> = {
Expand Down

0 comments on commit 0946376

Please sign in to comment.