diff options
Diffstat (limited to 'src/components/FormElements')
| -rw-r--r-- | src/components/FormElements/Field/Field.module.scss | 48 | ||||
| -rw-r--r-- | src/components/FormElements/Field/Field.tsx | 106 | ||||
| -rw-r--r-- | src/components/FormElements/Form/Form.module.scss | 37 | ||||
| -rw-r--r-- | src/components/FormElements/Form/Form.tsx | 27 | ||||
| -rw-r--r-- | src/components/FormElements/FormItem/FormItem.module.scss | 4 | ||||
| -rw-r--r-- | src/components/FormElements/FormItem/FormItem.tsx | 7 | ||||
| -rw-r--r-- | src/components/FormElements/Label/Label.module.scss | 22 | ||||
| -rw-r--r-- | src/components/FormElements/Label/Label.tsx | 24 | ||||
| -rw-r--r-- | src/components/FormElements/Toggle/Toggle.module.scss | 75 | ||||
| -rw-r--r-- | src/components/FormElements/Toggle/Toggle.tsx | 46 | ||||
| -rw-r--r-- | src/components/FormElements/index.tsx | 7 |
11 files changed, 403 insertions, 0 deletions
diff --git a/src/components/FormElements/Field/Field.module.scss b/src/components/FormElements/Field/Field.module.scss new file mode 100644 index 0000000..3836856 --- /dev/null +++ b/src/components/FormElements/Field/Field.module.scss @@ -0,0 +1,48 @@ +@use "@styles/abstracts/functions" as fun; + +.field { + background: var(--color-bg-tertiary); + border: fun.convert-px(2) solid var(--color-border); + box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow); + transition: all 0.25s linear 0s; + + &:not(.select) { + width: 100%; + padding: var(--spacing-2xs) var(--spacing-xs); + } + + &:hover { + box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1) + var(--color-shadow); + transform: translate(#{fun.convert-px(-3)}, #{fun.convert-px(-3)}); + } + + &:focus { + background: var(--color-bg); + border-color: var(--color-primary); + box-shadow: 0 0 0 0 var(--color-shadow); + transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)}); + outline: none; + transition: all 0.2s ease-in-out 0s, transform 0.3s ease-out 0s; + } +} + +.select { + padding: fun.convert-px(3) var(--spacing-xs); + cursor: pointer; + + &:hover { + box-shadow: fun.convert-px(4) fun.convert-px(4) 0 fun.convert-px(1) + var(--color-shadow); + transform: translate(#{fun.convert-px(-2)}, #{fun.convert-px(-2)}); + } + + &:focus { + box-shadow: 0 0 0 0 var(--color-shadow); + transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)}); + } +} + +.textarea { + min-height: fun.convert-px(200); +} diff --git a/src/components/FormElements/Field/Field.tsx b/src/components/FormElements/Field/Field.tsx new file mode 100644 index 0000000..c8df0f9 --- /dev/null +++ b/src/components/FormElements/Field/Field.tsx @@ -0,0 +1,106 @@ +import { + ChangeEvent, + ForwardedRef, + forwardRef, + ReactElement, + SetStateAction, +} from 'react'; +import styles from './Field.module.scss'; + +type FieldType = 'email' | 'number' | 'search' | 'select' | 'text' | 'textarea'; +type SelectOptions = { + id: string; + name: string; + value: string; +}; + +const Field = ( + { + id, + name, + value, + setValue, + required = false, + kind = 'text', + label, + options, + }: { + id: string; + name: string; + value: string; + setValue: (value: SetStateAction<string>) => void; + required?: boolean; + kind?: FieldType; + label?: ReactElement; + options?: SelectOptions[]; + }, + ref: ForwardedRef<HTMLInputElement | HTMLTextAreaElement> +) => { + const updateValue = ( + e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> + ) => { + setValue(e.target.value); + }; + + const getOptions = () => { + return options + ? options.map((option) => ( + <option key={option.id} value={option.value}> + {option.name} + </option> + )) + : ''; + }; + + const getField = () => { + switch (kind) { + case 'select': + return ( + <select + name={name} + id={id} + value={value} + onChange={updateValue} + required={required} + className={`${styles.field} ${styles.select}`} + > + {getOptions()} + </select> + ); + case 'textarea': + return ( + <textarea + ref={ref as ForwardedRef<HTMLTextAreaElement>} + id={id} + name={name} + value={value} + required={required} + onChange={updateValue} + className={`${styles.field} ${styles.textarea}`} + /> + ); + default: + return ( + <input + ref={ref as ForwardedRef<HTMLInputElement>} + type={kind} + id={id} + name={name} + value={value} + required={required} + onChange={updateValue} + className={styles.field} + /> + ); + } + }; + + return ( + <> + {label} + {getField()} + </> + ); +}; + +export default forwardRef(Field); diff --git a/src/components/FormElements/Form/Form.module.scss b/src/components/FormElements/Form/Form.module.scss new file mode 100644 index 0000000..0f7c437 --- /dev/null +++ b/src/components/FormElements/Form/Form.module.scss @@ -0,0 +1,37 @@ +@use "@styles/abstracts/functions" as fun; + +.wrapper { + width: 100%; +} + +.centered { + max-width: 45ch; + margin-left: auto; + margin-right: auto; +} + +.search { + display: flex; + flex-flow: row nowrap; + align-items: center; + + > input { + padding-right: calc(var(--btn-size) + var(--spacing-2xs)); + + &:hover ~ button { + transform: translate(fun.convert-px(-3), fun.convert-px(-3)); + } + + &:focus ~ button { + transform: translate(fun.convert-px(3), fun.convert-px(3)); + } + } +} + +.settings { + display: flex; + flex-flow: row nowrap; + align-items: center; + margin: var(--spacing-sm) 0; + position: relative; +} diff --git a/src/components/FormElements/Form/Form.tsx b/src/components/FormElements/Form/Form.tsx new file mode 100644 index 0000000..10fdcdf --- /dev/null +++ b/src/components/FormElements/Form/Form.tsx @@ -0,0 +1,27 @@ +import { ReactNode } from 'react'; +import styles from './Form.module.scss'; + +type FormKind = 'centered' | 'search' | 'settings'; + +const Form = ({ + children, + submitHandler, + kind, + id, +}: { + children: ReactNode; + submitHandler: any; + kind?: FormKind; + id?: string; +}) => { + const kindStyles = kind ? styles[kind] : ''; + const classes = `${styles.wrapper} ${kindStyles}`; + + return ( + <form onSubmit={submitHandler} className={classes} id={id}> + {children} + </form> + ); +}; + +export default Form; diff --git a/src/components/FormElements/FormItem/FormItem.module.scss b/src/components/FormElements/FormItem/FormItem.module.scss new file mode 100644 index 0000000..07ef56f --- /dev/null +++ b/src/components/FormElements/FormItem/FormItem.module.scss @@ -0,0 +1,4 @@ +.wrapper { + margin: var(--spacing-xs) 0; + max-width: 45ch; +} diff --git a/src/components/FormElements/FormItem/FormItem.tsx b/src/components/FormElements/FormItem/FormItem.tsx new file mode 100644 index 0000000..8d674f1 --- /dev/null +++ b/src/components/FormElements/FormItem/FormItem.tsx @@ -0,0 +1,7 @@ +import styles from './FormItem.module.scss'; + +const FormItem: React.FunctionComponent = ({ children }) => { + return <div className={styles.wrapper}>{children}</div>; +}; + +export default FormItem; diff --git a/src/components/FormElements/Label/Label.module.scss b/src/components/FormElements/Label/Label.module.scss new file mode 100644 index 0000000..c527b16 --- /dev/null +++ b/src/components/FormElements/Label/Label.module.scss @@ -0,0 +1,22 @@ +@use "@styles/abstracts/functions" as fun; + +.regular { + display: block; + color: var(--color-primary-darker); + font-size: var(--font-size-sm); + font-variant: small-caps; + font-weight: 600; +} + +.settings { + --icon-size: #{fun.convert-px(25)}; + --toggle-width: #{fun.convert-px(45)}; + --toggle-height: calc(var(--toggle-width) / 2); + + display: inline-flex; + align-items: center; +} + +.required { + color: var(--color-secondary); +} diff --git a/src/components/FormElements/Label/Label.tsx b/src/components/FormElements/Label/Label.tsx new file mode 100644 index 0000000..baedff0 --- /dev/null +++ b/src/components/FormElements/Label/Label.tsx @@ -0,0 +1,24 @@ +import styles from './Label.module.scss'; + +type LabelKind = 'regular' | 'settings'; + +const Label = ({ + body, + htmlFor, + required = false, + kind = 'regular', +}: { + body: string; + htmlFor: string; + required?: boolean; + kind?: LabelKind; +}) => { + return ( + <label htmlFor={htmlFor} className={styles[kind]}> + {body} + {required && <span className={styles.required}> *</span>} + </label> + ); +}; + +export default Label; diff --git a/src/components/FormElements/Toggle/Toggle.module.scss b/src/components/FormElements/Toggle/Toggle.module.scss new file mode 100644 index 0000000..48c88f6 --- /dev/null +++ b/src/components/FormElements/Toggle/Toggle.module.scss @@ -0,0 +1,75 @@ +@use "@styles/abstracts/functions" as fun; + +.label { + --icon-size: #{fun.convert-px(25)}; + --toggle-width: #{fun.convert-px(45)}; + --toggle-height: calc(var(--toggle-width) / 2); + + display: inline-flex; + align-items: center; +} + +.title { + margin-right: var(--spacing-xs); +} + +.toggle { + display: inline-flex; + align-items: center; + width: var(--toggle-width); + height: var(--toggle-height); + background: var(--color-shadow-light); + border: fun.convert-px(1) solid var(--color-primary); + border-radius: fun.convert-px(32); + box-shadow: inset 0 0 fun.convert-px(3) 0 var(--color-shadow-dark); + margin: 0 var(--spacing-2xs); + position: relative; + + &::after { + content: ""; + display: block; + width: calc(var(--toggle-width) / 2); + height: calc(var(--toggle-width) / 2); + background: var(--color-primary-light); + border: fun.convert-px(1) solid var(--color-primary); + border-radius: 50%; + box-shadow: inset 0 0 fun.convert-px(1) fun.convert-px(1) + var(--color-shadow), + 0 0 fun.convert-px(2) fun.convert-px(1) var(--color-shadow-light); + position: absolute; + left: fun.convert-px(-2); + transition: all 0.3s ease-in-out 0s; + } +} + +.checkbox { + position: absolute; + opacity: 0; + cursor: pointer; + + &:checked ~ .label { + .toggle::after { + position: absolute; + left: calc(100% - (var(--toggle-width) / 2) + #{fun.convert-px(2)}); + } + } + + &:hover, + &:focus { + ~ .label { + .toggle::after { + background: var(--color-primary-lighter); + } + } + } + + &:focus ~ .label { + .title { + text-decoration: underline solid var(--color-primary) fun.convert-px(2); + } + + .toggle { + outline: var(--color-border) solid fun.convert-px(5); + } + } +} diff --git a/src/components/FormElements/Toggle/Toggle.tsx b/src/components/FormElements/Toggle/Toggle.tsx new file mode 100644 index 0000000..4db7d43 --- /dev/null +++ b/src/components/FormElements/Toggle/Toggle.tsx @@ -0,0 +1,46 @@ +import { FormEvent, ReactElement } from 'react'; +import { Form } from '..'; +import styles from './Toggle.module.scss'; + +const Toggle = ({ + id, + label, + value, + changeHandler, + leftChoice, + rightChoice, + name, +}: { + id: string; + label: string; + value: boolean; + changeHandler: (value: boolean) => void; + leftChoice: ReactElement | string; + rightChoice: ReactElement | string; + name?: string; +}) => { + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + }; + + return ( + <Form kind="settings" submitHandler={onSubmit}> + <input + className={styles.checkbox} + type="checkbox" + id={id} + name={name ? name : id} + checked={value} + onChange={() => changeHandler(!value)} + /> + <label htmlFor={id} className={styles.label}> + <span className={styles.title}>{label}</span> + {leftChoice} + <span className={styles.toggle}></span> + {rightChoice} + </label> + </Form> + ); +}; + +export default Toggle; diff --git a/src/components/FormElements/index.tsx b/src/components/FormElements/index.tsx new file mode 100644 index 0000000..8ca69b4 --- /dev/null +++ b/src/components/FormElements/index.tsx @@ -0,0 +1,7 @@ +import Field from './Field/Field'; +import Form from './Form/Form'; +import FormItem from './FormItem/FormItem'; +import Label from './Label/Label'; +import Toggle from './Toggle/Toggle'; + +export { Field, Form, FormItem, Label, Toggle }; |
