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!