-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[react-interactions] Refine custom active element blur logic #17354
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit eb5ba18:
|
2b81cbc
to
8ac453f
Compare
Details of bundled changes.Comparing: b53ea6c...eb5ba18 react-dom
react-interactions
Size changes (stable) |
Details of bundled changes.Comparing: b53ea6c...eb5ba18 react-dom
react-interactions
ReactDOM: size: 0.0%, gzip: 0.0% Size changes (experimental) |
8ac453f
to
e7ec5e1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome!!
remove debugger Add beforeRemoveInstance fix typi
e7ec5e1
to
26a362a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the names need to be changed, as every blur
event occurs on the activeElement
. I suggest something like onFocusedElementDetached
and onBeforeFocusedElementDetached
.
An alternative approach might be not to use dedicated events and instead overload the blur events with additional meta-information, but this was not deemed a desirable approach internally because other event responders that listen to blur also need to know about this additional meta-information.
I don't understand this reasoning. Which event responders listening to blur
would this impact? How does a new event avoid the issues?
@necolas Thanks. I've addressed all your feedback :) |
88b33d1
to
911ea82
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but needs more name updates
Fix typo remove dead code prettier
cae9f5c
to
eb5ba18
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this code now dispatches 2 blur
events when an element is removed. It's not quite what I was suggesting; I added inline comments related to renaming things and the events that get dispatched. LMK what you think
@@ -104,6 +104,12 @@ export type ChildSet = void; // Unused | |||
export type TimeoutHandle = TimeoutID; | |||
export type NoTimeout = -1; | |||
|
|||
type SelectionInformation = {| | |||
blurredActiveElement: null | HTMLElement, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blurredActiveElement: null | HTMLElement, | |
detachedActiveElement: null | HTMLElement, |
|
||
type FocusWithinProps = { | ||
disabled?: boolean, | ||
onFocusWithin?: (e: FocusEvent) => void, | ||
onBlurWithin?: (e: FocusEvent) => void, | ||
onFocusWithinChange?: boolean => void, | ||
onFocusWithinVisibleChange?: boolean => void, | ||
onDetachedVisibleNode?: (e: FocusEvent) => void, | ||
onBeforeFocusedElementDetached?: (e: FocusEvent) => void, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onBeforeBlurWithin
, only called when the element is going to be removed from the document
|
||
type FocusWithinProps = { | ||
disabled?: boolean, | ||
onFocusWithin?: (e: FocusEvent) => void, | ||
onBlurWithin?: (e: FocusEvent) => void, | ||
onFocusWithinChange?: boolean => void, | ||
onFocusWithinVisibleChange?: boolean => void, | ||
onDetachedVisibleNode?: (e: FocusEvent) => void, | ||
onBeforeFocusedElementDetached?: (e: FocusEvent) => void, | ||
onFocusedElementDetached?: (e: FocusEvent) => void, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wouldn't be needed, onBlurWithin
payload can include the elementAttached
data
| 'focusedelementdetached' | ||
| 'beforefocusedelementdetached'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced with beforeblurwithin
function dispatchBeforeActiveElementBlur(element: HTMLElement): void { | ||
const targtInstance = getClosestInstanceFromNode(element); | ||
((selectionInformation: any): SelectionInformation).blurredActiveElement = element; | ||
dispatchFlareDetachedBlurEvent(false, targtInstance, element); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function dispatchBeforeActiveElementBlur(element: HTMLElement): void { | |
const targtInstance = getClosestInstanceFromNode(element); | |
((selectionInformation: any): SelectionInformation).blurredActiveElement = element; | |
dispatchFlareDetachedBlurEvent(false, targtInstance, element); | |
} | |
function dispatchBeforeDetachedBlur(target: HTMLElement): void { | |
const targtInstance = getClosestInstanceFromNode(element); | |
selectionInformation.detachedActiveElement = target; | |
dispatchEventForResponderEventSystem( | |
'beforeblur', | |
targetInstance, | |
({ | |
target, | |
timeStamp: Date.now(), | |
}: any), | |
target, | |
RESPONDER_EVENT_SYSTEM | IS_PASSIVE, | |
); |
const blurredActiveElement = (selectionInformation: any) | ||
.blurredActiveElement; | ||
if (blurredActiveElement !== null) { | ||
dispatchActiveElementBlur(blurredActiveElement); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const blurredActiveElement = (selectionInformation: any) | |
.blurredActiveElement; | |
if (blurredActiveElement !== null) { | |
dispatchActiveElementBlur(blurredActiveElement); | |
} | |
const activeElementDetached = (selectionInformation: any).activeElementDetached; | |
if (activeElementDetached !== null) { | |
dispatchDetachedBlur(activeElementDetached); | |
} |
function dispatchActiveElementBlur( | ||
node: Instance | TextInstance | SuspenseInstance, | ||
): void { | ||
dispatchFlareDetachedBlurEvent(true, null, ((node: any): HTMLElement)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function dispatchActiveElementBlur( | |
node: Instance | TextInstance | SuspenseInstance, | |
): void { | |
dispatchFlareDetachedBlurEvent(true, null, ((node: any): HTMLElement)); | |
} | |
function dispatchDetachedBlur(target) { | |
const targtInstance = getClosestInstanceFromNode(target); | |
dispatchEventForResponderEventSystem( | |
'blur', | |
targetInstance, | |
({ | |
isTargetAttached: false, | |
target, | |
timeStamp: Date.now(), | |
}: any), | |
target, | |
RESPONDER_EVENT_SYSTEM | IS_PASSIVE, | |
); |
((child: any): Document | Element), | ||
RESPONDER_EVENT_SYSTEM | IS_PASSIVE, | ||
); | ||
dispatchBeforeActiveElementBlur(((instance: any): HTMLElement)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dispatchBeforeActiveElementBlur(((instance: any): HTMLElement)); | |
dispatchBeforeDetachedBlur(((instance: any): HTMLElement)); |
@@ -100,6 +100,8 @@ export function hasSelectionCapabilities(elem) { | |||
export function getSelectionInformation() { | |||
const focusedElem = getActiveElementDeep(); | |||
return { | |||
// Used by Flare | |||
blurredActiveElement: null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blurredActiveElement: null, | |
activeElementDetached: null, |
This is a follow up to #17291. Specifically, these PR tackles three things:
onDetachedVisibleNode
->onBeforeFocusedElementDetached
onFocusedElementDetached
.beforeRemoveInstance
to correctly unify this logic (removing it from theremoveChild
andremoveChildFromContainer
methods)Why do we need two events? What is the for?
These callbacks are to help with our experimental internal accessibility work at Facebook. There are various ways to tackle this problem. I've been working closely with @tatermelon (Tatiana from the accessibility team) to guide this implementation internally and apply it.
The problem these tackle are when active elements (elements that are focused on the document) are unmounted. This is a big problem with accessibility and it's been a big problem that folks have tried to tackle for a while with hacks. The realism is that there's no real good way to do this in user-land today, which is evident by how many websites simply don't handle this use-case.
This problem is made more evident with the introduction of Suspense fallbacks, where content if focused for a spinner, but is then removed to show the actual real content after X time. We need some way to know that this has occurred, so we can re-align with the document and imperatively move focus to the right node.
Two events are essential here:
onBeforeFocusedElementDetached
fires before the active focused element is removed from the DOM. This is the perfect place to work out the "before" state, find what nodes are in view and do some calculations needed for the actual active blur event.onFocusedElementDetached
fires after all the changes have been committed and it's time to use the logic we stored in the before event, to work out what node we should imperatively focus next.