diff --git a/futurenhs.app/assets/scss/_imports.scss b/futurenhs.app/assets/scss/_imports.scss index 85d589b95..7d3c75d39 100644 --- a/futurenhs.app/assets/scss/_imports.scss +++ b/futurenhs.app/assets/scss/_imports.scss @@ -61,6 +61,7 @@ @import 'modules/notification-banner/notification-banner'; @import 'modules/group-privacy/group-privacy'; @import 'modules/group-teaser/group-teaser'; +@import 'modules/multi-input/multi-input'; @import 'objects/objects'; @import 'utilities/utilities'; diff --git a/futurenhs.app/assets/scss/modules/button/_button.scss b/futurenhs.app/assets/scss/modules/button/_button.scss index feb3d8306..b0619bfcf 100644 --- a/futurenhs.app/assets/scss/modules/button/_button.scss +++ b/futurenhs.app/assets/scss/modules/button/_button.scss @@ -1,6 +1,14 @@ .c-button { @extend %nhsuk-button; + &--alt { + @extend %nhsuk-button; + background-color: $blue; + } + &--disabled { + @extend %nhsuk-button; + filter: grayscale(100%); + } &--outline { @extend %toggle-button; } @@ -18,11 +26,6 @@ } &--themeable { - @extend %nhsuk-button-themeable + @extend %nhsuk-button-themeable; } - } - -.c-button-outline { - @extend %toggle-button; -} \ No newline at end of file diff --git a/futurenhs.app/assets/scss/modules/multi-input/_multi-input.scss b/futurenhs.app/assets/scss/modules/multi-input/_multi-input.scss new file mode 100644 index 000000000..e73990e54 --- /dev/null +++ b/futurenhs.app/assets/scss/modules/multi-input/_multi-input.scss @@ -0,0 +1,18 @@ +.c-multi-input { + &-enter-button { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 0; + right: 0; + height: 100%; + background: rgba(0, 0, 0, 0); + background-color: $green; + border: none; + &--disabled, + &--pressed { + filter: grayscale(100%) brightness(200%); + } + } +} diff --git a/futurenhs.app/components/blocks/KeyLinksBlock/index.tsx b/futurenhs.app/components/blocks/KeyLinksBlock/index.tsx index 5b01a9ddf..4ea1040f8 100644 --- a/futurenhs.app/components/blocks/KeyLinksBlock/index.tsx +++ b/futurenhs.app/components/blocks/KeyLinksBlock/index.tsx @@ -11,7 +11,7 @@ import { useCsrf } from '@helpers/hooks/useCsrf' import { Heading } from '@components/layouts/Heading' import { LayoutColumnContainer } from '@components/layouts/LayoutColumnContainer' import { LayoutColumn } from '@components/layouts/LayoutColumn' -import { Form } from '@components/forms/Form' +import { Form } from '@components/old_forms/Form' import { SVGIcon } from '@components/generic/SVGIcon' import { Theme } from '@appTypes/theme' import { FormConfig } from '@appTypes/form' diff --git a/futurenhs.app/components/blocks/PendingMemberActions/index.tsx b/futurenhs.app/components/blocks/PendingMemberActions/index.tsx index fcd5fc88b..0de40a870 100644 --- a/futurenhs.app/components/blocks/PendingMemberActions/index.tsx +++ b/futurenhs.app/components/blocks/PendingMemberActions/index.tsx @@ -3,7 +3,7 @@ import { useFormConfig } from '@helpers/hooks/useForm' import { formTypes } from '@constants/forms' import { FormConfig } from '@appTypes/form' import { useCsrf } from '@helpers/hooks/useCsrf' -import { Form } from '@components/forms/Form' +import { Form } from '@components/old_forms/Form' export const PendingMemberActions: (props: Props) => JSX.Element = ({ acceptAction, diff --git a/futurenhs.app/components/blocks/Reply/index.tsx b/futurenhs.app/components/blocks/Reply/index.tsx index 5b5b9a47b..ec4c4a815 100644 --- a/futurenhs.app/components/blocks/Reply/index.tsx +++ b/futurenhs.app/components/blocks/Reply/index.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react' import classNames from 'classnames' import { formTypes } from '@constants/forms' -import { Form } from '@components/forms/Form' +import { Form } from '@components/old_forms/Form' import { Accordion } from '@components/generic/Accordion' import { SVGIcon } from '@components/generic/SVGIcon' import forms from '@config/form-configs/index' diff --git a/futurenhs.app/components/blocks/TextContentBlock/index.tsx b/futurenhs.app/components/blocks/TextContentBlock/index.tsx index cac5c7ca3..2b8934058 100644 --- a/futurenhs.app/components/blocks/TextContentBlock/index.tsx +++ b/futurenhs.app/components/blocks/TextContentBlock/index.tsx @@ -7,7 +7,7 @@ import { Heading } from '@components/layouts/Heading' import { RichText } from '@components/generic/RichText' import { LayoutColumnContainer } from '@components/layouts/LayoutColumnContainer' import { LayoutColumn } from '@components/layouts/LayoutColumn' -import { Form } from '@components/forms/Form' +import { Form } from '@components/old_forms/Form' import { formTypes } from '@constants/forms' import { FormConfig } from '@appTypes/form' import { CmsContentBlock } from '@appTypes/contentBlock' diff --git a/futurenhs.app/components/forms/Input.tsx b/futurenhs.app/components/forms/Input.tsx new file mode 100644 index 000000000..78afc76e9 --- /dev/null +++ b/futurenhs.app/components/forms/Input.tsx @@ -0,0 +1,46 @@ +import { VMessage } from '@helpers/validation' +import { ReactNode } from 'react' + +export interface Props extends React.HTMLProps { + validation?: Array + label?: string + hint?: string + children?: ReactNode +} + +export const Input = ({ + validation, + label, + hint, + children, + ...inputProps +}: Props) => { + const isError = validation && validation.some((v) => v.error === true) + const classNames = { + input: `nhsuk-input nhsuk-u-width-full + ${isError ? 'nhsuk-input--error ' : null} + ${inputProps.className ?? ''}`, + hint: `nhsuk-hint ${isError ? 'nhsuk-input--error' : null}`, + wrapper: `nhsuk-form-group ${ + isError ? 'nhsuk-form-group--error' : null + }`, + } + + // const onChange = (e) => {} + + return ( +
+ {!!hint && {hint}} + {!!label && } + {!!isError && ( + + {validation[0].message} + + )} +
+ {children} + +
+
+ ) +} diff --git a/futurenhs.app/components/forms/Input/index.tsx b/futurenhs.app/components/forms/Input/index.tsx deleted file mode 100644 index c1cd996dc..000000000 --- a/futurenhs.app/components/forms/Input/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import classNames from 'classnames' - -import { RichText } from '@components/generic/RichText' -import { RemainingCharacterCount } from '@components/forms/RemainingCharacterCount' -import { getAriaFieldAttributes } from '@helpers/util/form' - -import { Props } from './interfaces' - -/** - * Derived from the NHS Design System Text Input component: https://service-manual.nhs.uk/design-system/components/text-input. - * Used to allow users to enter text that’s no longer than a single line, such as their name or phone number. - */ -export const Input: (props: Props) => JSX.Element = ({ - inputType, - input, - initialError, - meta: { touched, error, submitError }, - text, - shouldRenderRemainingCharacterCount, - validators, - className, - autoComplete, - disabled, -}) => { - const { label, hint } = text ?? {} - const id: string = input.name - const shouldRenderError: boolean = - Boolean(initialError) || - ((Boolean(error) || Boolean(submitError)) && touched) - const isRequired: boolean = Boolean( - validators?.find(({ type }) => type === 'required') - ) - const maxLength: boolean = validators?.find( - ({ type }) => type === 'maxLength' - )?.maxLength - - const generatedIds: any = { - hint: `${id}-hint`, - errorLabel: `${id}-error`, - remainingCharacters: `${id}-remaining-characters`, - } - - const generatedClasses: any = { - wrapper: classNames('nhsuk-form-group', className, { - ['nhsuk-form-group--error']: shouldRenderError, - ['u-clearfix']: shouldRenderRemainingCharacterCount && maxLength, - }), - label: classNames('nhsuk-label'), - hint: classNames('nhsuk-hint'), - error: classNames('nhsuk-error-message'), - input: classNames('nhsuk-input nhsuk-u-width-full', { - ['nhsuk-input--error']: shouldRenderError, - ['u-border-0 u-p-0']: inputType === 'file', - }), - } - - const ariaInputProps: any = getAriaFieldAttributes( - isRequired, - shouldRenderError, - [ - Boolean(hint) ? generatedIds.hint : null, - shouldRenderError ? generatedIds.errorLabel : null, - shouldRenderRemainingCharacterCount - ? generatedIds.remainingCharacters - : null, - ] - ) - - return ( -
- - {hint && ( - - )} - {shouldRenderError && ( - - {error || submitError || initialError} - - )} - - {shouldRenderRemainingCharacterCount && maxLength && ( - - )} -
- ) -} diff --git a/futurenhs.app/components/forms/MultiInput.tsx b/futurenhs.app/components/forms/MultiInput.tsx new file mode 100644 index 000000000..14dfbf733 --- /dev/null +++ b/futurenhs.app/components/forms/MultiInput.tsx @@ -0,0 +1,103 @@ +import { FormEvent, KeyboardEvent, useState } from 'react' +import { Input, Props as InputProps } from './Input' +import { mdiKeyboardReturn } from '@mdi/js' +import { SVGIcon } from '@components/generic/SVGIcon' + +interface Props extends InputProps { + getMulti?: (list: Array) => void +} + +export const MultiInput = ({ getMulti, ...inputProps }: Props) => { + const { validation } = inputProps + const [multi, setMulti] = useState>([]) + const [value, setValue] = useState('') + const [enterPress, setEnterPress] = useState(false) + + const isError = validation && validation.some((v) => v.error === true) + const hasOnKeyDown = typeof inputProps.onKeyDown === 'function' + const hasOnKeyUp = typeof inputProps.onKeyDown === 'function' + const hasOnChange = typeof inputProps.onChange === 'function' + const hasGetMulti = typeof getMulti === 'function' + + const onKeyDown = (e: KeyboardEvent) => { + const enterKey = + (e.keyCode === 13 || e.key.toLowerCase() === 'enter') && !isError + + if (enterKey) { + setEnterPress(true) + handleMulti() + } + } + + const onKeyUp = (e: KeyboardEvent) => { + const enterKey = + (e.keyCode === 13 || e.key.toLowerCase() === 'enter') && !isError + if (enterKey) { + setEnterPress(false) + } + } + + const onChange = (e: FormEvent) => { + const el = e.target as HTMLInputElement + setValue(el.value) + } + + const handleMulti = () => { + const val = value + const arr = multi + setValue('') + if (val && !arr.includes(val)) { + const newArr = [...arr, val] + setMulti(newArr) + if (hasGetMulti) { + getMulti(newArr) + } + } + } + + return ( +
+ { + onChange(e) + if (hasOnChange) inputProps.onChange(e) + }} + onKeyDown={(e) => { + onKeyDown(e) + if (hasOnKeyDown) inputProps.onKeyDown(e) + }} + onKeyUp={(e) => { + onKeyUp(e) + if (hasOnKeyUp) inputProps.onKeyUp(e) + }} + > + + + {multi.map((text, i) => ( +
+ setMulti(multi.filter((item) => item !== text)) + } + > + {text} X +
+ ))} +
+ ) +} diff --git a/futurenhs.app/components/generic/SVGIcon/index.tsx b/futurenhs.app/components/generic/SVGIcon/index.tsx index d8b85a9cc..ad870fc86 100644 --- a/futurenhs.app/components/generic/SVGIcon/index.tsx +++ b/futurenhs.app/components/generic/SVGIcon/index.tsx @@ -14,6 +14,7 @@ export const SVGIcon: (props: Props) => JSX.Element = ({ className, material, size = '21px', + color, }) => { const fullPath: string = useAssetPath(url) const xlinkHref: string = fullPath ? `${fullPath}#${name}` : `#${name}` @@ -31,6 +32,12 @@ export const SVGIcon: (props: Props) => JSX.Element = ({ ) : ( - + ) } diff --git a/futurenhs.app/components/generic/SVGIcon/interfaces.ts b/futurenhs.app/components/generic/SVGIcon/interfaces.ts index e6954e859..368e6b867 100644 --- a/futurenhs.app/components/generic/SVGIcon/interfaces.ts +++ b/futurenhs.app/components/generic/SVGIcon/interfaces.ts @@ -4,6 +4,7 @@ export interface Props { className?: string material?: true size?: string + color?: string } export interface State {} diff --git a/futurenhs.app/components/forms/AutoComplete/component.stories.tsx b/futurenhs.app/components/old_forms/AutoComplete/component.stories.tsx similarity index 100% rename from futurenhs.app/components/forms/AutoComplete/component.stories.tsx rename to futurenhs.app/components/old_forms/AutoComplete/component.stories.tsx diff --git a/futurenhs.app/components/forms/AutoComplete/index.tsx b/futurenhs.app/components/old_forms/AutoComplete/index.tsx similarity index 98% rename from futurenhs.app/components/forms/AutoComplete/index.tsx rename to futurenhs.app/components/old_forms/AutoComplete/index.tsx index 6c0276175..723496d31 100644 --- a/futurenhs.app/components/forms/AutoComplete/index.tsx +++ b/futurenhs.app/components/old_forms/AutoComplete/index.tsx @@ -4,7 +4,7 @@ import Autocomplete from 'accessible-autocomplete/react' import { getSiteUsersByTerm } from '@services/getSiteUsersByTerm' import { RichText } from '@components/generic/RichText' -import { RemainingCharacterCount } from '@components/forms/RemainingCharacterCount' +import { RemainingCharacterCount } from '@components/old_forms/RemainingCharacterCount' import { Option } from '@appTypes/option' import { Service } from '@appTypes/service' diff --git a/futurenhs.app/components/forms/AutoComplete/interfaces.ts b/futurenhs.app/components/old_forms/AutoComplete/interfaces.ts similarity index 100% rename from futurenhs.app/components/forms/AutoComplete/interfaces.ts rename to futurenhs.app/components/old_forms/AutoComplete/interfaces.ts diff --git a/futurenhs.app/components/forms/CheckBox/component.stories.tsx b/futurenhs.app/components/old_forms/CheckBox/component.stories.tsx similarity index 100% rename from futurenhs.app/components/forms/CheckBox/component.stories.tsx rename to futurenhs.app/components/old_forms/CheckBox/component.stories.tsx diff --git a/futurenhs.app/components/forms/CheckBox/index.tsx b/futurenhs.app/components/old_forms/CheckBox/index.tsx similarity index 100% rename from futurenhs.app/components/forms/CheckBox/index.tsx rename to futurenhs.app/components/old_forms/CheckBox/index.tsx diff --git a/futurenhs.app/components/forms/CheckBox/interfaces.ts b/futurenhs.app/components/old_forms/CheckBox/interfaces.ts similarity index 100% rename from futurenhs.app/components/forms/CheckBox/interfaces.ts rename to futurenhs.app/components/old_forms/CheckBox/interfaces.ts diff --git a/futurenhs.app/components/forms/FieldSet/index.tsx b/futurenhs.app/components/old_forms/FieldSet/index.tsx similarity index 100% rename from futurenhs.app/components/forms/FieldSet/index.tsx rename to futurenhs.app/components/old_forms/FieldSet/index.tsx diff --git a/futurenhs.app/components/forms/FieldSet/interfaces.ts b/futurenhs.app/components/old_forms/FieldSet/interfaces.ts similarity index 100% rename from futurenhs.app/components/forms/FieldSet/interfaces.ts rename to futurenhs.app/components/old_forms/FieldSet/interfaces.ts diff --git a/futurenhs.app/components/forms/Form/component.stories.tsx b/futurenhs.app/components/old_forms/Form/component.stories.tsx similarity index 100% rename from futurenhs.app/components/forms/Form/component.stories.tsx rename to futurenhs.app/components/old_forms/Form/component.stories.tsx diff --git a/futurenhs.app/components/forms/Form/component.test.tsx b/futurenhs.app/components/old_forms/Form/component.test.tsx similarity index 100% rename from futurenhs.app/components/forms/Form/component.test.tsx rename to futurenhs.app/components/old_forms/Form/component.test.tsx diff --git a/futurenhs.app/components/forms/Form/index.tsx b/futurenhs.app/components/old_forms/Form/index.tsx similarity index 96% rename from futurenhs.app/components/forms/Form/index.tsx rename to futurenhs.app/components/old_forms/Form/index.tsx index ee9c9af38..d18979d52 100644 --- a/futurenhs.app/components/forms/Form/index.tsx +++ b/futurenhs.app/components/old_forms/Form/index.tsx @@ -4,7 +4,7 @@ import { Form as FinalForm, Field, FormSpy } from 'react-final-form' import classNames from 'classnames' import { SVGIcon } from '@components/generic/SVGIcon' import { Link } from '@components/generic/Link' -import { formComponents } from '@components/forms' +import { formComponents } from '@components/old_forms' import { Dialog } from '@components/generic/Dialog' import { validate } from '@helpers/validators' import { requestMethods } from '@constants/fetch' @@ -380,22 +380,20 @@ export const Form: (props: Props) => JSX.Element = ({ {submitButton} )} - {shouldRenderBackToTopIcon && ( - + )} - - +