From 6bd0d2da1047e142d7e0bcdc8c488d6ceed70da4 Mon Sep 17 00:00:00 2001
From: Paul Abumov
Date: Wed, 7 Aug 2024 09:53:08 -0400
Subject: [PATCH] Accommodate lengthy task instructions in Form Composer
---
.../configuration/config_files.md | 4 +-
.../data/dynamic/form_config.json | 1 +
.../config_validation_constants.py | 4 +
.../src/FormComposer/FormComposer.css | 63 ++++++++++++++-
.../src/FormComposer/FormComposer.js | 62 +++++++++++++--
.../FormComposer/FormInstructionsButton.js | 26 ++++++
.../src/FormComposer/FormInstructionsModal.js | 79 +++++++++++++++++++
7 files changed, 230 insertions(+), 9 deletions(-)
create mode 100644 packages/react-form-composer/src/FormComposer/FormInstructionsButton.js
create mode 100644 packages/react-form-composer/src/FormComposer/FormInstructionsModal.js
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
+
+