Scroll selected item into view

This commit is contained in:
Asger F 2022-10-19 15:14:08 +02:00
Родитель 45b6288363
Коммит 0e3679d186
4 изменённых файлов: 92 добавлений и 13 удалений

Просмотреть файл

@ -2,6 +2,7 @@ import * as React from 'react';
import { ResultRow } from '../../pure/bqrs-cli-types';
import { selectedRowClassName, zebraStripe } from './result-table-utils';
import RawTableValue from './RawTableValue';
import { ScrollIntoViewHelper } from './scroll-into-view-helper';
interface Props {
rowIndex: number;
@ -10,6 +11,7 @@ interface Props {
className?: string;
selectedColumn?: number;
onSelected?: (row: number, column: number) => void;
scroller?: ScrollIntoViewHelper;
}
export default function RawTableRow(props: Props) {
@ -17,15 +19,18 @@ export default function RawTableRow(props: Props) {
<tr key={props.rowIndex} {...zebraStripe(props.rowIndex, props.className || '')}>
<td key={-1}>{props.rowIndex + 1}</td>
{props.row.map((value, columnIndex) => (
<td key={columnIndex} {...props.selectedColumn === columnIndex ? { className: selectedRowClassName } : {}}>
<RawTableValue
value={value}
databaseUri={props.databaseUri}
onSelected={() => props.onSelected?.(props.rowIndex, columnIndex)}
/>
</td>
))}
{props.row.map((value, columnIndex) => {
const isSelected = props.selectedColumn === columnIndex;
return (
<td ref={props.scroller?.ref(isSelected)} key={columnIndex} {...isSelected ? { className: selectedRowClassName } : {}}>
<RawTableValue
value={value}
databaseUri={props.databaseUri}
onSelected={() => props.onSelected?.(props.rowIndex, columnIndex)}
/>
</td>
);
})}
</tr>
);
}

Просмотреть файл

@ -14,6 +14,7 @@ import {
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../../pure/interface-types';
import { vscode } from '../vscode-api';
import { isWholeFileLoc, isLineColumnLoc } from '../../pure/bqrs-utils';
import { ScrollIntoViewHelper } from './scroll-into-view-helper';
export type PathTableProps = ResultTableProps & { resultSet: InterpretedResultSet<SarifInterpretationData> };
export interface PathTableState {
@ -22,6 +23,8 @@ export interface PathTableState {
}
export class PathTable extends React.Component<PathTableProps, PathTableState> {
private scroller = new ScrollIntoViewHelper();
constructor(props: PathTableProps) {
super(props);
this.state = { expanded: new Set<string>(), selectedItem: undefined };
@ -211,7 +214,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
if (result.codeFlows === undefined) {
rows.push(
<tr key={resultIndex} {...selectableZebraStripe(resultRowIsSelected, resultIndex)}>
<tr ref={this.scroller.ref(resultRowIsSelected)} key={resultIndex} {...selectableZebraStripe(resultRowIsSelected, resultIndex)}>
<td className="vscode-codeql__icon-cell">{octicons.info}</td>
<td colSpan={3}>{msg}</td>
{locationCells}
@ -227,7 +230,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
[resultKey];
rows.push(
<tr {...selectableZebraStripe(resultRowIsSelected, resultIndex)} key={resultIndex}>
<tr ref={this.scroller.ref(resultRowIsSelected)} {...selectableZebraStripe(resultRowIsSelected, resultIndex)} key={resultIndex}>
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler(indices)}>
{indicator}
</td>
@ -248,7 +251,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
const isPathSpecificallySelected = Keys.equalsNotUndefined(pathKey, selectedItem);
rows.push(
<tr {...selectableZebraStripe(isPathSpecificallySelected, resultIndex)} key={`${resultIndex}-${pathIndex}`}>
<tr ref={this.scroller.ref(isPathSpecificallySelected)} {...selectableZebraStripe(isPathSpecificallySelected, resultIndex)} key={`${resultIndex}-${pathIndex}`}>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler([pathKey])}>{indicator}</td>
<td className="vscode-codeql__text-center" colSpan={3}>
@ -273,7 +276,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
const zebraIndex = resultIndex + stepIndex;
rows.push(
<tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined} key={`${resultIndex}-${pathIndex}-${pathNodeIndex}`}>
<tr ref={this.scroller.ref(isSelected)} className={isSelected ? 'vscode-codeql__selected-path-node' : undefined} key={`${resultIndex}-${pathIndex}-${pathNodeIndex}`}>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__path-index-cell')}>{stepIndex}</td>
@ -349,6 +352,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
expanded.delete(Keys.keyToString(prevState.selectedItem));
}
}
this.scroller.scrollIntoViewOnNextUpdate();
return {
...prevState,
expanded,
@ -393,7 +397,12 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
}
}
componentDidUpdate() {
this.scroller.update();
}
componentDidMount() {
this.scroller.update();
onNavigation.addListener(this.handleNavigationEvent);
}

Просмотреть файл

@ -7,6 +7,7 @@ import RawTableRow from './RawTableRow';
import { ResultRow } from '../../pure/bqrs-cli-types';
import { onNavigation } from './results';
import { tryGetResolvableLocation } from '../../pure/bqrs-utils';
import { ScrollIntoViewHelper } from './scroll-into-view-helper';
export type RawTableProps = ResultTableProps & {
resultSet: RawTableResultSet;
@ -19,6 +20,8 @@ interface RawTableState {
}
export class RawTable extends React.Component<RawTableProps, RawTableState> {
private scroller = new ScrollIntoViewHelper();
constructor(props: RawTableProps) {
super(props);
this.setSelection = this.setSelection.bind(this);
@ -55,6 +58,7 @@ export class RawTable extends React.Component<RawTableProps, RawTableState> {
databaseUri={databaseUri}
selectedColumn={this.state.selectedItem?.row === rowIndex ? this.state.selectedItem?.column : undefined}
onSelected={this.setSelection}
scroller={this.scroller}
/>
);
@ -127,6 +131,7 @@ export class RawTable extends React.Component<RawTableProps, RawTableState> {
jumpToLocation(location, this.props.databaseUri);
}
}
this.scroller.scrollIntoViewOnNextUpdate();
return {
...prevState,
selectedItem: { row: nextRow, column: nextColumn }
@ -134,7 +139,12 @@ export class RawTable extends React.Component<RawTableProps, RawTableState> {
});
}
componentDidUpdate() {
this.scroller.update();
}
componentDidMount() {
this.scroller.update();
onNavigation.addListener(this.handleNavigationEvent);
}

Просмотреть файл

@ -0,0 +1,55 @@
import * as React from 'react';
/**
* Some book-keeping needed to scroll a specific HTML element into view in a React component.
*/
export class ScrollIntoViewHelper {
private selectedElementRef = React.createRef<HTMLElement | any>(); // need 'any' to work around typing bug in React
private shouldScrollIntoView = true;
/**
* If `isSelected` is true, gets the `ref={}` attribute to use for an element that we might want to scroll into view.
*/
public ref(isSelected: boolean) {
return isSelected ? this.selectedElementRef : undefined;
}
/**
* Causes the element whose `ref={}` was set to be scrolled into view after the next render.
*/
public scrollIntoViewOnNextUpdate() {
this.shouldScrollIntoView = true;
}
/**
* Should be called from `componentDidUpdate` and `componentDidMount`.
*
* Scrolls the component into view if requested.
*/
public update() {
if (!this.shouldScrollIntoView) {
return;
}
this.shouldScrollIntoView = false;
const element = this.selectedElementRef.current as HTMLElement | null;
if (element == null) {
return;
}
const rect = element.getBoundingClientRect();
// The selected item's bounding box might be on screen, but hidden underneath the sticky header
// which overlaps the table view. As a workaround we hardcode a fixed distance from the top which
// we consider to be obscured. It does not have to exact, as it's just a threshold for when to scroll.
const heightOfStickyHeader = 30;
if (rect.top < heightOfStickyHeader || rect.bottom > window.innerHeight) {
element.scrollIntoView({
block: 'center', // vertically align to center
});
}
if (rect.left < 0 || rect.right > window.innerWidth) {
element.scrollIntoView({
block: 'nearest',
inline: 'nearest', // horizontally align as little as possible
});
}
}
}