Skip to content

Commit

Permalink
Merge pull request #1222 from facebookresearch/form-composer-instruct…
Browse files Browse the repository at this point in the history
…ions-as-modal

Accommodate lengthy task instructions in Form Composer
  • Loading branch information
meta-paul authored Aug 7, 2024
2 parents 8cdc9c2 + 6bd0d2d commit 177ae60
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions examples/form_composer_demo/data/dynamic/form_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"form": {
"title": "Form example",
"instruction": "insertions/form_instruction.html",
"show_instructions_as_modal": true,
"sections": [
{
"name": "section_about",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"type": list,
"required": True,
},
"show_instructions_as_modal": {
"type": bool,
"required": False,
},
"submit_button": {
"type": dict,
"required": True,
Expand Down
63 changes: 61 additions & 2 deletions packages/react-form-composer/src/FormComposer/FormComposer.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ 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;
padding-top: 20px;
display: flex;
flex-direction: column;
justify-content: center;
max-width: 1280px;
max-width: var(--form-max-width);
}

.form-composer .form-header {
Expand All @@ -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 {
}
Expand Down Expand Up @@ -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 {
Expand Down
62 changes: 56 additions & 6 deletions packages/react-form-composer/src/FormComposer/FormComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -63,6 +65,12 @@ function FormComposer({
// Fild list by section index for error display: { <sectionIndex>: <Array<field>> }
const [sectionsFields, setSectionsFields] = React.useState({});

// Form instruction modal state
const [
formInstrupctionModalOpen,
setFormInstrupctionModalOpen,
] = React.useState(false);

const inReviewState = finalResults !== null;

const formatStringWithTokens = inReviewState
Expand All @@ -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;

Expand Down Expand Up @@ -233,13 +243,38 @@ function FormComposer({
></h2>
)}

{formTitle && formInstruction && <hr />}
{/* Show instruction or button that opens a modal with instructions */}
{showFormInstructionAsModal ? (
<>
{/* Instructions */}
{formTitle && formInstruction && <hr />}

{formInstruction && (
<p
className={`form-instruction`}
dangerouslySetInnerHTML={{ __html: formInstruction }}
></p>
{formInstruction && (
<div>
For instructions, click "Task Instruction" button in the
top-right corner.
</div>
)}

{/* Button (modal in the end of the component) */}
<FormInstructionsButton
onClick={() =>
setFormInstrupctionModalOpen(!formInstrupctionModalOpen)
}
/>
</>
) : (
<>
{/* Instructions */}
{formTitle && formInstruction && <hr />}

{formInstruction && (
<p
className={`form-instruction`}
dangerouslySetInnerHTML={{ __html: formInstruction }}
></p>
)}
</>
)}
</div>
)}
Expand Down Expand Up @@ -689,6 +724,21 @@ function FormComposer({

{/* Unexpected server errors */}
{!!submitErrors.length && <FormErrors errorMessages={submitErrors} />}

{/* Modal with form instructions */}
{showFormInstructionAsModal && formInstruction && (
<FormInstructionsModal
instructions={
<p
className={`form-instruction`}
dangerouslySetInnerHTML={{ __html: formInstruction }}
></p>
}
open={formInstrupctionModalOpen}
setOpen={setFormInstrupctionModalOpen}
title={"Task Instructions"}
/>
)}
</form>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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

<button
className={`form-instruction-button btn btn-primary btn-sm`}
data-target={"#id-form-instruction-modal"}
data-toggle={"modal"}
onClick={onClick}
type={"button"}
>
<span>&#9432;</span> Task Instructions
</button>
);
}
Original file line number Diff line number Diff line change
@@ -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

<div
className={"form-instruction-modal modal"}
id={"id-form-instruction-modal"}
data-backdrop={"static"}
data-keyboard={"false"}
tabIndex={"-1"}
aria-labelledby={"id-modal-title"}
aria-hidden={"true"}
>
<div className={"modal-dialog modal-dialog-scrollable"}>
<div className={"modal-content"}>
<div className={"modal-header"}>
<div className={"modal-title"} id={"id-modal-title"}>
{title}
</div>

<button
type={"button"}
className={"close"}
data-dismiss={"modal"}
aria-label={"Close"}
onClick={() => setOpen(false)}
>
<span aria-hidden={"true"}>&times;</span>
</button>
</div>

<div
className={"modal-body"}
onScroll={onScrollModalContent}
ref={modalContentRef}
>
{instructions}
</div>
</div>
</div>
</div>
);
}

0 comments on commit 177ae60

Please sign in to comment.