Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit f5a3dd0

Browse files
committed
fix(plugins/plugin-client-common): improved focus behavior of terminals
1) avoid spurious scrolling: when clicking on a table entry while the active prompt is not in view 2) ibid: when switching tabs while the active prompt is not in view this PR removes the old hackFocus() hack in TabContainer, which did ugly querySelectors Fixes #5615
1 parent 74c6498 commit f5a3dd0

File tree

6 files changed

+61
-52
lines changed

6 files changed

+61
-52
lines changed

plugins/plugin-client-common/src/components/Client/TabContainer.tsx

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -69,28 +69,6 @@ export default class TabContainer extends React.PureComponent<Props, State> {
6969
})
7070
}
7171

72-
/**
73-
* Temporary hack to regrab focus to the repl. The hack part is the
74-
* querySelector. This really needs to be done in TabContent, which
75-
* owns the Tab impl.
76-
*
77-
*/
78-
private hackFocus() {
79-
setTimeout(() => {
80-
try {
81-
const selector = `.kui--tab-content.visible .repl-active[data-is-focused] input`
82-
const selector2 = `.kui--tab-content.visible .repl-active input`
83-
const input =
84-
(document.querySelector(selector) as HTMLElement) || (document.querySelector(selector2) as HTMLElement)
85-
if (input) {
86-
input.focus()
87-
}
88-
} catch (err) {
89-
console.error(err)
90-
}
91-
})
92-
}
93-
9472
/** save tab state such as CWD prior to a tab switch */
9573
private captureState() {
9674
try {
@@ -118,8 +96,6 @@ export default class TabContainer extends React.PureComponent<Props, State> {
11896
this.setState({
11997
activeIdx: idx
12098
})
121-
122-
this.hackFocus()
12399
}
124100

125101
setTimeout(() => eventBus.emit('/tab/switch/request/done', idx))
@@ -142,8 +118,6 @@ export default class TabContainer extends React.PureComponent<Props, State> {
142118
tabs: residualTabs,
143119
activeIdx
144120
})
145-
146-
this.hackFocus()
147121
}
148122
}
149123

plugins/plugin-client-common/src/components/Client/TabContent.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ type State = Partial<WithTab> & {
6666
sidecarHasContent: boolean
6767

6868
activeView: CurrentlyShowing
69+
70+
/** grab a ref (below) so that we can maintain focus */
71+
_terminal: ScrollableTerminal
6972
}
7073

7174
/**
@@ -88,9 +91,6 @@ export default class TabContent extends React.PureComponent<Props, State> {
8891
/** switching back or away from this tab */
8992
private activateHandlers: ((isActive: boolean) => void)[] = []
9093

91-
/** grab a ref (below) so that we can maintain focus */
92-
private _terminal: ScrollableTerminal
93-
9494
public constructor(props: Props) {
9595
super(props)
9696

@@ -101,7 +101,8 @@ export default class TabContent extends React.PureComponent<Props, State> {
101101
sidecarWidth: Width.Closed,
102102
priorSidecarWidth: Width.Closed,
103103
sidecarHasContent: false,
104-
activeView: 'TerminalOnly'
104+
activeView: 'TerminalOnly',
105+
_terminal: undefined
105106
}
106107
}
107108

@@ -177,6 +178,9 @@ export default class TabContent extends React.PureComponent<Props, State> {
177178
console.error(err)
178179
}
179180
} else {
181+
if (props.active && state._terminal) {
182+
state._terminal.doFocus()
183+
}
180184
return state
181185
}
182186
}
@@ -225,9 +229,9 @@ export default class TabContent extends React.PureComponent<Props, State> {
225229
// eslint-disable-next-line react/no-direct-mutation-state
226230
this.state.tab.state.desiredStatusStripeDecoration = { type: 'default' }
227231
}}
228-
ref={c => {
232+
ref={_terminal => {
229233
// so that we can refocus/blur
230-
this._terminal = c
234+
this.setState({ _terminal })
231235
}}
232236
>
233237
{this.children()}
@@ -284,8 +288,8 @@ export default class TabContent extends React.PureComponent<Props, State> {
284288
}
285289

286290
private onWillLoseFocus() {
287-
if (this._terminal) {
288-
this._terminal.doFocus()
291+
if (this.state._terminal) {
292+
this.state._terminal.doFocus()
289293
}
290294
}
291295

plugins/plugin-client-common/src/components/Views/Terminal/Block/Input.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Actions from './Actions'
2323
import onPaste from './OnPaste'
2424
import onKeyDown from './OnKeyDown'
2525
import onKeyPress from './OnKeyPress'
26+
import isInViewport from '../visible'
2627
import KuiContext from '../../../Client/context'
2728
import { TabCompletionState } from './TabCompletion'
2829
import ActiveISearch, { onKeyUp } from './ActiveISearch'
@@ -381,27 +382,37 @@ export default class Input extends InputProvider {
381382
if (c && !this.state.prompt) {
382383
c.value = hasValue(this.props.model) ? this.props.model.value : ''
383384
this.setState({ prompt: c })
384-
} else if (c && this.props.isFocused) {
385+
} else if (c && this.props.isFocused && isInViewport(c)) {
385386
c.focus()
386387
}
387388
}
388389

389390
private readonly _onRef = this.onRef.bind(this)
390391

392+
/** This is the onFocus property of the active prompt */
393+
private readonly _onFocus = (evt: React.FocusEvent<HTMLInputElement>) => {
394+
this.props.onInputFocus && this.props.onInputFocus(evt)
395+
this.props.willFocusBlock(evt)
396+
}
397+
391398
/** the element that represents the command being/having been/going to be executed */
392399
protected input() {
393400
const active = isActive(this.props.model)
394401

395402
if (active) {
396-
if (this.props.isFocused && this.state.prompt) {
397-
this.state.prompt.focus()
403+
if (this.props.isFocused && this.state.prompt && document.activeElement !== this.state.prompt) {
404+
setTimeout(() => {
405+
if (isInViewport(this.state.prompt)) {
406+
this.state.prompt.focus()
407+
}
408+
})
398409
}
399410

400411
return (
401412
<React.Fragment>
402413
<input
403414
type="text"
404-
autoFocus={this.props.isFocused}
415+
autoFocus={this.props.isFocused && isInViewport(this.props._block)}
405416
autoCorrect="off"
406417
autoComplete="off"
407418
spellCheck="false"
@@ -413,10 +424,7 @@ export default class Input extends InputProvider {
413424
tabIndex={1}
414425
placeholder={this.props.promptPlaceholder}
415426
onBlur={this.props.onInputBlur}
416-
onFocus={evt => {
417-
this.props.onInputFocus && this.props.onInputFocus(evt)
418-
this.props.willFocusBlock(evt)
419-
}}
427+
onFocus={this._onFocus}
420428
onMouseDown={this.props.onInputMouseDown}
421429
onMouseMove={this.props.onInputMouseMove}
422430
onChange={this.props.onInputChange}

plugins/plugin-client-common/src/components/Views/Terminal/Block/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export default class Block extends React.PureComponent<Props, State> {
118118
if (isFinished(this.props.model) || isProcessing(this.props.model)) {
119119
return (
120120
<Output
121+
key={`Output-${this.props.idx}`}
121122
uuid={this.props.uuid}
122123
tab={this.props.tab}
123124
idx={this.props.idx}

plugins/plugin-client-common/src/components/Views/Terminal/ScrollableTerminal.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@ import {
5353
isOk,
5454
isOutputOnly,
5555
isProcessing,
56+
isPresentedElsewhere,
5657
hasStartEvent,
5758
hasCommand,
5859
snapshot,
5960
hasUUID,
6061
BlockModel
6162
} from './Block/BlockModel'
6263

64+
import isInViewport from './visible'
6365
import '../../../../web/scss/components/Terminal/_index.scss'
6466

6567
const strings = i18n('plugin-client-common')
@@ -136,13 +138,6 @@ interface State {
136138
splits: ScrollbackState[]
137139
}
138140

139-
/** Is the given `elm` on visible in the current viewport? */
140-
function isInViewport(elm: HTMLElement) {
141-
const rect = elm.getBoundingClientRect()
142-
const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight)
143-
return !(rect.bottom < 0 || rect.top - viewHeight >= 0)
144-
}
145-
146141
/** get the selected texts in window */
147142
function getSelectionText() {
148143
let text = ''
@@ -392,8 +387,13 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
392387
}
393388

394389
/** Output.tsx finished rendering something */
395-
private onOutputRender(scrollback: ScrollbackState) {
396-
setTimeout(() => scrollback.facade.scrollToBottom())
390+
private onOutputRender(scrollback: ScrollbackState, idx) {
391+
const block = scrollback.blocks[idx]
392+
if (isOk(block) && (isTabLayoutModificationResponse(block.response) || isPresentedElsewhere(block))) {
393+
// no scroll to bottom for these responses
394+
} else {
395+
setTimeout(() => scrollback.facade.scrollToBottom())
396+
}
397397
}
398398

399399
/** the REPL started executing a command */
@@ -965,7 +965,7 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
965965
uuid={scrollback.uuid}
966966
tab={tab}
967967
noActiveInput={this.props.noActiveInput || isOfflineClient()}
968-
onOutputRender={this.onOutputRender.bind(this, scrollback)}
968+
onOutputRender={this.onOutputRender.bind(this, scrollback, idx)}
969969
willInsertBlock={this.willInsertBlock.bind(this, scrollback.uuid, idx)}
970970
willRemove={this.willRemoveBlock.bind(this, scrollback.uuid, idx)}
971971
hasBlockAfter={this.hasBlockAfter(scrollback.blocks, idx)}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2020 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/** Is the given `elm` on visible in the current viewport? */
18+
export default function isInViewport(elm: HTMLElement) {
19+
const rect = elm.getBoundingClientRect()
20+
const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight)
21+
return !(rect.bottom < 0 || rect.top - viewHeight >= 0)
22+
}

0 commit comments

Comments
 (0)