@@ -85,7 +85,12 @@ const charKeys = {
8585
8686// Button layout. Each entry: { id, label, type }
8787// type: 'seq' = terminal sequence, 'char' = insert character,
88- // 'toggle' = modifier toggle, 'done' = dismiss keyboard
88+ // 'toggle' = modifier toggle, 'done' = dismiss keyboard,
89+ // 'hide' = collapse the bar (keep the keyboard)
90+ //
91+ // NOTE: the 'hide' button is intentionally NOT in this list. It is rendered
92+ // outside the scroll container (see render()) so it stays pinned to the left
93+ // even when the user scrolls the key row all the way to the end.
8994const buttonLayout = [
9095 { id : 'esc' , label : 'ESC' , type : 'seq' } ,
9196 { id : 'tab' , label : 'TAB' , type : 'seq' } ,
@@ -124,6 +129,7 @@ export default class KeyboardAccessoryBar extends Component {
124129 super ( props )
125130 this . state = {
126131 visible : false ,
132+ hidden : false ,
127133 isTerminal : false ,
128134 ctrlActive : false ,
129135 altActive : false
@@ -137,13 +143,32 @@ export default class KeyboardAccessoryBar extends Component {
137143 // Use focusin/focusout (bubbling) to detect when any input is focused.
138144 document . addEventListener ( 'focusin' , this . handleFocusIn , true )
139145 document . addEventListener ( 'focusout' , this . handleFocusOut , true )
146+ this . _syncBarHeight ( )
147+ }
148+
149+ componentDidUpdate ( ) {
150+ this . _syncBarHeight ( )
140151 }
141152
142153 componentWillUnmount ( ) {
143154 document . removeEventListener ( 'focusin' , this . handleFocusIn , true )
144155 document . removeEventListener ( 'focusout' , this . handleFocusOut , true )
145156 clearTimeout ( this . _focusTimer )
146157 this . _restoreSendData ( )
158+ // Give the reserved space back.
159+ document . documentElement . style . setProperty ( '--kb-bar-h' , '0px' )
160+ }
161+
162+ /**
163+ * Publish the bar's height as --kb-bar-h on :root so the page can reserve
164+ * that much space at the bottom and the bar never covers content (see the
165+ * matching rules in keyboard-accessory-bar.styl). 0 when hidden/collapsed.
166+ */
167+ _syncBarHeight = ( ) => {
168+ const h = ( this . state . visible && ! this . state . hidden && this . _barEl )
169+ ? this . _barEl . offsetHeight
170+ : 0
171+ document . documentElement . style . setProperty ( '--kb-bar-h' , h + 'px' )
147172 }
148173
149174 handleFocusIn = ( e ) => {
@@ -159,7 +184,7 @@ export default class KeyboardAccessoryBar extends Component {
159184 // Small delay so focusout→focusin rapid switching (e.g. tapping a bar
160185 // button) doesn't cause a hide→show flicker.
161186 this . _focusTimer = setTimeout ( ( ) => {
162- this . setState ( { visible : true , isTerminal } )
187+ this . setState ( { visible : true , hidden : false , isTerminal } )
163188 } , 50 )
164189 }
165190
@@ -173,6 +198,7 @@ export default class KeyboardAccessoryBar extends Component {
173198 if ( ! isInput ) {
174199 this . setState ( {
175200 visible : false ,
201+ hidden : false ,
176202 ctrlActive : false ,
177203 altActive : false
178204 } )
@@ -318,11 +344,18 @@ export default class KeyboardAccessoryBar extends Component {
318344 const { isTerminal } = this . state
319345
320346 switch ( btn . type ) {
347+ case 'hide' : {
348+ // Collapse the bar but keep the keyboard open so the user can still
349+ // type. A floating button (rendered in render()) brings it back.
350+ this . setState ( { hidden : true , ctrlActive : false , altActive : false } )
351+ this . _restoreSendData ( )
352+ break
353+ }
321354 case 'done' : {
322355 // Dismiss the keyboard by blurring the focused element.
323356 const el = document . activeElement
324357 if ( el && el . blur ) el . blur ( )
325- this . setState ( { visible : false , ctrlActive : false , altActive : false } )
358+ this . setState ( { visible : false , hidden : false , ctrlActive : false , altActive : false } )
326359 this . _restoreSendData ( )
327360 break
328361 }
@@ -382,6 +415,7 @@ export default class KeyboardAccessoryBar extends Component {
382415 const active = btn . id === 'ctrl' ? ctrlActive : altActive
383416 cls += active ? ' kb-key-active' : ''
384417 }
418+ if ( btn . id === 'hide' ) cls += ' kb-key-hide'
385419 if ( btn . id === 'done' ) cls += ' kb-key-done'
386420 if ( btn . id === 'backspace' ) cls += ' kb-key-wide'
387421 if ( btn . id === 'enter' ) cls += ' kb-key-enter'
@@ -401,12 +435,46 @@ export default class KeyboardAccessoryBar extends Component {
401435 }
402436
403437 render ( ) {
404- if ( ! this . state . visible ) return null
438+ const { visible, hidden } = this . state
439+ if ( ! visible ) return null
440+ // Collapsed: render just a small floating button to bring the bar back.
441+ // The keyboard stays open; only the bar is hidden.
442+ if ( hidden ) {
443+ return (
444+ < button
445+ className = 'kb-accessory-show'
446+ aria-label = 'Show keyboard toolbar'
447+ // Keep focus on the terminal/input underneath.
448+ onMouseDown = { ( e ) => e . preventDefault ( ) }
449+ onTouchStart = { ( e ) => e . preventDefault ( ) }
450+ onClick = { ( ) => this . setState ( { hidden : false } ) }
451+ type = 'button'
452+ >
453+ ▴
454+ </ button >
455+ )
456+ }
405457 const { isTerminal } = this . state
406458 return (
407- < div className = 'kb-accessory-bar' >
408- < div className = 'kb-accessory-scroll' >
409- { buttonLayout . map ( btn => this . renderButton ( btn ) ) }
459+ < div
460+ className = 'kb-accessory-bar'
461+ ref = { ( el ) => { this . _barEl = el } }
462+ >
463+ < div className = 'kb-accessory-row' >
464+ { /* Pinned hide button — always visible at the left, never scrolls. */ }
465+ < button
466+ className = 'kb-key kb-key-hide'
467+ aria-label = 'Hide keyboard toolbar'
468+ onMouseDown = { ( e ) => e . preventDefault ( ) }
469+ onTouchStart = { ( e ) => e . preventDefault ( ) }
470+ onClick = { ( e ) => this . handleButton ( { id : 'hide' , type : 'hide' } , e ) }
471+ type = 'button'
472+ >
473+ ▾
474+ </ button >
475+ < div className = 'kb-accessory-scroll' >
476+ { buttonLayout . map ( btn => this . renderButton ( btn ) ) }
477+ </ div >
410478 </ div >
411479 { ! isTerminal && (
412480 < div className = 'kb-accessory-hint' >
0 commit comments