77import * as Blockly from 'blockly/core' ;
88import { NavigationController } from './navigation_controller' ;
99import { CursorOptions , LineCursor } from './line_cursor' ;
10+ import { getFlyoutElement , getToolboxElement } from './workspace_utilities' ;
1011
1112/** Options object for KeyboardNavigation instances. */
1213export type NavigationOptions = {
@@ -24,7 +25,7 @@ export class KeyboardNavigation {
2425 protected workspace : Blockly . WorkspaceSvg ;
2526
2627 /** Event handler run when the workspace gains focus. */
27- private focusListener : ( ) => void ;
28+ private focusListener : ( e : Event ) => void ;
2829
2930 /** Event handler run when the workspace loses focus. */
3031 private blurListener : ( ) => void ;
@@ -33,7 +34,13 @@ export class KeyboardNavigation {
3334 private toolboxFocusListener : ( ) => void ;
3435
3536 /** Event handler run when the toolbox loses focus. */
36- private toolboxBlurListener : ( ) => void ;
37+ private toolboxBlurListener : ( e : Event ) => void ;
38+
39+ /** Event handler run when the flyout gains focus. */
40+ private flyoutFocusListener : ( ) => void ;
41+
42+ /** Event handler run when the flyout loses focus. */
43+ private flyoutBlurListener : ( e : Event ) => void ;
3744
3845 /** Keyboard navigation controller instance for the workspace. */
3946 private navigationController : NavigationController ;
@@ -86,31 +93,79 @@ export class KeyboardNavigation {
8693 // We add a focus listener below so use -1 so it doesn't become focusable.
8794 workspace . getParentSvg ( ) . setAttribute ( 'tabindex' , '-1' ) ;
8895
89- this . focusListener = ( ) => {
90- this . navigationController . updateWorkspaceFocus ( workspace , true ) ;
96+ // Move the flyout for logical tab order.
97+ const flyoutElement = getFlyoutElement ( workspace ) ;
98+ flyoutElement ?. parentElement ?. insertBefore (
99+ flyoutElement ,
100+ workspace . getParentSvg ( ) ,
101+ ) ;
102+ // Allow tab to the flyout only when there's no toolbox.
103+ if ( workspace . getToolbox ( ) && flyoutElement ) {
104+ flyoutElement . tabIndex = - 1 ;
105+ }
106+
107+ this . focusListener = ( e : Event ) => {
108+ if ( e . currentTarget === this . workspace . getParentSvg ( ) ) {
109+ // Starting a gesture unconditionally calls markFocused on the parent SVG
110+ // but we really don't want to move to the workspace (and close the
111+ // flyout) if all you did was click in a flyout, potentially on a
112+ // button. See also `gesture_monkey_patch.js`.
113+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114+ const gestureInternals = this . workspace . currentGesture_ as any ;
115+ const gestureFlyout = gestureInternals ?. flyout ;
116+ const gestureFlyoutAutoClose = gestureFlyout ?. autoClose ;
117+ const gestureOnBlock = gestureInternals ?. startBlock ;
118+ if (
119+ // When clicking on flyout that cannot close.
120+ ( gestureFlyout && ! gestureFlyoutAutoClose ) ||
121+ // When clicking on a block in a flyout that can close.
122+ ( gestureFlyout && gestureFlyoutAutoClose && ! gestureOnBlock )
123+ ) {
124+ this . navigationController . focusFlyout ( workspace ) ;
125+ } else {
126+ this . navigationController . focusWorkspace ( workspace ) ;
127+ }
128+ } else {
129+ this . navigationController . handleFocusWorkspace ( workspace ) ;
130+ }
91131 } ;
92132 this . blurListener = ( ) => {
93- this . navigationController . updateWorkspaceFocus ( workspace , false ) ;
133+ this . navigationController . handleBlurWorkspace ( workspace ) ;
94134 } ;
95135
96136 workspace . getSvgGroup ( ) . addEventListener ( 'focus' , this . focusListener ) ;
97137 workspace . getSvgGroup ( ) . addEventListener ( 'blur' , this . blurListener ) ;
98138
139+ const toolboxElement = getToolboxElement ( workspace ) ;
99140 this . toolboxFocusListener = ( ) => {
100- this . navigationController . updateToolboxFocus ( workspace , true ) ;
141+ this . navigationController . handleFocusToolbox ( workspace ) ;
101142 } ;
102- this . toolboxBlurListener = ( ) => {
103- this . navigationController . updateToolboxFocus ( workspace , false ) ;
143+ this . toolboxBlurListener = ( e : Event ) => {
144+ this . navigationController . handleBlurToolbox (
145+ workspace ,
146+ this . shouldCloseFlyoutOnBlur (
147+ ( e as FocusEvent ) . relatedTarget ,
148+ flyoutElement ,
149+ ) ,
150+ ) ;
104151 } ;
152+ toolboxElement ?. addEventListener ( 'focus' , this . toolboxFocusListener ) ;
153+ toolboxElement ?. addEventListener ( 'blur' , this . toolboxBlurListener ) ;
105154
106- const toolbox = workspace . getToolbox ( ) ;
107- if ( toolbox != null && toolbox instanceof Blockly . Toolbox ) {
108- const contentsDiv = toolbox . HtmlDiv ?. querySelector (
109- '.blocklyToolboxContents' ,
155+ this . flyoutFocusListener = ( ) => {
156+ this . navigationController . handleFocusFlyout ( workspace ) ;
157+ } ;
158+ this . flyoutBlurListener = ( e : Event ) => {
159+ this . navigationController . handleBlurFlyout (
160+ workspace ,
161+ this . shouldCloseFlyoutOnBlur (
162+ ( e as FocusEvent ) . relatedTarget ,
163+ toolboxElement ,
164+ ) ,
110165 ) ;
111- contentsDiv ?. addEventListener ( 'focus' , this . toolboxFocusListener ) ;
112- contentsDiv ?. addEventListener ( 'blur ' , this . toolboxBlurListener ) ;
113- }
166+ } ;
167+ flyoutElement ?. addEventListener ( 'focus ' , this . flyoutFocusListener ) ;
168+ flyoutElement ?. addEventListener ( 'blur' , this . flyoutBlurListener ) ;
114169
115170 // Temporary workaround for #136.
116171 // TODO(#136): fix in core.
@@ -136,14 +191,13 @@ export class KeyboardNavigation {
136191 . getSvgGroup ( )
137192 . removeEventListener ( 'focus' , this . focusListener ) ;
138193
139- const toolbox = this . workspace . getToolbox ( ) ;
140- if ( toolbox != null && toolbox instanceof Blockly . Toolbox ) {
141- const contentsDiv = toolbox . HtmlDiv ?. querySelector (
142- '.blocklyToolboxContents' ,
143- ) ;
144- contentsDiv ?. removeEventListener ( 'focus' , this . toolboxFocusListener ) ;
145- contentsDiv ?. removeEventListener ( 'blur' , this . toolboxBlurListener ) ;
146- }
194+ const toolboxElement = getToolboxElement ( this . workspace ) ;
195+ toolboxElement ?. removeEventListener ( 'focus' , this . toolboxFocusListener ) ;
196+ toolboxElement ?. removeEventListener ( 'blur' , this . toolboxBlurListener ) ;
197+
198+ const flyoutElement = getFlyoutElement ( this . workspace ) ;
199+ flyoutElement ?. removeEventListener ( 'focus' , this . flyoutFocusListener ) ;
200+ flyoutElement ?. removeEventListener ( 'blur' , this . flyoutBlurListener ) ;
147201
148202 if ( this . workspaceParentTabIndex ) {
149203 this . workspace
@@ -189,4 +243,32 @@ export class KeyboardNavigation {
189243 } ) ;
190244 this . workspace . setTheme ( newTheme ) ;
191245 }
246+
247+ /**
248+ * Identify whether we should close the flyout when the toolbox or flyout
249+ * blurs. If a gesture is in progerss or we're moving from one the other
250+ * then we leave it open.
251+ *
252+ * @param relatedTarget The related target from the event on the flyout or toolbox.
253+ * @param container The other element of flyout or toolbox (opposite to the event).
254+ * @returns true if the flyout should be closed, false otherwise.
255+ */
256+ private shouldCloseFlyoutOnBlur (
257+ relatedTarget : EventTarget | null ,
258+ container : Element | null ,
259+ ) {
260+ if ( Blockly . Gesture . inProgress ( ) ) {
261+ return false ;
262+ }
263+ if ( ! relatedTarget ) {
264+ return false ;
265+ }
266+ if (
267+ relatedTarget instanceof Node &&
268+ container ?. contains ( relatedTarget as Node )
269+ ) {
270+ return false ;
271+ }
272+ return true ;
273+ }
192274}
0 commit comments