/apr 28, 2015

Closing a Modal on Focus Lost in AngularJS

By Tyler Waneka

At SRC:CLR we have a few different modals that we use in our application. In some cases, we want the modals to close if the user clicks elsewhere on the page, or in web terms, if the focus is lost from the modal. Angular has a couple directives to help us manage focus - ngFocus and ngBlur.

They work like this:

  <input ng-focus="doSomething()" ng-blur="doSomethingElse()">

In the example above, when a user clicks on the input field the doSomething method will be invoked. And when the user clicks off the input field (losing their focus), the doSomethingElse method would be invoked.

In our case, we want manage the focus on a specific div element, not necessarily an input field. There are a limited number of HTML elements that can receive focus, and those are defined by the browser. On a modern browser, you can apply focus to input, select, text-area, anchor, iframe, area, and button elements, as well as any element with a tabindex attribute.

That last part is key, and that's how we'll manage the focus on our modal. For context, tabindex is the way the browser keeps track of focus when a user is pushing tab to navigate through a web page. For example, if you visit twitter.com and press tab once, it highlights the home tab. Push tab again and you can see the focus switch to the notifications tab, and so on. The browser follows the tabindex order, starting at 1, and going up from there (if no tabindex is defined, the browser has a default way of determining it's own order - which I won't get into). For our modal, here's how we can leverage that:

  <div
    ng-if="dropdowns.exampleModal"
    ng-controller="ExampleModalCtrl"
    class="dropdown"
    tabindex="1"
    ng-blur="closeModal()"
  >
    Modal Content...
  </div>

Setting the tabindex means that this div element can now receive focus. Clicking off the div element would invoke the closeModal method because of the ngBlur directive. This is great, but we've got one big problem left - the modal isn't an element that the user can see and click on, rather it's hidden from sight and only shown when the user opens it. So with that said, how do we establish focus on the modal to begin with?

In my search for a solution to that problem, I came across a nice little directive that just happens to be written by a friend of mine, Max Edmands who is a developer at Good Eggs. The folks at Good Eggs are proactive contributors to the open source community, and Max has made the code for this directive available here. The code is pretty straightforward, here's a look:

  angular.module('exampleApp')
    .directive('focusOn', function() {
      'use strict';

      return function(scope, elem, attr) {
        return scope.$on('focusOn', function(e, name) {
          if (name === attr.focusOn) {
            return elem[0].focus();
          }
        });
      };
    });

Which is paired with the following service:

  angular.module('exampleApp')
    .factory('focus', [
      '$rootScope', '$timeout', function($rootScope, $timeout) {
        'use strict';

        return function(name) {
          return $timeout(function() {
            return $rootScope.$broadcast('focusOn', name);
          });
        };
      }
    ]);

Using those in combination allows us to manually apply the focus to the modal when it's opened. Here's the code in the controller:

  angular.module('exampleApp')
    .controller('ExampleModalCtrl', function ($scope, focus) {

    $scope.openModal = function () {
      $scope.dropdowns.exampleModal = true;
      focus('exampleModalFocus');
    };

    $scope.closeModal = function () {
      $scope.dropdowns.exampleModal = false;
    };

  });

Now we can adjust our HTML like so:

  <div
    ng-if="dropdowns.exampleModal"
    class="dropdown focus--no-outline"
    tabindex="1"
    focus-on="exampleModalFocus"
    ng-blur="closeModal()"
  >
    Modal Content...
  </div>

So now when the openModal method is invoked, the modal is rendered on the DOM and the focus is set on the div element. And clicking off the div element will cause the closeModal method to be invoked because of the ngBlur. You might also notice that I've added a class - focus--no-outline. This is because when the browser applies focus to an element, there's some CSS that automatically kicks in to apply a blue outline to the element. While this is helpful most of the time, I want to remove it for the modal. That's done so with a simple CSS rule:

  .focus--no-outline:focus {
    outline: 0;
  }

This will remove the outline from the focus state of this div, which is exactly what we're going for.

Happy modaling!

Related Posts

By Tyler Waneka