Scroll selected item into view
This commit is contained in:
Родитель
45b6288363
Коммит
0e3679d186
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче