Skip to content
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

Event API: Add FocusScope surface #15487

Merged
merged 12 commits into from
Apr 25, 2019
Merged

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Apr 24, 2019

This adds a new FocusScope event API surface/responder. For now, FocusScope is a very basic implementation that only supports autoFocus, restoreFocus and trap. When you tab in a trapped FocusScope, you're limited to the scope of the event component. Pressing tab on the last focusable element of a FocusScope that is trapped will result in the focus moving to the first focusable element of the FocusScope. Furthermore, elements that aren't focusable (i.e. with tabIndex set to -1) will be skipped in all cases and elements nested in portals will traverse as expected via the React fiber tree rather than the DOM tree.

Example of FocusScope with focus trapping and a few other props:

<div className="Dialog">
  <div className="Dialog-inner">
    <FocusScope trap={true} autoFocus={true} restoreFocus={true}>
      <h2>Focus Trapped Modal</h2>
      <input placeholder="You can focus me" />
      <div className="FakeButton" tabIndex={0}>
        I am a div but can also be focused
      </div>
      <input placeholder="Focus is blocked" tabIndex={-1} />
      <Press onPress={close}>
        <div className="FakeButton" tabIndex={0}>Close Modal</div>
      </Press>
    </FocusScope>
  </div>
</div>;

Ref #15257

@sizebot
Copy link

sizebot commented Apr 24, 2019

ReactDOM: size: -0.1%, gzip: -0.0%

Details of bundled changes.

Comparing: 3f058de...410a786

react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react.development.js -0.4% -0.3% 107.29 KB 106.9 KB 27.11 KB 27.03 KB UMD_DEV
react.development.js -0.6% -0.4% 70.04 KB 69.65 KB 18.02 KB 17.95 KB NODE_DEV
React-dev.js -0.6% -0.3% 68.32 KB 67.92 KB 17.35 KB 17.29 KB FB_WWW_DEV

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js -0.0% -0.0% 815.95 KB 815.63 KB 185.98 KB 185.91 KB UMD_DEV
react-dom.production.min.js -0.1% -0.0% 103.69 KB 103.61 KB 33.69 KB 33.68 KB UMD_PROD
react-dom.profiling.min.js -0.1% -0.0% 106.84 KB 106.76 KB 34.67 KB 34.65 KB UMD_PROFILING
react-dom.development.js -0.0% -0.0% 810.44 KB 810.12 KB 184.45 KB 184.37 KB NODE_DEV
react-dom.production.min.js -0.1% -0.0% 103.69 KB 103.6 KB 33.14 KB 33.13 KB NODE_PROD
react-dom.profiling.min.js -0.1% -0.0% 107.01 KB 106.93 KB 33.98 KB 33.97 KB NODE_PROFILING
ReactDOM-dev.js -0.0% -0.0% 834.71 KB 834.45 KB 185.85 KB 185.77 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.2% 🔺+0.1% 341.26 KB 341.97 KB 63.22 KB 63.28 KB FB_WWW_PROD
ReactDOM-profiling.js +0.2% +0.1% 346.66 KB 347.36 KB 64.19 KB 64.25 KB FB_WWW_PROFILING
react-dom-unstable-fire.development.js -0.0% -0.0% 816.28 KB 815.96 KB 186.12 KB 186.04 KB UMD_DEV
react-dom-unstable-fire.production.min.js -0.1% -0.0% 103.71 KB 103.62 KB 33.7 KB 33.69 KB UMD_PROD
react-dom-unstable-fire.profiling.min.js -0.1% -0.0% 106.85 KB 106.77 KB 34.67 KB 34.66 KB UMD_PROFILING
react-dom-unstable-fire.development.js -0.0% -0.0% 810.76 KB 810.44 KB 184.58 KB 184.51 KB NODE_DEV
react-dom-unstable-fire.production.min.js -0.1% -0.0% 103.7 KB 103.62 KB 33.15 KB 33.14 KB NODE_PROD
react-dom-unstable-fire.profiling.min.js -0.1% -0.0% 107.03 KB 106.94 KB 33.99 KB 33.98 KB NODE_PROFILING
ReactFire-dev.js -0.0% -0.0% 833.89 KB 833.64 KB 185.83 KB 185.79 KB FB_WWW_DEV
ReactFire-prod.js 🔺+0.2% 0.0% 329.26 KB 329.96 KB 60.82 KB 60.84 KB FB_WWW_PROD
ReactFire-profiling.js +0.2% 0.0% 334.62 KB 335.32 KB 61.79 KB 61.81 KB FB_WWW_PROFILING
react-dom-test-utils.development.js 0.0% 0.0% 54.19 KB 54.19 KB 14.98 KB 14.98 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 10.52 KB 10.52 KB 3.89 KB 3.89 KB UMD_PROD
react-dom-test-utils.development.js 0.0% 0.0% 53.91 KB 53.91 KB 14.91 KB 14.91 KB NODE_DEV
react-dom-unstable-native-dependencies.development.js 0.0% 0.0% 60.76 KB 60.76 KB 15.85 KB 15.85 KB UMD_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% 0.0% 10.69 KB 10.69 KB 3.67 KB 3.67 KB UMD_PROD
react-dom-unstable-native-dependencies.development.js 0.0% 0.0% 60.43 KB 60.43 KB 15.72 KB 15.72 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% 0.0% 10.42 KB 10.42 KB 3.56 KB 3.57 KB NODE_PROD
react-dom-server.browser.development.js -0.2% -0.0% 136.73 KB 136.42 KB 35.92 KB 35.9 KB UMD_DEV
react-dom-server.browser.production.min.js -0.4% -0.2% 19.18 KB 19.09 KB 7.21 KB 7.2 KB UMD_PROD
react-dom-server.browser.development.js -0.2% -0.1% 132.86 KB 132.55 KB 34.99 KB 34.97 KB NODE_DEV
react-dom-server.browser.production.min.js -0.4% -0.2% 19.1 KB 19.02 KB 7.2 KB 7.19 KB NODE_PROD
ReactDOMServer-dev.js -0.2% -0.0% 134.95 KB 134.71 KB 34.61 KB 34.61 KB FB_WWW_DEV
ReactDOMServer-prod.js -0.5% -0.4% 47.14 KB 46.88 KB 10.8 KB 10.76 KB FB_WWW_PROD
react-dom-server.node.development.js -0.2% -0.1% 134.8 KB 134.49 KB 35.54 KB 35.51 KB NODE_DEV
react-dom-server.node.production.min.js -0.4% -0.2% 19.96 KB 19.88 KB 7.51 KB 7.49 KB NODE_PROD
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.66 KB 3.66 KB 1.45 KB 1.45 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.1% 1.21 KB 1.21 KB 705 B 706 B UMD_PROD
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.49 KB 3.49 KB 1.41 KB 1.41 KB NODE_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.2% 1.05 KB 1.05 KB 636 B 637 B NODE_PROD
react-dom-unstable-fizz.node.development.js 0.0% +0.1% 3.74 KB 3.74 KB 1.43 KB 1.43 KB NODE_DEV
react-dom-unstable-fizz.node.production.min.js 0.0% 🔺+0.2% 1.1 KB 1.1 KB 666 B 667 B NODE_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js -0.0% -0.0% 501.95 KB 501.7 KB 107.06 KB 107.02 KB UMD_DEV
react-test-renderer.production.min.js -0.1% -0.0% 61.79 KB 61.71 KB 18.97 KB 18.96 KB UMD_PROD
react-test-renderer.development.js -0.0% -0.0% 497.49 KB 497.24 KB 105.95 KB 105.92 KB NODE_DEV
react-test-renderer.production.min.js -0.1% -0.1% 61.47 KB 61.39 KB 18.82 KB 18.8 KB NODE_PROD
ReactTestRenderer-dev.js -0.0% -0.0% 508.24 KB 507.99 KB 105.66 KB 105.62 KB FB_WWW_DEV
react-test-renderer-shallow.development.js -0.9% -0.6% 41.88 KB 41.49 KB 10.71 KB 10.64 KB UMD_DEV
react-test-renderer-shallow.production.min.js -0.7% -0.3% 11.63 KB 11.54 KB 3.55 KB 3.54 KB UMD_PROD
react-test-renderer-shallow.development.js -1.1% -0.7% 36.11 KB 35.72 KB 9.36 KB 9.29 KB NODE_DEV
react-test-renderer-shallow.production.min.js -0.7% -0.3% 11.81 KB 11.73 KB 3.67 KB 3.66 KB NODE_PROD
ReactShallowRenderer-dev.js -1.1% -0.8% 35.08 KB 34.68 KB 8.72 KB 8.65 KB FB_WWW_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js -0.1% -0.0% 485.96 KB 485.72 KB 102.77 KB 102.74 KB NODE_DEV
react-reconciler.production.min.js -0.1% -0.1% 61.51 KB 61.42 KB 18.38 KB 18.37 KB NODE_PROD
react-reconciler-persistent.development.js -0.1% -0.0% 483.87 KB 483.63 KB 101.89 KB 101.86 KB NODE_DEV
react-reconciler-persistent.production.min.js -0.1% -0.1% 61.52 KB 61.44 KB 18.39 KB 18.38 KB NODE_PROD
react-reconciler-reflection.development.js -2.0% -1.0% 19.18 KB 18.79 KB 5.98 KB 5.92 KB NODE_DEV
react-reconciler-reflection.production.min.js 0.0% 🔺+0.2% 2.43 KB 2.43 KB 1.09 KB 1.09 KB NODE_PROD

react-events

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-events.development.js -27.1% -10.2% 1.59 KB 1.16 KB 687 B 617 B UMD_DEV
react-events.production.min.js -20.4% -9.5% 857 B 682 B 476 B 431 B UMD_PROD
react-events.development.js -30.7% -11.2% 1.4 KB 996 B 625 B 555 B NODE_DEV
react-events.production.min.js -26.6% -11.8% 698 B 512 B 400 B 353 B NODE_PROD
ReactEvents-dev.js -32.0% -11.3% 1.37 KB 956 B 604 B 536 B FB_WWW_DEV
ReactEvents-prod.js -40.0% -15.3% 1.12 KB 687 B 484 B 410 B FB_WWW_PROD
react-events-Press.development.js n/a n/a 0 B 17.57 KB 0 B 4.49 KB UMD_DEV
react-events-Press.production.min.js n/a n/a 0 B 6.8 KB 0 B 2.57 KB UMD_PROD
react-events-Press.development.js n/a n/a 0 B 17.4 KB 0 B 4.45 KB NODE_DEV
react-events-Press.production.min.js n/a n/a 0 B 6.64 KB 0 B 2.51 KB NODE_PROD
ReactEventsPress-dev.js 0.0% 0.0% 15.46 KB 15.46 KB 3.71 KB 3.71 KB FB_WWW_DEV
react-events-Hover.development.js n/a n/a 0 B 9.45 KB 0 B 2.33 KB UMD_DEV
react-events-Hover.production.min.js n/a n/a 0 B 3.89 KB 0 B 1.45 KB UMD_PROD
react-events-Hover.development.js n/a n/a 0 B 9.28 KB 0 B 2.28 KB NODE_DEV
react-events-Hover.production.min.js n/a n/a 0 B 3.73 KB 0 B 1.41 KB NODE_PROD
ReactEventsHover-dev.js 0.0% 0.0% 9.29 KB 9.3 KB 2.29 KB 2.29 KB FB_WWW_DEV
react-events-Focus.development.js n/a n/a 0 B 4.21 KB 0 B 1.32 KB UMD_DEV
react-events-Focus.production.min.js n/a n/a 0 B 1.75 KB 0 B 807 B UMD_PROD
react-events-Focus.development.js n/a n/a 0 B 4.04 KB 0 B 1.27 KB NODE_DEV
react-events-Focus.production.min.js n/a n/a 0 B 1.58 KB 0 B 738 B NODE_PROD
ReactEventsFocus-dev.js +0.1% +0.1% 3.96 KB 3.96 KB 1.24 KB 1.25 KB FB_WWW_DEV
react-events-FocusScope.development.js n/a n/a 0 B 4.39 KB 0 B 1.38 KB UMD_DEV
react-events-FocusScope.production.min.js n/a n/a 0 B 1.82 KB 0 B 916 B UMD_PROD
react-events-FocusScope.development.js n/a n/a 0 B 4.21 KB 0 B 1.33 KB NODE_DEV
react-events-FocusScope.production.min.js n/a n/a 0 B 1.64 KB 0 B 850 B NODE_PROD
ReactEventsFocusScope-dev.js n/a n/a 0 B 4.16 KB 0 B 1.3 KB FB_WWW_DEV
ReactEventsFocusScope-prod.js n/a n/a 0 B 3.32 KB 0 B 1.04 KB FB_WWW_PROD
react-events-Swipe.development.js n/a n/a 0 B 8.41 KB 0 B 2.57 KB UMD_DEV
react-events-Swipe.production.min.js n/a n/a 0 B 3.52 KB 0 B 1.61 KB UMD_PROD
react-events-Swipe.development.js n/a n/a 0 B 8.24 KB 0 B 2.54 KB NODE_DEV
react-events-Swipe.production.min.js n/a n/a 0 B 3.36 KB 0 B 1.55 KB NODE_PROD
ReactEventsSwipe-dev.js +0.1% 0.0% 6.33 KB 6.33 KB 1.8 KB 1.8 KB FB_WWW_DEV
react-events-Drag.development.js n/a n/a 0 B 7.95 KB 0 B 2.46 KB UMD_DEV
react-events-Drag.production.min.js n/a n/a 0 B 3.38 KB 0 B 1.53 KB UMD_PROD
react-events-Drag.development.js n/a n/a 0 B 7.79 KB 0 B 2.42 KB NODE_DEV
react-events-Drag.production.min.js n/a n/a 0 B 3.22 KB 0 B 1.47 KB NODE_PROD
ReactEventsDrag-dev.js +0.1% +0.1% 6.04 KB 6.04 KB 1.72 KB 1.72 KB FB_WWW_DEV

Generated by 🚫 dangerJS

packages/react-events/src/FocusScope.js Outdated Show resolved Hide resolved
@trueadm trueadm merged commit 64e3da2 into facebook:master Apr 25, 2019
@trueadm trueadm deleted the add-focus-scope branch April 25, 2019 01:01
type === 'textarea' ||
type === 'input' ||
type === 'object' ||
type === 'select'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list is missing a few elements: iframe, embed and contenteditable.
Should this component skip subtrees that have the inert attribute set on the root?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can deal with inert in a future PR. It's a bit more involved as what if a parent tree has inert and its sub-trees have the FocusScope?

Copy link
Contributor

@giuseppeg giuseppeg Apr 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that is not straightforward, I guess that FocusScope should noop when an ancestor has the inert attribute i.e. (force) disable trap and restore.

I am afraid that, because of portals, React needs to polyfill inert and can't rely on the native implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be pretty great if React polyfilled inert anyway, as it is only supported in Chrome behind a flag.

Copy link
Contributor

@necolas necolas Apr 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WICG have a polyfill https://github.com/WICG/inert. There doesn't seem to be vendor agreement on whether to proceed with this attribute or use a different approach. I don't think we need to be too concerned about it here at this stage

state: FocusScopeState,
) {
if (props.restoreFocus) {
state.nodeToRestore = document.activeElement;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this component to work inside of iframes this should be the ownerDocument of the current node.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback. I addressed these points in #15496 :)

@Andarist
Copy link
Contributor

Work around the Events API seems to be even more exciting for me as a developer than Concurrent Mode etc, and I'm stoked about the latter 🔥 .

Great work!

@mattiamanzati
Copy link

@trueadm Will the focusscope remain only a react-dom thing or will it be expanded also over react native eventually? I would love that! Thanks for making this happening to everyone contributing!

@trueadm
Copy link
Contributor Author

trueadm commented Apr 26, 2019

@mattiamanzati it’s very early to say but that’s definitely our long term ambition with this new event API.

@giuseppeg
Copy link
Contributor

giuseppeg commented Jun 7, 2019

@trueadm FWIW back then I forgot to mention that in order for elements to be focusable they should be visible. At work I do this:

export function getFocusableElements(node: HTMLElement): Array<HTMLElement> {
  return Array.prototype.filter.call(
    node.querySelectorAll(FOCUSABLE_ELEMENTS_SELECTOR),
    child =>
      !(child.inert || child.hasAttribute('inert') || child.getAttribute('tabindex') === '-1') &&
      !!(child.offsetWidth || child.offsetHeight || child.getClientRects().length),
  )
}

@trueadm
Copy link
Contributor Author

trueadm commented Jun 7, 2019

We don’t want to use query selectors. They don’t work with suspense and portals correctly.

@giuseppeg
Copy link
Contributor

@trueadm oh yea definitely, that part is not relevant. The filter function filters out non visible elements this way !!(child.offsetWidth || child.offsetHeight || child.getClientRects().length). Obviously you'd need the actual DOM node and this thing can trigger layout

@ryankshaw
Copy link

sorry, noob question, I see this is merged to master, does that mean it is this something we can expect to be in react 17 but won't be in any 16.x version?

@trueadm
Copy link
Contributor Author

trueadm commented Oct 4, 2019

@ryankshaw No, this won't be in React 17. It's not even in React's master branch now and has since been removed as it was only a short experiment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants