Skip to content

Commit

Permalink
Set keyboard focus on graph/table when selecting an analysis
Browse files Browse the repository at this point in the history
fixes #414

When clicking on an analysis in the Available Analysis list, the analysis
is scrolled into view and given keyboard focus. When clicking on an analysis
title in the trace tab, keyboard focus should be the analysis' graph/chart region.
If a View that is already open is selected in the sidebar, the View's title area
should pulse to temporarily bring attention to the View panel. 

Signed-off-by: hriday-panchasara <hriday.panchasara@ericsson.com>
  • Loading branch information
hriday-panchasara authored and PatrickTasse committed Feb 7, 2022
1 parent 61895a7 commit de7fd50
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,20 @@ export abstract class AbstractOutputComponent<P extends AbstractOutputProps, S e
const localStyle = Object.assign({}, this.props.style);
localStyle.width = this.props.widthWPBugWorkaround;
return <div style={localStyle}
id={this.props.outputDescriptor.id}
id={this.props.traceId + this.props.outputDescriptor.id}
tabIndex={-1}
className={'output-container ' + this.props.className}
onMouseUp={this.props.onMouseUp}
onMouseDown={this.props.onMouseDown}
onTouchStart={this.props.onTouchStart}
onTouchEnd={this.props.onTouchEnd}
data-tip=''
data-for="tooltip-component">
<div className='widget-handle' style={{ width: this.props.style.handleWidth, height: this.props.style.height }}>
<div
id={this.props.traceId + this.props.outputDescriptor.id + 'handle'}
className='widget-handle'
style={{ width: this.props.style.handleWidth, height: this.props.style.height }}
>
{this.renderTitleBar()}
</div>
<div className='main-output-container' ref={this.mainOutputContainer}
Expand All @@ -88,7 +93,7 @@ export abstract class AbstractOutputComponent<P extends AbstractOutputProps, S e
<button className='remove-component-button' onClick={this.closeComponent}>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className='title-bar-label' title={outputName}>
<div className='title-bar-label' title={outputName} onClick={() => this.setFocus()}>
{outputName}
</div>
</React.Fragment>;
Expand Down Expand Up @@ -120,6 +125,7 @@ export abstract class AbstractOutputComponent<P extends AbstractOutputProps, S e

return this.renderMainArea();
}
abstract setFocus(): void;

abstract renderMainArea(): React.ReactNode;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
this.onToggleCollapse = this.onToggleCollapse.bind(this);
this.onOrderChange = this.onOrderChange.bind(this);
return this.state.xyTree.length
? <div className='scrollable' style={{ height: this.props.style.height, width: this.getMainAreaWidth() }}>
? <div
tabIndex={0}
id={this.props.traceId + this.props.outputDescriptor.id + 'focusContainer'}
className='scrollable' style={{ height: this.props.style.height, width: this.getMainAreaWidth() }}
>
<EntryTree
entries={this.state.xyTree}
showCheckboxes={false}
Expand All @@ -94,6 +98,7 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
: undefined
;
}

renderMainArea(): React.ReactNode {
return <React.Fragment>
{this.state.outputStatus === ResponseStatus.COMPLETED ?
Expand All @@ -102,13 +107,22 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
>
{this.renderTree()}
</div> :
<div className='analysis-running-main-area'>
<div tabIndex={0} id={this.props.traceId + this.props.outputDescriptor.id + 'focusContainer'} className='analysis-running-main-area'>
<i className='fa fa-refresh fa-spin' style={{ marginRight: '5px' }} />
<span>Analysis running</span>
</div>
}
</React.Fragment>;
}

setFocus(): void {
if (document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')) {
document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')?.focus();
} else {
document.getElementById(this.props.traceId + this.props.outputDescriptor.id)?.focus();
}
}

private onToggleCollapse(id: number, nodes: TreeNode[]) {
let newList = [...this.state.collapsedNodes];

Expand All @@ -122,6 +136,7 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
const orderedIds = getAllExpandedNodeIds(nodes, newList);
this.setState({collapsedNodes: newList, orderedNodes: orderedIds});
}

private onOrderChange(ids: number[]) {
this.setState({orderedNodes: ids});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class NullOutputComponent extends AbstractOutputComponent<NullOutputProps
constructor(props: NullOutputProps) {
super(props);
}

renderMainArea(): React.ReactNode {
const treeWidth = Math.min(this.getMainAreaWidth(), this.props.style.sashOffset + this.props.style.sashWidth);
const chartWidth = this.getMainAreaWidth() - treeWidth;
Expand All @@ -29,4 +30,8 @@ export class NullOutputComponent extends AbstractOutputComponent<NullOutputProps
resultsAreEmpty(): boolean {
return true;
}

setFocus(): void {
return;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,18 @@ export class TableOutputComponent extends AbstractOutputComponent<TableOutputPro
params.successCallback(rowsThisPage, this.props.nbEvents);
}
};

this.onEventClick = this.onEventClick.bind(this);
this.onModelUpdated = this.onModelUpdated.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.searchEvents = this.searchEvents.bind(this);
this.findMatchedEvent = this.findMatchedEvent.bind(this);
this.checkFocus = this.checkFocus.bind(this);
}

renderMainArea(): React.ReactNode {
return <div id='events-table'
return <div id={this.props.traceId + this.props.outputDescriptor.id + 'focusContainer'}
tabIndex={-1}
onFocus={event=>this.checkFocus(event)}
className={this.props.backgroundTheme === 'light' ? 'ag-theme-balham' : 'ag-theme-balham-dark'}
style={{ height: this.props.style.height, width: this.props.widthWPBugWorkaround }}>
<AgGridReact
Expand Down Expand Up @@ -127,6 +129,20 @@ export class TableOutputComponent extends AbstractOutputComponent<TableOutputPro
this.props.unitController.onSelectionRangeChange(range => { this.handleTimeSelectionChange(range); });
}

private checkFocus(event: React.FocusEvent<HTMLDivElement, Element>): void {
if (!event.currentTarget?.contains(event.relatedTarget as Node)) {
this.setFocus();
}
}

setFocus(): void {
if (document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')) {
document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')?.focus();
} else {
document.getElementById(this.props.traceId + this.props.outputDescriptor.id)?.focus();
}
}

componentWillUnmount(): void {
// TODO: replace with removing the handler from unit controller
// See timeline-chart issue #98
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,9 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent<Timegr
// TODO Show header, when we can have entries in-line with timeline-chart
return <>
<div ref={this.timeGraphTreeRef} className='scrollable' onScroll={() => this.synchronizeTreeScroll()}
style={{ height: parseInt(this.props.style.height.toString()) - this.getMarkersLayerHeight() }}>
style={{ height: parseInt(this.props.style.height.toString()) - this.getMarkersLayerHeight() }}
tabIndex={0}
>
<EntryTree
collapsedNodes={this.state.collapsedNodes}
showFilter={false}
Expand Down Expand Up @@ -447,7 +449,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent<Timegr
return <ReactTimeGraphContainer
options={
{
id: 'timegraph-chart',
id: this.props.traceId + this.props.outputDescriptor.id + 'focusContainer',
height: parseInt(this.props.style.height.toString()) - this.getMarkersLayerHeight(),
width: this.getChartWidth(),
backgroundColor: this.props.style.chartBackgroundColor,
Expand All @@ -458,14 +460,22 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent<Timegr
addWidgetResizeHandler={this.props.addWidgetResizeHandler}
removeWidgetResizeHandler={this.props.removeWidgetResizeHandler}
unitController={this.props.unitController}
id='timegraph-chart'
id={this.props.traceId + this.props.outputDescriptor.id + 'focusContainer'}
layers={[
grid, this.chartLayer, selectionRange, this.chartCursors, this.arrowLayer, this.rangeEventsLayer
]}
>
</ReactTimeGraphContainer>;
}

setFocus(): void {
if (document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')) {
document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')?.focus();
} else {
document.getElementById(this.props.traceId + this.props.outputDescriptor.id)?.focus();
}
}

protected getVerticalScrollbar(): JSX.Element {
return <ReactTimeGraphContainer
id='vscroll'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
private scrollToBottom(): void {
if (this.props.outputs.length) {
const bottomOutputId = this.props.outputs[this.props.outputs.length - 1].id;
document.getElementById(bottomOutputId)?.scrollIntoView();
document.getElementById(this.state.experiment.UUID+bottomOutputId)?.focus();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class ReactTimeGraphContainer extends React.Component<ReactTimeGraphConta
}

render(): JSX.Element {
return <canvas ref={ ref => this.ref = ref || undefined } onWheel={ e => e.preventDefault() } tabIndex={ 1 }></canvas>;
return <canvas ref={ ref => this.ref = ref || undefined } onWheel={ e => e.preventDefault() } tabIndex={ 0 }></canvas>;
}

private resize(): void {
Expand Down
18 changes: 16 additions & 2 deletions packages/react-components/src/components/xy-output-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,10 @@ export class XYOutputComponent extends AbstractTreeOutputComponent<AbstractOutpu
}
return <React.Fragment>
{this.state.outputStatus === ResponseStatus.COMPLETED ?
<div id='xy-main' tabIndex={0}
<div
id={this.props.traceId + this.props.outputDescriptor.id + 'focusContainer'}
className='xy-main'
tabIndex={0}
onKeyDown={event => this.onKeyDown(event)}
onWheel={event => this.onWheel(event)}
onMouseMove={event => this.onMouseMove(event)}
Expand All @@ -336,7 +339,10 @@ export class XYOutputComponent extends AbstractTreeOutputComponent<AbstractOutpu
>
{this.chooseChart()}
</div> :
<div className='analysis-running'>
<div
id={this.props.traceId + this.props.outputDescriptor.id + 'focusContainer'}
className='analysis-running'
>
<i className='fa fa-refresh fa-spin' style={{ marginRight: '5px' }} />
<span>Analysis running</span>
</div>
Expand Down Expand Up @@ -374,6 +380,14 @@ export class XYOutputComponent extends AbstractTreeOutputComponent<AbstractOutpu
});
}

setFocus(): void {
if (document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')) {
document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')?.focus();
} else {
document.getElementById(this.props.traceId + this.props.outputDescriptor.id)?.focus();
}
}

private afterChartDraw(chart: Chart) {
const ctx = chart.ctx;
if (ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,7 @@ export class ReactAvailableViewsWidget extends React.Component<ReactAvailableVie
const outputs = this.state.availableOutputDescriptors;

if (outputs && this._selectedExperiment) {
signalManager().fireExperimentSelectedSignal(this._selectedExperiment);
signalManager().fireOutputAddedSignal(new OutputAddedSignalPayload(outputs[index], this._selectedExperiment));

}
}

Expand Down
9 changes: 8 additions & 1 deletion packages/react-components/style/output-components-style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
/* Main container*/
.output-container {
display: flex;
position: relative;
color: var(--theia-ui-font-color0)
}

.output-container:focus {
outline: none;
}

.widget-handle {
background-color: var(--theia-layout-color4);
display: grid;
Expand Down Expand Up @@ -38,6 +43,8 @@
.main-output-container {
display: flex;
overflow: hidden;
position: relative;
z-index: -1;
}

.output-component-tree {
Expand Down Expand Up @@ -67,7 +74,7 @@
display: flex;
}

#xy-main {
.xy-main {
width: 100%;
display: flex;
}
Expand Down
3 changes: 2 additions & 1 deletion theia-extensions/viewer-prototype/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"@theia/filesystem": "1.19.0",
"traceviewer-base": "0.1.0",
"traceviewer-react-components": "0.1.0",
"tree-kill": "latest"
"tree-kill": "latest",
"animate.css": "^4.1.1"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^3.4.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { DisposableCollection, MessageService, Path } from '@theia/core';
import { ApplicationShell, Message, StatusBar, WidgetManager } from '@theia/core/lib/browser';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor';
import { Trace } from 'tsp-typescript-client/lib/models/trace';
import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client';
Expand All @@ -20,6 +19,8 @@ import { TraceExplorerContribution } from '../trace-explorer/trace-explorer-cont
import { MarkerSet } from 'tsp-typescript-client/lib/models/markerset';
import { BackendFileService } from '../../common/backend-file-service';
import { CancellationTokenSource } from '@theia/core';
import * as React from 'react';
import 'animate.css';

export const TraceViewerWidgetOptions = Symbol('TraceViewerWidgetOptions');
export interface TraceViewerWidgetOptions {
Expand Down Expand Up @@ -316,7 +317,22 @@ export class TraceViewerWidget extends ReactWidget {
await this.fetchAnnotationCategories(output);
this.update();
} else {
document.getElementById(exist.id)?.scrollIntoView();
const traceId = this.openedExperiment.UUID;
if (document.getElementById(traceId + exist.id + 'focusContainer')) {
document.getElementById(traceId + exist.id + 'focusContainer')?.focus();
} else {
document.getElementById(traceId + exist.id)?.focus();
}

await new Promise(resolve => {
const titleHandle = document.getElementById(traceId + exist.id + 'handle');
titleHandle?.classList.add('animate__animated', 'animate__pulse');
titleHandle?.addEventListener('animationend', event => {
event.stopPropagation();
titleHandle?.classList.remove('animate__animated', 'animate__pulse');
resolve('Animation ended');
}, {once: true});
});
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4556,6 +4556,11 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=

animate.css@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/animate.css/-/animate.css-4.1.1.tgz#614ec5a81131d7e4dc362a58143f7406abd68075"
integrity sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==

anser@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/anser/-/anser-2.1.0.tgz#a7309c9f29886f19af56cb30c79fc60ea483944e"
Expand Down

0 comments on commit de7fd50

Please sign in to comment.