diff --git a/browser/src/Services/DragAndDrop.tsx b/browser/src/Services/DragAndDrop.tsx index 915643d4dc..baa67a6cfc 100644 --- a/browser/src/Services/DragAndDrop.tsx +++ b/browser/src/Services/DragAndDrop.tsx @@ -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

} A react class component diff --git a/browser/src/Services/Explorer/ExplorerSplit.tsx b/browser/src/Services/Explorer/ExplorerSplit.tsx index 8bc8fb9122..919c2e46c6 100644 --- a/browser/src/Services/Explorer/ExplorerSplit.tsx +++ b/browser/src/Services/Explorer/ExplorerSplit.tsx @@ -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)} /> ) diff --git a/browser/src/Services/Explorer/ExplorerView.tsx b/browser/src/Services/Explorer/ExplorerView.tsx index 31addceef1..2c7a1bb55b 100644 --- a/browser/src/Services/Explorer/ExplorerView.tsx +++ b/browser/src/Services/Explorer/ExplorerView.tsx @@ -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" @@ -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) => { if (e) { @@ -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; @@ -94,12 +93,6 @@ interface ITransitionProps { updated: boolean } -const Transition = ({ children, updated }: ITransitionProps) => ( - - {children} - -) - const renameStyles = css` width: 100%; background-color: inherit; @@ -116,7 +109,13 @@ const createStyles = css` margin-top: 0.2em; ` -export class NodeView extends React.PureComponent { +const Transition = ({ children, updated }: ITransitionProps) => ( + + {children} + +) + +export class NodeView extends React.PureComponent { public moveFileOrFolder = ({ drag, drop }: IMoveNode) => { this.props.moveFileOrFolder(drag.node, drop.node) } @@ -125,15 +124,12 @@ export class NodeView extends React.PureComponent { 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 ( - + {renameInProgress ? ( { 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) @@ -270,13 +266,67 @@ 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) => ( + props.onClick(node.id)}> + props.onClick(node.id)} + /> + +) -export class ExplorerView extends React.PureComponent { - public render(): JSX.Element { +const ExplorerContainer = styled.div` + height: 100%; + ${enableMouse}; +` + +export class ExplorerView extends React.PureComponent { + private _list = React.createRef() + + 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, 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 ( @@ -284,43 +334,57 @@ export class ExplorerView extends React.PureComponent { active={this.props.isActive} contentsText="Nothing to show here, yet!" actionButtonText="Open a Folder" - onClickButton={() => commandManager.executeCommand("workspace.openFolder")} + onClickButton={this.openWorkspaceFolder} /> ) } return ( - + this.props.onClick(id)} - render={(selectedId: string) => { - const nodes = this.props.nodes.map(node => ( - this.props.onClick(node.id)} key={node.id}> - this.props.onClick(node.id)} - /> - - )) - + onSelectionChanged={this.props.onSelectionChanged} + render={selectedId => { return ( -

-
{nodes}
-
+ + + {measurements => ( + ( + +
+ +
+
+ )} + /> + )} +
+
) }} /> @@ -329,6 +393,19 @@ export class ExplorerView extends React.PureComponent { } } +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, @@ -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, } diff --git a/browser/src/Services/Sidebar/SidebarContentSplit.tsx b/browser/src/Services/Sidebar/SidebarContentSplit.tsx index 0171097e79..826b7374df 100644 --- a/browser/src/Services/Sidebar/SidebarContentSplit.tsx +++ b/browser/src/Services/Sidebar/SidebarContentSplit.tsx @@ -127,8 +127,8 @@ export class SidebarHeaderView extends React.PureComponent(styled.div)` flex: 1 1 auto; - overflow-y: auto; position: relative; + height: 100%; ` export class SidebarContentView extends React.PureComponent< diff --git a/browser/src/UI/components/SidebarItemView.tsx b/browser/src/UI/components/SidebarItemView.tsx index 28e80a03d6..1720be2e10 100644 --- a/browser/src/UI/components/SidebarItemView.tsx +++ b/browser/src/UI/components/SidebarItemView.tsx @@ -6,34 +6,53 @@ import * as React from "react" -import { styled, withProps } from "./common" +import { OniStyledProps, pixel, styled, withProps } from "./common" import Caret from "./../../UI/components/Caret" import { Sneakable } from "./../../UI/components/Sneakable" -export interface ISidebarItemViewProps { +interface IItemProps { yanked?: boolean updated?: boolean isOver?: boolean canDrop?: boolean didDrop?: boolean - text: string | JSX.Element isFocused: boolean isContainer?: boolean - indentationLevel: number icon?: JSX.Element + text: string | JSX.Element onClick: (e?: React.MouseEvent) => void } -const px = (num: number): string => num.toString() + "px" +export interface ISidebarItemViewProps extends IItemProps { + indentationLevel: number +} -const SidebarItemStyleWrapper = withProps(styled.div)` - padding-left: ${props => px(INDENT_AMOUNT * props.indentationLevel)}; - border-left: ${props => - props.isFocused - ? `4px solid ${props.theme["highlight.mode.normal.background"]}` - : "4px solid transparent"}; +export interface ISidebarContainerViewProps extends IItemProps { + isExpanded: boolean + indentationLevel?: number +} + +type SidebarStyleProps = OniStyledProps + +const INDENT_AMOUNT = 12 +const getLeftBorder = (props: SidebarStyleProps) => { + switch (true) { + case props.isFocused: + return `4px solid ${props.theme["highlight.mode.normal.background"]}` + case !props.isContainer: + return "4px solid transparent" + case props.isContainer: + return `4px solid rgba(0, 0, 0, 0.2)` + default: + return "" + } +} + +const SidebarItemStyleWrapper = withProps(styled.div)` + padding-left: ${props => pixel(INDENT_AMOUNT * props.indentationLevel)}; + border-left: ${getLeftBorder}; ${p => (p.isOver || p.yanked) && `border: 3px solid ${p.theme["highlight.mode.insert.background"]};`}; @@ -44,7 +63,6 @@ const SidebarItemStyleWrapper = withProps(styled.div)` padding-top: 4px; padding-bottom: 3px; position: relative; - cursor: pointer; pointer-events: all; @@ -63,18 +81,19 @@ const SidebarItemStyleWrapper = withProps(styled.div)` } ` +const getSidebarBackground = (props: SidebarStyleProps) => { + if (props.isFocused && !props.isContainer) { + return props.theme["highlight.mode.normal.background"] + } else if (props.isContainer) { + return "rgb(0, 0, 0)" + } else { + return "transparent" + } +} + const SidebarItemBackground = withProps(styled.div)` - background-color: ${props => { - if (props.isFocused && !props.isContainer) { - return props.theme["highlight.mode.normal.background"] - } else if (props.isContainer) { - return "rgb(0, 0, 0)" - } else { - return "transparent" - } - }}; + background-color: ${getSidebarBackground}; opacity: ${props => (props.isContainer || props.isFocused ? "0.2" : "0")}; - position: absolute; top: 0px; left: 0px; @@ -82,76 +101,38 @@ const SidebarItemBackground = withProps(styled.div)` bottom: 0px; ` -const INDENT_AMOUNT = 12 - -export class SidebarItemView extends React.PureComponent { - public render(): JSX.Element { - const icon = this.props.icon ?
{this.props.icon}
: null - return ( - - - - {icon} -
{this.props.text}
-
-
- ) - } -} - -export interface ISidebarContainerViewProps extends IContainerProps { - yanked?: boolean - updated?: boolean - didDrop?: boolean - text: string - isExpanded: boolean - isFocused: boolean - indentationLevel?: number - isContainer?: boolean - onClick: (e: React.MouseEvent) => void +export const SidebarItemView: React.SFC = props => { + const icon = props.icon ?
{props.icon}
: null + return ( + + + + {icon} +
{props.text}
+
+
+ ) } - -interface IContainerProps { - isOver?: boolean - canDrop?: boolean - yanked?: boolean - updated?: boolean -} - -const SidebarContainer = withProps(styled.div)` - ${p => - (p.isOver || p.yanked) && - `border: 3px solid ${p.theme["highlight.mode.insert.background"]};`}; -` - -export class SidebarContainerView extends React.PureComponent { - public render(): JSX.Element { - const indentationlevel = this.props.indentationLevel || 0 - - return ( - - } - text={this.props.text} - isFocused={this.props.isFocused} - isContainer={this.props.isContainer} - onClick={this.props.onClick} - /> - {this.props.isExpanded ? this.props.children : null} - - ) - } +const SidebarContainer = styled.div`` + +export const SidebarContainerView: React.SFC = ({ + indentationLevel = 0, + ...props +}) => { + return ( + + } + text={props.text} + isFocused={props.isFocused} + isContainer={props.isContainer} + onClick={props.onClick} + /> + {props.isExpanded ? props.children : null} + + ) } diff --git a/browser/src/UI/components/VimNavigator.tsx b/browser/src/UI/components/VimNavigator.tsx index 93c1f3f809..2fe9e1413e 100644 --- a/browser/src/UI/components/VimNavigator.tsx +++ b/browser/src/UI/components/VimNavigator.tsx @@ -18,6 +18,8 @@ import { Event } from "oni-types" import { KeyboardInputView } from "./../../Input/KeyboardInput" import { getInstance, IMenuBinding } from "./../../neovim/SharedNeovimInstance" +import styled from "./../../UI/components/common" + import { CallbackCommand, commandManager } from "./../../Services/CommandManager" export interface IVimNavigatorProps { @@ -42,6 +44,10 @@ export interface IVimNavigatorState { selectedId: string } +const NavigatorContainer = styled.div` + height: 100%; +` + export class VimNavigator extends React.PureComponent { private _activeBinding: IMenuBinding = null private _activateEvent = new Event() @@ -89,9 +95,9 @@ export class VimNavigator extends React.PureComponent -
+ {this.props.render(this.state.selectedId, this.updateSelection)} -
+ {this.props.active ? inputElement : null} ) diff --git a/browser/src/UI/components/common.ts b/browser/src/UI/components/common.ts index 06546eaa1f..6d3e45d13b 100644 --- a/browser/src/UI/components/common.ts +++ b/browser/src/UI/components/common.ts @@ -162,6 +162,9 @@ const fallBackFonts = ` sans-serif `.trim() +export type OniThemeProps = ThemeProps +export type OniStyledProps = OniThemeProps & T + export { css, injectGlobal, diff --git a/browser/src/units-css.d.ts b/browser/src/units-css.d.ts new file mode 100644 index 0000000000..99976aa522 --- /dev/null +++ b/browser/src/units-css.d.ts @@ -0,0 +1 @@ +declare module "units-css" diff --git a/browser/webpack.development.config.js b/browser/webpack.development.config.js index b5ae20fff1..ab0097e965 100644 --- a/browser/webpack.development.config.js +++ b/browser/webpack.development.config.js @@ -1,6 +1,9 @@ var path = require("path") var webpack = require("webpack") +const createStyledComponentsTransformer = require("typescript-plugin-styled-components").default +const styledComponentsTransformer = createStyledComponentsTransformer() + module.exports = { mode: "development", entry: [path.join(__dirname, "src/index.tsx")], @@ -48,7 +51,12 @@ module.exports = { }, { test: /\.tsx?$/, - use: "ts-loader", + use: { + loader: "ts-loader", + options: { + getCustomTransformers: () => ({ before: [styledComponentsTransformer] }), + }, + }, exclude: /node_modules/, }, ], diff --git a/package.json b/package.json index e66300f5b5..c1d59dd758 100644 --- a/package.json +++ b/package.json @@ -919,7 +919,7 @@ "@types/react-redux": "5.0.12", "@types/react-test-renderer": "^16.0.0", "@types/react-transition-group": "^2.0.11", - "@types/react-virtualized": "^9.7.10", + "@types/react-virtualized": "^9.18.3", "@types/redux-batched-subscribe": "^0.1.2", "@types/redux-mock-store": "^1.0.0", "@types/rimraf": "^2.0.2", @@ -980,7 +980,7 @@ "react-redux": "5.0.6", "react-test-renderer": "^16.2.0", "react-transition-group": "2.2.1", - "react-virtualized": "^9.18.0", + "react-virtualized": "^9.19.1", "redux": "3.7.2", "redux-mock-store": "^1.5.3", "redux-observable": "0.17.0", @@ -995,6 +995,7 @@ "ts-jest": "^23.0.0", "ts-loader": "^4.2.0", "tslint": "5.9.1", + "typescript-plugin-styled-components": "^0.0.6", "vscode-snippet-parser": "0.0.5", "wcwidth": "1.0.1", "webdriverio": "4.8.0", diff --git a/test/ci/PaintPerformanceTest.config.js b/test/ci/PaintPerformanceTest.config.js index 7b3ea89085..bcced937a4 100644 --- a/test/ci/PaintPerformanceTest.config.js +++ b/test/ci/PaintPerformanceTest.config.js @@ -1,9 +1,10 @@ // For more information on customizing Oni, // check out our wiki page: // https://github.com/onivim/oni/wiki/Configuration - +// tslint:disable module.exports = { + "sidebar.enabled": false, "statusbar.enabled": false, "tabs.mode": "hidden", "ui.animations.enabled": false, -}; +} diff --git a/ui-tests/NodeView.test.tsx b/ui-tests/NodeView.test.tsx index 53f8967613..4cdf0a3222 100644 --- a/ui-tests/NodeView.test.tsx +++ b/ui-tests/NodeView.test.tsx @@ -1,4 +1,4 @@ -import { mount, shallow } from "enzyme" +import { shallow } from "enzyme" import { shallowToJson } from "enzyme-to-json" import * as React from "react" @@ -20,6 +20,8 @@ describe("", () => { const Node = ( null} + isCreating={false} isRenaming={testNode} moveFileOrFolder={() => ({})} node={testNode} @@ -38,6 +40,8 @@ describe("", () => { const wrapper = shallow( null} + isCreating={false} isRenaming={testNode} moveFileOrFolder={() => ({})} node={testNode} diff --git a/ui-tests/__snapshots__/NodeView.test.tsx.snap b/ui-tests/__snapshots__/NodeView.test.tsx.snap index 186a4e11f7..a593930f55 100644 --- a/ui-tests/__snapshots__/NodeView.test.tsx.snap +++ b/ui-tests/__snapshots__/NodeView.test.tsx.snap @@ -1,14 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` Should match the snapshot 1`] = ` - +