11import * as React from 'react' ;
2+ import { observer } from 'mobx-react-lite' ;
23
34import { css , styled } from '../../styles' ;
4- import { UnstyledButton } from '../common/inputs' ;
5+
56import { SendRequest } from '../../model/send/send-request-model' ;
67import { getMethodColor } from '../../model/events/categorization' ;
7- import { observer } from 'mobx-react' ;
8+
9+ import { UnstyledButton } from '../common/inputs' ;
810import { IconButton } from '../common/icon-button' ;
9- import { noPropagation } from '../component-utils' ;
1011
1112export const TAB_BAR_HEIGHT = '38px' ;
1213
@@ -62,23 +63,6 @@ const TabContainer = styled.div<{ selected: boolean }>`
6263 }
6364` ;
6465
65- const TabButton = styled ( UnstyledButton ) . attrs ( ( p : { selected : boolean } ) => ( {
66- role : 'tab' ,
67- 'aria-selected' : p . selected . toString ( ) ,
68- 'tabindex' : p . selected ? '0' : '-1'
69- } ) ) `
70- flex-basis: 100%;
71- flex-grow: 1;
72- flex-shrink: 1;
73-
74- text-align: left;
75- text-overflow: ellipsis;
76- overflow: hidden;
77- white-space: nowrap;
78-
79- padding: 0 10px;
80- ` ;
81-
8266const TabMethodMarker = styled . span < { method : string } > `
8367 color: ${ p => getMethodColor ( p . method ) } ;
8468 font-size: ${ p => p . theme . textInputFontSize } ;
@@ -101,51 +85,148 @@ const AddTabButton = styled(IconButton)`
10185 align-self: center;
10286` ;
10387
88+ const TabButton = styled ( UnstyledButton ) . attrs ( ( p : { selected : boolean } ) => ( {
89+ role : 'tab' ,
90+ 'aria-selected' : p . selected . toString ( ) ,
91+ 'tabindex' : p . selected ? '0' : '-1'
92+ } ) ) `
93+ flex-basis: 100%;
94+ flex-grow: 1;
95+ flex-shrink: 1;
96+
97+ text-align: left;
98+ text-overflow: ellipsis;
99+ overflow: hidden;
100+ white-space: nowrap;
101+
102+ padding: 0 10px;
103+
104+ :focus-visible {
105+ outline: none;
106+ font-weight: bold;
107+
108+ & + ${ CloseTabButton } {
109+ color: ${ p => p . theme . popColor } ;
110+ }
111+ }
112+ ` ;
113+
114+ const SendTab = observer ( ( props : {
115+ sendRequest : SendRequest ,
116+ isSelectedTab : boolean ,
117+ onSelectTab : ( request : SendRequest ) => void ,
118+ onCloseTab : ( request : SendRequest ) => void
119+ } ) => {
120+ const { id, request } = props . sendRequest ;
121+
122+ const onTabClick = React . useCallback ( ( ) => {
123+ props . onSelectTab ( props . sendRequest )
124+ } , [ props . onSelectTab , props . sendRequest ] ) ;
125+
126+ const onTaxAuxClick = React . useCallback ( ( event ) => {
127+ if ( event . button === 1 ) { // Middle mouse click
128+ props . onCloseTab ( props . sendRequest ) ;
129+ }
130+ } , [ props . onCloseTab , props . sendRequest ] ) ;
131+
132+ const onCloseClick = React . useCallback ( ( event : React . SyntheticEvent ) => {
133+ props . onCloseTab ( props . sendRequest ) ;
134+ event . stopPropagation ( ) ;
135+ } , [ props . onCloseTab , props . sendRequest ] ) ;
136+
137+ return < TabContainer
138+ key = { id }
139+ selected = { props . isSelectedTab }
140+ onClick = { onTabClick }
141+ onAuxClick = { onTaxAuxClick }
142+ >
143+ < TabButton
144+ selected = { props . isSelectedTab }
145+ tabIndex = { props . isSelectedTab ? 0 : - 1 }
146+ >
147+ < TabMethodMarker method = { request . method } >
148+ { request . method }
149+ </ TabMethodMarker >
150+
151+ < TabName > {
152+ request . url . replace ( / ^ h t t p s ? : \/ \/ / , '' ) || ''
153+ } </ TabName >
154+ </ TabButton >
155+
156+ {
157+ props . isSelectedTab && < CloseTabButton
158+ title = 'Close this tab'
159+ icon = { [ 'fas' , 'times' ] }
160+ onClick = { onCloseClick }
161+ tabIndex = { - 1 } // No focus - keyboard closes via 'Delete' instead
162+ />
163+ }
164+ </ TabContainer > ;
165+ } ) ;
166+
104167export const SendTabs = observer ( ( props : {
105168 sendRequests : Array < SendRequest > ;
106169 selectedTab : SendRequest ;
107170 onSelectTab : ( sendRequest : SendRequest ) => void ;
171+ onMoveSelection : ( distance : number ) => void ;
108172 onCloseTab : ( sendRequest : SendRequest ) => void ;
109173 onAddTab : ( ) => void ;
110174} ) => {
175+ const containerRef = React . useRef < HTMLDivElement > ( null ) ;
176+
177+ const focusSelectedEvent = React . useCallback ( ( ) => {
178+ const container = containerRef . current ;
179+ if ( ! container ) return ;
180+
181+ const selectedTab = container . querySelector ( '[role=tab][aria-selected=true]' ) as HTMLButtonElement ;
182+ if ( ! selectedTab ) return ;
183+ selectedTab . focus ( ) ;
184+ } , [ containerRef ] ) ;
185+
186+ const onKeyDown = React . useCallback ( ( event : React . KeyboardEvent < HTMLElement > ) => {
187+ // Note that selected tab === focused tab so no worries differentiating the two
188+ if ( event . key === 'Delete' ) {
189+ props . onCloseTab ( props . selectedTab ) ;
190+ } else if ( event . key === 'ArrowRight' ) {
191+ props . onMoveSelection ( 1 ) ;
192+ } else if ( event . key === 'ArrowLeft' ) {
193+ props . onMoveSelection ( - 1 ) ;
194+ } else if ( event . key === 'Home' ) {
195+ props . onMoveSelection ( - Infinity ) ;
196+ } else if ( event . key === 'End' ) {
197+ props . onMoveSelection ( Infinity ) ;
198+ } else {
199+ return ;
200+ }
201+
202+ // In all the above cases, we want to update the focus to match:
203+ requestAnimationFrame ( ( ) => focusSelectedEvent ( ) ) ;
204+ } , [ props . onCloseTab , props . selectedTab , props . onMoveSelection , focusSelectedEvent ] ) ;
111205
206+ const onAddButtonKeyDown = React . useCallback ( ( event : React . KeyboardEvent < HTMLElement > ) => {
207+ event . stopPropagation ( ) ;
208+ } , [ ] ) ;
112209
113- return < TabsContainer >
210+ return < TabsContainer
211+ ref = { containerRef }
212+ onKeyDown = { onKeyDown }
213+ >
114214 {
115215 props . sendRequests . map ( ( sendRequest ) => {
116- const { id, request } = sendRequest ;
117-
118- const isSelected = props . selectedTab === sendRequest ;
119-
120- return < TabContainer
121- key = { id }
122- selected = { isSelected }
123- onClick = { ( ) => props . onSelectTab ( sendRequest ) }
124- >
125- < TabButton selected = { isSelected } >
126- < TabMethodMarker method = { request . method } >
127- { request . method }
128- </ TabMethodMarker >
129-
130- < TabName > {
131- request . url . replace ( / ^ h t t p s ? : \/ \/ / , '' ) || ''
132- } </ TabName >
133- </ TabButton >
134-
135- {
136- isSelected && < CloseTabButton
137- title = 'Close this tab'
138- icon = { [ 'fas' , 'times' ] }
139- onClick = { noPropagation ( ( ) => props . onCloseTab ( sendRequest ) ) }
140- />
141- }
142- </ TabContainer > ;
216+ const isSelectedTab = props . selectedTab === sendRequest ;
217+ return < SendTab
218+ sendRequest = { sendRequest }
219+ isSelectedTab = { isSelectedTab }
220+ onSelectTab = { props . onSelectTab }
221+ onCloseTab = { props . onCloseTab }
222+ />
143223 } )
144224 }
145225
146226 < AddTabButton
147227 title = 'Add another tab to send a new request'
148228 icon = { [ 'fas' , 'plus' ] }
229+ onKeyDown = { onAddButtonKeyDown }
149230 onClick = { ( ) => props . onAddTab ( ) }
150231 />
151232 </ TabsContainer >
0 commit comments