From c43998e0b6d85e482ccd7b79584ad82e0ce56afe Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Thu, 29 Oct 2020 22:26:30 +0100 Subject: [PATCH 01/17] WIP inputs --- __tests__/Input.text.tsx | 19 +++++++++ src/components/Input/Input.tsx | 54 +++++++++++++++++++++++++ src/components/Input/index.ts | 1 + src/style/base/_variables.scss | 17 +++++++- src/style/components/_inputs.scss | 66 +++++++++++++++++++++++++++++++ src/style/index.scss | 1 + www/src/index.tsx | 25 +++--------- www/webpack.config.js | 2 +- 8 files changed, 163 insertions(+), 22 deletions(-) create mode 100644 __tests__/Input.text.tsx create mode 100644 src/components/Input/Input.tsx create mode 100644 src/components/Input/index.ts create mode 100644 src/style/components/_inputs.scss diff --git a/__tests__/Input.text.tsx b/__tests__/Input.text.tsx new file mode 100644 index 0000000..a8d904d --- /dev/null +++ b/__tests__/Input.text.tsx @@ -0,0 +1,19 @@ +import { shallow } from 'enzyme'; +import React from 'react'; +import { Variant } from '@/components'; +import { Input } from '@/components/Input'; + +describe('Input test', () => { + it('should render input', () => { + const container = shallow( + + Hello world + + ); + + expect(container.find('input').length).toBe(1); + }); +}); diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx new file mode 100644 index 0000000..c8a502b --- /dev/null +++ b/src/components/Input/Input.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { Variant } from '@/components'; +import clsx from 'clsx'; +import { Button } from '@/components/Button'; + +interface InputProps extends React.HTMLAttributes { + variant?: Variant; + type?: string; +} + +const Input = ({ + onChange, + type +}: React.PropsWithChildren): React.ReactElement => { + const [value, setValue] = useState(''); + const [hasFocus, setHasFocus] = useState(false); + + return ( +
+ + setHasFocus(true)} + onBlur={() => setHasFocus(false)} + className={clsx( + 'cui-input' + )} + onChange={(event: React.ChangeEvent) => { + setValue(event.target.value); + if (onChange) { + onChange(event); + } + }} + /> +
+ +
+
+ ); +}; + +export default Input; diff --git a/src/components/Input/index.ts b/src/components/Input/index.ts new file mode 100644 index 0000000..b4d3864 --- /dev/null +++ b/src/components/Input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './Input'; diff --git a/src/style/base/_variables.scss b/src/style/base/_variables.scss index 580ece2..1318704 100644 --- a/src/style/base/_variables.scss +++ b/src/style/base/_variables.scss @@ -8,18 +8,18 @@ $base-background-color: mixins.color('gray', 50); $base-body-gutter: 0; $base-font-color: mixins.color('blueGray', 900); $base-font-family: 'Inter, apple-sf-pro-text, Helvetica, Arial, sans-serif'; -$base-font-size: 16px; $base-border-color: mixins.color('gray', 300); +$bse-border-hover-color: mixins.color('gray', 400); $base-border-radius: 4px; $base-gutter: 1rem; :root { - --base-font-size: #{$base-font-size}; --base-body-background: #{$base-background-color}; --base-body-gutter: #{$base-body-gutter}; --base-font-color: #{$base-font-color}; --base-font-family: #{$base-font-family}; --base-border-color: #{$base-border-color}; + --base-border-hover-color: #{$bse-border-hover-color}; --base-border-radius: #{$base-border-radius}; --base-gutter: #{$base-gutter}; } @@ -123,3 +123,16 @@ $danger-button-ghost-active-background: mixins.color('red', 75); --danger-button-ghost-hover-background: #{$danger-button-ghost-hover-background}; --danger-button-ghost-active-background: #{$danger-button-ghost-active-background}; } + +/** + * 7. Inputs + */ +$input-padding: 1rem; +$input-font-weight: 500; +$input-font-size: 1rem; + +:root { + --input-padding: #{$input-padding}; + --input-font-weight: #{$input-font-weight}; + --input-font-size: #{$input-font-size}; +} diff --git a/src/style/components/_inputs.scss b/src/style/components/_inputs.scss new file mode 100644 index 0000000..b8ad158 --- /dev/null +++ b/src/style/components/_inputs.scss @@ -0,0 +1,66 @@ +@use "../base/mixins"; + +.cui-input-base { + background: #fff; + border: solid 1px var(--base-border-color); + border-radius: var(--base-border-radius); + pointer-events: none; + position: relative; + width: auto; + display: flex; + padding: 0 var(--input-padding); + transition: all .1s ease-in-out; + + .cui-input-actions { + align-self: center; + pointer-events: auto; + margin-left: var(--input-padding); + } + + &:hover { + border-color: var(--base-border-hover-color); + } + + &.has-focus { + border-color: var(--primary-color); + + .cui-input-label { + color: var(--primary-color); + } + } + + &.has-focus, &.has-value { + .cui-input-label { + transform: translateY(calc((var(--input-padding) / 2) / 2)); + top: 0; + font: { + size: 0.8rem; + } + } + } + + .cui-input { + padding: calc(var(--input-padding) + (var(--input-padding) / 2)) 0 calc(var(--input-padding) / 2); + background: none; + border-radius: var(--base-border-radius); + border: none; + outline: none; + width: 100%; + font: { + size: var(--input-font-size); + weight: var(--input-font-weight); + } + pointer-events: auto; + } + + .cui-input-label { + font: { + size: var(--input-font-size); + } + color: mixins.color('gray', 700); + position: absolute; + top: 50%; + transform: translateY(-50%); + transition: all .1s ease-in-out; + } +} diff --git a/src/style/index.scss b/src/style/index.scss index 6a062b6..521040f 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -13,3 +13,4 @@ @use "components/button"; @use "components/page"; @use "components/panel"; +@use "components/inputs"; diff --git a/www/src/index.tsx b/www/src/index.tsx index 193d6c6..aa0a92d 100644 --- a/www/src/index.tsx +++ b/www/src/index.tsx @@ -1,31 +1,18 @@ import * as React from 'react'; import * as ReactDom from 'react-dom'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faArrowAltCircleDown, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; import '../../src/style/index.scss'; -import { Page, Variant } from '../../src/components'; -import Button from '@/components/Button'; +import { Page } from '../../src/components/Page'; +import { Input } from '../../src/components/Input'; +import { Button } from '../../src/components/Button'; +import { Variant } from '../../src/components/utils'; ReactDom.render( - - + + , document.getElementById('root') diff --git a/www/webpack.config.js b/www/webpack.config.js index 1febccf..6abdd64 100644 --- a/www/webpack.config.js +++ b/www/webpack.config.js @@ -25,7 +25,7 @@ const setEnvVars = () => { module.exports = { entry: { - main: './www/src/Page.tsx' + main: './www/src/index.tsx' }, module: { rules: [ From 6ca8e7c85334c393f83a81b872b5e5dfb304ed63 Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Fri, 30 Oct 2020 11:16:09 +0100 Subject: [PATCH 02/17] WIP inputs --- __tests__/Input.text.tsx | 30 +++++-- src/components/FormField/FormField.tsx | 32 ++++++++ src/components/FormField/TextField.tsx | 78 +++++++++++++++++++ src/components/FormField/index.ts | 2 + src/components/Input/Input.tsx | 54 ------------- src/components/Input/index.ts | 1 - src/components/index.ts | 1 + src/style/base/_variables.scss | 14 ++-- .../{_inputs.scss => _form-fields.scss} | 37 +++++---- src/style/index.scss | 2 +- tsconfig.json | 8 +- www/src/index.tsx | 7 +- 12 files changed, 176 insertions(+), 90 deletions(-) create mode 100644 src/components/FormField/FormField.tsx create mode 100644 src/components/FormField/TextField.tsx create mode 100644 src/components/FormField/index.ts delete mode 100644 src/components/Input/Input.tsx delete mode 100644 src/components/Input/index.ts rename src/style/components/{_inputs.scss => _form-fields.scss} (55%) diff --git a/__tests__/Input.text.tsx b/__tests__/Input.text.tsx index a8d904d..987ad28 100644 --- a/__tests__/Input.text.tsx +++ b/__tests__/Input.text.tsx @@ -1,19 +1,37 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { Variant } from '@/components'; -import { Input } from '@/components/Input'; +import { FormField, Variant } from '@/components'; describe('Input test', () => { it('should render input', () => { const container = shallow( - + ); + + expect(container.find('.cui-form-field-base > .cui-form-field').length).toBe(1); + }); + + it('should render variant class', () => { + const container = shallow( + Hello world - + ); - expect(container.find('input').length).toBe(1); + expect(container.find('.cui-form-field-base').at(0).hasClass('cui-form-field-primary')).toBeTruthy(); + }); + + it('should give focus class when focused', () => { + const container = shallow( + + ); + + container.find('input').simulate('focus'); + + expect(container.find('.cui-form-field-base').hasClass('has-focus')).toBeTruthy(); }); }); diff --git a/src/components/FormField/FormField.tsx b/src/components/FormField/FormField.tsx new file mode 100644 index 0000000..74f9900 --- /dev/null +++ b/src/components/FormField/FormField.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import TextField, { TextFieldProps } from '@/components/FormField/TextField'; + +interface FormFieldProps extends TextFieldProps { + as?: 'input' | 'textarea' | 'select'; +} + +const FormField = React.forwardRef(( + { + as = 'input', + label + }, + ref +): React.ReactElement => { + switch (as) { + case 'input': + default: + return React.createElement(TextField, { + ref: ref as React.RefObject, + label + }) + } +}); + +FormField.displayName = 'FormField'; +FormField.propTypes = { + as: PropTypes.oneOf(['input', 'textarea', 'select']), + label: PropTypes.node +} + +export default FormField; diff --git a/src/components/FormField/TextField.tsx b/src/components/FormField/TextField.tsx new file mode 100644 index 0000000..a7f709b --- /dev/null +++ b/src/components/FormField/TextField.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { useState } from 'react'; +import clsx from 'clsx'; +import { Variant } from '@/components'; +import PropTypes from 'prop-types'; + +export interface TextFieldProps extends React.HTMLAttributes { + variant?: Variant | string; + actions?: React.ReactNode; + type?: string; + label?: React.ReactNode; +} + +const TextField = React.forwardRef(( + { + actions, + label, + onChange, + type = 'text', + variant = Variant.PRIMARY + }, + ref +): React.ReactElement => { + const [value, setValue] = useState(''); + const [hasFocus, setHasFocus] = useState(false); + + return ( +
+ {label && ( + + )} + setHasFocus(true)} + onBlur={() => setHasFocus(false)} + className={clsx( + 'cui-form-field' + )} + onChange={(event: React.ChangeEvent) => { + setValue(event.target.value); + if (onChange) { + onChange(event); + } + }} + /> + {actions && ( +
+ {actions} +
+ )} +
+ ); +}); + +TextField.displayName = 'TextField'; +TextField.propTypes = { + actions: PropTypes.node, + label: PropTypes.node, + type: PropTypes.oneOf(['password', 'text', 'reset']), + onChange: PropTypes.func, + variant: PropTypes.string +} + +export default TextField; diff --git a/src/components/FormField/index.ts b/src/components/FormField/index.ts new file mode 100644 index 0000000..e656c62 --- /dev/null +++ b/src/components/FormField/index.ts @@ -0,0 +1,2 @@ +export { default as FormField } from './FormField'; +export { default as TextField } from './TextField'; diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx deleted file mode 100644 index c8a502b..0000000 --- a/src/components/Input/Input.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react'; -import { useState } from 'react'; -import { Variant } from '@/components'; -import clsx from 'clsx'; -import { Button } from '@/components/Button'; - -interface InputProps extends React.HTMLAttributes { - variant?: Variant; - type?: string; -} - -const Input = ({ - onChange, - type -}: React.PropsWithChildren): React.ReactElement => { - const [value, setValue] = useState(''); - const [hasFocus, setHasFocus] = useState(false); - - return ( -
- - setHasFocus(true)} - onBlur={() => setHasFocus(false)} - className={clsx( - 'cui-input' - )} - onChange={(event: React.ChangeEvent) => { - setValue(event.target.value); - if (onChange) { - onChange(event); - } - }} - /> -
- -
-
- ); -}; - -export default Input; diff --git a/src/components/Input/index.ts b/src/components/Input/index.ts deleted file mode 100644 index b4d3864..0000000 --- a/src/components/Input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Input } from './Input'; diff --git a/src/components/index.ts b/src/components/index.ts index afbc652..d577f36 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,4 @@ export * from './utils'; export * from './Page'; export * from './Panel'; +export * from './FormField'; diff --git a/src/style/base/_variables.scss b/src/style/base/_variables.scss index 1318704..2e41c00 100644 --- a/src/style/base/_variables.scss +++ b/src/style/base/_variables.scss @@ -125,14 +125,14 @@ $danger-button-ghost-active-background: mixins.color('red', 75); } /** - * 7. Inputs + * 7. Form fields */ -$input-padding: 1rem; -$input-font-weight: 500; -$input-font-size: 1rem; +$form-field-padding: 1rem; +$form-field-font-weight: 500; +$form-field-font-size: 1rem; :root { - --input-padding: #{$input-padding}; - --input-font-weight: #{$input-font-weight}; - --input-font-size: #{$input-font-size}; + --form-field-padding: #{$form-field-padding}; + --form-field-font-weight: #{$form-field-font-weight}; + --form-field-font-size: #{$form-field-font-size}; } diff --git a/src/style/components/_inputs.scss b/src/style/components/_form-fields.scss similarity index 55% rename from src/style/components/_inputs.scss rename to src/style/components/_form-fields.scss index b8ad158..9da5f47 100644 --- a/src/style/components/_inputs.scss +++ b/src/style/components/_form-fields.scss @@ -1,6 +1,6 @@ @use "../base/mixins"; -.cui-input-base { +.cui-form-field-base { background: #fff; border: solid 1px var(--base-border-color); border-radius: var(--base-border-radius); @@ -8,30 +8,30 @@ position: relative; width: auto; display: flex; - padding: 0 var(--input-padding); + padding: 0 var(--form-field-padding); transition: all .1s ease-in-out; - .cui-input-actions { + .cui-form-field-actions { align-self: center; pointer-events: auto; - margin-left: var(--input-padding); + margin-left: var(--form-field-padding); } &:hover { border-color: var(--base-border-hover-color); } - &.has-focus { + &.cui-focused { border-color: var(--primary-color); - .cui-input-label { + .cui-form-field-label { color: var(--primary-color); } } - &.has-focus, &.has-value { - .cui-input-label { - transform: translateY(calc((var(--input-padding) / 2) / 2)); + &.cui-focused, &.cui-has-value { + .cui-form-field-label { + transform: translateY(calc((var(--form-field-padding) / 2) / 2)); top: 0; font: { size: 0.8rem; @@ -39,23 +39,30 @@ } } - .cui-input { - padding: calc(var(--input-padding) + (var(--input-padding) / 2)) 0 calc(var(--input-padding) / 2); + &.cui-floating-label .cui-form-field { + padding: calc(var(--form-field-padding) + (var(--form-field-padding) / 2)) 0 calc(var(--form-field-padding) / 2); + } + + &:not(.cui-floating-label) .cui-form-field { + padding: var(--form-field-padding) 0; + } + + .cui-form-field { background: none; border-radius: var(--base-border-radius); border: none; outline: none; width: 100%; font: { - size: var(--input-font-size); - weight: var(--input-font-weight); + size: var(--form-field-font-size); + weight: var(--form-field-font-weight); } pointer-events: auto; } - .cui-input-label { + .cui-form-field-label { font: { - size: var(--input-font-size); + size: var(--form-field-font-size); } color: mixins.color('gray', 700); position: absolute; diff --git a/src/style/index.scss b/src/style/index.scss index 521040f..0f19f28 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -13,4 +13,4 @@ @use "components/button"; @use "components/page"; @use "components/panel"; -@use "components/inputs"; +@use "components/form-fields"; diff --git a/tsconfig.json b/tsconfig.json index f2acd7d..8337dc1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,10 +4,10 @@ "module": "esnext", "target": "es5", "lib": [ - "es6", - "dom", - "es2016", - "es2017" + "ES6", + "DOM", + "ES2016", + "ES2017" ], "sourceMap": true, "jsx": "react", diff --git a/www/src/index.tsx b/www/src/index.tsx index aa0a92d..7d6eedc 100644 --- a/www/src/index.tsx +++ b/www/src/index.tsx @@ -3,16 +3,19 @@ import * as ReactDom from 'react-dom'; import '../../src/style/index.scss'; import { Page } from '../../src/components/Page'; -import { Input } from '../../src/components/Input'; import { Button } from '../../src/components/Button'; import { Variant } from '../../src/components/utils'; +import { TextField } from '../../src/components/FormField'; ReactDom.render( - + , document.getElementById('root') From 3fca55197b53ae888ef8477478774c88bea4329b Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Fri, 30 Oct 2020 11:27:20 +0100 Subject: [PATCH 03/17] Added (in))valid colors --- src/components/FormField/FormField.tsx | 10 +++++++--- src/components/FormField/TextField.tsx | 11 +++++++---- src/style/base/_variables.scss | 8 ++++++++ src/style/components/_form-fields.scss | 20 ++++++++++++++++++-- www/src/index.tsx | 1 + 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/components/FormField/FormField.tsx b/src/components/FormField/FormField.tsx index 74f9900..711e6ba 100644 --- a/src/components/FormField/FormField.tsx +++ b/src/components/FormField/FormField.tsx @@ -4,12 +4,14 @@ import TextField, { TextFieldProps } from '@/components/FormField/TextField'; interface FormFieldProps extends TextFieldProps { as?: 'input' | 'textarea' | 'select'; + valid?: boolean; } const FormField = React.forwardRef(( { as = 'input', - label + label, + valid }, ref ): React.ReactElement => { @@ -18,7 +20,8 @@ const FormField = React.forwardRef(( default: return React.createElement(TextField, { ref: ref as React.RefObject, - label + label, + valid }) } }); @@ -26,7 +29,8 @@ const FormField = React.forwardRef(( FormField.displayName = 'FormField'; FormField.propTypes = { as: PropTypes.oneOf(['input', 'textarea', 'select']), - label: PropTypes.node + label: PropTypes.node, + valid: PropTypes.bool } export default FormField; diff --git a/src/components/FormField/TextField.tsx b/src/components/FormField/TextField.tsx index a7f709b..1449e37 100644 --- a/src/components/FormField/TextField.tsx +++ b/src/components/FormField/TextField.tsx @@ -5,10 +5,11 @@ import { Variant } from '@/components'; import PropTypes from 'prop-types'; export interface TextFieldProps extends React.HTMLAttributes { - variant?: Variant | string; actions?: React.ReactNode; - type?: string; label?: React.ReactNode; + type?: string; + valid?: boolean; + variant?: Variant | string; } const TextField = React.forwardRef(( @@ -17,7 +18,8 @@ const TextField = React.forwardRef(( label, onChange, type = 'text', - variant = Variant.PRIMARY + variant = Variant.PRIMARY, + valid }, ref ): React.ReactElement => { @@ -30,7 +32,8 @@ const TextField = React.forwardRef(( `cui-form-field-${variant}`, hasFocus && 'cui-focused', value && 'cui-has-value', - label && 'cui-floating-label' + label && 'cui-floating-label', + valid === true ? 'cui-form-field-valid' : valid === false ? 'cui-form-field-invalid' : null )}> {label && (