Repository: grommet/grommet (JavaScript)

Issue:
autoFocus does not work with child components of Layer
<!--- Provide a general summary of the issue in the Title above -->

### Expected Behavior
Child components of Layer should be able to be autofocused
<!--- Tell us what should happen -->

### Actual Behavior
Autofocus does not work.
<!--- Tell us what happens instead -->

### URL, screen shot, or Codepen exhibiting the issue
Codesandbox demonstrating that autofocus of the TextInput works unless it is a child of the Layer component: https://codesandbox.io/s/grommet-v2-template-ltsyf?fontsize=14
<!--
  -- Here's a Codesandbox template that serves as a nice starting point
  -- for demonstrating an issue: https://codesandbox.io/s/m7mml8l0zj
  -->

### Steps to Reproduce

1. Add `autoFocus` to a TextInput
2. Make TextInput a child of Layer

### Your Environment

<!--- Include as many relevant details about the environment you experienced the bug in -->

- Grommet version: 2.7.0
- Browser Name and version: Chrome 74
- Operating System and version (desktop or mobile): MacOS 10.14.4



Discussion:
I've an upcoming PR and I'll discuss a few approaches here to determine validity of the pull request.

The current situation is for a11y, we have a hidden anchor element is created in layer and focus is set to this anchor element. Problem is this happens without `Layer` being aware of if children components carry an `autoFocus` prop or not, like an input element.

The approaches I found are:
1. Have a new prop `focusLayer` that is by default `true`, and when it is `false`, it does not autofocus anchor tag as something else in `Layer` has focus. I got the idea from https://github.com/reactstrap/reactstrap/blob/8.0.0/src/Modal.js#L66 after looking at several solutions.
2. `Layer` looks recursively inside `props.children` tree to detect an `autoFocus` prop. Expensive & inconvenient to write.

I'd like to use approach 1, but change the prop name to be `focusLayer` like react-aria-modal (a more accessible modal) does. Ex. https://github.com/davidtheclark/react-aria-modal/blob/master/src/react-aria-modal.js#L216



Patch:
diff --git a/src/js/components/Layer/LayerContainer.js b/src/js/components/Layer/LayerContainer.js
index 05ce13fdc24..fe039ba5a83 100644
--- a/src/js/components/Layer/LayerContainer.js
+++ b/src/js/components/Layer/LayerContainer.js
@@ -37,9 +37,19 @@ class LayerContainer extends Component {
     const { position } = this.props;
     if (position !== 'hidden') {
       this.makeLayerVisible();
-      // once layer is open we set the focus in the hidden
-      // anchor so that you can start tabbing inside the layer
-      if (this.anchorRef.current) {
+      // Once layer is open we make sure it has focus so that you
+      // can start tabbing inside the layer. If the caller put focus
+      // on an element already, we honor that. Otherwise, we put
+      // the focus in the hidden anchor.
+      let element = document.activeElement;
+      while (element) {
+        if (element === this.containerRef.current) {
+          // already have focus inside the container
+          break;
+        }
+        element = element.parentElement;
+      }
+      if (!element && this.anchorRef.current) {
         this.anchorRef.current.focus();
       }
     }


Test patch:
diff --git a/src/js/components/Layer/__tests__/Layer-test.js b/src/js/components/Layer/__tests__/Layer-test.js
index 1589c266f0e..8de57823a92 100644
--- a/src/js/components/Layer/__tests__/Layer-test.js
+++ b/src/js/components/Layer/__tests__/Layer-test.js
@@ -212,4 +212,38 @@ describe('Layer', () => {
     ref.current.componentDidMount();
     expect(queryByTestId(document, 'test-layer-container')).toMatchSnapshot();
   });
+
+  test('focus on layer', () => {
+    /* eslint-disable jsx-a11y/no-autofocus */
+    render(
+      <Grommet>
+        <Layer data-testid="focus-layer-test">
+          <input />
+        </Layer>
+        <input autoFocus />
+      </Grommet>,
+    );
+    /* eslint-disable jsx-a11y/no-autofocus */
+
+    const layerNode = getByTestId(document, 'focus-layer-test');
+    expect(layerNode).toMatchSnapshot();
+    expect(document.activeElement.nodeName).toBe('A');
+  });
+
+  test('not steal focus from an autofocus focusable element', () => {
+    /* eslint-disable jsx-a11y/no-autofocus */
+    render(
+      <Grommet>
+        <Layer data-testid="focus-layer-input-test">
+          <input autoFocus data-testid="focus-input" />
+          <button type="button">Button</button>
+        </Layer>
+      </Grommet>,
+    );
+    /* eslint-disable jsx-a11y/no-autofocus */
+    const layerNode = getByTestId(document, 'focus-layer-input-test');
+    const inputNode = getByTestId(document, 'focus-input');
+    expect(layerNode).toMatchSnapshot();
+    expect(document.activeElement).toBe(inputNode);
+  });
 });
diff --git a/src/js/components/Layer/__tests__/__snapshots__/Layer-test.js.snap b/src/js/components/Layer/__tests__/__snapshots__/Layer-test.js.snap
index 7674c4b7d0c..c6341da937d 100644
--- a/src/js/components/Layer/__tests__/__snapshots__/Layer-test.js.snap
+++ b/src/js/components/Layer/__tests__/__snapshots__/Layer-test.js.snap
@@ -223,6 +223,81 @@ exports[`Layer dark context 2`] = `
 }"
 `;
 
+exports[`Layer focus on layer 1`] = `
+.c0 {
+  font-size: 18px;
+  line-height: 24px;
+  box-sizing: border-box;
+  -webkit-text-size-adjust: 100%;
+  -ms-text-size-adjust: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-direction: column;
+  -ms-flex-direction: column;
+  flex-direction: column;
+  min-height: 48px;
+  background: #FFFFFF;
+  color: #444444;
+  outline: none;
+  pointer-events: all;
+  z-index: 15;
+  position: fixed;
+  max-height: calc(100% - 0px - 0px);
+  max-width: calc(100% - 0px - 0px);
+  border-radius: 4px;
+  top: 50%;
+  left: 50%;
+  -webkit-transform: translate(-50%,-50%);
+  -ms-transform: translate(-50%,-50%);
+  transform: translate(-50%,-50%);
+  -webkit-animation: bBMZdm 0.2s ease-in-out forwards;
+  animation: bBMZdm 0.2s ease-in-out forwards;
+}
+
+.c1 {
+  width: 0;
+  height: 0;
+  overflow: hidden;
+  position: absolute;
+}
+
+@media only screen and (max-width:768px) {
+  .c0 {
+    position: relative;
+    max-height: none;
+    max-width: none;
+    border-radius: 0;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    -webkit-transform: none;
+    -ms-transform: none;
+    transform: none;
+    -webkit-animation: none;
+    animation: none;
+    height: 100vh;
+    width: 100vw;
+  }
+}
+
+<div
+  class="c0"
+  data-testid="focus-layer-test"
+>
+  <a
+    aria-hidden="true"
+    class="c1"
+    tabindex="-1"
+  />
+  <input />
+</div>
+`;
+
 exports[`Layer full false 1`] = `
 .c0 {
   font-size: 18px;
@@ -1678,6 +1753,88 @@ exports[`Layer non-modal 1`] = `
 
 exports[`Layer non-modal 2`] = `""`;
 
+exports[`Layer not steal focus from an autofocus focusable element 1`] = `
+.c0 {
+  font-size: 18px;
+  line-height: 24px;
+  box-sizing: border-box;
+  -webkit-text-size-adjust: 100%;
+  -ms-text-size-adjust: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-direction: column;
+  -ms-flex-direction: column;
+  flex-direction: column;
+  min-height: 48px;
+  background: #FFFFFF;
+  color: #444444;
+  outline: none;
+  pointer-events: all;
+  z-index: 15;
+  position: fixed;
+  max-height: calc(100% - 0px - 0px);
+  max-width: calc(100% - 0px - 0px);
+  border-radius: 4px;
+  top: 50%;
+  left: 50%;
+  -webkit-transform: translate(-50%,-50%);
+  -ms-transform: translate(-50%,-50%);
+  transform: translate(-50%,-50%);
+  -webkit-animation: bBMZdm 0.2s ease-in-out forwards;
+  animation: bBMZdm 0.2s ease-in-out forwards;
+}
+
+.c1 {
+  width: 0;
+  height: 0;
+  overflow: hidden;
+  position: absolute;
+}
+
+@media only screen and (max-width:768px) {
+  .c0 {
+    position: relative;
+    max-height: none;
+    max-width: none;
+    border-radius: 0;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    -webkit-transform: none;
+    -ms-transform: none;
+    transform: none;
+    -webkit-animation: none;
+    animation: none;
+    height: 100vh;
+    width: 100vw;
+  }
+}
+
+<div
+  class="c0"
+  data-testid="focus-layer-input-test"
+>
+  <a
+    aria-hidden="true"
+    class="c1"
+    tabindex="-1"
+  />
+  <input
+    data-testid="focus-input"
+  />
+  <button
+    type="button"
+  >
+    Button
+  </button>
+</div>
+`;
+
 exports[`Layer plain 1`] = `
 .c0 {
   font-size: 18px;