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

Commit 94d6cbf

Browse files
texhnolyzebryphe
authored andcommitted
Enable use of middle click to close tabs (#2190)
* Add middle click tab closing (#2069) by adding a onMouseDown directive to both tab file icon and tab name and checking the resulting React.MouseEvent for a middle click * Fix unused imports and whitespace in Tabs ui test (#2069) * Update Tabs component test snapshot (#2069) * Fix Tabs ui test to correctly retrieve children (#2069) * Differentiate between single und multiple tabs in test (#2069) * Use mousedown event for tab selection and closing (#2069) by middle clicking and let the tab itself handle the logic for it by checking React.MouseEvent.button value * Update classnames dependency to lastest master branch and write own @types module declaration so that it can be used in testing nstead of using a mock. The mock does not suffice as the way the actual module previously had to be imported was "import * as classNames" where classNames was the actual function. It is not possible to build a module in typescript/es6 imports, which will directly return a function in the same way the dependency did in commonjs module syntax. Instead when defining a function to be returned as default export it is returned as an Object like this "{ default: [Function] }", which is correctly resolved when importing with "import classNames from 'classnames'". This only previously worked in production as webpacks ts-loader, handles this issue, whereas when testing the sources are only compiled with tsc. There is an update to the classnames dependency on the current master, but there hasn't been a release since 2006. So options were to setup webpack for tests as well or add updated classnames dependency which sets a "default" value on its commonjs exports. Links for reference: JedWatson/classnames#152 JedWatson/classnames#106 DefinitelyTyped/DefinitelyTyped#25206 microsoft/TypeScript#2719 * Fix tab click onMouseDown callback * Test tab clicks to select/close trigger callbacks * Mock child react components directly to test smaller unit * Reset calls to callback mocks in each test case * Add tests for tabs interaction with FileIcon/Sneakable
1 parent 0ce4fb1 commit 94d6cbf

File tree

11 files changed

+249
-85
lines changed

11 files changed

+249
-85
lines changed

@types/classnames/index.d.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Custom type definitions for classnames master branch
2+
// Project: https://github.com/JedWatson/classnames
3+
4+
declare module "classnames" {
5+
type ClassValue = string | number | ClassDictionary | ClassArray | undefined | null | false
6+
7+
interface ClassDictionary {
8+
[id: string]: boolean | undefined | null
9+
}
10+
11+
// This is the only way I found to break circular references between ClassArray and ClassValue
12+
// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
13+
interface ClassArray extends Array<ClassValue> {} // tslint:disable-line no-empty-interface
14+
15+
type ClassNamesFn = (...classes: ClassValue[]) => string
16+
17+
const classNames: ClassNamesFn
18+
19+
export default classNames
20+
}

browser/src/UI/components/Tabs.tsx

+36-34
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as path from "path"
77
import * as React from "react"
88
import { connect } from "react-redux"
99

10-
import * as classNames from "classnames"
10+
import classNames from "classnames"
1111
import { keyframes } from "styled-components"
1212

1313
import * as BufferSelectors from "./../../Editor/NeovimEditor/NeovimEditorSelectors"
@@ -40,8 +40,8 @@ export interface ITabContainerProps {
4040
}
4141

4242
export interface ITabsProps {
43-
onSelect?: (id: number) => void
44-
onClose?: (id: number) => void
43+
onTabSelect?: (id: number) => void
44+
onTabClose?: (id: number) => void
4545

4646
visible: boolean
4747
tabs: ITabProps[]
@@ -65,16 +65,6 @@ const InnerName = styled.span`
6565
`
6666

6767
export class Tabs extends React.PureComponent<ITabsProps, {}> {
68-
private _boundOnSelect: (tabId: number) => void
69-
private _boundOnClose: (tabId: number) => void
70-
71-
constructor(props: ITabsProps) {
72-
super(props)
73-
74-
this._boundOnSelect = (id: number) => this._onSelect(id)
75-
this._boundOnClose = (id: number) => this._onClickClose(id)
76-
}
77-
7868
public render(): JSX.Element {
7969
if (!this.props.visible) {
8070
return null
@@ -98,8 +88,8 @@ export class Tabs extends React.PureComponent<ITabsProps, {}> {
9888
<Tab
9989
key={t.id}
10090
{...t}
101-
onClickName={this._boundOnSelect}
102-
onClickClose={this._boundOnClose}
91+
onSelect={this.props.onTabSelect}
92+
onClose={this.props.onTabClose}
10393
backgroundColor={this.props.backgroundColor}
10494
foregroundColor={this.props.foregroundColor}
10595
height={this.props.height}
@@ -114,19 +104,11 @@ export class Tabs extends React.PureComponent<ITabsProps, {}> {
114104
</div>
115105
)
116106
}
117-
118-
private _onSelect(id: number): void {
119-
this.props.onSelect(id)
120-
}
121-
122-
private _onClickClose(id: number): void {
123-
this.props.onClose(id)
124-
}
125107
}
126108

127109
export interface ITabPropsWithClick extends ITabProps {
128-
onClickName: (id: number) => void
129-
onClickClose: (id: number) => void
110+
onSelect: (id: number) => void
111+
onClose: (id: number) => void
130112

131113
backgroundColor: string
132114
foregroundColor: string
@@ -175,28 +157,28 @@ export class Tab extends React.PureComponent<ITabPropsWithClick> {
175157
borderTop: "2px solid " + this.props.highlightColor,
176158
}
177159

160+
const handleTitleClick = this._handleTitleClick.bind(this)
161+
const handleCloseButtonClick = this._handleCloseButtonClick.bind(this)
162+
178163
return (
179-
<Sneakable callback={() => this.props.onClickName(this.props.id)} tag={this.props.name}>
164+
<Sneakable callback={() => this.props.onSelect(this.props.id)} tag={this.props.name}>
180165
<TabWrapper
181166
innerRef={(e: IChromeDivElement) => (this._tab = e)}
182167
className={cssClasses}
183168
title={this.props.description}
184169
style={style}
185170
>
186-
<div className="corner" onClick={() => this.props.onClickName(this.props.id)}>
171+
<div className="corner" onMouseDown={handleTitleClick}>
187172
<FileIcon
188173
fileName={this.props.iconFileName}
189174
isLarge={true}
190175
playAppearAnimation={true}
191176
/>
192177
</div>
193-
<div className="name" onClick={() => this.props.onClickName(this.props.id)}>
178+
<div className="name" onMouseDown={handleTitleClick}>
194179
<InnerName>{this.props.name}</InnerName>
195180
</div>
196-
<div
197-
className="corner enable-hover"
198-
onClick={() => this.props.onClickClose(this.props.id)}
199-
>
181+
<div className="corner enable-hover" onClick={handleCloseButtonClick}>
200182
<div className="icon-container x-icon-container">
201183
<Icon name="times" />
202184
</div>
@@ -220,6 +202,26 @@ export class Tab extends React.PureComponent<ITabPropsWithClick> {
220202
}
221203
}
222204
}
205+
206+
private _handleTitleClick(event: React.MouseEvent<HTMLElement>): void {
207+
if (this._isLeftClick(event)) {
208+
this.props.onSelect(this.props.id)
209+
} else if (this._isMiddleClick(event)) {
210+
this.props.onClose(this.props.id)
211+
}
212+
}
213+
214+
private _handleCloseButtonClick(): void {
215+
this.props.onClose(this.props.id)
216+
}
217+
218+
private _isMiddleClick(event: React.MouseEvent<HTMLElement>): boolean {
219+
return event.button === 1
220+
}
221+
222+
private _isLeftClick(event: React.MouseEvent<HTMLElement>): boolean {
223+
return event.button === 0
224+
}
223225
}
224226

225227
export const getTabName = (name: string, isDuplicate?: boolean): string => {
@@ -365,8 +367,8 @@ const mapStateToProps = (state: State.IState, ownProps: ITabContainerProps): ITa
365367
fontSize: addDefaultUnitIfNeeded(state.configuration["ui.fontSize"]),
366368
backgroundColor: state.colors["tabs.background"],
367369
foregroundColor: state.colors["tabs.foreground"],
368-
onSelect: selectFunc,
369-
onClose: closeFunc,
370+
onTabSelect: selectFunc,
371+
onTabClose: closeFunc,
370372
height,
371373
maxWidth,
372374
shouldWrap,

browser/tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@
2626
"sourceMap": true,
2727
"types": ["mocha", "webgl2"]
2828
},
29-
"include": ["src/**/*.ts", "test/**/*.ts"],
29+
"include": ["../@types/**/*.d.ts", "src/**/*.ts", "test/**/*.ts"],
3030
"exclude": ["node_modules"]
3131
}

browser/tsconfig.test.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@
2222
"sourceMap": true,
2323
"types": ["mocha", "webgl2"]
2424
},
25-
"include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"],
25+
"include": ["../@types/**/*.d.ts", "src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"],
2626
"exclude": ["node_modules"]
2727
}

jest.config.js

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ module.exports = {
1010
PersistentSettings: "<rootDir>/ui-tests/mocks/PersistentSettings.ts",
1111
Utility: "<rootDir>/ui-tests/mocks/Utility.ts",
1212
Configuration: "<rootDir>/ui-tests/mocks/Configuration.ts",
13-
classnames: "<rootDir>/ui-tests/mocks/classnames.ts",
1413
KeyboardLayout: "<rootDir>/ui-tests/mocks/keyboardLayout.ts",
1514
},
1615
snapshotSerializers: ["enzyme-to-json/serializer"],

package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,6 @@
720720
},
721721
"devDependencies": {
722722
"@types/chokidar": "^1.7.5",
723-
"@types/classnames": "0.0.32",
724723
"@types/color": "2.0.0",
725724
"@types/detect-indent": "^5.0.0",
726725
"@types/dompurify": "^0.0.31",
@@ -759,7 +758,7 @@
759758
"babel-minify-webpack-plugin": "^0.3.1",
760759
"babel-plugin-dynamic-import-node": "^1.2.0",
761760
"bs-platform": "2.1.0",
762-
"classnames": "2.2.5",
761+
"classnames": "JedWatson/classnames",
763762
"codecov": "^3.0.0",
764763
"color": "2.0.0",
765764
"concurrently": "3.1.0",

0 commit comments

Comments
 (0)