Skip to content

Commit

Permalink
React events: make nested Focus work as expected (#15421)
Browse files Browse the repository at this point in the history
This patch makes a change to the Focus module so that it only reports
focus/blur on the host node that's a direct child of the event component. This
brings the expected behaviour in line with the browser default of focus/blur
events not bubbling for Pressable.
  • Loading branch information
necolas authored Apr 16, 2019
1 parent 4221565 commit c73ab39
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 39 deletions.
61 changes: 22 additions & 39 deletions packages/react-events/src/Focus.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,67 +50,39 @@ function createFocusEvent(
}

function dispatchFocusInEvents(
event: null | ReactResponderEvent,
context: ReactResponderContext,
props: FocusProps,
state: FocusState,
) {
if (event != null) {
const {nativeEvent} = event;
if (
context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)
) {
return;
}
}
const target = ((state.focusTarget: any): Element | Document);
if (props.onFocus) {
const syntheticEvent = createFocusEvent(
'focus',
((state.focusTarget: any): Element | Document),
);
const syntheticEvent = createFocusEvent('focus', target);
context.dispatchEvent(syntheticEvent, props.onFocus, {discrete: true});
}
if (props.onFocusChange) {
const listener = () => {
props.onFocusChange(true);
};
const syntheticEvent = createFocusEvent(
'focuschange',
((state.focusTarget: any): Element | Document),
);
const syntheticEvent = createFocusEvent('focuschange', target);
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
}
}

function dispatchFocusOutEvents(
event: null | ReactResponderEvent,
context: ReactResponderContext,
props: FocusProps,
state: FocusState,
) {
if (event != null) {
const {nativeEvent} = event;
if (
context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)
) {
return;
}
}
const target = ((state.focusTarget: any): Element | Document);
if (props.onBlur) {
const syntheticEvent = createFocusEvent(
'blur',
((state.focusTarget: any): Element | Document),
);
const syntheticEvent = createFocusEvent('blur', target);
context.dispatchEvent(syntheticEvent, props.onBlur, {discrete: true});
}
if (props.onFocusChange) {
const listener = () => {
props.onFocusChange(false);
};
const syntheticEvent = createFocusEvent(
'focuschange',
((state.focusTarget: any): Element | Document),
);
const syntheticEvent = createFocusEvent('focuschange', target);
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
}
}
Expand All @@ -121,7 +93,7 @@ function unmountResponder(
state: FocusState,
): void {
if (state.isFocused) {
dispatchFocusOutEvents(null, context, props, state);
dispatchFocusOutEvents(context, props, state);
}
}

Expand All @@ -140,6 +112,8 @@ const FocusResponder = {
state: FocusState,
): boolean {
const {type, phase, target} = event;
const shouldStopPropagation =
props.stopPropagation === undefined ? true : props.stopPropagation;

// Focus doesn't handle capture target events at this point
if (phase === CAPTURE_PHASE) {
Expand All @@ -148,22 +122,31 @@ const FocusResponder = {
switch (type) {
case 'focus': {
if (!state.isFocused) {
state.focusTarget = target;
dispatchFocusInEvents(event, context, props, state);
// Limit focus events to the direct child of the event component.
// Browser focus is not expected to bubble.
let currentTarget = (target: any);
if (
currentTarget.parentNode &&
context.isTargetWithinEventComponent(currentTarget.parentNode)
) {
break;
}
state.focusTarget = currentTarget;
dispatchFocusInEvents(context, props, state);
state.isFocused = true;
}
break;
}
case 'blur': {
if (state.isFocused) {
dispatchFocusOutEvents(event, context, props, state);
dispatchFocusOutEvents(context, props, state);
state.isFocused = false;
state.focusTarget = null;
}
break;
}
}
return false;
return shouldStopPropagation;
},
onUnmount(
context: ReactResponderContext,
Expand Down
44 changes: 44 additions & 0 deletions packages/react-events/src/__tests__/Focus-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,50 @@ describe('Focus event responder', () => {
});
});

describe('nested Focus components', () => {
it('does not propagate events by default', () => {
const events = [];
const innerRef = React.createRef();
const outerRef = React.createRef();
const createEventHandler = msg => () => {
events.push(msg);
};

const element = (
<Focus
onBlur={createEventHandler('outer: onBlur')}
onFocus={createEventHandler('outer: onFocus')}
onFocusChange={createEventHandler('outer: onFocusChange')}>
<div ref={outerRef}>
<Focus
onBlur={createEventHandler('inner: onBlur')}
onFocus={createEventHandler('inner: onFocus')}
onFocusChange={createEventHandler('inner: onFocusChange')}>
<div ref={innerRef} />
</Focus>
</div>
</Focus>
);

ReactDOM.render(element, container);

outerRef.current.dispatchEvent(createFocusEvent('focus'));
outerRef.current.dispatchEvent(createFocusEvent('blur'));
innerRef.current.dispatchEvent(createFocusEvent('focus'));
innerRef.current.dispatchEvent(createFocusEvent('blur'));
expect(events).toEqual([
'outer: onFocus',
'outer: onFocusChange',
'outer: onBlur',
'outer: onFocusChange',
'inner: onFocus',
'inner: onFocusChange',
'inner: onBlur',
'inner: onFocusChange',
]);
});
});

it('expect displayName to show up for event component', () => {
expect(Focus.displayName).toBe('Focus');
});
Expand Down

0 comments on commit c73ab39

Please sign in to comment.