Skip to content

Commit 9d98d8f

Browse files
committed
fix(plugins/plugin-client-common): top tab buttons should be owned by the top tab stripe
they had been owned and rendered (with position: absolute) by TabContent. this was ugly. worse, any clients that had their own top stripe would find kui elements rudely interjected. Fixes #4690
1 parent 6aac96a commit 9d98d8f

File tree

5 files changed

+128
-67
lines changed

5 files changed

+128
-67
lines changed

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

+23-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import * as React from 'react'
1818
import { inElectron, Tab, eventBus } from '@kui-shell/core'
1919

20-
import TabModel from './TabModel'
20+
import TabModel, { TopTabButton } from './TabModel'
2121
import TabContent, { TabContentOptions } from './TabContent'
2222
import TopTabStripe, { TopTabStripeConfiguration } from './TopTabStripe'
2323

@@ -202,6 +202,20 @@ export default class TabContainer extends React.PureComponent<Props, State> {
202202
}
203203
}
204204

205+
private willUpdateTopTabButtons(uuid: string, buttons: TopTabButton[]) {
206+
this.setState(curState => {
207+
const idx = curState.tabs.findIndex(_ => _.uuid === uuid)
208+
if (idx >= 0) {
209+
return {
210+
tabs: curState.tabs
211+
.slice(0, idx)
212+
.concat([curState.tabs[idx].update(buttons)])
213+
.concat(curState.tabs.slice(idx + 1))
214+
}
215+
}
216+
})
217+
}
218+
205219
public render() {
206220
return (
207221
<div className="kui--full-height">
@@ -216,7 +230,14 @@ export default class TabContainer extends React.PureComponent<Props, State> {
216230
{this.search()}
217231
<div className="tab-container">
218232
{this.state.tabs.map((_, idx) => (
219-
<TabContent key={idx} uuid={_.uuid} active={idx === this.state.activeIdx} state={_.state} {...this.props}>
233+
<TabContent
234+
key={idx}
235+
uuid={_.uuid}
236+
active={idx === this.state.activeIdx}
237+
willUpdateTopTabButtons={this.willUpdateTopTabButtons.bind(this, _.uuid)}
238+
state={_.state}
239+
{...this.props}
240+
>
220241
{this.children(_.uuid)}
221242
</TabContent>
222243
))}

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

+70-57
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Icons from '../spi/Icons'
2222
import KuiContext from './context'
2323
import Confirm from '../Views/Confirm'
2424
import Loading from '../spi/Loading'
25+
import { TopTabButton } from './TabModel'
2526
import Width from '../Views/Sidecar/width'
2627
import WatchPane, { Height } from '../Views/WatchPane'
2728

@@ -53,6 +54,7 @@ type Props = TabContentOptions &
5354
active: boolean
5455
state: TabState
5556
onTabReady?: (tab: KuiTab) => void
57+
willUpdateTopTabButtons?: (buttons: TopTabButton[]) => void
5658
}
5759

5860
type CurrentlyShowing = 'TerminalOnly' | 'TerminalPlusSidecar' | 'TerminalPlusWatcher' | 'TerminalSidecarWatcher'
@@ -274,7 +276,7 @@ export default class TabContent extends React.PureComponent<Props, State> {
274276
const sidecarWidth = desiredWidth
275277
const watchPaneOpen = curState.primaryHeight === Height.Split
276278

277-
const activeView =
279+
const activeView: CurrentlyShowing =
278280
sidecarWidth === Width.Closed
279281
? watchPaneOpen
280282
? 'TerminalPlusWatcher'
@@ -283,12 +285,15 @@ export default class TabContent extends React.PureComponent<Props, State> {
283285
? 'TerminalSidecarWatcher'
284286
: 'TerminalPlusSidecar'
285287

286-
return {
288+
const newState = {
287289
sidecarHasContent: true,
288290
sidecarWidth,
289291
priorSidecarWidth: curState.sidecarWidth,
290292
activeView
291293
}
294+
295+
this.updateTopTabButtons(newState)
296+
return newState
292297
})
293298
}
294299

@@ -300,10 +305,26 @@ export default class TabContent extends React.PureComponent<Props, State> {
300305
const sidecarWidth = showSidecar ? Width.Split60 : Width.Closed
301306
const primaryHeight = showWatchPane ? Height.Split : Height.NotSplit
302307

303-
return { sidecarWidth, activeView, priorSidecarWidth: curState.sidecarWidth, primaryHeight }
308+
const newState = {
309+
sidecarWidth,
310+
activeView,
311+
priorSidecarWidth: curState.sidecarWidth,
312+
primaryHeight,
313+
sidecarHasContent: curState.sidecarHasContent
314+
}
315+
316+
this.updateTopTabButtons(newState)
317+
return newState
304318
})
305319
}
306320

321+
/** Switch to the given view, if we aren't already there */
322+
private showIfNot(desiredView: CurrentlyShowing) {
323+
if (this.state.activeView !== desiredView) {
324+
this.show(desiredView)
325+
}
326+
}
327+
307328
private openWatchPane() {
308329
const notWatching = this.state.activeView === 'TerminalOnly' || this.state.activeView === 'TerminalPlusSidecar'
309330

@@ -444,8 +465,6 @@ export default class TabContent extends React.PureComponent<Props, State> {
444465
</div>
445466
{this.state.tab && <Confirm tab={this.state.tab} uuid={this.props.uuid} />}
446467
</div>
447-
448-
{this.topTabButtons()}
449468
</React.Fragment>
450469
)
451470
}
@@ -505,62 +524,56 @@ export default class TabContent extends React.PureComponent<Props, State> {
505524
}
506525

507526
/**
508-
* Buttons that are placed in the TopTabStripe and which controller
509-
* the visibility of various views.
527+
* If given, use the top tab button controller to provide the
528+
* latest button model.
529+
*
510530
*/
511-
protected topTabButtons() {
512-
if (this.props.active && (this.state.sidecarHasContent || this.state.watchPaneHasContent)) {
513-
const buttonNum = this.state.sidecarHasContent && this.state.watchPaneHasContent ? 4 : 2
531+
private updateTopTabButtons(newState: Pick<State, 'activeView' | 'sidecarHasContent'>) {
532+
if (this.props.willUpdateTopTabButtons) {
533+
this.props.willUpdateTopTabButtons([this.terminalButton(newState), this.sidecarButton(newState)].filter(_ => _))
534+
}
535+
}
514536

515-
return (
516-
<div
517-
id="kui--custom-top-tab-stripe-button-container"
518-
num-button={buttonNum} // helps with css to calculate the right position of the container
519-
className="kui--hide-in-narrower-windows" // re: kui--hide-in-narrower-windows, see https://github.com/IBM/kui/issues/4459
520-
>
537+
/**
538+
* Note how we use the argument `state` to initialize things, but we
539+
* intentionally use this.state in the onClick. The onClick handler
540+
* may be invoked after any number of onClicks in this or other
541+
* buttons. So it must pay attention to the *current* state, not the
542+
* state at the time of creation.
543+
*
544+
*/
545+
private terminalButton(state: Pick<State, 'activeView'>): TopTabButton {
546+
const key = 'show only terminal'
547+
548+
return {
549+
icon: (
550+
<Icons
551+
icon="TerminalOnly"
552+
key={key}
553+
data-mode={key}
554+
data-active={state.activeView === 'TerminalOnly' || undefined}
555+
onClick={this.showIfNot.bind(this, 'TerminalOnly')}
556+
/>
557+
)
558+
}
559+
}
560+
561+
/** Caution: see the Note for this.terminalButton, re: `state` versus `this.state` */
562+
private sidecarButton(state: Pick<State, 'activeView' | 'sidecarHasContent'>): TopTabButton {
563+
if (state.sidecarHasContent) {
564+
const key = 'show terminal and sidecar'
565+
566+
return {
567+
icon: (
521568
<Icons
522-
icon="TerminalOnly"
523-
data-mode="show only terminal"
524-
data-active={this.state.activeView === 'TerminalOnly' || undefined}
525-
onClick={this.state.activeView !== 'TerminalOnly' ? () => this.show('TerminalOnly') : undefined}
569+
icon="TerminalPlusSidecar"
570+
key={key}
571+
data-mode={key}
572+
data-active={state.activeView === 'TerminalPlusSidecar' || undefined}
573+
onClick={this.showIfNot.bind(this, 'TerminalPlusSidecar')}
526574
/>
527-
528-
{this.state.sidecarHasContent && (
529-
<Icons
530-
icon="TerminalPlusSidecar"
531-
data-mode="show terminal and sidecar"
532-
data-active={this.state.activeView === 'TerminalPlusSidecar' || undefined}
533-
onClick={
534-
this.state.activeView !== 'TerminalPlusSidecar' ? () => this.show('TerminalPlusSidecar') : undefined
535-
}
536-
/>
537-
)}
538-
539-
{this.state.watchPaneHasContent && (
540-
<Icons
541-
icon="TerminalPlusWatcher"
542-
data-mode="show terminal and watcher"
543-
data-active={this.state.activeView === 'TerminalPlusWatcher' || undefined}
544-
onClick={
545-
this.state.activeView !== 'TerminalPlusWatcher' ? () => this.show('TerminalPlusWatcher') : undefined
546-
}
547-
/>
548-
)}
549-
550-
{this.state.watchPaneHasContent && this.state.sidecarHasContent && (
551-
<Icons
552-
icon="TerminalSidecarWatcher"
553-
data-mode="show terminal sidecar and watcher"
554-
data-active={this.state.activeView === 'TerminalSidecarWatcher' || undefined}
555-
onClick={
556-
this.state.activeView !== 'TerminalSidecarWatcher'
557-
? () => this.show('TerminalSidecarWatcher')
558-
: undefined
559-
}
560-
/>
561-
)}
562-
</div>
563-
)
575+
)
576+
}
564577
}
565578
}
566579
}

Diff for: plugins/plugin-client-common/src/components/Client/TabModel.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ export function uuid() {
2222
return (_uuidCounter++).toString()
2323
}
2424

25-
export default class TabModel {
26-
private readonly _uuid: string
27-
private readonly _state: TabState
25+
export interface TopTabButton<P extends { key: string } = { key: string }> {
26+
icon: React.ReactElement<P>
27+
}
2828

29-
public constructor() {
30-
this._uuid = uuid()
31-
this._state = new TabState(this._uuid)
29+
export default class TabModel {
30+
public constructor(
31+
private readonly _uuid = uuid(),
32+
private readonly _state = new TabState(_uuid),
33+
private readonly _buttons: TopTabButton[] = []
34+
) {
3235
this._state.capture()
3336
}
3437

@@ -39,4 +42,12 @@ export default class TabModel {
3942
public get state() {
4043
return this._state
4144
}
45+
46+
public get buttons() {
47+
return this._buttons
48+
}
49+
50+
public update(buttons: TopTabButton[]) {
51+
return new TabModel(this.uuid, this.state, buttons)
52+
}
4253
}

Diff for: plugins/plugin-client-common/src/components/Client/TopTabStripe/index.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,22 @@ export default class TopTabStripe extends React.PureComponent<Props> {
163163
)
164164
}
165165

166+
/** Buttons that appear in the top right */
167+
private buttons() {
168+
if (this.props.tabs[this.props.activeIdx]) {
169+
const { buttons } = this.props.tabs[this.props.activeIdx]
170+
return (
171+
<div
172+
id="kui--custom-top-tab-stripe-button-container"
173+
num-button={buttons.length} // helps with css to calculate the right position of the container
174+
className="kui--hide-in-narrower-windows" // re: kui--hide-in-narrower-windows, see https://github.com/IBM/kui/issues/4459
175+
>
176+
{buttons.map(_ => _.icon)}
177+
</div>
178+
)
179+
}
180+
}
181+
166182
/**
167183
* React render handler
168184
*
@@ -173,6 +189,7 @@ export default class TopTabStripe extends React.PureComponent<Props> {
173189
{/* this.headerMenu(args) */}
174190
{this.headerName()}
175191
{this.tabs()}
192+
{this.buttons()}
176193
{/* this.sidenav(args) */}
177194
</Header>
178195
)

Diff for: plugins/plugin-client-common/web/css/static/TopTabStripe.scss

+1-2
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,7 @@
218218
#kui--custom-top-tab-stripe-button-container {
219219
display: flex;
220220
justify-content: flex-end;
221-
position: absolute;
222-
z-index: 10000;
221+
position: relative;
223222

224223
&[num-button='2'] {
225224
/* number of buttons in the container */

0 commit comments

Comments
 (0)