aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/FormElements
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/FormElements')
-rw-r--r--src/components/FormElements/Field/Field.module.scss48
-rw-r--r--src/components/FormElements/Field/Field.tsx106
-rw-r--r--src/components/FormElements/Form/Form.module.scss37
-rw-r--r--src/components/FormElements/Form/Form.tsx27
-rw-r--r--src/components/FormElements/FormItem/FormItem.module.scss4
-rw-r--r--src/components/FormElements/FormItem/FormItem.tsx7
-rw-r--r--src/components/FormElements/Label/Label.module.scss22
-rw-r--r--src/components/FormElements/Label/Label.tsx24
-rw-r--r--src/components/FormElements/Toggle/Toggle.module.scss75
-rw-r--r--src/components/FormElements/Toggle/Toggle.tsx46
-rw-r--r--src/components/FormElements/index.tsx7
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 };