@@ -327,17 +327,62 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, provider, singleStory
327327 ) ;
328328 } ,
329329
330+ /**
331+ * Attempts to focus (and select) an element identified by its ID. It is the responsibility of
332+ * the callee to ensure that the element is present in the DOM and that no focus trap is
333+ * available. This API polls and attempts to perform the focus for a set duration (max 500ms),
334+ * so that race conditions can be avoided with the current API design. Because this API is
335+ * historically synchronous, it cannot report errors or failure to focus. It fails silently.
336+ *
337+ * @param elementId The id of the element to focus.
338+ * @param select Whether to call select() on the element after focusing it.
339+ */
330340 focusOnUIElement ( elementId ?: string , select ?: boolean ) {
341+ // See RFC https://github.com/storybookjs/storybook/discussions/32983 for
342+ // ways to make this API more robust to focus-trap race conditions.
343+
331344 if ( ! elementId ) {
332345 return ;
333346 }
334- const element = document . getElementById ( elementId ) ;
335- if ( element ) {
347+
348+ const startTime = Date . now ( ) ;
349+ const maxDuration = 500 ;
350+ const pollInterval = 50 ;
351+
352+ const attemptFocus = ( ) => {
353+ const element = document . getElementById ( elementId ) ;
354+ if ( ! element ) {
355+ return false ;
356+ }
357+
336358 element . focus ( ) ;
359+ if ( element !== document . activeElement ) {
360+ return false ;
361+ }
362+
337363 if ( select ) {
338- ( element as any ) . select ( ) ;
364+ ( element as any ) . select ?. ( ) ;
339365 }
366+ return true ;
367+ } ;
368+
369+ if ( attemptFocus ( ) ) {
370+ return ;
340371 }
372+
373+ // Poll every 50ms for up to 500ms to account for race conditions.
374+ const intervalId = setInterval ( ( ) => {
375+ const elapsed = Date . now ( ) - startTime ;
376+
377+ if ( elapsed >= maxDuration ) {
378+ clearInterval ( intervalId ) ;
379+ return ;
380+ }
381+
382+ if ( attemptFocus ( ) ) {
383+ clearInterval ( intervalId ) ;
384+ }
385+ } , pollInterval ) ;
341386 } ,
342387
343388 getInitialOptions ( ) {
0 commit comments