Skip to content

Commit

Permalink
Support hidden configuration settings of all field types. (#145)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdilauro authored Feb 12, 2025
1 parent 831e14a commit 5c88315
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 96 deletions.
10 changes: 1 addition & 9 deletions src/components/EditableInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export interface EditableInputProps extends React.HTMLProps<EditableInput> {
className?: string;
minLength?: number;
maxLength?: number;
hidden?: boolean;
}

export interface EditableInputState {
Expand All @@ -41,6 +40,7 @@ export default class EditableInput extends React.Component<
EditableInputState
> {
private elementRef = React.createRef<HTMLInputElement>();
static displayName = "EditableInput";
static defaultProps = {
optionalText: true,
readOnly: false,
Expand All @@ -66,11 +66,7 @@ export default class EditableInput extends React.Component<
error,
label,
extraContent,
hidden,
} = this.props;
if (hidden) {
return this.renderHiddenElement();
}
const checkboxOrRadioOrSelect = !!(
type === "checkbox" ||
type === "radio" ||
Expand Down Expand Up @@ -161,10 +157,6 @@ export default class EditableInput extends React.Component<
);
}

renderHiddenElement() {
return this.renderElement({ ...this.props, readOnly: true });
}

renderDescription(id: string, description: string) {
return (
<p
Expand Down
2 changes: 2 additions & 0 deletions src/components/InputList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export default class InputList extends React.Component<
InputListProps,
InputListState
> {
static displayName = "InputList";
private addListItemRef = React.createRef<LanguageField>();

constructor(props: InputListProps) {
super(props);
const isMenu = props.setting.type === "menu";
Expand Down
54 changes: 26 additions & 28 deletions src/components/ProtocolFormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ export default class ProtocolFormField extends React.Component<
ProtocolFormFieldProps,
object
> {
static displayName = "ProtocolFormField";
private inputListRef = React.createRef<InputList>();
private colorPickerRef = React.createRef<ColorPicker>();
private elementRef = React.createRef<EditableInput>();
static defaultProps = {
readOnly: false,
};

constructor(props: ProtocolFormFieldProps) {
super(props);
this.randomize = this.randomize.bind(this);
Expand All @@ -54,36 +56,32 @@ export default class ProtocolFormField extends React.Component<

render(): JSX.Element {
const setting = this.props.setting as SettingData | CustomListsSetting;
if (setting.hidden) {
// TODO: Hijacking for any hidden fields for now, but need to handle
// some types (e.g., "menu", "list") differently.
return this.renderHiddenElement(setting);
}
if (setting.type === "select") {
return this.renderSelectSetting(setting);
} else if (setting.type === "list" || setting.type === "menu") {
return this.renderListSetting(setting);
} else if (setting.type === "color-picker") {
return this.renderColorPickerSetting(setting);
} else {
return this.renderSetting(setting);
}
const element: JSX.Element =
setting.type === "select"
? this.renderSelectSetting(setting)
: setting.type === "list" || setting.type === "menu"
? this.renderListSetting(setting)
: setting.type === "color-picker"
? this.renderColorPickerSetting(setting)
: this.renderSetting(setting);
// Special handling for hidden settings.
return setting.hidden ? this.renderHiddenElement(element) : element;
}

renderHiddenElement(setting: SettingData) {
const { value, disabled = false, error = null } = this.props;
const props = {
key: setting.key,
hidden: true,
elementType: "input",
type: "hidden",
name: setting.key,
value: defaultValueIfMissing(value, setting.default),
ref: this.elementRef,
disabled,
error,
};
return React.createElement(EditableInput, props, null);
renderHiddenElement(element: JSX.Element): JSX.Element {
// Wrap hidden element in invisible div to prevent it from affecting layout.
return (
<div
style={{
visibility: "hidden",
position: "absolute",
top: "-9999px",
left: "-9999px",
}}
>
{element}
</div>
);
}

renderSetting(setting: SettingData): JSX.Element {
Expand Down
172 changes: 113 additions & 59 deletions src/components/__tests__/ProtocolFormField-test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from "chai";

import * as React from "react";
import { mount } from "enzyme";
import { mount, ReactWrapper } from "enzyme";
import { stub } from "sinon";

import ProtocolFormField from "../ProtocolFormField";
Expand Down Expand Up @@ -324,30 +324,33 @@ describe("ProtocolFormField", () => {
label: "label",
description: "<p>description</p>",
};
let wrapper;
let wrapper: ReactWrapper;

const expectHiddenValue = (value) => {
const input = wrapper.find("input");
expect(input.length).to.equal(1);
expect(input.prop("type")).to.equal("hidden");
expect(input.prop("name")).to.equal("setting");
// The field contains the correct value.
expect(input.prop("value")).to.deep.equal(value);
// ProtocolFormField.getValue returns the correct value for the field.
expect((wrapper.instance() as ProtocolFormField).getValue()).to.equal(
value
const expectHiddenValue = (value: any): ReactWrapper => {
const hiddenDiv = wrapper.findWhere(
(node) => node.type() === "div" && node.prop("style") !== undefined
);

expect(
(wrapper.instance() as ProtocolFormField).getValue()
).to.deep.equal(value);
return hiddenDiv;
};

beforeEach(() => {
wrapper = mount(<ProtocolFormField setting={setting} disabled={false} />);
});

it("renders hidden text setting", () => {
const textFieldSetting = { ...setting, type: "text" };
wrapper.setProps({ setting: textFieldSetting, value: "test" });
wrapper.setProps({ value: "test" });

expectHiddenValue("test");
const hiddenDiv = expectHiddenValue("test");
const input = hiddenDiv.find(EditableInput);
const inputElement = input.find("input").getDOMNode();

expect(input.prop("value")).to.equal("test");
expect(inputElement.value).to.equal("test");
expect(wrapper.instance().getValue()).to.equal("test");
});

it("renders hidden date-picker setting", () => {
Expand All @@ -366,16 +369,27 @@ describe("ProtocolFormField", () => {

it("renders hidden setting with default", () => {
const defaultSetting = { ...setting, default: "default" };
wrapper.setProps({ setting: defaultSetting });
wrapper.setProps({ setting: defaultSetting, value: undefined });

expectHiddenValue("default");
});

it("renders hidden number setting", () => {
const numberSetting = { ...setting, type: "number" };
wrapper.setProps({ setting: numberSetting, value: "42" });
wrapper.setProps({ setting: numberSetting });

let hiddenDiv = expectHiddenValue("");

expectHiddenValue("42");
let input = hiddenDiv.find(EditableInput);
expect(input.length).to.equal(1);
expect(input.prop("validation")).to.equal("number");
expect(input.prop("name")).to.equal("setting");
expect(input.prop("value")).to.be.undefined;

wrapper.setProps({ setting: numberSetting, value: "42" });
hiddenDiv = expectHiddenValue("42");
input = hiddenDiv.find(EditableInput);
expect(input.prop("value")).to.equal("42");
});

it("renders hidden select setting", () => {
Expand All @@ -385,30 +399,64 @@ describe("ProtocolFormField", () => {
options: [
{ key: "option1", label: "option 1" },
{ key: "option2", label: "option 2" },
{ key: "option3", label: "option 3" },
],
};
wrapper.setProps({ setting: selectSetting });

// With default value.
expectHiddenValue("");
// If there is no value and no default, then we use the first option.
wrapper.setProps({
setting: { ...selectSetting, default: undefined },
value: undefined,
});
const hiddenDiv = expectHiddenValue("option1");

const input = hiddenDiv.find(EditableInput);
const children = input.find("option");

expect(input.length).to.equal(1);
expect(input.prop("name")).to.equal("setting");
expect(input.prop("label")).to.equal("label");
expect(input.prop("value")).to.be.undefined;

// With provided value.
wrapper.setProps({ value: "indigo" });
expectHiddenValue("indigo");
expect(children.length).to.equal(3);
expect(children.at(0).prop("value")).to.equal("option1");
expect(children.at(1).prop("value")).to.equal("option2");
expect(children.at(2).prop("value")).to.equal("option3");
expect(children.at(0).text()).to.contain("option 1");
expect(children.at(1).text()).to.contain("option 2");
expect(children.at(2).text()).to.contain("option 3");

// If no value set and there is a default value, then we should get the default back.
wrapper.setProps({
setting: { ...selectSetting, default: "option3" },
value: undefined,
});
expectHiddenValue("option3");

// If a value is already set, then we should get that value.
wrapper.setProps({ value: "option2" });
expectHiddenValue("option2");
});

it("renders hidden textarea setting", () => {
const textareaSetting = {
...setting,
type: "textarea",
description: "<p>Textarea</p>",
};
wrapper.setProps({ setting: textareaSetting, value: "test" });
const textareaSetting = { ...setting, type: "textarea" };
wrapper.setProps({ setting: textareaSetting });
const hiddenDiv = expectHiddenValue("");

const input = hiddenDiv.find(EditableInput);
expect(input.length).to.equal(1);
const inputElement = input.find("textarea").at(0) as any;
expect(inputElement.length).to.equal(1);
expect(inputElement.text()).to.equal("");
expect(input.prop("type")).to.equal("text");
expect(input.prop("name")).to.equal("setting");
expect(input.prop("value")).to.be.undefined;

wrapper.setProps({ setting: textareaSetting, value: "test" });
expectHiddenValue("test");
});

it.skip("renders hidden menu setting", () => {
it("renders hidden menu setting", () => {
const menuSetting = {
...setting,
type: "menu",
Expand All @@ -426,17 +474,34 @@ describe("ProtocolFormField", () => {
disableButton: true,
});

expectHiddenValue([]);
const hiddenDiv = expectHiddenValue([]);
const inputList = hiddenDiv.find(InputList);
expect(inputList.length).to.equal(1);
expect(inputList.prop("setting")).to.equal(menuSetting);
expect(inputList.prop("altValue")).to.equal("Alternate");
expect(inputList.find("select").length).to.equal(1);
expect(inputList.prop("readOnly")).to.be.true;
expect(inputList.prop("disableButton")).to.be.true;
});

it("renders hidden image setting", () => {
const imageSetting = { ...setting, type: "image" };
wrapper.setProps({
setting: imageSetting,
value: "data:image/png;base64,...",
});

expectHiddenValue("data:image/png;base64,...");
wrapper.setProps({ setting: imageSetting });
let hiddenDiv = expectHiddenValue("");

const input = hiddenDiv.find(EditableInput);
expect(input.length).to.equal(1);
expect(input.prop("type")).to.equal("file");
expect(input.prop("name")).to.equal("setting");
expect(input.prop("label")).to.equal("label");
expect(input.prop("value")).to.be.undefined;
expect(input.prop("accept")).to.equal("image/*");

wrapper.setProps({ value: "data:image/png;base64,..." });
hiddenDiv = expectHiddenValue("");
const img = hiddenDiv.find("img");
expect(img.prop("src")).to.equal("data:image/png;base64,...");
});

it("renders hidden color picker setting", () => {
Expand All @@ -445,36 +510,25 @@ describe("ProtocolFormField", () => {
type: "color-picker",
default: "#aaaaaa",
};
wrapper.setProps({ setting: colorPickerSetting });

// Use the default value.
expectHiddenValue("#aaaaaa");
// If there is no value, then we use the default.
wrapper.setProps({ setting: colorPickerSetting, value: undefined });
const hiddenDiv = expectHiddenValue("#aaaaaa");
const picker = hiddenDiv.find(ColorPicker);
expect(picker.length).to.equal(1);
expect(picker.prop("setting")).to.equal(colorPickerSetting);
expect(picker.prop("value")).to.equal("#aaaaaa");

// Explicitly set the value.
// If a value is provided, then we use that value.
wrapper.setProps({ value: "#222222" });
expectHiddenValue("#222222");
expectHiddenValue("#aaaaaa");
});

it.skip("gets value of hidden list setting without options", () => {
it("gets value of hidden list setting without options", () => {
wrapper.setProps({
setting: { ...setting, ...{ type: "list" } },
setting: { ...setting, type: "list" },
value: ["item 1", "item 2"],
});
expectHiddenValue(["item 1", "item 2"]);
expect(
(wrapper.instance() as ProtocolFormField).getValue()
).to.deep.equal(["item 1", "item 2"]);
});

it("accepts instructions, but ignores them when hidden", () => {
const instructionsSetting = {
...setting,
...{ instructions: "<ul><li>Step 1</li></ul>", type: "list" },
};
wrapper.setProps({ setting: instructionsSetting });

const instructions = wrapper.find(".well");
expect(instructions.length).to.equal(0);
});
});
});

0 comments on commit 5c88315

Please sign in to comment.