diff options
author | 2021-02-20 03:50:58 +0300 | |
---|---|---|
committer | 2021-02-20 03:53:45 +0300 | |
commit | 0278c8f567bfc50fcb65aaf6afe7cd82c5031023 (patch) | |
tree | a20d335623dc8b9e9038de1d5dc03e481039ecd8 /src/components | |
parent | Removes Path From Auth (diff) | |
parent | Adds Missing Fields To Test Models (diff) |
Merge branch 'main' into discord-oauth
Signed-off-by: Hassan Abouelela <[email protected]>
# Conflicts:
# package.json
# src/commonStyles.tsx
# src/pages/FormPage.tsx
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/ErrorMessage.tsx | 24 | ||||
-rw-r--r-- | src/components/InputTypes/Radio.tsx | 5 | ||||
-rw-r--r-- | src/components/InputTypes/Range.tsx | 6 | ||||
-rw-r--r-- | src/components/InputTypes/Select.tsx | 18 | ||||
-rw-r--r-- | src/components/InputTypes/ShortText.tsx | 14 | ||||
-rw-r--r-- | src/components/InputTypes/TextArea.tsx | 14 | ||||
-rw-r--r-- | src/components/InputTypes/index.tsx | 19 | ||||
-rw-r--r-- | src/components/Question.tsx | 112 |
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>; } |