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

[Flare] First pass at an implementation of experimental FocusManager API #15849

Closed
wants to merge 15 commits into from

Conversation

devongovett
Copy link
Contributor

This is a first pass at implementing the FocusManager API specified in reactjs/rfcs#109. It goes along with the already implemented FocusScope in react-events.

I adjusted the API slightly, and will be updating the RFC to match. In particular, the focusNext and focusNextTabStop methods have been merged, along with the corresponding previous methods. There is now an options object that can be passed to specify whether the element must be tabbable or only focusable. In addition, there are options to enable wrapping behavior at the ends of a scope, along with starting the search from a particular element rather than only document.activeElement. The methods also return the element they focused if any, which allows the calling code to do something with that information.

The API is now:

type FocusManagerOptions = {
  from?: HTMLElement,
  tabbable?: boolean,
  wrap?: boolean,
};

interface FocusManager {
  focusNext(opts: FocusManagerOptions): HTMLElement,
  focusPrevious(opts: FocusManagerOptions): HTMLElement
}

Perhaps we will also want focusFirst and focusLast methods at some point as well. Happy to hear whatever feedback you have on the API.

Most of the implementation is moved from the existing code for FocusScope. There were a few things I wasn't sure about though:

  • How it should be exposed. For now, it is available as ReactDOM.unstable_FocusManager if the event API feature flag is enabled. Perhaps it should be exposed as part of react-events instead of react-dom, but I didn't see a good way of exposing the necessary internals.
  • How to detect FocusScope event components in the tree. For now, I added an isFocusScope option to the event responder, but this is kind of a hack. I didn't want react-dom to have a dependency on react-events, so I wasn't sure how else to do it.
  • What should happen if you call focusNext({from: node}), where node contains focusable children? Should it focus the first item within that node, or go to the next focusable element within a sibling?

Thanks in advance for your feedback! 🙏

@sizebot
Copy link

sizebot commented Jun 10, 2019

Details of bundled changes.

Comparing: 824e9be...ef25df7

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js 0.0% 0.0% 114.17 KB 114.17 KB 36.04 KB 36.05 KB NODE_PROFILING
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
ReactDOM-dev.js -0.2% -0.2% 922.95 KB 921.36 KB 204.53 KB 204.21 KB FB_WWW_DEV
react-dom-unstable-fire.development.js -0.2% -0.1% 899.55 KB 897.98 KB 204.21 KB 204.03 KB UMD_DEV
react-dom-unstable-fire.production.min.js 🔺+0.1% 🔺+0.2% 110.58 KB 110.64 KB 35.6 KB 35.68 KB UMD_PROD
react-dom-unstable-fire.profiling.min.js 0.0% +0.2% 113.91 KB 113.96 KB 36.64 KB 36.71 KB UMD_PROFILING
react-dom-unstable-fire.development.js -0.2% -0.1% 893.83 KB 892.27 KB 202.69 KB 202.41 KB NODE_DEV
react-dom-unstable-fire.production.min.js 0.0% -0.0% 110.66 KB 110.66 KB 35.16 KB 35.15 KB NODE_PROD
react-dom.development.js -0.2% -0.1% 899.2 KB 897.64 KB 204.06 KB 203.89 KB UMD_DEV
react-dom-unstable-fire.profiling.min.js 0.0% 0.0% 114.19 KB 114.19 KB 36.05 KB 36.06 KB NODE_PROFILING
react-dom.production.min.js 🔺+0.1% 🔺+0.2% 110.57 KB 110.63 KB 35.59 KB 35.67 KB UMD_PROD
ReactFire-dev.js -0.2% -0.1% 922.16 KB 920.56 KB 204.41 KB 204.12 KB FB_WWW_DEV
react-dom-server.browser.production.min.js 0.0% -0.0% 19.45 KB 19.45 KB 7.28 KB 7.28 KB UMD_PROD
react-dom.profiling.min.js 0.0% +0.2% 113.89 KB 113.95 KB 36.63 KB 36.71 KB UMD_PROFILING
ReactFire-prod.js -0.4% -0.7% 363.51 KB 361.9 KB 66.43 KB 65.99 KB FB_WWW_PROD
react-dom.development.js -0.2% -0.1% 893.49 KB 891.93 KB 202.55 KB 202.27 KB NODE_DEV
ReactFire-profiling.js -0.4% -0.7% 370.38 KB 368.77 KB 67.74 KB 67.29 KB FB_WWW_PROFILING
react-dom.production.min.js 0.0% -0.0% 110.64 KB 110.64 KB 35.15 KB 35.14 KB NODE_PROD
ReactDOM-prod.js -0.4% -0.6% 375.53 KB 373.92 KB 68.82 KB 68.44 KB FB_WWW_PROD
ReactDOMServer-prod.js 0.0% -0.0% 48.36 KB 48.36 KB 11.12 KB 11.12 KB FB_WWW_PROD
ReactDOM-profiling.js -0.4% -0.6% 382.44 KB 380.83 KB 70.14 KB 69.75 KB FB_WWW_PROFILING
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 3.88 KB 3.88 KB 1.51 KB 1.51 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 10.48 KB 10.48 KB 3.58 KB 3.58 KB NODE_PROD
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.81 KB 3.81 KB 1.54 KB 1.53 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 10.95 KB 10.95 KB 4.01 KB 4 KB UMD_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.1% 1.21 KB 1.21 KB 706 B 705 B UMD_PROD
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.64 KB 3.64 KB 1.49 KB 1.49 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 10.71 KB 10.71 KB 3.94 KB 3.94 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1.05 KB 1.05 KB 637 B 636 B NODE_PROD

react-events

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-events-hover.development.js 0.0% -0.0% 8.79 KB 8.79 KB 2.11 KB 2.11 KB UMD_DEV
react-events-drag.development.js 0.0% -0.1% 5.93 KB 5.93 KB 1.67 KB 1.67 KB UMD_DEV
react-events-hover.production.min.js 0.0% -0.1% 3.82 KB 3.82 KB 1.41 KB 1.41 KB UMD_PROD
react-events-drag.production.min.js 0.0% -0.1% 2.56 KB 2.56 KB 1.16 KB 1.16 KB UMD_PROD
react-events.development.js 0.0% -0.1% 1.31 KB 1.31 KB 707 B 706 B UMD_DEV
react-events-focus-scope.development.js +286.1% +285.5% 4.25 KB 16.41 KB 1.32 KB 5.1 KB UMD_DEV
react-events.production.min.js 0.0% -0.2% 682 B 682 B 431 B 430 B UMD_PROD
react-events-focus-scope.production.min.js 🔺+118.9% 🔺+93.7% 1.82 KB 3.98 KB 956 B 1.81 KB UMD_PROD
react-events-focus-scope.development.js +485.1% +513.9% 4.06 KB 23.76 KB 1.27 KB 7.79 KB NODE_DEV
react-events.production.min.js 0.0% -0.3% 512 B 512 B 354 B 353 B NODE_PROD
react-events-focus-scope.production.min.js 🔺+257.8% 🔺+196.8% 1.62 KB 5.81 KB 874 B 2.53 KB NODE_PROD
react-events-focus.development.js 0.0% -0.1% 7.19 KB 7.19 KB 1.84 KB 1.84 KB UMD_DEV
react-events-scroll.development.js 0.0% -0.1% 3.92 KB 3.92 KB 1.22 KB 1.22 KB UMD_DEV
react-events-focus.production.min.js 0.0% -0.1% 3.09 KB 3.09 KB 1.18 KB 1.18 KB UMD_PROD
ReactEventsFocusScope-dev.js +429.8% +428.5% 4.04 KB 21.38 KB 1.25 KB 6.58 KB FB_WWW_DEV
ReactEventsFocusScope-prod.js 🔺+239.4% 🔺+182.8% 3.32 KB 11.28 KB 1.06 KB 3 KB FB_WWW_PROD
react-events-scroll.development.js 0.0% -0.1% 3.73 KB 3.73 KB 1.16 KB 1.16 KB NODE_DEV
react-events-focus.production.min.js 0.0% -0.1% 2.91 KB 2.91 KB 1.1 KB 1.1 KB NODE_PROD
react-events-scroll.production.min.js 0.0% -0.1% 1.59 KB 1.59 KB 797 B 796 B NODE_PROD
react-events-hover.development.js 0.0% -0.0% 8.61 KB 8.61 KB 2.06 KB 2.05 KB NODE_DEV
react-events-hover.production.min.js 0.0% -0.1% 3.64 KB 3.64 KB 1.34 KB 1.33 KB NODE_PROD
react-events-swipe.development.js 0.0% -0.1% 6.21 KB 6.21 KB 1.72 KB 1.72 KB UMD_DEV
react-events-press.production.min.js 0.0% -0.1% 8.97 KB 8.97 KB 3.18 KB 3.18 KB UMD_PROD
react-events-swipe.production.min.js 0.0% -0.1% 2.65 KB 2.65 KB 1.2 KB 1.19 KB UMD_PROD
react-events-swipe.development.js 0.0% -0.1% 6.03 KB 6.03 KB 1.68 KB 1.68 KB NODE_DEV
react-events-press.production.min.js 0.0% -0.1% 8.78 KB 8.78 KB 3.12 KB 3.12 KB NODE_PROD
react-events-swipe.production.min.js 0.0% -0.1% 2.47 KB 2.47 KB 1.14 KB 1.14 KB NODE_PROD

Generated by 🚫 dangerJS

@devongovett
Copy link
Contributor Author

I'm not exactly sure why the test_build check failed. The test_source check is passing... It's getting Cannot assign to read only property 'enableEventAPI' of object '#<Object>'.

@tjallingt
Copy link
Contributor

tjallingt commented Jun 10, 2019

I think might be running tests on the bundles and since the feature flags aren't bundled it is failing.
I'm not sure but based on what i've seen in other PR's I believe that the fix is to rename FocusManager-test.js to FocusManager-test.internal.js.

@devongovett
Copy link
Contributor Author

Ah ok, thanks @tjallingt!

@devongovett
Copy link
Contributor Author

That seems to have fixed everything except fire. Maybe there is another flag for that somewhere...

@tjallingt
Copy link
Contributor

tjallingt commented Jun 10, 2019

React Fire is a reimplementation of react-dom right? Since FocusManager is exported on react-dom you probably also need to change the export of Fire to include unstable_FocusManager to make it available to the fire tests.

EDIT: the changes you made to ReactDOM.js need to be replicated in this file https://github.com/facebook/react/blob/master/packages/react-dom/src/fire/ReactFire.js

@devongovett
Copy link
Contributor Author

Awesome, thanks for the help @tjallingt! 🎉

@trueadm
Copy link
Contributor

trueadm commented Jun 10, 2019

This looks awesome and I'll take a proper deep dive tomorrow. One thing I was thinking though - why is FocusManager coupled with ReactDOM? I thought it might make sense to couple it with FocusScope. i.e.

import { FocusScope, FocusManager } from 'react-events/focus-scope';

or

import FocusScope from 'react-events/focus-scope';
const FocusManager = FocusScope.Manager;

You wouldn't really ever use one without the other too, but also it addresses some of the code-smells you had in regards to ReactDOM having to know implementation details relating to specific event responder modules (thus thus .isFocusScope hack). Moving the logic out of ReactDOM would also reduce the side of the core for those who don't need to pay the size cost of the FocusManager and its logic on initial render.

Furthermore, this isn't only relevant for ReactDOM. We might want the same high-level APIs to be exposed to other surfaces, such as React Native and React VR. In which case, they'd both be consuming the same APIs - so it doesn't make as much sense to be importing the manager from ReactDOM.

If they are tightly coupled, then <FocusScope> could actually work with the FocusManager. Imagine: Left Panel - Centre Content - Right Panel. Each of those surfaces might have their own <FocuScope>. Pressing right/left would move focus to another manager, in RTL, this would reverse. Thus a better API might be (using a key on the focus scopes). When you press left or right on your keyboard, you can then move between focus areas:

import {FocusScope, FocusManager} from 'react-events/focus-scope'

function LeftPanel() {
  function focusLeft() {
    if (isRTL) {
      FocusManager.focusScopeByKey('centre-panel');
    } else {
      FocusManager.focusScopeByKey('left-content');
    }
  }
  function focusRight() {
    if (isRTL) {
      FocusManager.focusScopeByKey('right-panel');
    } else {
      FocusManager.focusScopeByKey('centre-content');
    }
  }
  return (
    <FocusScope key="left-panel" onLeftArrow={focusLeft} onRightArrow={focusRight}>
      ....
    </FocusScope>
  )
}

Using keys on the page seems like a far more powerful mechanism for controlling rich interactions with accessibility than simply doing focusNext. It also allows for UI patterns that traditionally hard to interact with, like tables and forms. You could use focusElementByKey and focusScopeByKey, which allows moving focus to explicitly outlined UI components on the page.

@devongovett
Copy link
Contributor Author

@trueadm yeah I totally agree - it should ideally be in react-events. I just couldn't find a good way to expose the necessary internals from react-dom for react-events to access, e.g. getClosestInstanceFromNode etc. In the existing event components in react-events, there is a context which allows access to those things, but since FocusManager is a singleton it doesn't have access to that context. So we'd need another way of exposing those internals.

If you have any guidance around how to do that, I'd be happy to update it.

@trueadm
Copy link
Contributor

trueadm commented Jun 10, 2019

@devongovett One way would be to make it explicit:

let focusManager = null;
<FocusScope getFocusManager={fm => focusManager = fm}>
   ...
</FocusScope>

I also updated my post. Not sure if you saw the updated version. :)

@devongovett
Copy link
Contributor Author

Definitely a possibility - I thought of maybe using a ref for it (make a ref to the FocusScope). It makes the API a little harder to use though IMO. Currently, the scope is found automatically based on the node you pass in, or the active element if no option is given. This means you don't need to know what scope an element is in, just that you need to move the focus.

Also, sometimes a scope is defined further up the tree, but the events to move focus (e.g. arrow key handlers) are further down. Not the worst thing in the world, but it would require either manual propagation of those key events up to the parent with the FocusScope, or passing a FocusManager instance down the tree to the component that needs it.

I guess this is an API ergonomics question more than anything.

@trueadm
Copy link
Contributor

trueadm commented Jun 10, 2019

@devongovett I guess I was just hinting at a ref there. Automatic scope is nice, but really, if the manager is tied to the current FocusScope, that magic might actually be a problem and causes bugs further on down the line. If you want to do focusFirst or anything like that, it's definitely coupled to a specific FocusScope, which means in many ways, FocusManager isn't actually global, but more of a context-like object.

@devongovett
Copy link
Contributor Author

Just saw your updated post. I think both usecases would be valuable to support. One usecase is to move focus within a scope: for example a keyboard navigable list component with up and down arrow support. In many cases, you don't know how many items will appear in that list, and shouldn't need to manually pass the keys of each item to focusNext and focusPrevious just to move within the scope.

Another usecase is to move focus between focus scopes like in your example of panels. I think the idea of using keys for that seems good. In addition, focusing a particular element by key might also be useful in case you know exactly which element to focus and want to avoid refs.

Definitely open to expanding on this API, I was mostly focused on the first usecase for this initial implementation.

@devongovett
Copy link
Contributor Author

Perhaps a hook like API to get the parent focus manager?

let focusManager = useFocusManager();

let onKeyDown = e => {
  if (e.key === 'ArrowRight') {
    focusManager.focusNext();
  }
};

@facebook facebook deleted a comment from tjallingt Jun 10, 2019
@trueadm
Copy link
Contributor

trueadm commented Jun 10, 2019

Perhaps a hook like API to get the parent focus manager?

@devongovett That's a neat way. We can totally do that using hooks :)

Definitely open to expanding on this API, I was mostly focused on the first usecase for this initial implementation.

That's fine. I just feel we should stay away from using DOM references. It makes this API hard to bring over to RN and other surfaces as soon as we make it about the DOM (which is definitely something we want to look into doing next half).

@devongovett
Copy link
Contributor Author

Alright. I'll try experimenting with a hooks based API to get the focus manager. Would you recommend implementing that with context? Can event components expose a value to context somehow? My knowledge of react internals is very limited so far. 😉

@trueadm
Copy link
Contributor

trueadm commented Jun 10, 2019

@devongovett The idea was to always have a way of using event components via hooks. We never focused on that this half, but the createEventComponent API always allowed for props which would be the default props passed to the event component. i.e.:

function useFocusScope() {
  const focusManager = useRef(null);
  const FocusScope = useEventComponent(FocusScope, { ref: focusManager });

  return [FocusScope, focusManager ];
}

const [FocusScope, focusMananger] = useFocusScope();

<FocusScope>
   ...
</FocusScope>

Does that make sense? I can add a React.unstable_useReactEventComponent this week. I already designed this ages ago, like I said, so this should make more sense here.

Alternatively, there can be another hooks API that would look up event components in the tree (kind of like you mentioned above), but that might be more difficult to design without it being too fragile right now.

@devongovett
Copy link
Contributor Author

devongovett commented Jun 10, 2019

Oh, so rather than importing FocusScope and using it directly, you'd have to call some sort of factory function for it? Wouldn't that mean you still have to pass the focus manager it returns down the tree yourself? Maybe I'm misunderstanding.

I guess I was more thinking you'd use FocusScope the same way as is currently implemented, and import a useFocusManager hook to use in child components to access the focus manager for the current subtree (like useContext).

import {FocusScope, useFocusManager} from 'react-events/focus-scope';

function Parent() {
  return (
    <FocusScope>
      <Child />
    </FocusScope>
  );
}

function Child() {
  let focusManager = useFocusManager();
  let onKeyDown = e => {
    if (e.key === 'ArrowRight') {
      focusManager.focusNext();
   }
  };

  return <div onKeyDown={onKeyDown}>Child</div>;
}

If you needed the focus manager in a component that rendered the scope (e.g. Parent), you could create a ref to the FocusScope directly.

@trueadm
Copy link
Contributor

trueadm commented Jun 10, 2019

@devongovett Yeah, that was what I meant at the end of my comment. It's definitely possible to do, just will require some thought on how to best do it. :)

Give me tonight/tomorrow to think about such an API.

@trueadm
Copy link
Contributor

trueadm commented Jun 11, 2019

Thinking about this more, I feel that you'd probably use React context to capture the focus manager instead of introducing new primitive hooks into React. Introducing new primitive hooks that are so coupled to a specific event component implementation is risky and somewhat brittle to change – so I think think we should avoid that.

@devongovett
Copy link
Contributor Author

Yeah I agree. What if react implemented it internally using context though:

const FocusScopeInternal = React.unstable_createEventComponent(
  FocusScopeResponder,
  'FocusScope',
);

const FocusContext = React.createContext();

export function FocusScope(props) {
  let focusManagerContext = {/* ... */};

  return (
    <FocusContext.Provider value={focusManagerContext}>
      <FocusScopeInternal {...props} />
    </FocusContext.Provider>
}

export function useFocusManager() {
  return useContext(FocusContext);
}

I guess the only small downside is that you'd see two components in dev tools instead of just one, but maybe there is a way to hide that.

@trueadm
Copy link
Contributor

trueadm commented Jun 11, 2019

@devongovett I don't think we want to start embedding context into FocusScope. However, FocusScope.js could export a functional component that does make use of context.

export function FocusScopeWithManager(props) {
  return React.createElement(
    FocusContext.Provider, {
      value: focusManagerContext,
      children: React.createElement(FocusScope, props);
    }
  );
}

The reason is so that people can still access the original event component to be used in other APIs (like hooks).

@devongovett
Copy link
Contributor Author

Aside from basic focus containment, I think usage with a focus manager will be the most common use for a FocusScope. So not sure why we'd want two different components. I think that might get confusing.

@trueadm
Copy link
Contributor

trueadm commented Jun 11, 2019

@devongovett They are different components though: one is a functional component and the other is an event component. Event components can be consumed in different ways, so they both need to be exported from the event module. Unless we add a way of somehow mutating the children passed to an event component, but that means a bigger change in the React Flare architecture.

@devongovett
Copy link
Contributor Author

devongovett commented Jun 16, 2019

Isn't the focus manager meant to be the imperative controller for the focus scope? And aren't refs basically meant to expose imperative controllers for components? For DOM components, that's the underlying DOM node sure, but you can also call imperative methods on it. useImperativeHandle seems like it was designed for exactly this purpose. I guess I'm not sure about the overhead you mentioned. We could go with another name, but this seems to me like exactly what refs were meant for. The other event components don't currently expose a controller object, but perhaps they might in the future? I feel like it's more confusing to have separate ref props for each one rather than a consistent interface for accessing imperative controllers for components in general.

Either way it won't get rid of the function wrapper component anyway because I still need to expose the controller to context.

@trueadm
Copy link
Contributor

trueadm commented Jun 16, 2019

@devongovett Let me explain what I'm saying with some code:

let ListContainer = (props) => {
  return (
    <FocusScope>
      <List {...props} />
    </FocusScope>
  );
};

let List = (props) => {
  const mananger = useFocusManager();

  let onKeyDown = (e) => {
    switch (e.key) {
      case 'ArrowUp':
        e.preventDefault();
        mananger.focusPrevious();
        break;
      case 'ArrowDown':
        e.preventDefault();
        mananger.focusNext();
        break;
    }
  };

  return (
    <ul onKeyDown={onKeyDown}>
      <li tabIndex="0">Item 1</li>
      <li tabIndex="0">Item 2</li>
      <li tabIndex="0">Item 3</li>
    </ul>
  );
};

The implementation of these looks like this:

const FocusScopeImpl = React.unstable_createEventComponent(FocusScopeResponder, 'FocusScope');
const FocusManagerContext = React.createContext(null);

function createFocusManager() {
  return {
    // ...
  };
}

export function FocusScope({ autoFocus, contain, children }) {
  const focusManager = useRef(() => createFocusManager()));
  return (
    <FocusManagerContext.Provider value={focusManager}>
      <FocusScopeImpl
        autoFocus={autoFocus}
        contain={contain}
        // let the FocusScope responder know about
        // its associated mananger
        focusManager={focusManager}
        children={children}
      />
    </FocusManagerContext.Provider>
  )
}

export function useFocusManager() {
  return useContext(FocusManagerContext);
}

I'm essentially saying that you shouldn't be able to access the FocusScope with a ref. If you want to access it, break your component into another component so you can use the useFousManager hook. There is a very good reason for this, it discourages access focus managers in child sub-trees, which I feel will lead to more confusion and many instances of hard to find bugs.

@devongovett
Copy link
Contributor Author

Hmm ok... that's not what we discussed earlier in this thread. Do you have an example for why you think accessing via ref is bad? I didn't quite follow this last sentence:

There is a very good reason for this, it discourages access focus managers in child sub-trees, which I feel will lead to more confusion and many instances of hard to find bugs.

Isn't the hook for accessing focus managers in child sub-trees?

@trueadm
Copy link
Contributor

trueadm commented Jun 17, 2019

@devongovett

ok... that's not what we discussed earlier in this thread

I think we were talking about different things. I was trying to come up with an approach that didn't use a ref on the FocusScope but did use a ref internally.

Isn't the hook for accessing focus managers in child sub-trees?

No, they access the immediate parent instance in the current branch of the tree, rather than the child of a tree. It's only context after-all.

Do you have an example for why you think accessing via ref is bad?

They're simply not needed here. Having one way of accessing the focus manager, via the hook is much better than having many different ways (hooks + ref) IMO. Plus, I'd really like a future world where React did not have to use refs, and instead could use hooks, effects and declarative UI via props to describe everything. Given event components are new, I think we should try harder before just use the escape hatch approach of ref, which really doesn't give any signal to me. useFocusManager does exactly what it says on the tin!

@devongovett
Copy link
Contributor Author

I'm all for getting rid of refs, but it might be confusing for people that they need to wrap their component in another component in order to use a focus manager. Calling useFocusManager in the same component where they render the <FocusScope> would give them back the parent focus manager instead, which might not have been what they thought. 🤷‍♂

I'll go ahead and change it. Just thought I'd throw that out there.

@devongovett
Copy link
Contributor Author

@trueadm Ref support removed.

@trueadm
Copy link
Contributor

trueadm commented Jun 19, 2019

There are some merge conflicts.

@devongovett
Copy link
Contributor Author

@trueadm Fixed the conflicts.

@devongovett
Copy link
Contributor Author

... and now there are more. Let me know when you're about ready to merge it and I'll resolve again. 😉

Copy link
Contributor

@aweary aweary left a comment

Choose a reason for hiding this comment

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

So is the plan to address focus management across FocusScope instances in a follow up? I think that part is really important and will likely have an impact on the API design.

One sticking point I have with useFocusManager is that you have to call it in a child of FocusScope. I could see scenarios where the component rendering FocusScope would define methods that might want to manage the focus of that scope. I know we want to restrict access to the focus manager, but it feels awkward having to add a container component just for FocusScope.

This approach also means that every FocusScope instance pays the cost of rendering a context provider, and it's not clear to me if that's the right default. I have a feeling most people will just rely on the default semantics of FocusScope

const FocusScopeContext: React.Context<FocusManager> = React.createContext();

export function FocusScope(props: FocusScopeProps) {
let internalFocusManager: ?FocusManager;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it intentional that this gets reset every time FocusScope renders? Maybe this should be stored in a ref?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good suggestion

);
return internalFocusManager.focusPrevious(options);
},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

focusManager should be memoized if it's being passed down through context.

focusManager.focusPrevious({from: divRef.current});
expect(document.activeElement).toBe(inputRef.current);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

@devongovett can you add a test for nested FocusScopes?

@devongovett
Copy link
Contributor Author

@aweary well... I already suggested two possible APIs to do this in this PR which were both rejected (singleton, and refs), so I think a follow up would be needed. I'd love to hear other suggestions!

@devongovett
Copy link
Contributor Author

Fixed the conflicts again and updated to incorporate @aweary's comments.

@stale
Copy link

stale bot commented Jan 10, 2020

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution.

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Jan 10, 2020
@devongovett
Copy link
Contributor Author

Closing since this won’t be merged. Will follow up with @trueadm separately on the current state of focus management in React.

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.

6 participants