diff --git a/docs/content/guide/accessibility.ngdoc b/docs/content/guide/accessibility.ngdoc
index 3df8e9c69718..ef41f5e7eeec 100644
--- a/docs/content/guide/accessibility.ngdoc
+++ b/docs/content/guide/accessibility.ngdoc
@@ -34,6 +34,7 @@ Currently, ngAria interfaces with the following directives:
* {@link guide/accessibility#ngmodel ngModel}
* {@link guide/accessibility#ngdisabled ngDisabled}
* {@link guide/accessibility#ngrequired ngRequired}
+ * {@link guide/accessibility#ngReadonly ngReadonly}
* {@link guide/accessibility#ngvaluechecked ngChecked}
* {@link guide/accessibility#ngvaluechecked ngValue}
* {@link guide/accessibility#ngshow ngShow}
@@ -57,6 +58,7 @@ attributes (if they have not been explicitly specified by the developer):
* aria-valuenow
* aria-invalid
* aria-required
+ * aria-readonly
###Example
@@ -203,6 +205,25 @@ Becomes:
```
+
ngReadonly
+
+The boolean `readonly` attribute is only valid for native form controls such as `input` and
+`textarea`. To properly indicate custom element directives such as `` or ``
+as required, using ngAria with {@link ng.ngReadonly ngReadonly} will also add
+`aria-readonly`. This tells accessibility APIs when a custom control is required.
+
+###Example
+
+```html
+
+```
+
+Becomes:
+
+```html
+
+```
+
ngShow
>The {@link ng.ngShow ngShow} directive shows or hides the
diff --git a/src/ngAria/aria.js b/src/ngAria/aria.js
index 400d951e2bc0..a06545a4646d 100644
--- a/src/ngAria/aria.js
+++ b/src/ngAria/aria.js
@@ -16,7 +16,7 @@
*
* For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following
* directives are supported:
- * `ngModel`, `ngChecked`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
+ * `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
* `ngDblClick`, and `ngMessages`.
*
* Below is a more detailed breakdown of the attributes handled by ngAria:
@@ -25,8 +25,9 @@
* |---------------------------------------------|----------------------------------------------------------------------------------------|
* | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required, input roles |
* | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled |
- * | {@link ng.directive:ngRequired ngRequired} | aria-required |
- * | {@link ng.directive:ngChecked ngChecked} | aria-checked |
+ * | {@link ng.directive:ngRequired ngRequired} | aria-required
+ * | {@link ng.directive:ngChecked ngChecked} | aria-checked
+ * | {@link ng.directive:ngReadonly ngReadonly} | aria-readonly ||
* | {@link ng.directive:ngValue ngValue} | aria-checked |
* | {@link ng.directive:ngShow ngShow} | aria-hidden |
* | {@link ng.directive:ngHide ngHide} | aria-hidden |
@@ -91,6 +92,7 @@ function $AriaProvider() {
var config = {
ariaHidden: true,
ariaChecked: true,
+ ariaReadonly: true,
ariaDisabled: true,
ariaRequired: true,
ariaInvalid: true,
@@ -108,6 +110,7 @@ function $AriaProvider() {
*
* - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags
* - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags
+ * - **ariaReadonly** – `{boolean}` – Enables/disables aria-readonly tags
* - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags
* - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags
* - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags
@@ -170,6 +173,7 @@ function $AriaProvider() {
* The full list of directives that interface with ngAria:
* * **ngModel**
* * **ngChecked**
+ * * **ngReadonly**
* * **ngRequired**
* * **ngDisabled**
* * **ngValue**
@@ -209,6 +213,9 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
.directive('ngChecked', ['$aria', function($aria) {
return $aria.$$watchExpr('ngChecked', 'aria-checked', nodeBlackList, false);
}])
+.directive('ngReadonly', ['$aria', function($aria) {
+ return $aria.$$watchExpr('ngReadonly', 'aria-readonly', nodeBlackList, false);
+}])
.directive('ngRequired', ['$aria', function($aria) {
return $aria.$$watchExpr('ngRequired', 'aria-required', nodeBlackList, false);
}])
diff --git a/test/ngAria/ariaSpec.js b/test/ngAria/ariaSpec.js
index 37c4f17bf1d8..cb50ccd6078e 100644
--- a/test/ngAria/ariaSpec.js
+++ b/test/ngAria/ariaSpec.js
@@ -396,6 +396,66 @@ describe('$aria', function() {
});
});
+ describe('aria-readonly', function() {
+ beforeEach(injectScopeAndCompiler);
+
+ they('should not attach itself to native $prop controls', {
+ input: ' ',
+ textarea: '',
+ select: ' ',
+ button: ' '
+ }, function(tmpl) {
+ var element = $compile(tmpl)(scope);
+ scope.$apply('val = true');
+
+ expect(element.attr('readonly')).toBeDefined();
+ expect(element.attr('aria-readonly')).toBeUndefined();
+ });
+
+ it('should attach itself to custom controls', function() {
+ compileElement('
');
+ expect(element.attr('aria-readonly')).toBe('false');
+
+ scope.$apply('val = true');
+ expect(element.attr('aria-readonly')).toBe('true');
+
+ });
+
+ it('should not attach itself if an aria-readonly attribute is already present', function() {
+ compileElement('
');
+
+ expect(element.attr('aria-readonly')).toBe('userSetValue');
+ });
+
+ it('should always set aria-readonly to a boolean value', function() {
+ compileElement('
');
+
+ scope.$apply('val = "test angular"');
+ expect(element.attr('aria-readonly')).toBe('true');
+
+ scope.$apply('val = null');
+ expect(element.attr('aria-readonly')).toBe('false');
+
+ scope.$apply('val = {}');
+ expect(element.attr('aria-readonly')).toBe('true');
+ });
+ });
+
+ describe('aria-readonly when disabled', function() {
+ beforeEach(configAriaProvider({
+ ariaReadonly: false
+ }));
+ beforeEach(injectScopeAndCompiler);
+
+ it('should not add the aria-readonly attribute', function() {
+ compileElement(" ");
+ expect(element.attr('aria-readonly')).toBeUndefined();
+
+ compileElement("
");
+ expect(element.attr('aria-readonly')).toBeUndefined();
+ });
+ });
+
describe('aria-required', function() {
beforeEach(injectScopeAndCompiler);