diff --git a/docs/web/docs/guides/how_to_use/form_composer/configuration/config_files.md b/docs/web/docs/guides/how_to_use/form_composer/configuration/config_files.md index 91ee09b99..68675c48e 100644 --- a/docs/web/docs/guides/how_to_use/form_composer/configuration/config_files.md +++ b/docs/web/docs/guides/how_to_use/form_composer/configuration/config_files.md @@ -44,6 +44,7 @@ Task data config file `task_data.json` specifies layout of all form versions tha "form": { "title": "Form example", "instruction": "Please answer all questions to the best of your ability as part of our study.", + "show_instructions_as_modal": false, "sections": [ // Two sections { @@ -159,8 +160,9 @@ TBD: Other classes and styles insertions `form` is a top-level config object with the following attributes: - `id` - Unique HTML id of the form, in case we need to refer to it from custom handlers code (String, Optional) -- `classes` = Custom classes that you can use to restyle element or refer to it from custom handlers code (String, Optional) +- `classes` - Custom classes that you can use to restyle element or refer to it from custom handlers code (String, Optional) - `instruction` - HTML content describing this form; it is located before all contained sections (String, Optional) +- `show_instructions_as_modal` - Enables showing `instruction` content as a modal (opened by clicking a sticky button in top-right corner); this make lengthy task instructions available from any place of a lengthy form without scrolling the page (Boolean, Optional, Default: false) - `title` - HTML header of the form (String) - `submit_button` - Button to submit the whole form and thus finish a task (Object) - `id` - Unique HTML id of the button, in case we need to refer to it from custom handlers code (String, Optional) diff --git a/examples/form_composer_demo/data/dynamic/form_config.json b/examples/form_composer_demo/data/dynamic/form_config.json index fcaf62231..f293f928d 100644 --- a/examples/form_composer_demo/data/dynamic/form_config.json +++ b/examples/form_composer_demo/data/dynamic/form_config.json @@ -2,6 +2,7 @@ "form": { "title": "Form example", "instruction": "insertions/form_instruction.html", + "show_instructions_as_modal": true, "sections": [ { "name": "section_about", diff --git a/mephisto/generators/form_composer/config_validation/config_validation_constants.py b/mephisto/generators/form_composer/config_validation/config_validation_constants.py index 5a7fad653..860f0af42 100644 --- a/mephisto/generators/form_composer/config_validation/config_validation_constants.py +++ b/mephisto/generators/form_composer/config_validation/config_validation_constants.py @@ -40,6 +40,10 @@ "type": list, "required": True, }, + "show_instructions_as_modal": { + "type": bool, + "required": False, + }, "submit_button": { "type": dict, "required": True, diff --git a/packages/react-form-composer/src/FormComposer/FormComposer.css b/packages/react-form-composer/src/FormComposer/FormComposer.css index 6cc26b7cb..beff3b976 100644 --- a/packages/react-form-composer/src/FormComposer/FormComposer.css +++ b/packages/react-form-composer/src/FormComposer/FormComposer.css @@ -16,8 +16,9 @@ video { /* --- Form --- */ .form-composer { /* Variables */ - --input-bg-color: #fafafa; --error-color: red; + --form-max-width: 1280px; + --input-bg-color: #fafafa; --orange-color: orange; margin: 0 auto; @@ -25,7 +26,7 @@ video { display: flex; flex-direction: column; justify-content: center; - max-width: 1280px; + max-width: var(--form-max-width); } .form-composer .form-header { @@ -38,6 +39,12 @@ video { .form-composer .form-header .form-instruction { } +.form-composer .form-header .form-instruction-button { + position: fixed; + right: 10px; + top: 10px; +} + /* --- Section --- */ .form-composer .section { } @@ -311,6 +318,58 @@ video { .form-composer .form-buttons .button-submit { } +/* --- Form instruction modal --- */ +.form-composer .form-instruction-modal { + padding: 10px 0; + background-color: #ffffff; +} + +.form-composer .form-instruction-modal .modal-dialog { + width: initial; + max-width: var(--form-max-width); + max-height: 100%; + margin: 0 auto; +} + +.form-composer .form-instruction-modal .modal-dialog .modal-content { + box-shadow: 0 10px 20px 10px rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 10px 20px 10px rgba(0, 0, 0, 0.5); +} + +.form-composer + .form-instruction-modal + .modal-dialog + .modal-content + .modal-header { + padding: 10px 20px; + align-items: center; + background-color: #cce5ff; +} + +.form-composer + .form-instruction-modal + .modal-dialog + .modal-content + .modal-header + .modal-title { + font-size: 21px; + font-weight: 500; + line-height: initial; +} + +.form-composer + .form-instruction-modal + .modal-dialog + .modal-content + .modal-header + .close { + margin: 0 0 0 auto; + font-size: 30px; + width: 40px; + height: 40px; + line-height: 0; +} + /* --- Bootstrap overriding --- */ .form-control::placeholder { diff --git a/packages/react-form-composer/src/FormComposer/FormComposer.js b/packages/react-form-composer/src/FormComposer/FormComposer.js index 8ed760dae..1b3399735 100644 --- a/packages/react-form-composer/src/FormComposer/FormComposer.js +++ b/packages/react-form-composer/src/FormComposer/FormComposer.js @@ -20,6 +20,8 @@ import { SelectField } from "./fields/SelectField"; import { TextareaField } from "./fields/TextareaField"; import "./FormComposer.css"; import { FormErrors } from "./FormErrors"; +import { FormInstructionsButton } from "./FormInstructionsButton"; +import { FormInstructionsModal } from "./FormInstructionsModal"; import { SectionErrors } from "./SectionErrors"; import { SectionErrorsCountBadge } from "./SectionErrorsCountBadge"; import { @@ -63,6 +65,12 @@ function FormComposer({ // Fild list by section index for error display: { : > } const [sectionsFields, setSectionsFields] = React.useState({}); + // Form instruction modal state + const [ + formInstrupctionModalOpen, + setFormInstrupctionModalOpen, + ] = React.useState(false); + const inReviewState = finalResults !== null; const formatStringWithTokens = inReviewState @@ -79,6 +87,8 @@ function FormComposer({ formComposerConfig.instruction, setRenderingErrors ); + let showFormInstructionAsModal = + formComposerConfig.show_instructions_as_modal || false; let formSections = formComposerConfig.sections; let formSubmitButton = formComposerConfig.submit_button; @@ -233,13 +243,38 @@ function FormComposer({ > )} - {formTitle && formInstruction &&
} + {/* Show instruction or button that opens a modal with instructions */} + {showFormInstructionAsModal ? ( + <> + {/* Instructions */} + {formTitle && formInstruction &&
} - {formInstruction && ( -

+ {formInstruction && ( +
+ For instructions, click "Task Instruction" button in the + top-right corner. +
+ )} + + {/* Button (modal in the end of the component) */} + + setFormInstrupctionModalOpen(!formInstrupctionModalOpen) + } + /> + + ) : ( + <> + {/* Instructions */} + {formTitle && formInstruction &&
} + + {formInstruction && ( +

+ )} + )} )} @@ -689,6 +724,21 @@ function FormComposer({ {/* Unexpected server errors */} {!!submitErrors.length && } + + {/* Modal with form instructions */} + {showFormInstructionAsModal && formInstruction && ( +

+ } + open={formInstrupctionModalOpen} + setOpen={setFormInstrupctionModalOpen} + title={"Task Instructions"} + /> + )} ); } diff --git a/packages/react-form-composer/src/FormComposer/FormInstructionsButton.js b/packages/react-form-composer/src/FormComposer/FormInstructionsButton.js new file mode 100644 index 000000000..afb75e627 --- /dev/null +++ b/packages/react-form-composer/src/FormComposer/FormInstructionsButton.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) Meta Platforms and its affiliates. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from "react"; + +export function FormInstructionsButton({ onClick }) { + return ( + // bootstrap classes: + // - btn + // - btn-primary + // - btn-sm + + + ); +} diff --git a/packages/react-form-composer/src/FormComposer/FormInstructionsModal.js b/packages/react-form-composer/src/FormComposer/FormInstructionsModal.js new file mode 100644 index 000000000..184d9e75a --- /dev/null +++ b/packages/react-form-composer/src/FormComposer/FormInstructionsModal.js @@ -0,0 +1,79 @@ +/* + * Copyright (c) Meta Platforms and its affiliates. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from "react"; + +export function FormInstructionsModal({ instructions, open, setOpen, title }) { + const modalContentRef = React.useRef(null); + + const [modalContentTopPosition, setModalContentTopPosition] = React.useState( + 0 + ); + + function onScrollModalContent(e) { + // Save scrolling position to restore it when we open this modal again + setModalContentTopPosition(e.currentTarget.scrollTop); + } + + React.useEffect(() => { + if (open) { + // Set saved scrolling position to continue reading from that place we stopped. + // This is needed in case if instruction is too long, + // and it is hard to start searching previous place again + modalContentRef.current.scrollTo(0, modalContentTopPosition); + } + }, [open]); + + return ( + // bootstrap classes: + // - modal + // - modal-dialog + // - modal-dialog-scrollable + // - modal-content + // - modal-header + // - modal-title + // - close + // - modal-body + +
+
+
+
+
+ {title} +
+ + +
+ +
+ {instructions} +
+
+
+
+ ); +}