diff options
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>;          } | 
