Skip to content
This repository has been archived by the owner on Apr 1, 2020. It is now read-only.

Commit

Permalink
Feature/ switch explorer rendering to use react-virtualized (#2182)
Browse files Browse the repository at this point in the history
* experiment with changing scrollIntoView fn in explorer

* switch to using react virtualized to render explorer

* add react virtualised list and typescript Styled components plugin

* add missing style item to rowrenderer which fixes scrolling

* add key as per docs example

* update snapshot test for explorer

* add ability to dynamically calculate row with inputs

* remove overscan rows

* re-add overscan re-trigger build

* use ui.fontSize for row sizing

* destructure vars (primarily to trigger re-test)

* switch scroll to alignment to end as center causes scroll lag

* add font size in px function switch to cell measurer

* add the definition file for units-css

* update jest mocks

* remove shadowed variable in explorer

* update react-virtualized and its types to fix type errors

* prettier lint fix for performance config

* lint fix

* merge upstream

* fix double border in sidebar item component
also recalculate height when component yanrk status changes

* separate should measure calculation into function
add iss selected check to it

* restructure comparison function to be more readable and work hopefully

* use cache clear and recompute row height for list resizing

* add is or was selected to props checked component did update check

* remove outline from List, separate out should measure

* switch to using _oni in explorer split

* prettier fix

* fix missing space on left edge of container component
add helper for quickly adding types for styled component oni props

* remove unnecessary type annotation

* use pixel function from common in sidebar items

* fix lint error

* remove unnecessary dependency and associated utility
use more thorough method for checking if props changed
simplify sidebar items components

* fix lint error

* remove references to unused fontsize prop

* remove measurement from node entirely
switch to invalidating the cache if yanking, pasting etc occur
  • Loading branch information
akinsho authored Sep 1, 2018
1 parent f5ca695 commit c06df64
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 189 deletions.
2 changes: 1 addition & 1 deletion browser/src/Services/DragAndDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const DragCollect = (connect: DND.DragSourceConnector, monitor: DND.DragSourceMo
*
* @name props
* @function
* @param {String | String[]} >props.target The target Type that responds to the drop
* @param {String | String[]} props.target The target Type that responds to the drop
* @param {Object} DragSource Object with a beginDrag which return the dragged props
* @param {React.Component} A component which is dragged onto another
* @returns {React.Component<P>} A react class component
Expand Down
2 changes: 1 addition & 1 deletion browser/src/Services/Explorer/ExplorerSplit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ export class ExplorerSplit {
onCompleteCreate={this._completeCreation}
onCompleteRename={this._completeRename}
onCancelRename={this._cancelRename}
onSelectionChanged={id => this._onSelectionChanged(id)}
onClick={id => this._onOpenItem(id)}
moveFileOrFolder={this.moveFileOrFolder}
onSelectionChanged={id => this._onSelectionChanged(id)}
/>
</Provider>
)
Expand Down
205 changes: 135 additions & 70 deletions browser/src/Services/Explorer/ExplorerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import * as React from "react"
import * as DND from "react-dnd"
import HTML5Backend from "react-dnd-html5-backend"
import { connect } from "react-redux"
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized"
import { compose } from "redux"

import { CSSTransition, TransitionGroup } from "react-transition-group"

import { css, styled } from "./../../UI/components/common"
import { css, enableMouse, styled } from "./../../UI/components/common"
import { TextInputView } from "./../../UI/components/LightweightText"
import { SidebarEmptyPaneView } from "./../../UI/components/SidebarEmptyPaneView"
import { SidebarContainerView, SidebarItemView } from "./../../UI/components/SidebarItemView"
import { Sneakable } from "./../../UI/components/Sneakable"
import { VimNavigator } from "./../../UI/components/VimNavigator"
import { DragAndDrop, Droppeable } from "./../DragAndDrop"

import { commandManager } from "./../CommandManager"
import { FileIcon } from "./../FileIcon"

import * as ExplorerSelectors from "./ExplorerSelectors"
Expand All @@ -38,20 +41,9 @@ export interface INodeViewProps {
updated?: string[]
isRenaming: Node
isCreating: boolean
children?: React.ReactNode
}

export const NodeWrapper = styled.div`
&:hover {
text-decoration: underline;
}
`

// tslint:disable-next-line
const noop = (elem: HTMLElement) => {}
const scrollIntoViewIfNeeded = (elem: HTMLElement) => {
// tslint:disable-next-line
elem && elem["scrollIntoViewIfNeeded"] && elem["scrollIntoViewIfNeeded"]()
}
const stopPropagation = (fn: () => void) => {
return (e?: React.MouseEvent<HTMLElement>) => {
if (e) {
Expand All @@ -75,6 +67,13 @@ interface IMoveNode {
}
}

export const NodeWrapper = styled.div`
cursor: pointer;
&:hover {
text-decoration: underline;
}
`

const NodeTransitionWrapper = styled.div`
transition: all 400ms 50ms ease-in-out;
Expand All @@ -94,12 +93,6 @@ interface ITransitionProps {
updated: boolean
}

const Transition = ({ children, updated }: ITransitionProps) => (
<CSSTransition in={updated} classNames="move" timeout={1000}>
<NodeTransitionWrapper className={updated && "move"}>{children}</NodeTransitionWrapper>
</CSSTransition>
)

const renameStyles = css`
width: 100%;
background-color: inherit;
Expand All @@ -116,7 +109,13 @@ const createStyles = css`
margin-top: 0.2em;
`

export class NodeView extends React.PureComponent<INodeViewProps, {}> {
const Transition = ({ children, updated }: ITransitionProps) => (
<CSSTransition in={updated} classNames="move" timeout={1000}>
<NodeTransitionWrapper className={updated && "move"}>{children}</NodeTransitionWrapper>
</CSSTransition>
)

export class NodeView extends React.PureComponent<INodeViewProps> {
public moveFileOrFolder = ({ drag, drop }: IMoveNode) => {
this.props.moveFileOrFolder(drag.node, drop.node)
}
Expand All @@ -125,15 +124,12 @@ export class NodeView extends React.PureComponent<INodeViewProps, {}> {
return !(drag.node.name === drop.node.name)
}

public render(): JSX.Element {
public render() {
const { isCreating, isRenaming, isSelected, node } = this.props
const renameInProgress = isRenaming.name === node.name && isSelected && !isCreating
const creationInProgress = isCreating && isSelected && !renameInProgress
return (
<NodeWrapper
style={{ cursor: "pointer" }}
innerRef={this.props.isSelected ? scrollIntoViewIfNeeded : noop}
>
<NodeWrapper>
{renameInProgress ? (
<TextInputView
styles={renameStyles}
Expand All @@ -159,7 +155,7 @@ export class NodeView extends React.PureComponent<INodeViewProps, {}> {
public hasUpdated = (path: string) =>
!!this.props.updated && this.props.updated.some(nodePath => nodePath === path)

public getElement(): JSX.Element {
public getElement() {
const { node } = this.props
const yanked = this.props.yanked.includes(node.id)

Expand Down Expand Up @@ -270,57 +266,125 @@ export interface IExplorerViewProps extends IExplorerViewContainerProps {
idToSelect: string
}

import { SidebarEmptyPaneView } from "./../../UI/components/SidebarEmptyPaneView"
interface ISneakableNode extends IExplorerViewProps {
node: Node
selectedId: string
}

import { commandManager } from "./../CommandManager"
const SneakableNode = ({ node, selectedId, ...props }: ISneakableNode) => (
<Sneakable callback={() => props.onClick(node.id)}>
<NodeView
node={node}
isSelected={node.id === selectedId}
isCreating={props.isCreating}
onCancelCreate={props.onCancelCreate}
onCompleteCreate={props.onCompleteCreate}
onCompleteRename={props.onCompleteRename}
isRenaming={props.isRenaming}
onCancelRename={props.onCancelRename}
updated={props.updated}
yanked={props.yanked}
moveFileOrFolder={props.moveFileOrFolder}
onClick={() => props.onClick(node.id)}
/>
</Sneakable>
)

export class ExplorerView extends React.PureComponent<IExplorerViewProps, {}> {
public render(): JSX.Element {
const ExplorerContainer = styled.div`
height: 100%;
${enableMouse};
`

export class ExplorerView extends React.PureComponent<IExplorerViewProps> {
private _list = React.createRef<List>()

private _cache = new CellMeasurerCache({
defaultHeight: 30,
fixedWidth: true,
})

public openWorkspaceFolder = () => {
commandManager.executeCommand("workspace.openFolder")
}

public getSelectedNode = (selectedId: string) => {
return this.props.nodes.findIndex(n => selectedId === n.id)
}

public propsChanged(keys: Array<keyof IExplorerViewProps>, prevProps: IExplorerViewProps) {
return keys.some(prop => this.props[prop] !== prevProps[prop])
}

public componentDidUpdate(prevProps: IExplorerViewProps) {
if (this.propsChanged(["isCreating", "isRenaming", "yanked"], prevProps)) {
// TODO: if we could determine which nodes actually were involved
// in the change this could potentially be optimised
this._cache.clearAll()
this._list.current.recomputeRowHeights()
}
}

public render() {
const ids = this.props.nodes.map(node => node.id)
const isActive = this.props.isActive && !this.props.isRenaming && !this.props.isCreating

if (!this.props.nodes || !this.props.nodes.length) {
return (
<SidebarEmptyPaneView
active={this.props.isActive}
contentsText="Nothing to show here, yet!"
actionButtonText="Open a Folder"
onClickButton={() => commandManager.executeCommand("workspace.openFolder")}
onClickButton={this.openWorkspaceFolder}
/>
)
}

return (
<TransitionGroup>
<TransitionGroup style={{ height: "100%" }}>
<VimNavigator
ids={ids}
active={this.props.isActive && !this.props.isRenaming && !this.props.isCreating}
active={isActive}
style={{ height: "100%" }}
idToSelect={this.props.idToSelect}
onSelectionChanged={this.props.onSelectionChanged}
onSelected={id => this.props.onClick(id)}
render={(selectedId: string) => {
const nodes = this.props.nodes.map(node => (
<Sneakable callback={() => this.props.onClick(node.id)} key={node.id}>
<NodeView
node={node}
isSelected={node.id === selectedId}
isCreating={this.props.isCreating}
onCancelCreate={this.props.onCancelCreate}
onCompleteCreate={this.props.onCompleteCreate}
onCompleteRename={this.props.onCompleteRename}
isRenaming={this.props.isRenaming}
onCancelRename={this.props.onCancelRename}
updated={this.props.updated}
yanked={this.props.yanked}
moveFileOrFolder={this.props.moveFileOrFolder}
onClick={() => this.props.onClick(node.id)}
/>
</Sneakable>
))

onSelectionChanged={this.props.onSelectionChanged}
render={selectedId => {
return (
<div className="explorer enable-mouse">
<div className="items">{nodes}</div>
</div>
<ExplorerContainer className="explorer">
<AutoSizer>
{measurements => (
<List
{...measurements}
ref={this._list}
scrollToAlignment="end"
style={{ outline: "none" }}
rowCount={this.props.nodes.length}
rowHeight={this._cache.rowHeight}
scrollToIndex={this.getSelectedNode(selectedId)}
rowRenderer={({ index, style, key, parent }) => (
<CellMeasurer
key={key}
cache={this._cache}
columnIndex={0}
parent={parent}
rowIndex={index}
>
<div
style={style}
key={this.props.nodes[index].id}
>
<SneakableNode
{...this.props}
selectedId={selectedId}
node={this.props.nodes[index]}
/>
</div>
</CellMeasurer>
)}
/>
)}
</AutoSizer>
</ExplorerContainer>
)
}}
/>
Expand All @@ -329,6 +393,19 @@ export class ExplorerView extends React.PureComponent<IExplorerViewProps, {}> {
}
}

const getIdToSelect = (fileToSelect: string, nodes: ExplorerSelectors.ExplorerNode[]) => {
// If parent has told us to select a file, attempt to convert the file path into a node ID.
if (fileToSelect) {
const [nodeToSelect] = nodes.filter(node => {
const nodePath = getPathForNode(node)
return nodePath === fileToSelect
})

return nodeToSelect ? nodeToSelect.id : null
}
return null
}

const mapStateToProps = (
state: IExplorerState,
containerProps: IExplorerViewContainerProps,
Expand All @@ -341,25 +418,13 @@ const mapStateToProps = (

const nodes: ExplorerSelectors.ExplorerNode[] = ExplorerSelectors.mapStateToNodeList(state)

let idToSelect: string = null
// If parent has told us to select a file, attempt to convert the file path into a node ID.
if (fileToSelect) {
const [nodeToSelect] = nodes.filter((node: ExplorerSelectors.ExplorerNode) => {
const nodePath: string = getPathForNode(node)
return nodePath === fileToSelect
})
if (nodeToSelect) {
idToSelect = nodeToSelect.id
}
}

return {
...containerProps,
isActive: state.hasFocus,
nodes,
updated,
yanked,
idToSelect,
idToSelect: getIdToSelect(fileToSelect, nodes),
isCreating: state.register.create.active,
isRenaming: rename.active && rename.target,
}
Expand Down
2 changes: 1 addition & 1 deletion browser/src/Services/Sidebar/SidebarContentSplit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ export class SidebarHeaderView extends React.PureComponent<ISidebarHeaderProps,

export const SidebarInnerPaneWrapper = withProps<{}>(styled.div)`
flex: 1 1 auto;
overflow-y: auto;
position: relative;
height: 100%;
`

export class SidebarContentView extends React.PureComponent<
Expand Down
Loading

0 comments on commit c06df64

Please sign in to comment.