Repository: mozilla-services/react-jsonschema-form (js)

Issue:
Readonly on radio and checkboxes
### Prerequisites

- [x] I have read the [documentation](https://github.com/mozilla-services/react-jsonschema-form/blob/master/README.md#readme);
- [x] In the case of a bug report, I understand that providing a [SSCCE](http://sscce.org/) example is tremendously useful to the maintainers.

### Description

I was surprised to find that I could still update radio buttons and checkboxes when they were set to readonly.

While the controls themselves may not support the property directly it seemed to me that that "disabled" was closer than nothing at all.  At least the user could update the data.

While disabled and readonly are subtly different, my suggestion is that using disabled is possibly better than doing nothing.

### Steps to Reproduce

1. [First Step]
Set a field using a RadioWidget or CheckboxesWidget to readonly.

#### Expected behavior
The user can't update those fields

#### Actual behavior
The user can still update the field.

### Version
Latest

Suggestion is to use "disabled" when the readonly flag is passed in to a control that doesn't support readonly inherently.

Fixes in my fork:
Commit: 41053bb3a08672cf78d70cff1dd6624c5a10b463 [41053bb]
Commit: 2573f73a40b21182ff4baa8126874a6639eaa7c5 [2573f73]
Commit: dda05d567ecdf09656538e8c40a9413b11e48300 [dda05d5]

Definitely made a meal of that one too.  Sorry for three commits instead of one!





Pull request:
Fix #412: Readonly widgets behavior consistency
Supersedes #470.

Patch:
diff --git a/src/components/Form.js b/src/components/Form.js
index 98c5f79b4..eb9b5f4ca 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -78,6 +78,7 @@ export default class Form extends Component {
   renderErrors() {
     const { status, errors } = this.state;
     const { ErrorList, showErrorList } = this.props;
+
     if (status !== "editing" && errors.length && showErrorList != false) {
       return <ErrorList errors={errors} />;
     }
diff --git a/src/components/widgets/BaseInput.js b/src/components/widgets/BaseInput.js
index 033df01df..819d90b85 100644
--- a/src/components/widgets/BaseInput.js
+++ b/src/components/widgets/BaseInput.js
@@ -6,6 +6,7 @@ function BaseInput(props) {
   const {
     value,
     readonly,
+    disabled,
     autofocus,
     onBlur,
     options,
@@ -22,6 +23,7 @@ function BaseInput(props) {
       {...inputProps}
       className="form-control"
       readOnly={readonly}
+      disabled={disabled}
       autoFocus={autofocus}
       value={value == null ? "" : value}
       onChange={_onChange}
diff --git a/src/components/widgets/CheckboxWidget.js b/src/components/widgets/CheckboxWidget.js
index 7616fe3eb..093bebed7 100644
--- a/src/components/widgets/CheckboxWidget.js
+++ b/src/components/widgets/CheckboxWidget.js
@@ -8,12 +8,13 @@ function CheckboxWidget(props) {
     value,
     required,
     disabled,
+    readonly,
     label,
     autofocus,
     onChange,
   } = props;
   return (
-    <div className={`checkbox ${disabled ? "disabled" : ""}`}>
+    <div className={`checkbox ${disabled || readonly ? "disabled" : ""}`}>
       {schema.description &&
         <DescriptionField description={schema.description} />}
       <label>
@@ -22,7 +23,7 @@ function CheckboxWidget(props) {
           id={id}
           checked={typeof value === "undefined" ? false : value}
           required={required}
-          disabled={disabled}
+          disabled={disabled || readonly}
           autoFocus={autofocus}
           onChange={event => onChange(event.target.checked)}
         />
@@ -42,6 +43,8 @@ if (process.env.NODE_ENV !== "production") {
     id: PropTypes.string.isRequired,
     value: PropTypes.bool,
     required: PropTypes.bool,
+    disabled: PropTypes.bool,
+    readonly: PropTypes.bool,
     autofocus: PropTypes.bool,
     onChange: PropTypes.func,
   };
diff --git a/src/components/widgets/CheckboxesWidget.js b/src/components/widgets/CheckboxesWidget.js
index 6debaea25..42f4930b6 100644
--- a/src/components/widgets/CheckboxesWidget.js
+++ b/src/components/widgets/CheckboxesWidget.js
@@ -13,20 +13,20 @@ function deselectValue(value, selected) {
 }
 
 function CheckboxesWidget(props) {
-  const { id, disabled, options, value, autofocus, onChange } = props;
+  const { id, disabled, options, value, autofocus, readonly, onChange } = props;
   const { enumOptions, inline } = options;
   return (
     <div className="checkboxes" id={id}>
       {enumOptions.map((option, index) => {
         const checked = value.indexOf(option.value) !== -1;
-        const disabledCls = disabled ? "disabled" : "";
+        const disabledCls = disabled || readonly ? "disabled" : "";
         const checkbox = (
           <span>
             <input
               type="checkbox"
               id={`${id}_${index}`}
               checked={checked}
-              disabled={disabled}
+              disabled={disabled || readonly}
               autoFocus={autofocus && index === 0}
               onChange={event => {
                 const all = enumOptions.map(({ value }) => value);
@@ -71,6 +71,7 @@ if (process.env.NODE_ENV !== "production") {
     }).isRequired,
     value: PropTypes.any,
     required: PropTypes.bool,
+    readonly: PropTypes.bool,
     disabled: PropTypes.bool,
     multiple: PropTypes.bool,
     autofocus: PropTypes.bool,
diff --git a/src/components/widgets/ColorWidget.js b/src/components/widgets/ColorWidget.js
index 0bc17654f..1c1ac3865 100644
--- a/src/components/widgets/ColorWidget.js
+++ b/src/components/widgets/ColorWidget.js
@@ -3,12 +3,20 @@ import React, { PropTypes } from "react";
 import BaseInput from "./BaseInput";
 
 function ColorWidget(props) {
-  return <BaseInput type="color" {...props} />;
+  const { disabled, readonly } = props;
+  return <BaseInput type="color" {...props} disabled={disabled || readonly} />;
 }
 
 if (process.env.NODE_ENV !== "production") {
   ColorWidget.propTypes = {
+    schema: PropTypes.object.isRequired,
+    id: PropTypes.string.isRequired,
     value: PropTypes.string,
+    required: PropTypes.bool,
+    disabled: PropTypes.bool,
+    readonly: PropTypes.bool,
+    autofocus: PropTypes.bool,
+    onChange: PropTypes.func,
   };
 }
 
diff --git a/src/components/widgets/RadioWidget.js b/src/components/widgets/RadioWidget.js
index 67482794d..d4c9f395b 100644
--- a/src/components/widgets/RadioWidget.js
+++ b/src/components/widgets/RadioWidget.js
@@ -1,7 +1,15 @@
 import React, { PropTypes } from "react";
 
 function RadioWidget(props) {
-  const { options, value, required, disabled, autofocus, onChange } = props;
+  const {
+    options,
+    value,
+    required,
+    disabled,
+    readonly,
+    autofocus,
+    onChange,
+  } = props;
   // Generating a unique field name to identify this set of radio buttons
   const name = Math.random().toString();
   const { enumOptions, inline } = options;
@@ -11,7 +19,7 @@ function RadioWidget(props) {
     <div className="field-radio-group">
       {enumOptions.map((option, i) => {
         const checked = option.value === value;
-        const disabledCls = disabled ? "disabled" : "";
+        const disabledCls = disabled || readonly ? "disabled" : "";
         const radio = (
           <span>
             <input
@@ -20,7 +28,7 @@ function RadioWidget(props) {
               name={name}
               required={required}
               value={option.value}
-              disabled={disabled}
+              disabled={disabled || readonly}
               autoFocus={autofocus && i === 0}
               onChange={_ => onChange(option.value)}
             />
@@ -56,6 +64,8 @@ if (process.env.NODE_ENV !== "production") {
     }).isRequired,
     value: PropTypes.any,
     required: PropTypes.bool,
+    disabled: PropTypes.bool,
+    readonly: PropTypes.bool,
     autofocus: PropTypes.bool,
     onChange: PropTypes.func,
   };
diff --git a/src/components/widgets/SelectWidget.js b/src/components/widgets/SelectWidget.js
index 27a0acada..cdd3ba9f6 100644
--- a/src/components/widgets/SelectWidget.js
+++ b/src/components/widgets/SelectWidget.js
@@ -56,8 +56,7 @@ function SelectWidget(props) {
       className="form-control"
       value={typeof value === "undefined" ? emptyValue : value}
       required={required}
-      disabled={disabled}
-      readOnly={readonly}
+      disabled={disabled || readonly}
       autoFocus={autofocus}
       onBlur={
         onBlur &&
@@ -91,6 +90,8 @@ if (process.env.NODE_ENV !== "production") {
     }).isRequired,
     value: PropTypes.any,
     required: PropTypes.bool,
+    disabled: PropTypes.bool,
+    readonly: PropTypes.bool,
     multiple: PropTypes.bool,
     autofocus: PropTypes.bool,
     onChange: PropTypes.func,
diff --git a/src/components/widgets/TextareaWidget.js b/src/components/widgets/TextareaWidget.js
index 3051d1f92..3ecee57af 100644
--- a/src/components/widgets/TextareaWidget.js
+++ b/src/components/widgets/TextareaWidget.js
@@ -48,6 +48,8 @@ if (process.env.NODE_ENV !== "production") {
     }),
     value: PropTypes.string,
     required: PropTypes.bool,
+    disabled: PropTypes.bool,
+    readonly: PropTypes.bool,
     autofocus: PropTypes.bool,
     onChange: PropTypes.func,
     onBlur: PropTypes.func,


Test patch:
diff --git a/test/uiSchema_test.js b/test/uiSchema_test.js
index 571057adc..510439124 100644
--- a/test/uiSchema_test.js
+++ b/test/uiSchema_test.js
@@ -1742,6 +1742,10 @@ describe("uiSchema", () => {
         const { node } = createFormComponent({ schema, uiSchema });
         expect(node.querySelector(selector).hasAttribute("readonly")).eql(true);
       }
+      function shouldBeDisabled(selector, schema, uiSchema) {
+        const { node } = createFormComponent({ schema, uiSchema });
+        expect(node.querySelector(selector).disabled).eql(true);
+      }
 
       it("should mark as readonly a text widget", () => {
         shouldBeReadonly(
@@ -1794,8 +1798,8 @@ describe("uiSchema", () => {
         );
       });
 
-      it("should mark as readonly a select widget", () => {
-        shouldBeReadonly(
+      it("should mark readonly as disabled on a select widget", () => {
+        shouldBeDisabled(
           "select",
           { type: "string", enum: ["a", "b"] },
           { "ui:readonly": true }
@@ -1850,25 +1854,25 @@ describe("uiSchema", () => {
         );
       });
 
-      it("should mark as readonly an alternative date widget", () => {
+      it("should mark readonly as disabled on an alternative date widget", () => {
         const { node } = createFormComponent({
           schema: { type: "string", format: "date" },
           uiSchema: { "ui:readonly": true, "ui:widget": "alt-date" },
         });
 
         const readonly = [].map.call(node.querySelectorAll("select"), node =>
-          node.hasAttribute("readonly"));
+          node.hasAttribute("disabled"));
         expect(readonly).eql([true, true, true]);
       });
 
-      it("should mark as readonly an alternative datetime widget", () => {
+      it("should mark readonly as disabled on an alternative datetime widget", () => {
         const { node } = createFormComponent({
           schema: { type: "string", format: "date-time" },
           uiSchema: { "ui:readonly": true, "ui:widget": "alt-datetime" },
         });
 
         const readonly = [].map.call(node.querySelectorAll("select"), node =>
-          node.hasAttribute("readonly"));
+          node.hasAttribute("disabled"));
         expect(readonly).eql([true, true, true, true, true, true]);
       });
     });