You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The focusOnUIElement function is used to focus elements based on their ID, but it does not manage necessary preconditions to ensure focus works. It also does not return a success/error state.
This leads to two issues:
Race conditions or errors can prevent focus from working, and we can't detect that; this happens a lot more now that our modals/popovers implement focus traps
Preconditions to focus a specific UI element must be implemented everywhere where the API is called, instead of being centralised
Problem Statement
Problem 1 - error handling and focus traps
This is the primary problem. Now that we have focus traps on popovers, calling focusOnUIElement from within a popover fails. The focus() call silently fails as the popover is open and actively focus traps (making the bulk of the document inert).
It is near-impossible for devs to reliably request their own popover to close and then call focusOnUIElement. That's because closing the popover means setting a state, and then an async state update will later close the popover. To detect that the popover is closed, a dev would need to set up a MutationObserver.
It's even worse when considering that multiple popovers can be open (submenu in a menu, menu in a dialog). Then, the only way to ensure modals/popovers have been cleared is to have a central API to do that.
This being said, our modal API currently allows callees to prevent a close. There can also be, within a modal, specific code to prevent closing (e.g. to avoid losing unsubmitted form content). Such modals have a controlled close state.
So, a central API requesting that modals/popovers close would need to start from the latest opened modal and walk all its way up to the root, allowing modals to refuse to close. If a specific modal failed to close, the central API would need to assume that this specific modal performs UX that allows the user to understand why it did not close successfully.
Such a central API would allow us to navigate focus anywhere in the app more reliably than we currently can.
Problem 2 - repetition of preconditions
There are three particular elements that we want to focus to:
It would make sense to have dedicated API endpoints to focus these, so that they can centralise their preconditions (e.g. for search, ensure the sidebar is visible or the nav is open on mobile). Considering these IDs are exported away from the actual function to use to trigger focus, it would also make it easier to discover and exploit the IDs if they were API endpoints.
Non-goals
We don't want to implement an API right away, we just want to ensure we have a well-thought plan for when it becomes absolutely necessary
We don't want focusOnUIElement to be able to handle error states; we're ok with it causing a UI state change that in itself communicates error to the user (e.g. a Modal alerts that it can't be closed)
Implementation
Problem 1
In the short term, we're making focusOnUIElement poll for up to 500ms to try and focus the element. This handles race conditions linked to closing a single popover, with 50ms delay. It will never handle closing multiple modals (because of encapsulation, we can't assume a child element in a child modal has a way of knowing a parent modal exists).
In the long term, if this issue occurs again with
Chains of multiple modals
A parent modal that can refuse to close
Then we'll need a centralised async API to request full modal closure and wait for confirmation, before focusOnUIElement is called. The API would imply that the Popover, Tooltip, Modal components all register their presence and open state to a singleton heap.
// See RFC https://github.com/storybookjs/storybook/discussions/32983 for
// ways to make this API more robust to focus-trap race conditions.
if(!elementId){
return;
}
conststartTime=Date.now();
constmaxDuration=500;
constpollInterval=50;
constattemptFocus=()=>{
constelement=document.getElementById(elementId);
if(!element){
returnfalse;
}
element.focus();
if(element!==document.activeElement){
returnfalse;
}
if(select){
(elementasany).select?.();
}
returntrue;
};
if(attemptFocus()){
return;
}
// Poll every 50ms for up to 500ms to account for race conditions.
constintervalId=setInterval(()=>{
constelapsed=Date.now()-startTime;
if(elapsed>=maxDuration){
clearInterval(intervalId);
return;
}
if(attemptFocus()){
clearInterval(intervalId);
}
},pollInterval);
},
Deliverables
Allow all overlays to register their open state to a global heap
Make the overlays provide close() methods to the heap too
Write a function that allows closing modals from the last-inserted elements of the heap
Add ways to focus known UI elements without using the generic focusOnUIElement function so that preconditions can be centralised
Adjust existing SidebarMenu and Settings code
Risks
The cost of implementing this may be more than we care for. We should wait until the problem escalates to a state where the current polling is no longer enough.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
The
focusOnUIElementfunction is used to focus elements based on their ID, but it does not manage necessary preconditions to ensure focus works. It also does not return a success/error state.This leads to two issues:
Problem Statement
Problem 1 - error handling and focus traps
This is the primary problem. Now that we have focus traps on popovers, calling
focusOnUIElementfrom within a popover fails. Thefocus()call silently fails as the popover is open and actively focus traps (making the bulk of the documentinert).It is near-impossible for devs to reliably request their own popover to close and then call
focusOnUIElement. That's because closing the popover means setting a state, and then an async state update will later close the popover. To detect that the popover is closed, a dev would need to set up a MutationObserver.It's even worse when considering that multiple popovers can be open (submenu in a menu, menu in a dialog). Then, the only way to ensure modals/popovers have been cleared is to have a central API to do that.
This being said, our modal API currently allows callees to prevent a close. There can also be, within a modal, specific code to prevent closing (e.g. to avoid losing unsubmitted form content). Such modals have a controlled close state.
So, a central API requesting that modals/popovers close would need to start from the latest opened modal and walk all its way up to the root, allowing modals to refuse to close. If a specific modal failed to close, the central API would need to assume that this specific modal performs UX that allows the user to understand why it did not close successfully.
Such a central API would allow us to navigate focus anywhere in the app more reliably than we currently can.
Problem 2 - repetition of preconditions
There are three particular elements that we want to focus to:
storybook/code/core/src/manager-api/modules/layout.ts
Lines 135 to 139 in fa75e0b
It would make sense to have dedicated API endpoints to focus these, so that they can centralise their preconditions (e.g. for search, ensure the sidebar is visible or the nav is open on mobile). Considering these IDs are exported away from the actual function to use to trigger focus, it would also make it easier to discover and exploit the IDs if they were API endpoints.
Non-goals
focusOnUIElementto be able to handle error states; we're ok with it causing a UI state change that in itself communicates error to the user (e.g. a Modal alerts that it can't be closed)Implementation
Problem 1
In the short term, we're making
focusOnUIElementpoll for up to 500ms to try and focus the element. This handles race conditions linked to closing a single popover, with 50ms delay. It will never handle closing multiple modals (because of encapsulation, we can't assume a child element in a child modal has a way of knowing a parent modal exists).In the long term, if this issue occurs again with
Then we'll need a centralised async API to request full modal closure and wait for confirmation, before
focusOnUIElementis called. The API would imply that the Popover, Tooltip, Modal components all register their presence and open state to a singleton heap.Then, the API could look like:
Problem 2 - centralisation of preconditions
Like said before, the preconditions for known UI elements to be focusable could be centralised if:
Prior Art
storybook/code/core/src/manager-api/modules/layout.ts
Lines 340 to 386 in 5482845
Deliverables
focusOnUIElementfunction so that preconditions can be centralisedRisks
The cost of implementing this may be more than we care for. We should wait until the problem escalates to a state where the current polling is no longer enough.
Unresolved Questions
No response
Alternatives considered / Abandoned Ideas
No response
Beta Was this translation helpful? Give feedback.
All reactions