2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ng.material.components.radioButton');
8 goog.require('ng.material.core');
11 * @name material.components.radioButton
12 * @description radioButton module!
14 angular.module('material.components.radioButton', [
17 .directive('mdRadioGroup', mdRadioGroupDirective)
18 .directive('mdRadioButton', mdRadioButtonDirective);
22 * @module material.components.radioButton
28 * The `<md-radio-group>` directive identifies a grouping
29 * container for the 1..n grouped radio buttons; specified using nested
30 * `<md-radio-button>` tags.
32 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
33 * the radio button is in the accent color by default. The primary color palette may be used with
34 * the `md-primary` class.
36 * Note: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently
37 * than the native `<input type='radio'>` controls. Whereas the native controls
38 * force the user to tab through all the radio buttons, `<md-radio-group>`
39 * is focusable, and by default the `<md-radio-button>`s are not.
41 * @param {string} ng-model Assignable angular expression to data-bind to.
42 * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects.
46 * <md-radio-group ng-model="selected">
49 * ng-repeat="d in colorOptions"
50 * ng-value="d.value" aria-label="{{ d.label }}">
60 function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) {
61 RadioGroupController.prototype = createRadioGroupControllerProto();
65 controller: ['$element', RadioGroupController],
66 require: ['mdRadioGroup', '?ngModel'],
67 link: { pre: linkRadioGroup }
70 function linkRadioGroup(scope, element, attr, ctrls) {
72 var rgCtrl = ctrls[0];
73 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
76 if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
79 function keydownListener(ev) {
80 var keyCode = ev.which || ev.keyCode;
82 case $mdConstant.KEY_CODE.LEFT_ARROW:
83 case $mdConstant.KEY_CODE.UP_ARROW:
85 rgCtrl.selectPrevious();
89 case $mdConstant.KEY_CODE.RIGHT_ARROW:
90 case $mdConstant.KEY_CODE.DOWN_ARROW:
96 case $mdConstant.KEY_CODE.ENTER:
97 var form = angular.element($mdUtil.getClosest(element[0], 'form'));
98 if (form.length > 0) {
99 form.triggerHandler('submit');
105 rgCtrl.init(ngModelCtrl);
107 scope.mouseActive = false;
109 'role': 'radiogroup',
110 'tabIndex': element.attr('tabindex') || '0'
112 .on('keydown', keydownListener)
113 .on('mousedown', function(event) {
114 scope.mouseActive = true;
115 $timeout(function() {
116 scope.mouseActive = false;
119 .on('focus', function() {
120 if(scope.mouseActive === false) { rgCtrl.$element.addClass('md-focused'); }
122 .on('blur', function() { rgCtrl.$element.removeClass('md-focused'); });
125 function RadioGroupController($element) {
126 this._radioButtonRenderFns = [];
127 this.$element = $element;
130 function createRadioGroupControllerProto() {
132 init: function(ngModelCtrl) {
133 this._ngModelCtrl = ngModelCtrl;
134 this._ngModelCtrl.$render = angular.bind(this, this.render);
136 add: function(rbRender) {
137 this._radioButtonRenderFns.push(rbRender);
139 remove: function(rbRender) {
140 var index = this._radioButtonRenderFns.indexOf(rbRender);
142 this._radioButtonRenderFns.splice(index, 1);
146 this._radioButtonRenderFns.forEach(function(rbRender) {
150 setViewValue: function(value, eventType) {
151 this._ngModelCtrl.$setViewValue(value, eventType);
152 // update the other radio buttons as well
155 getViewValue: function() {
156 return this._ngModelCtrl.$viewValue;
158 selectNext: function() {
159 return changeSelectedButton(this.$element, 1);
161 selectPrevious: function() {
162 return changeSelectedButton(this.$element, -1);
164 setActiveDescendant: function (radioId) {
165 this.$element.attr('aria-activedescendant', radioId);
170 * Change the radio group's selected button by a given increment.
171 * If no button is selected, select the first button.
173 function changeSelectedButton(parent, increment) {
174 // Coerce all child radio buttons into an array, then wrap then in an iterator
175 var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true);
177 if (buttons.count()) {
178 var validate = function (button) {
179 // If disabled, then NOT valid
180 return !angular.element(button).attr("disabled");
182 var selected = parent[0].querySelector('md-radio-button.md-checked');
183 var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first();
184 // Activate radioButton's click listener (triggerHandler won't create a real click event)
185 angular.element(target).triggerHandler('click');
192 mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"];
196 * @module material.components.radioButton
197 * @name mdRadioButton
202 * The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements.
204 * While similar to the `<input type="radio" ng-model="" value="">` directive,
205 * the `<md-radio-button>` directive provides ink effects, ARIA support, and
206 * supports use within named radio groups.
208 * @param {string} ngModel Assignable angular expression to data-bind to.
209 * @param {string=} ngChange Angular expression to be executed when input changes due to user
210 * interaction with the input element.
211 * @param {string} ngValue Angular expression which sets the value to which the expression should
212 * be set when selected.*
213 * @param {string} value The value to which the expression should be set when selected.
214 * @param {string=} name Property name of the form under which the control is published.
215 * @param {string=} aria-label Adds label to radio button for accessibility.
216 * Defaults to radio button's text. If no text content is available, a warning will be logged.
221 * <md-radio-button value="1" aria-label="Label 1">
225 * <md-radio-button ng-model="color" ng-value="specialValue" aria-label="Green">
232 function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
234 var CHECKED_CSS = 'md-checked';
238 require: '^mdRadioGroup',
240 template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
241 '<div class="md-off"></div>' +
242 '<div class="md-on"></div>' +
244 '<div ng-transclude class="md-label"></div>',
248 function link(scope, element, attr, rgCtrl) {
252 configureAria(element, scope);
255 attr.$observe('value', render);
258 .on('click', listener)
259 .on('$destroy', function() {
260 rgCtrl.remove(render);
263 function listener(ev) {
264 if (element[0].hasAttribute('disabled')) return;
266 scope.$apply(function() {
267 rgCtrl.setViewValue(attr.value, ev && ev.type);
272 var checked = (rgCtrl.getViewValue() == attr.value);
273 if (checked === lastChecked) {
276 lastChecked = checked;
277 element.attr('aria-checked', checked);
279 element.addClass(CHECKED_CSS);
280 rgCtrl.setActiveDescendant(element.attr('id'));
282 element.removeClass(CHECKED_CSS);
286 * Inject ARIA-specific attributes appropriate for each radio button
288 function configureAria( element, scope ){
289 scope.ariaId = buildAriaID();
294 'aria-checked' : 'false'
297 $mdAria.expectWithText(element, 'aria-label');
300 * Build a unique ID for each radio button that will be used with aria-activedescendant.
301 * Preserve existing ID if already specified.
302 * @returns {*|string}
304 function buildAriaID() {
305 return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() );
310 mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"];
312 ng.material.components.radioButton = angular.module("material.components.radioButton");