aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ErrorMessage.tsx24
-rw-r--r--src/components/InputTypes/Radio.tsx5
-rw-r--r--src/components/InputTypes/Range.tsx6
-rw-r--r--src/components/InputTypes/Select.tsx18
-rw-r--r--src/components/InputTypes/ShortText.tsx14
-rw-r--r--src/components/InputTypes/TextArea.tsx14
-rw-r--r--src/components/InputTypes/index.tsx19
-rw-r--r--src/components/Question.tsx112
8 files changed, 189 insertions, 23 deletions
diff --git a/src/components/ErrorMessage.tsx b/src/components/ErrorMessage.tsx
new file mode 100644
index 0000000..650100d
--- /dev/null
+++ b/src/components/ErrorMessage.tsx
@@ -0,0 +1,24 @@
+/** @jsx jsx */
+import { jsx, css } from "@emotion/react";
+import colors from "../colors";
+
+interface ErrorMessageProps {
+ show: boolean,
+ message: string
+}
+
+export default function ErrorMessage(props: ErrorMessageProps): JSX.Element | null {
+ const styles = css`
+ color: ${colors.error};
+ font-size: 1.15rem;
+ line-height: 1.1rem;
+ margin: 1rem 0 0;
+ visibility: ${props.show ? "visible" : "hidden"};
+ position: absolute;
+ z-index: -1;
+ `;
+
+ return (
+ <p css={styles}>{props.message}</p>
+ );
+}
diff --git a/src/components/InputTypes/Radio.tsx b/src/components/InputTypes/Radio.tsx
index 3bf13ed..a857964 100644
--- a/src/components/InputTypes/Radio.tsx
+++ b/src/components/InputTypes/Radio.tsx
@@ -7,7 +7,8 @@ import { multiSelectInput, hiddenInput } from "../../commonStyles";
interface RadioProps {
option: string,
question_id: string,
- handler: (event: ChangeEvent<HTMLInputElement>) => void
+ handler: (event: ChangeEvent<HTMLInputElement>) => void,
+ onBlurHandler: () => void
}
const styles = css`
@@ -31,7 +32,7 @@ const styles = css`
export default function Radio(props: RadioProps): JSX.Element {
return (
<label css={styles}>
- <input type="radio" name={props.question_id} onChange={props.handler} css={hiddenInput}/>
+ <input type="radio" name={props.question_id} onChange={props.handler} css={hiddenInput} onBlur={props.onBlurHandler}/>
<div css={multiSelectInput}/>
{props.option}<br/>
</label>
diff --git a/src/components/InputTypes/Range.tsx b/src/components/InputTypes/Range.tsx
index e2f89f4..23cb3f6 100644
--- a/src/components/InputTypes/Range.tsx
+++ b/src/components/InputTypes/Range.tsx
@@ -7,7 +7,9 @@ import { hiddenInput, multiSelectInput } from "../../commonStyles";
interface RangeProps {
question_id: string,
options: Array<string>,
- handler: (event: ChangeEvent<HTMLInputElement>) => void
+ handler: (event: ChangeEvent<HTMLInputElement>) => void,
+ required: boolean,
+ onBlurHandler: () => void
}
const containerStyles = css`
@@ -99,7 +101,7 @@ export default function Range(props: RangeProps): JSX.Element {
return (
<label css={[selectorStyles, css`width: 1rem`]} key={index}>
<span css={optionStyles}>{option}</span>
- <input type="radio" name={props.question_id} css={hiddenInput} onChange={props.handler}/>
+ <input type="radio" name={props.question_id} css={hiddenInput} onChange={props.handler} onBlur={props.onBlurHandler}/>
<div css={multiSelectInput}/>
</label>
);
diff --git a/src/components/InputTypes/Select.tsx b/src/components/InputTypes/Select.tsx
index e753357..2d0187a 100644
--- a/src/components/InputTypes/Select.tsx
+++ b/src/components/InputTypes/Select.tsx
@@ -1,11 +1,13 @@
/** @jsx jsx */
import { jsx, css } from "@emotion/react";
import React from "react";
-import { hiddenInput } from "../../commonStyles";
+import { hiddenInput, invalidStyles } from "../../commonStyles";
interface SelectProps {
options: Array<string>,
- state_dict: Map<string, string | boolean | null>
+ state_dict: Map<string, string | boolean | null>,
+ valid: boolean,
+ onBlurHandler: () => void
}
const containerStyles = css`
@@ -175,6 +177,14 @@ class Select extends React.Component<SelectProps> {
}
}
+ focusOption(): void {
+ if (!this.props.state_dict.get("value")) {
+ this.props.state_dict.set("value", "temporary");
+ this.props.onBlurHandler();
+ this.props.state_dict.set("value", null);
+ }
+ }
+
render(): JSX.Element {
const container_ref: React.RefObject<HTMLDivElement> = React.createRef();
const selected_option_ref: React.RefObject<HTMLDivElement> = React.createRef();
@@ -182,8 +192,8 @@ class Select extends React.Component<SelectProps> {
const handle_click = (event: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => this.handle_click(container_ref, selected_option_ref, event);
return (
- <div css={[containerStyles, arrowStyles, optionContainerStyles]} ref={container_ref}>
- <div className="selected_container" css={mainWindowStyles}>
+ <div css={[containerStyles, arrowStyles, optionContainerStyles, invalidStyles]} onFocus={this.focusOption.bind(this)} ref={container_ref} onBlur={this.props.onBlurHandler}>
+ <div css={mainWindowStyles} className={!this.props.valid ? "invalid-box selected_container" : "selected_container"}>
<span className="arrow"/>
<div tabIndex={0} className="selected_option" ref={selected_option_ref} onMouseDown={handle_click} onKeyDown={handle_click}>...</div>
</div>
diff --git a/src/components/InputTypes/ShortText.tsx b/src/components/InputTypes/ShortText.tsx
index 1e38bcd..8d99dc6 100644
--- a/src/components/InputTypes/ShortText.tsx
+++ b/src/components/InputTypes/ShortText.tsx
@@ -1,12 +1,20 @@
/** @jsx jsx */
import { jsx } from "@emotion/react";
import React, { ChangeEvent } from "react";
-import { textInputs } from "../../commonStyles";
+import { textInputs, invalidStyles } from "../../commonStyles";
interface ShortTextProps {
- handler: (event: ChangeEvent<HTMLInputElement>) => void
+ handler: (event: ChangeEvent<HTMLInputElement>) => void,
+ onBlurHandler: () => void,
+ valid: boolean,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ focus_ref: React.RefObject<any>
}
export default function ShortText(props: ShortTextProps): JSX.Element {
- return <input type="text" css={textInputs} placeholder="Enter Text..." onChange={props.handler}/>;
+ return (
+ <div css={invalidStyles}>
+ <input type="text" css={textInputs} placeholder="Enter Text..." onChange={props.handler} onBlur={props.onBlurHandler} className={!props.valid ? "invalid-box" : ""} ref={props.focus_ref}/>
+ </div>
+ );
}
diff --git a/src/components/InputTypes/TextArea.tsx b/src/components/InputTypes/TextArea.tsx
index 6e46c27..08424fb 100644
--- a/src/components/InputTypes/TextArea.tsx
+++ b/src/components/InputTypes/TextArea.tsx
@@ -1,10 +1,14 @@
/** @jsx jsx */
import { jsx, css } from "@emotion/react";
import React, { ChangeEvent } from "react";
-import { textInputs } from "../../commonStyles";
+import { invalidStyles, textInputs } from "../../commonStyles";
interface TextAreaProps {
- handler: (event: ChangeEvent<HTMLTextAreaElement>) => void
+ handler: (event: ChangeEvent<HTMLTextAreaElement>) => void,
+ onBlurHandler: () => void,
+ valid: boolean,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ focus_ref: React.RefObject<any>
}
const styles = css`
@@ -17,5 +21,9 @@ const styles = css`
`;
export default function TextArea(props: TextAreaProps): JSX.Element {
- return <textarea css={[textInputs, styles]} placeholder="Enter Text..." onChange={props.handler}/>;
+ return (
+ <div css={invalidStyles}>
+ <textarea css={[textInputs, styles]} placeholder="Enter Text..." onChange={props.handler} onBlur={props.onBlurHandler} className={!props.valid ? "invalid-box" : ""} ref={props.focus_ref}/>
+ </div>
+ );
}
diff --git a/src/components/InputTypes/index.tsx b/src/components/InputTypes/index.tsx
index f1e0b30..bc65248 100644
--- a/src/components/InputTypes/index.tsx
+++ b/src/components/InputTypes/index.tsx
@@ -18,12 +18,17 @@ const require_options: Array<QuestionType> = [
QuestionType.Range
];
-export default function create_input({ question, public_state }: QuestionProp, handler: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void): JSX.Element | JSX.Element[] {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export default function create_input({ question, public_state }: QuestionProp, handler: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void, onBlurHandler: () => void, focus_ref: React.RefObject<any>): JSX.Element | JSX.Element[] {
let result: JSX.Element | JSX.Element[];
// eslint-disable-next-line
// @ts-ignore
let options: string[] = question.data["options"];
+ let valid = true;
+ if (!public_state.get("valid")) {
+ valid = false;
+ }
// Catch input types that require options but don't have any
if ((options === undefined || typeof options !== "object") && require_options.includes(question.type)) {
@@ -34,7 +39,7 @@ export default function create_input({ question, public_state }: QuestionProp, h
/* eslint-disable react/react-in-jsx-scope */
switch (question.type) {
case QuestionType.TextArea:
- result = <TextArea handler={handler}/>;
+ result = <TextArea handler={handler} valid={valid} onBlurHandler={onBlurHandler} focus_ref={focus_ref}/>;
break;
case QuestionType.Checkbox:
@@ -42,19 +47,19 @@ export default function create_input({ question, public_state }: QuestionProp, h
break;
case QuestionType.Radio:
- result = options.map((option, index) => <Radio option={option} question_id={question.id} handler={handler} key={index}/>);
+ result = options.map((option, index) => <Radio option={option} question_id={question.id} handler={handler} key={index} onBlurHandler={onBlurHandler}/>);
break;
case QuestionType.Select:
- result = <Select options={options} state_dict={public_state}/>;
+ result = <Select options={options} state_dict={public_state} valid={valid} onBlurHandler={onBlurHandler}/>;
break;
case QuestionType.ShortText:
- result = <ShortText handler={handler}/>;
+ result = <ShortText handler={handler} onBlurHandler={onBlurHandler} valid={valid} focus_ref={focus_ref}/>;
break;
case QuestionType.Range:
- result = <Range question_id={question.id} options={options} handler={handler}/>;
+ result = <Range question_id={question.id} options={options} handler={handler} required={question.required} onBlurHandler={onBlurHandler}/>;
break;
case QuestionType.Code:
@@ -63,7 +68,7 @@ export default function create_input({ question, public_state }: QuestionProp, h
break;
default:
- result = <TextArea handler={handler}/>;
+ result = <TextArea handler={handler} valid={valid} onBlurHandler={onBlurHandler} focus_ref={focus_ref}/>;
}
/* eslint-enable react/react-in-jsx-scope */
diff --git a/src/components/Question.tsx b/src/components/Question.tsx
index 735d69b..0af745e 100644
--- a/src/components/Question.tsx
+++ b/src/components/Question.tsx
@@ -5,6 +5,7 @@ import React, { ChangeEvent } from "react";
import { Question, QuestionType } from "../api/question";
import { selectable } from "../commonStyles";
import create_input from "./InputTypes";
+import ErrorMessage from "./ErrorMessage";
const skip_normal_state: Array<QuestionType> = [
QuestionType.Radio,
@@ -17,6 +18,9 @@ const skip_normal_state: Array<QuestionType> = [
export type QuestionProp = {
question: Question,
public_state: Map<string, string | boolean | null>,
+ scroll_ref: React.RefObject<HTMLDivElement>,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ focus_ref: React.RefObject<any>
}
class RenderedQuestion extends React.Component<QuestionProp> {
@@ -27,6 +31,10 @@ class RenderedQuestion extends React.Component<QuestionProp> {
} else {
this.handler = this.normal_handler.bind(this);
}
+ this.blurHandler = this.blurHandler.bind(this);
+
+ this.setPublicState("valid", true);
+ this.setPublicState("error", "");
if (!skip_normal_state.includes(props.question.type)) {
this.setPublicState("value", "");
@@ -41,6 +49,18 @@ class RenderedQuestion extends React.Component<QuestionProp> {
// This is here to allow dynamic selection between the general handler, and the textarea handler.
handler(_: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void {} // eslint-disable-line
+ blurHandler(): void {
+ if (this.props.question.required) {
+ if (!this.props.public_state.get("value")) {
+ this.setPublicState("error", "Field must be filled.");
+ this.setPublicState("valid", false);
+ } else {
+ this.setPublicState("error", "");
+ this.setPublicState("valid", true);
+ }
+ }
+ }
+
normal_handler(event: ChangeEvent<HTMLInputElement>): void {
let target: string;
let value: string | boolean;
@@ -73,12 +93,90 @@ class RenderedQuestion extends React.Component<QuestionProp> {
event.target.parentElement.classList.toggle("unselected");
event.target.parentElement.classList.toggle("selected");
}
+
+ const options: string | string[] = this.props.question.data["options"];
+ switch (event.target.type) {
+ case "text":
+ this.setPublicState("valid", true);
+ break;
+
+ case "checkbox":
+ // We need to check this here, because checkbox doesn't have onBlur
+ if (this.props.question.required && typeof options !== "string") {
+ const keys: string[] = [];
+ options.forEach((val, index) => {
+ keys.push(`${("000" + index).slice(-4)}. ${val}`);
+ });
+ if (keys.every(v => !this.props.public_state.get(v))) {
+ this.setPublicState("error", "Field must be filled.");
+ this.setPublicState("valid", false);
+ } else {
+ this.setPublicState("error", "");
+ this.setPublicState("valid", true);
+ }
+ }
+ break;
+
+ case "radio":
+ this.setPublicState("valid", true);
+ this.setPublicState("error", "");
+ break;
+ }
}
text_area_handler(event: ChangeEvent<HTMLTextAreaElement>): void {
+ // We will validate again when focusing out.
+ this.setPublicState("valid", true);
+ this.setPublicState("error", "");
+
this.setPublicState("value", event.target.value);
}
+ validateField(): void {
+ if (!this.props.question.required) {
+ return;
+ }
+
+ let invalid = false;
+ const options: string | string[] = this.props.question.data["options"];
+ switch (this.props.question.type) {
+ case QuestionType.TextArea:
+ case QuestionType.ShortText:
+ if (this.props.public_state.get("value") === "") {
+ invalid = true;
+ }
+ break;
+
+ case QuestionType.Select:
+ case QuestionType.Range:
+ case QuestionType.Radio:
+ if (!this.props.public_state.get("value")) {
+ invalid = true;
+ }
+ break;
+
+ case QuestionType.Checkbox:
+ if (typeof options !== "string") {
+ const keys: string[] = [];
+ options.forEach((val, index) => {
+ keys.push(`${("000" + index).slice(-4)}. ${val}`);
+ });
+ if (keys.every(v => !this.props.public_state.get(v))) {
+ invalid = true;
+ }
+ }
+ break;
+ }
+
+ if (invalid) {
+ this.setPublicState("error", "Field must be filled.");
+ this.setPublicState("valid", false);
+ } else {
+ this.setPublicState("error", "");
+ this.setPublicState("valid", true);
+ }
+ }
+
componentDidMount(): void {
// Initialize defaults for complex and nested fields
const options: string | string[] = this.props.question.data["options"];
@@ -151,12 +249,22 @@ class RenderedQuestion extends React.Component<QuestionProp> {
margin-left: 0.2rem;
}
`;
+ let valid = true;
+ if (!this.props.public_state.get("valid")) {
+ valid = false;
+ }
+ const rawError = this.props.public_state.get("error");
+ let error = "";
+ if (typeof rawError === "string") {
+ error = rawError;
+ }
- return <div>
+ return <div ref={this.props.scroll_ref}>
<h2 css={[selectable, requiredStarStyles]}>
{question.name}<span className={question.required ? "required" : ""}>*</span>
</h2>
- { create_input(this.props, this.handler) }
+ { create_input(this.props, this.handler, this.blurHandler, this.props.focus_ref) }
+ <ErrorMessage show={!valid} message={error} />
<hr css={css`color: gray; margin: 3rem 0;`}/>
</div>;
}