1313// Former goog.module ID: Blockly.dropDownDiv
1414
1515import type { BlockSvg } from './block_svg.js' ;
16+ import * as browserEvents from './browser_events.js' ;
1617import * as common from './common.js' ;
1718import type { Field } from './field.js' ;
1819import { ReturnEphemeralFocus , getFocusManager } from './focus_manager.js' ;
@@ -86,6 +87,9 @@ let positionToField: boolean | null = null;
8687/** Callback to FocusManager to return ephemeral focus when the div closes. */
8788let returnEphemeralFocus : ReturnEphemeralFocus | null = null ;
8889
90+ /** Identifier for shortcut keydown listener used to unbind it. */
91+ let keydownListener : browserEvents . Data | null = null ;
92+
8993/**
9094 * Dropdown bounds info object used to encapsulate sizing information about a
9195 * bounding element (bounding box and width/height).
@@ -122,13 +126,21 @@ export function createDom() {
122126 }
123127 div = document . createElement ( 'div' ) ;
124128 div . className = 'blocklyDropDownDiv' ;
129+ div . tabIndex = - 1 ;
125130 const parentDiv = common . getParentContainer ( ) || document . body ;
126131 parentDiv . appendChild ( div ) ;
127132
128133 content = document . createElement ( 'div' ) ;
129134 content . className = 'blocklyDropDownContent' ;
130135 div . appendChild ( content ) ;
131136
137+ keydownListener = browserEvents . conditionalBind (
138+ content ,
139+ 'keydown' ,
140+ null ,
141+ common . globalShortcutHandler ,
142+ ) ;
143+
132144 arrow = document . createElement ( 'div' ) ;
133145 arrow . className = 'blocklyDropDownArrow' ;
134146 div . appendChild ( arrow ) ;
@@ -167,6 +179,10 @@ export function getContentDiv(): HTMLDivElement {
167179
168180/** Clear the content of the drop-down. */
169181export function clearContent ( ) {
182+ if ( keydownListener ) {
183+ browserEvents . unbind ( keydownListener ) ;
184+ keydownListener = null ;
185+ }
170186 div . remove ( ) ;
171187 createDom ( ) ;
172188}
@@ -192,17 +208,24 @@ export function setColour(backgroundColour: string, borderColour: string) {
192208 * @param block Block to position the drop-down around.
193209 * @param opt_onHide Optional callback for when the drop-down is hidden.
194210 * @param opt_secondaryYOffset Optional Y offset for above-block positioning.
211+ * @param manageEphemeralFocus Whether ephemeral focus should be managed
212+ * according to the drop-down div's lifetime. Note that if a false value is
213+ * passed in here then callers should manage ephemeral focus directly
214+ * otherwise focus may not properly restore when the widget closes. Defaults
215+ * to true.
195216 * @returns True if the menu rendered below block; false if above.
196217 */
197218export function showPositionedByBlock < T > (
198219 field : Field < T > ,
199220 block : BlockSvg ,
200221 opt_onHide ?: ( ) => void ,
201222 opt_secondaryYOffset ?: number ,
223+ manageEphemeralFocus : boolean = true ,
202224) : boolean {
203225 return showPositionedByRect (
204226 getScaledBboxOfBlock ( block ) ,
205227 field as Field ,
228+ manageEphemeralFocus ,
206229 opt_onHide ,
207230 opt_secondaryYOffset ,
208231 ) ;
@@ -217,17 +240,24 @@ export function showPositionedByBlock<T>(
217240 * @param field The field to position the dropdown against.
218241 * @param opt_onHide Optional callback for when the drop-down is hidden.
219242 * @param opt_secondaryYOffset Optional Y offset for above-block positioning.
243+ * @param manageEphemeralFocus Whether ephemeral focus should be managed
244+ * according to the drop-down div's lifetime. Note that if a false value is
245+ * passed in here then callers should manage ephemeral focus directly
246+ * otherwise focus may not properly restore when the widget closes. Defaults
247+ * to true.
220248 * @returns True if the menu rendered below block; false if above.
221249 */
222250export function showPositionedByField < T > (
223251 field : Field < T > ,
224252 opt_onHide ?: ( ) => void ,
225253 opt_secondaryYOffset ?: number ,
254+ manageEphemeralFocus : boolean = true ,
226255) : boolean {
227256 positionToField = true ;
228257 return showPositionedByRect (
229258 getScaledBboxOfField ( field as Field ) ,
230259 field as Field ,
260+ manageEphemeralFocus ,
231261 opt_onHide ,
232262 opt_secondaryYOffset ,
233263 ) ;
@@ -271,16 +301,15 @@ function getScaledBboxOfField(field: Field): Rect {
271301 * @param manageEphemeralFocus Whether ephemeral focus should be managed
272302 * according to the drop-down div's lifetime. Note that if a false value is
273303 * passed in here then callers should manage ephemeral focus directly
274- * otherwise focus may not properly restore when the widget closes. Defaults
275- * to true.
304+ * otherwise focus may not properly restore when the widget closes.
276305 * @returns True if the menu rendered below block; false if above.
277306 */
278307function showPositionedByRect (
279308 bBox : Rect ,
280309 field : Field ,
310+ manageEphemeralFocus : boolean ,
281311 opt_onHide ?: ( ) => void ,
282312 opt_secondaryYOffset ?: number ,
283- manageEphemeralFocus : boolean = true ,
284313) : boolean {
285314 // If we can fit it, render below the block.
286315 const primaryX = bBox . left + ( bBox . right - bBox . left ) / 2 ;
@@ -352,10 +381,6 @@ export function show<T>(
352381 dom . addClass ( div , renderedClassName ) ;
353382 dom . addClass ( div , themeClassName ) ;
354383
355- if ( manageEphemeralFocus ) {
356- returnEphemeralFocus = getFocusManager ( ) . takeEphemeralFocus ( div ) ;
357- }
358-
359384 // When we change `translate` multiple times in close succession,
360385 // Chrome may choose to wait and apply them all at once.
361386 // Since we want the translation to initial X, Y to be immediate,
@@ -364,7 +389,15 @@ export function show<T>(
364389 // making the dropdown appear to fly in from (0, 0).
365390 // Using both `left`, `top` for the initial translation and then `translate`
366391 // for the animated transition to final X, Y is a workaround.
367- return positionInternal ( primaryX , primaryY , secondaryX , secondaryY ) ;
392+ const atOrigin = positionInternal ( primaryX , primaryY , secondaryX , secondaryY ) ;
393+
394+ // Ephemeral focus must happen after the div is fully visible in order to
395+ // ensure that it properly receives focus.
396+ if ( manageEphemeralFocus ) {
397+ returnEphemeralFocus = getFocusManager ( ) . takeEphemeralFocus ( div ) ;
398+ }
399+
400+ return atOrigin ;
368401}
369402
370403const internal = {
0 commit comments