diff --git a/__tests__/FieldBase.test.tsx b/__tests__/FieldBase.test.tsx new file mode 100644 index 0000000..4000909 --- /dev/null +++ b/__tests__/FieldBase.test.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { mount, render } from 'enzyme'; +import FieldBase from '@/components/Field/FieldBase'; +import { Variant } from '@/components'; +import { FieldContext } from '@/components/Field/FieldContext'; + +describe('FieldBase test', () => { + it('should render field base', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-field-base').length).toBe(1); + }); + + it('should set variant', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-field-base.form-field-primary').length).toBe(1); + }); + + it('should set variant', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-field-base.form-field-primary').length).toBe(1); + }); + + it('should make floating label component', () => { + const container = render( +
+ test} + /> +
+ ); + + expect(container.find('.form-field-base').hasClass('floating-label')).toBeTruthy(); + expect(container.find('.form-field-base .form-field-label-floating strong').text()) + .toBe('test'); + }); + + it('should make invalid state icon', () => { + const container = render( +
+ test} + valid={false} + > + + {({ stateIcon }) => stateIcon} + + +
+ ); + + expect(container.find('.field-state-icon .icon-close-circle').length).toBe(1); + }) + + it('should make valid state icon', () => { + const container = render( +
+ test} + valid={true} + > + + {({ stateIcon }) => stateIcon} + + +
+ ); + + expect(container.find('.field-state-icon .icon-checkmark-circle-2').length).toBe(1); + }) + + it('should change focus and value', () => { + const container = mount( +
+ + + {({ changeFocus, changeValue }) => { + changeFocus(true); + changeValue(true); + return undefined + }} + + +
+ ); + + expect(container.find('.form-field-base.focused.has-value').length).toBe(1); + }) + + it('should render actions', () => { + const container = mount( +
+ foo} /> +
+ ); + + expect(container.find('.form-field-base .form-field-actions span').text()).toBe('foo'); + }); +}); diff --git a/__tests__/FieldContainer.test.tsx b/__tests__/FieldContainer.test.tsx new file mode 100644 index 0000000..d7b6975 --- /dev/null +++ b/__tests__/FieldContainer.test.tsx @@ -0,0 +1,25 @@ +import { render } from 'enzyme'; +import React from 'react'; +import FieldContainer from '@/components/Field/FieldContainer'; + +describe('FieldBase test', () => { + it('should render field container', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-field-container').length).toBe(1); + }); + + it('should render field container toggle', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-field-container.toggles').length).toBe(1); + }); +}); diff --git a/__tests__/FieldContext.test.tsx b/__tests__/FieldContext.test.tsx new file mode 100644 index 0000000..37ff5e4 --- /dev/null +++ b/__tests__/FieldContext.test.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { FieldContext } from '@/components/Field/FieldContext'; + +describe('FieldContext test', () => { + it('should use default values when no consumer used', () => { + const mockChangeValue = jest.fn(); + const mockChangeFocus = jest.fn(); + const container = render( +
+ + {({ changeValue, changeFocus }) => { + const a = changeValue(true); + const b = changeFocus(true); + + return {a === undefined && b === undefined ? 'undefined' : 'defined'}; + }} + +
, + { context: { + changeValue: mockChangeValue, + changeFocus: mockChangeFocus + } } + ) + + expect(container.find('span').text()).toBe('undefined'); + }); +}) diff --git a/__tests__/Form.test.tsx b/__tests__/Form.test.tsx new file mode 100644 index 0000000..9a3f03f --- /dev/null +++ b/__tests__/Form.test.tsx @@ -0,0 +1,60 @@ +import { render } from 'enzyme'; +import React from 'react'; +import FormGroup from '@/components/Form/Group'; +import FormLabel from '@/components/Form/Label'; +import FormMessage from '@/components/Form/Message'; + +describe('Form test', () => { + it('should render form-group', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-group').length).toBe(1); + }); + + it('should render form label', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-label').length).toBe(1); + }); + + it('should render form message', () => { + const container = render( +
+ Message +
+ ); + + expect(container.find('.form-message').length).toBe(1); + expect((container.find('.form-message').text())).toBe('Message'); + }); + + it('should render valid form message', () => { + const container = render( +
+ Message +
+ ); + + expect(container.find('.form-message.form-message-valid').length).toBe(1); + expect((container.find('.form-message').text())).toBe('Message'); + }); + + it('should render invalid form message', () => { + const container = render( +
+ Message +
+ ); + + expect(container.find('.form-message.form-message-invalid').length).toBe(1); + expect((container.find('.form-message').text())).toBe('Message'); + }); +}); diff --git a/__tests__/Selectfield.test.tsx b/__tests__/Selectfield.test.tsx new file mode 100644 index 0000000..435b28a --- /dev/null +++ b/__tests__/Selectfield.test.tsx @@ -0,0 +1,112 @@ +import { mount, render } from 'enzyme'; +import React from 'react'; +import { SelectField } from '@/components'; + +describe('SelectField test', () => { + it('should render text input', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-field-base select').length).toBe(1); + }); + + it('Should set initial value with value prop', () => { + const fn = jest.fn(); + + const container = mount( + fn()}> + + + ); + + expect(container.find('select').props().value).toBe('foo'); + }); + + it('Should set initial value with defaultValue prop', () => { + const fn = jest.fn(); + + const container = mount( + fn()}> + + + ); + + expect(container.find('select').props().value).toBe('foo'); + }); + + it('Should set initial value based on selected option', () => { + const fn = jest.fn(); + + const container = mount( + fn()}> + + + ); + + expect(container.find('select').props().value).toBe('foo'); + }); + + it('should give focus class when focused', () => { + const container = mount( + + ); + + container.find('select').simulate('focus'); + + expect(container.find('.form-field-base').hasClass('focused')).toBeTruthy(); + }); + + it('should not have focus class when blurred after focus', () => { + const container = mount( + + ); + + container.find('select').simulate('focus').simulate('blur'); + + expect(container.find('.form-field-base').hasClass('focused')).toBeFalsy(); + }); + + it('should call onChange method when changing', () => { + const fn = jest.fn(); + + const container = mount( + fn()}/> + ); + + container.find('select').simulate('change'); + + expect(fn).toHaveBeenCalled(); + }); + + it('should not call onChange method when ommited', () => { + const fn = jest.fn(); + + const container = mount( + + ); + + container.find('select').simulate('change'); + + expect(fn).not.toHaveBeenCalled(); + }); + + it('should ignore invalid react elements and select last selected option', () => { + + const container = mount( + + {undefined} +
+ + + + ); + + expect(container.find('select').props().value).toBe('bar'); + }); +}); diff --git a/__tests__/Textfield.test.tsx b/__tests__/Textfield.test.tsx new file mode 100644 index 0000000..18dd492 --- /dev/null +++ b/__tests__/Textfield.test.tsx @@ -0,0 +1,130 @@ +import { mount, render } from 'enzyme'; +import React from 'react'; +import { Button, TextField, Variant } from '@/components'; + +describe('TextField test', () => { + it('should render text input', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-field-base input').length).toBe(1); + }); + + it('should render variant class', () => { + const container = render( +
+ +
+ ); + + expect(container.find('.form-field-base').hasClass('form-field-primary')).toBeTruthy(); + }); + + it('should give focus class when focused', () => { + const container = mount( + + ); + + container.find('input').simulate('focus'); + + expect(container.find('.form-field-base').hasClass('focused')).toBeTruthy(); + }); + + it('should not have focus class when blurred after focus', () => { + const container = mount( + + ); + + container.find('input').simulate('focus').simulate('blur'); + + expect(container.find('.form-field-base').hasClass('focused')).toBeFalsy(); + }); + + it('should call onChange method when changing', () => { + const fn = jest.fn(); + + const container = mount( + fn()}/> + ); + + container.find('input').simulate('change'); + + expect(fn).toHaveBeenCalled(); + }); + + it('should not call onChange method when ommited', () => { + const fn = jest.fn(); + + const container = mount( + + ); + + container.find('input').simulate('change'); + + expect(fn).not.toHaveBeenCalled(); + }); + + it('Should be controllable from outside', () => { + const fn = jest.fn(); + + const container = mount( + fn()}/> + ); + + expect(container.find('input').prop('value')).toBe('Test'); + + container.find('input').simulate('change', {target: {value: 'Foo'}}); + + expect(container.find('input').prop('value')).toBe('Foo'); + expect(fn).toHaveBeenCalled(); + }); + + it('should render floating label', () => { + const container = mount( +
+ +
+ ); + + + expect(container.find('input').hasClass('floating-label')).toBeFalsy(); + expect(container.find('.form-field-base .form-field-label-floating').length).toBe(1); + }); + + it('should render valid fields', () => { + const container = mount( +
+ +
+ ); + + + expect(container.find('.form-field-base').hasClass('form-field-valid')).toBeTruthy(); + }); + + it('should render invalid fields', () => { + const container = mount( +
+ +
+ ); + + + expect(container.find('.form-field-base').hasClass('form-field-invalid')).toBeTruthy(); + }); + + it('should render field actions', () => { + const container = mount( +
+ ]}/> +
+ ); + + expect(container.find('.form-field-base .form-field-actions > *').length).toBe(1); + }); +}); diff --git a/package-lock.json b/package-lock.json index 4574ecf..91cf2dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1261,9 +1261,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.0.tgz", - "integrity": "sha512-+cIGPCBdLCzqxdtwppswP+zTsH9BOIGzAeKfBIbtb4gW/giMlfMwP0HUSFfhzh20f9u8uZ8hOp62+4GPquTbwQ==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", + "integrity": "sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -2259,9 +2259,9 @@ } }, "@types/enzyme": { - "version": "3.10.7", - "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.7.tgz", - "integrity": "sha512-J+0wduPGAkzOvW7sr6hshGv1gBI3WXLRTczkRKzVPxLP3xAkYxZmvvagSBPw8Z452fZ8TGUxCmAXcb44yLQksw==", + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.8.tgz", + "integrity": "sha512-vlOuzqsTHxog6PV79+tvOHFb6hq4QZKMq1lLD9MaWD1oec2lHTKndn76XOpSwCA0oFTaIbKVPrgM3k78Jjd16g==", "dev": true, "requires": { "@types/cheerio": "*", @@ -2407,9 +2407,9 @@ "dev": true }, "@types/react": { - "version": "16.9.53", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.53.tgz", - "integrity": "sha512-4nW60Sd4L7+WMXH1D6jCdVftuW7j4Za6zdp6tJ33Rqv0nk1ZAmQKML9ZLD4H0dehA3FZxXR/GM8gXplf82oNGw==", + "version": "16.9.55", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.55.tgz", + "integrity": "sha512-6KLe6lkILeRwyyy7yG9rULKJ0sXplUsl98MGoCfpteXf9sPWFWWMknDcsvubcpaTdBuxtsLF6HDUwdApZL/xIg==", "dev": true, "requires": { "@types/prop-types": "*", @@ -2417,9 +2417,9 @@ } }, "@types/react-dom": { - "version": "16.9.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz", - "integrity": "sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==", + "version": "16.9.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.9.tgz", + "integrity": "sha512-jE16FNWO3Logq/Lf+yvEAjKzhpST/Eac8EMd1i4dgZdMczfgqC8EjpxwNgEe3SExHYLliabXDh9DEhhqnlXJhg==", "dev": true, "requires": { "@types/react": "*" @@ -2535,13 +2535,13 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.5.0.tgz", - "integrity": "sha512-mjb/gwNcmDKNt+6mb7Aj/TjKzIJjOPcoCJpjBQC9ZnTRnBt1p4q5dJSSmIqAtsZ/Pff5N+hJlbiPc5bl6QN4OQ==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.6.1.tgz", + "integrity": "sha512-SNZyflefTMK2JyrPfFFzzoy2asLmZvZJ6+/L5cIqg4HfKGiW2Gr1Go1OyEVqne/U4QwmoasuMwppoBHWBWF2nA==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.5.0", - "@typescript-eslint/scope-manager": "4.5.0", + "@typescript-eslint/experimental-utils": "4.6.1", + "@typescript-eslint/scope-manager": "4.6.1", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -2549,6 +2549,68 @@ "tsutils": "^3.17.1" }, "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.6.1.tgz", + "integrity": "sha512-qyPqCFWlHZXkEBoV56UxHSoXW2qnTr4JrWVXOh3soBP3q0o7p4pUEMfInDwIa0dB/ypdtm7gLOS0hg0a73ijfg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.6.1", + "@typescript-eslint/types": "4.6.1", + "@typescript-eslint/typescript-estree": "4.6.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.6.1.tgz", + "integrity": "sha512-f95+80r6VdINYscJY1KDUEDcxZ3prAWHulL4qRDfNVD0I5QAVSGqFkwHERDoLYJJWmEAkUMdQVvx7/c2Hp+Bjg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.6.1", + "@typescript-eslint/visitor-keys": "4.6.1" + } + }, + "@typescript-eslint/types": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.6.1.tgz", + "integrity": "sha512-k2ZCHhJ96YZyPIsykickez+OMHkz06xppVLfJ+DY90i532/Cx2Z+HiRMH8YZQo7a4zVd/TwNBuRCdXlGK4yo8w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.1.tgz", + "integrity": "sha512-/J/kxiyjQQKqEr5kuKLNQ1Finpfb8gf/NpbwqFFYEBjxOsZ621r9AqwS9UDRA1Rrr/eneX/YsbPAIhU2rFLjXQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.6.1", + "@typescript-eslint/visitor-keys": "4.6.1", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.6.1.tgz", + "integrity": "sha512-owABze4toX7QXwOLT3/D5a8NecZEjEWU1srqxENTfqsY3bwVnl3YYbOh6s1rp2wQKO9RTHFGjKes08FgE7SVMw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.6.1", + "eslint-visitor-keys": "^2.0.0" + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, "semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", @@ -2572,15 +2634,71 @@ } }, "@typescript-eslint/parser": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.5.0.tgz", - "integrity": "sha512-xb+gmyhQcnDWe+5+xxaQk5iCw6KqXd8VQxGiTeELTMoYeRjpocZYYRP1gFVM2C8Yl0SpUvLa1lhprwqZ00w3Iw==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.6.1.tgz", + "integrity": "sha512-lScKRPt1wM9UwyKkGKyQDqf0bh6jm8DQ5iN37urRIXDm16GEv+HGEmum2Fc423xlk5NUOkOpfTnKZc/tqKZkDQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.5.0", - "@typescript-eslint/types": "4.5.0", - "@typescript-eslint/typescript-estree": "4.5.0", + "@typescript-eslint/scope-manager": "4.6.1", + "@typescript-eslint/types": "4.6.1", + "@typescript-eslint/typescript-estree": "4.6.1", "debug": "^4.1.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.6.1.tgz", + "integrity": "sha512-f95+80r6VdINYscJY1KDUEDcxZ3prAWHulL4qRDfNVD0I5QAVSGqFkwHERDoLYJJWmEAkUMdQVvx7/c2Hp+Bjg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.6.1", + "@typescript-eslint/visitor-keys": "4.6.1" + } + }, + "@typescript-eslint/types": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.6.1.tgz", + "integrity": "sha512-k2ZCHhJ96YZyPIsykickez+OMHkz06xppVLfJ+DY90i532/Cx2Z+HiRMH8YZQo7a4zVd/TwNBuRCdXlGK4yo8w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.1.tgz", + "integrity": "sha512-/J/kxiyjQQKqEr5kuKLNQ1Finpfb8gf/NpbwqFFYEBjxOsZ621r9AqwS9UDRA1Rrr/eneX/YsbPAIhU2rFLjXQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.6.1", + "@typescript-eslint/visitor-keys": "4.6.1", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.6.1.tgz", + "integrity": "sha512-owABze4toX7QXwOLT3/D5a8NecZEjEWU1srqxENTfqsY3bwVnl3YYbOh6s1rp2wQKO9RTHFGjKes08FgE7SVMw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.6.1", + "eslint-visitor-keys": "^2.0.0" + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } } }, "@typescript-eslint/scope-manager": { @@ -6176,13 +6294,13 @@ } }, "eslint": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.12.0.tgz", - "integrity": "sha512-n5pEU27DRxCSlOhJ2rO57GDLcNsxO0LPpAbpFdh7xmcDmjmlGUfoyrsB3I7yYdQXO5N3gkSTiDrPSPNFiiirXA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.12.1.tgz", + "integrity": "sha512-HlMTEdr/LicJfN08LB3nM1rRYliDXOmfoO4vj39xN6BLpFzF00hbwBoqHk8UcJ2M/3nlARZWy/mslvGEuZFvsg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@eslint/eslintrc": "^0.2.0", + "@eslint/eslintrc": "^0.2.1", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -13798,7 +13916,6 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -13808,8 +13925,7 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, @@ -14018,12 +14134,13 @@ } }, "react": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", - "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" } }, "react-app-polyfill": { @@ -14104,26 +14221,15 @@ } }, "react-dom": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", - "integrity": "sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "scheduler": "^0.20.1" - }, - "dependencies": { - "scheduler": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.1.tgz", - "integrity": "sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - } + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" } }, "react-error-overlay": { @@ -14218,37 +14324,23 @@ } } }, - "react-shallow-renderer": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", - "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0" - } - }, "react-test-renderer": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.1.tgz", - "integrity": "sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", + "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", "dev": true, "requires": { "object-assign": "^4.1.1", - "react-is": "^17.0.1", - "react-shallow-renderer": "^16.13.1", - "scheduler": "^0.20.1" + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.19.1" }, "dependencies": { - "scheduler": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.1.tgz", - "integrity": "sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true } } }, @@ -16771,9 +16863,9 @@ "dev": true }, "ts-loader": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.6.tgz", - "integrity": "sha512-c8XkRbhKxFLbiIwZR7FBGWDq0MIz/QSpx3CGpj0abJxD5YVX8oDhQkJLeGbXUPRIlaX4Ajmr77fOiFVZ3gSU7g==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.7.tgz", + "integrity": "sha512-ooa4wxlZ9TOXaJ/iVyZlWsim79Ul4KyifSwyT2hOrbQA6NZJypsLOE198o8Ko+JV+ZHnMArvWcl4AnRqpCU/Mw==", "dev": true, "requires": { "chalk": "^2.3.0", @@ -16783,54 +16875,11 @@ "semver": "^6.0.0" }, "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } } } }, @@ -16952,9 +17001,9 @@ } }, "typescript": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", - "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", + "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", "dev": true }, "typical": { diff --git a/package.json b/package.json index fc2f54d..fdb176b 100644 --- a/package.json +++ b/package.json @@ -16,31 +16,31 @@ "@babel/preset-env": "^7.11.0", "@babel/preset-react": "^7.10.4", "@babel/preset-typescript": "^7.10.4", - "@types/enzyme": "^3.10.5", + "@types/enzyme": "^3.10.8", "@types/enzyme-adapter-react-16": "^1.0.6", "@types/jest": "^26.0.10", - "@types/react": "^16.9.46", - "@types/react-dom": "^16.9.8", + "@types/react": "^16.9.55", + "@types/react-dom": "^16.9.9", "@types/react-is": "^16.7.1", - "@typescript-eslint/eslint-plugin": "^4.5.0", - "@typescript-eslint/parser": "^4.5.0", + "@typescript-eslint/eslint-plugin": "^4.6.1", + "@typescript-eslint/parser": "^4.6.1", "babel-plugin-root-import": "^6.6.0", "dart-sass": "^1.25.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.3", - "eslint": "^7.7.0", + "eslint": "^7.12.1", "eslint-plugin-react": "^7.20.6", "flush-promises": "^1.0.2", - "react-dom": "^17.0.1", + "react-dom": "^16.14.0", "react-scripts": "^4.0.0", - "react-test-renderer": "^17.0.1", - "ts-loader": "^8.0.2", - "typescript": "^4.0.3", + "react-test-renderer": "^16.14.0", + "ts-loader": "^8.0.7", + "typescript": "^4.0.5", "webpack-cli": "^4.1.0", "webpack-dev-server": "^3.11.0" }, "dependencies": { "clsx": "^1.1.1", - "react": "^17.0.1" + "react": "^16.14.0" } } diff --git a/src/components/Field/FieldBase.tsx b/src/components/Field/FieldBase.tsx new file mode 100644 index 0000000..93e8e9b --- /dev/null +++ b/src/components/Field/FieldBase.tsx @@ -0,0 +1,97 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { Icon, Variant } from '@/components'; +import clsx from 'clsx'; +import { ReactElement, useState } from 'react'; +import { FieldContext } from './FieldContext'; + +interface FieldBaseProps { + actions?: React.ReactNode; + className?: string; + label?: React.ReactNode; + valid?: boolean; + variant?: Variant | string; + value?: string | ReadonlyArray | number; +} + +export const getStateIcon = (valid: boolean | undefined): ReactElement | undefined => { + if (valid === undefined) { + return undefined; + } + + return ( +
+ +
+ ); +} + +const FieldBaseProps = ({ + actions, + className, + children, + label, + variant = Variant.PRIMARY, + valid +}: React.PropsWithChildren, +): React.ReactElement => { + const [hasValue, setHasValue] = useState(false); + const [hasFocus, setHasFocus] = useState(false); + + return ( +
+ {label && ( + + )} + setHasValue(value), + changeFocus: (focus: boolean) => setHasFocus(focus), + stateIcon: getStateIcon(valid) + }}> + {children} + + {actions && ( +
+ {actions} +
+ )} +
+ ); +}; + +export const propTypes = { + actions: PropTypes.node, + children: PropTypes.node, + label: PropTypes.node, + onChange: PropTypes.func, + valid: PropTypes.bool, + value: PropTypes.string, + variant: PropTypes.string, +} + +FieldBaseProps.displayName = 'FieldBase'; +FieldBaseProps.propTypes = propTypes; + +export default FieldBaseProps; diff --git a/src/components/Field/FieldContainer.tsx b/src/components/Field/FieldContainer.tsx new file mode 100644 index 0000000..d989d49 --- /dev/null +++ b/src/components/Field/FieldContainer.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; + +interface FieldContainerProps extends React.HTMLAttributes { + toggles?: boolean; +} + +const FieldContainer = React.forwardRef(( + { + children, + className, + toggles + }, + ref +): React.ReactElement => ( +
+ {children} +
+)); + +FieldContainer.displayName = 'FieldContainer'; +FieldContainer.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + toggles: PropTypes.bool +} + +export default FieldContainer; diff --git a/src/components/Field/FieldContext.ts b/src/components/Field/FieldContext.ts new file mode 100644 index 0000000..8115387 --- /dev/null +++ b/src/components/Field/FieldContext.ts @@ -0,0 +1,13 @@ +import React from 'react'; + +export interface FieldContextProps { + changeFocus: (focus: boolean) => void; + changeValue: (value: boolean) => void; + stateIcon: React.ReactElement | undefined +} + +export const FieldContext = React.createContext({ + changeFocus: () => undefined, + changeValue: () => undefined, + stateIcon: undefined +}); diff --git a/src/components/Form/Group.tsx b/src/components/Form/Group.tsx new file mode 100644 index 0000000..1865bb4 --- /dev/null +++ b/src/components/Form/Group.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; + +interface FormGroupProps extends React.HTMLAttributes { +} + +const FormGroup = React.forwardRef(( + { + children, + className, + ...rest + }, + ref +): React.ReactElement => ( +
+ {children} +
+)); + +FormGroup.displayName = 'FormGroup'; +FormGroup.propTypes = { + children: PropTypes.node, + className: PropTypes.string, +} + +export default FormGroup; diff --git a/src/components/Form/Label.tsx b/src/components/Form/Label.tsx new file mode 100644 index 0000000..54d53eb --- /dev/null +++ b/src/components/Form/Label.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; + +type FormLabelProps = React.LabelHTMLAttributes; + +const FormLabel = React.forwardRef(( + { + children, + className, + htmlFor, + ...rest + }, + ref +): React.ReactElement => ( + +)); + +FormLabel.displayName = 'FormLabel'; +FormLabel.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + htmlFor: PropTypes.string +} + +export default FormLabel; diff --git a/src/components/Form/Message.tsx b/src/components/Form/Message.tsx new file mode 100644 index 0000000..f3f6473 --- /dev/null +++ b/src/components/Form/Message.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; + +interface FormMessageProps extends React.HTMLAttributes { + className?: string; + valid?: boolean; +} + +const FormMessage = React.forwardRef(( + { + children, + className, + valid + }, + ref +): React.ReactElement => ( +
+ {children} +
+)); + +FormMessage.displayName = 'FormMessage'; +FormMessage.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + valid: PropTypes.bool +} + +export default FormMessage; diff --git a/src/components/SelectField/SelectField.tsx b/src/components/SelectField/SelectField.tsx new file mode 100644 index 0000000..53b874d --- /dev/null +++ b/src/components/SelectField/SelectField.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import FieldBaseProps from '@/components/Field/FieldBase'; +import FieldBase, { propTypes as basePropTypes } from '@/components/Field/FieldBase'; +import SelectFieldInput from '@/components/SelectField/SelectFieldInput'; + +export type SelectFieldProps = React.SelectHTMLAttributes & FieldBaseProps; + +const SelectField = React.forwardRef(( + { + actions, + label, + valid, + variant, + ...rest + }, + ref +): React.ReactElement => ( + + + +)); + +export const propTypes = basePropTypes; + +SelectField.displayName = 'SelectField'; +SelectField.propTypes = propTypes; + +export default SelectField; diff --git a/src/components/SelectField/SelectFieldInput.tsx b/src/components/SelectField/SelectFieldInput.tsx new file mode 100644 index 0000000..4b8d320 --- /dev/null +++ b/src/components/SelectField/SelectFieldInput.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { propTypes, SelectFieldProps } from '@/components/SelectField/SelectField'; +import { Children, isValidElement, ReactNode, useContext, useEffect, useState } from 'react'; +import { FieldContext, FieldContextProps } from '@/components/Field/FieldContext'; +import clsx from 'clsx'; +import FieldContainer from '@/components/Field/FieldContainer'; + +const determineInitialValue = (children: ReactNode): string => { + let value = ''; + Children.forEach(children, (child: ReactNode) => { + if ( + isValidElement(child) + && child.type === 'option' + && (!value || child.props.selected) + ) { + value = child.props.value || 'initial'; + } + }); + + return value; +} + +const SelectFieldInput = React.forwardRef(( + { + children, + defaultValue, + value, + onChange, + ...rest + }, + ref +): React.ReactElement => { + const fieldContext = useContext(FieldContext); + const [inputValue, setInputValue] = useState | number>(''); + + useEffect(() => { + const initialValue = value || defaultValue || determineInitialValue(children); + fieldContext.changeValue(!!initialValue); + setInputValue(initialValue.toString()); + }, [value, defaultValue]); + + return ( + + {({ changeValue, changeFocus, stateIcon }) => ( + + + {stateIcon} + + )} + + ); +}); + +SelectFieldInput.displayName = 'SelectFieldInput'; +SelectFieldInput.propTypes = propTypes; + +export default SelectFieldInput; diff --git a/src/components/SelectField/index.ts b/src/components/SelectField/index.ts new file mode 100644 index 0000000..3b49bb8 --- /dev/null +++ b/src/components/SelectField/index.ts @@ -0,0 +1 @@ +export { default as SelectField } from './SelectField'; diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx new file mode 100644 index 0000000..b2a3d72 --- /dev/null +++ b/src/components/TextField/TextField.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import FieldBase, { propTypes as basePropTypes } from '@/components/Field/FieldBase'; +import FieldBaseProps from '@/components/Field/FieldBase'; +import TextFieldInput from '@/components/TextField/TextFieldInput'; + +export type TextFieldProps = React.InputHTMLAttributes & FieldBaseProps; + +const TextField = React.forwardRef(( + { + actions, + label, + valid, + variant, + ...rest + }, + ref +): React.ReactElement => ( + + + +)); + +export const propTypes = { + ...basePropTypes, + type: PropTypes.oneOf(['password', 'text', 'reset']) +} + +TextField.displayName = 'TextField'; +TextField.propTypes = propTypes; + +export default TextField; diff --git a/src/components/TextField/TextFieldInput.tsx b/src/components/TextField/TextFieldInput.tsx new file mode 100644 index 0000000..d24388b --- /dev/null +++ b/src/components/TextField/TextFieldInput.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { FieldContext, FieldContextProps } from '@/components/Field/FieldContext'; +import { propTypes, TextFieldProps } from './TextField'; +import { useContext, useEffect, useState } from 'react'; +import FieldContainer from '@/components/Field/FieldContainer'; + +const TextFieldInput = React.forwardRef(( + { + type, + value, + onChange, + ...rest + }, + ref +): React.ReactElement => { + const fieldContext = useContext(FieldContext); + const [inputValue, setInputValue] = useState | number>(''); + + useEffect(() => { + setInputValue(value || ''); + fieldContext.changeValue(!!value); + }, [value]); + return ( + + {({ changeValue, changeFocus, stateIcon }) => ( + + changeFocus(true)} + onBlur={() => changeFocus(false)} + className="form-field" + onChange={(event: React.ChangeEvent) => { + if (onChange) { + onChange(event); + } + setInputValue(event.target.value); + changeValue(!!event.target.value); + }} + {...rest} + /> + {stateIcon} + + )} + + ) +}); + +TextFieldInput.displayName = 'TextFieldInput'; +TextFieldInput.propTypes = propTypes; + +export default TextFieldInput; diff --git a/src/components/TextField/index.ts b/src/components/TextField/index.ts new file mode 100644 index 0000000..f38be84 --- /dev/null +++ b/src/components/TextField/index.ts @@ -0,0 +1 @@ +export { default as TextField } from '@/components/TextField/TextField'; diff --git a/src/components/index.ts b/src/components/index.ts index b3c70ae..22c7f1e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,3 +6,5 @@ export * from './Page'; export * from './Panel'; export * from './Icon'; export * from './utils'; +export * from './TextField'; +export * from './SelectField'; diff --git a/src/style/base/_typography.scss b/src/style/base/_typography.scss index 69b3e08..893f317 100644 --- a/src/style/base/_typography.scss +++ b/src/style/base/_typography.scss @@ -124,6 +124,7 @@ h5 { font-size: 1.2rem; margin: 0 0 1rem; color: mixins.color('gray', 900); + font-weight: 600; } h6 { diff --git a/src/style/base/_variables.scss b/src/style/base/_variables.scss index 6f9fc22..41b28a7 100644 --- a/src/style/base/_variables.scss +++ b/src/style/base/_variables.scss @@ -8,8 +8,8 @@ $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; $base-gutter-steps: 0.25; @@ -22,12 +22,12 @@ $breakpoints: ( ); :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}; } @@ -49,6 +49,10 @@ $danger-color-hover: mixins.color('red', 800); $danger-color-active: mixins.color('red', 900); $danger-color-light-background: mixins.color('red', 50); +$success-color: mixins.color('green', 700); +$success-color-hover: mixins.color('green', 800); +$success-color-active: mixins.color('green', 900); + :root { --primary-color: #{$primary-color}; --primary-color-hover: #{$primary-color-hover}; @@ -62,7 +66,11 @@ $danger-color-light-background: mixins.color('red', 50); --danger-color: #{$danger-color}; --danger-color-hover: #{$danger-color-hover}; --danger-color-active: #{$danger-color-active}; - --danger-color-light-background: #{$danger-color-light-background} + --danger-color-light-background: #{$danger-color-light-background}; + + --success-color: #{$success-color}; + --success-color-hover: #{$success-color}; + --success-color-active: #{$success-color-active} } /** @@ -88,7 +96,7 @@ $page-gutter: 2rem; /** * 5. Button */ -$button-padding: 0.9rem; +$button-padding: 1rem; $button-font-weight: 600; $button-font-size: 1rem; @@ -139,7 +147,7 @@ $danger-button-ghost-active-background: mixins.color('red', 75); /** * 6. Cards */ -$card-padding: 1rem; +$card-padding: 1.5rem; $card-background: #fff; :root { @@ -414,3 +422,16 @@ $icons: ( @function icon($name) { @return map-get($icons, $name); } + +/** + * 8. Form fields + */ +$form-field-padding: 1rem; +$form-field-font-weight: 500; +$form-field-font-size: 1rem; + +:root { + --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/_button.scss b/src/style/components/_button.scss index 55f64d3..715a77f 100644 --- a/src/style/components/_button.scss +++ b/src/style/components/_button.scss @@ -1,4 +1,5 @@ .btn { + background: transparent; border: solid 1px transparent; border-radius: var(--base-border-radius); cursor: pointer; diff --git a/src/style/components/_card.scss b/src/style/components/_card.scss index ad92d8b..fc2e89c 100644 --- a/src/style/components/_card.scss +++ b/src/style/components/_card.scss @@ -5,6 +5,7 @@ background: var(--card-background); border: solid 1px var(--base-border-color); border-radius: var(--base-border-radius); + width: 100%; .card-content { padding: var(--card-padding); @@ -22,16 +23,12 @@ margin-left: var(--card-padding); } } - - & + .card-content { - padding-top: 0; - } } .card-title { font: { weight: 600; - size: 1.1rem; + size: 1.2rem; } } diff --git a/src/style/components/_dropdown.scss b/src/style/components/_dropdown.scss new file mode 100644 index 0000000..6d8df4a --- /dev/null +++ b/src/style/components/_dropdown.scss @@ -0,0 +1,2 @@ + + diff --git a/src/style/components/_form-fields.scss b/src/style/components/_form-fields.scss new file mode 100644 index 0000000..f33df82 --- /dev/null +++ b/src/style/components/_form-fields.scss @@ -0,0 +1,173 @@ +@use "../base/mixins"; +@use "../base/variables"; +@use "icons"; + +.form-field-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; + transition: all .1s ease-in-out; + + .form-field-actions { + align-self: center; + pointer-events: auto; + padding-right: var(--form-field-padding); + } + + &:hover:not(.form-field-valid):not(.form-field-invalid) { + border-color: var(--base-border-hover-color); + } + + &.focused:not(.form-field-valid):not(.form-field-invalid) { + border-color: var(--primary-color); + + .form-field-label-floating { + color: var(--primary-color); + } + } + + &.focused, &.has-value { + .form-field-label-floating { + transform: translateY(calc((var(--form-field-padding) / 2) / 2)); + top: 0; + font: { + size: 0.8rem; + } + } + } + + &.floating-label .form-field { + padding: calc(var(--form-field-padding) + (var(--form-field-padding) / 2)) var(--form-field-padding) calc(var(--form-field-padding) / 2); + } + + &:not(.floating-label) .form-field { + padding: var(--form-field-padding); + } + + .form-field { + background: none; + border-radius: var(--base-border-radius); + border: none; + outline: none; + width: 100%; + font: { + size: var(--form-field-font-size); + weight: var(--form-field-font-weight); + } + pointer-events: auto; + line-height: 1.3rem; + } + + .form-field-label-floating { + font: { + size: var(--form-field-font-size); + } + color: mixins.color('gray', 700); + position: absolute; + top: 50%; + left: var(--form-field-padding); + transform: translateY(-50%); + transition: all .1s ease-in-out; + } + + & + .form-message { + margin-top: 0.25rem; + } + + .field-state-icon { + @extend .icon; + + position: absolute; + justify-content: center; + right: var(--form-field-padding); + align-items: center; + top: 50%; + transform: translateY(-50%); + } + + &.form-field-invalid { + border-color: var(--danger-color); + + .form-field-label-floating { + color: var(--danger-color); + } + + .form-field { + padding-right: calc(var(--form-field-padding) * 3); + } + + .field-state-icon { + color: var(--danger-color) + } + } + + &.form-field-valid { + border-color: var(--success-color); + + .form-field-label-floating { + color: var(--success-color); + } + + .form-field { + padding-right: calc(var(--form-field-padding) * 3); + } + + .field-state-icon { + color: var(--success-color) + } + } + + &.form-field-invalid .form-field-container .form-field, + &.form-field-valid .form-field-container .form-field { + padding-right: calc(var(--form-field-padding) * 3); + } + + .form-field-container { + flex: 1 1 100%; + display: flex; + position: relative; + + &.toggles { + position: relative; + + &:before { + content: variables.icon('arrow-alt-down'); + font-family: 'Cui-Icons'; + color: inherit; + position: absolute; + top: 50%; + transform: translateY(-50%); + right: var(--form-field-padding); + } + + .form-field-select { + -moz-appearance: none; + -webkit-appearance: none; + + &::-ms-expand { + display: none; + } + } + + .field-state-icon { + right: calc(var(--form-field-padding) * 2); + } + } + } + + &.form-field-base-toggles { + &.form-field-valid, &.form-field-invalid { + .field-state-icon { + right: calc(var(--form-field-padding) * 2.5); + } + + .form-field-container.toggles .form-field { + padding-right: calc(var(--form-field-padding) * 4.5); + } + } + } +} diff --git a/src/style/components/_forms.scss b/src/style/components/_forms.scss new file mode 100644 index 0000000..01695ab --- /dev/null +++ b/src/style/components/_forms.scss @@ -0,0 +1,22 @@ +.form-group { + margin-bottom: 1rem; +} + +.form-label { + font: { + weight: 600; + } + display: inline-block; + margin-bottom: 0.5rem; +} + +.form-message { + &.form-message-valid { + color: var(--success-color); + } + + &.form-message-invalid { + font-weight: 500; + color: var(--danger-color); + } +} diff --git a/src/style/components/_icons.scss b/src/style/components/_icons.scss index 7ac870c..461a0c6 100644 --- a/src/style/components/_icons.scss +++ b/src/style/components/_icons.scss @@ -4,6 +4,7 @@ font: { family: 'Cui-Icons'; style: normal; + size: 1.3rem; } line-height: 1; display: inline-block; diff --git a/src/style/index.scss b/src/style/index.scss index 5369252..abad4b0 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -13,8 +13,10 @@ @use "components/button"; @use "components/page"; @use "components/panel"; +@use "components/form-fields"; @use "components/card"; @use "components/icons"; +@use "components/forms"; /** * 3. Layout 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 f6c8498..54da55d 100644 --- a/www/src/index.tsx +++ b/www/src/index.tsx @@ -2,1297 +2,1647 @@ import * as React from 'react'; import * as ReactDom from 'react-dom'; import '../../src/style/index.scss'; -import { Button, Card, Col, Grid, Icon, Page, Row, Variant } from '../../src'; +import { Button, Card, Col, Grid, Icon, Page, Row, SelectField, TextField, Variant } from '../../src'; import Container from '../../src/components/Container/Container'; +import FormGroup from '../../src/components/Form/Group'; +import FormLabel from '../../src/components/Form/Label'; +import FormMessage from '../../src/components/Form/Message'; +import { useState } from 'react'; + +const TestControllable = () => { + const [value, setValue] = useState(''); + + return ( + <> +
Controllable inputs
+ + ) => setValue(event.target.value)} + /> + + + + + + ); +} ReactDom.render( - + - - - - -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- - - - - - Col 1/1 - - - Col 1/2 - Col 2/2 - + + + + + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + + + + + + Col 1/1 + + + Col 1/2 + Col 2/2 + + + + Col 1/3 + Col 2/3 + Col 3/4 + + + + Col 1/3 + Col 2/3 + Col 3/4 + - - Col 1/3 - Col 2/3 - Col 3/4 - + + Col 1/4 + Col 2/4 + Col 3/4 + Col 4/4 + + + + + + + + + + +
Standard input
+ Username + +
- - Col 1/3 - Col 2/3 - Col 3/4 - + +
Input with floating label
+ +
- - Col 1/4 - Col 2/4 - Col 3/4 - Col 4/4 - -
-
-
- - - -
Activity
- -
-
- -
Alert circle
- -
-
- -
Alert triangle
- -
-
- -
Archive
- -
-
- -
Arrow left
- -
-
- -
Arrow circle down
- -
-
- -
Arrow circle left
- -
-
- -
Arrow circle right
- -
-
- -
Arrow circle up
- -
-
- -
Sort down
- -
-
- -
Sort down
- -
-
- -
Arrow right
- -
-
- -
Arrow alt left
- -
-
- -
Arrow alt down
- -
-
- -
Arrow alt right
- -
-
- -
Arrow alt up
- -
-
- -
Sort left
- -
-
- -
Sort right
- -
-
- -
Sort up
- -
-
- -
Arrow up
- -
-
- -
Arrowhead down
- -
-
- -
Arrowhead left
- -
-
- -
Arrowhead right
- -
-
- -
Arrowhead up
- -
-
- -
At
- -
-
- -
Attach 2
- -
-
- -
Attach
- -
-
- -
Award
- -
-
- -
Backspace
- -
-
- -
Bar chart 2
- -
-
- -
Bar chart
- -
-
- -
Battery
- -
-
- -
Behance
- -
-
- -
Bell off
- -
-
- -
Bell
- -
-
- -
Bluetooth
- -
-
- -
Book open
- -
-
- -
Book
- -
-
- -
Bookmark
- -
-
- -
Briefcase
- -
-
- -
Browser
- -
-
- -
Brush
- -
-
- -
Bulb
- -
-
- -
Calendar
- -
-
- -
Camera
- -
-
- -
Car
- -
-
- -
Cast
- -
-
- -
Charging
- -
-
- -
Checkmark circle 2
- -
-
- -
Checkmark circle
- -
-
- -
Checkmark
- -
-
- -
Checkmark square 2
- -
-
- -
Checkmark square
- -
-
- -
Chevron down
- -
-
- -
Chevron left
- -
-
- -
Chevron right
- -
-
- -
Chevron up
- -
-
- -
Clipboard
- -
-
- -
Clock
- -
-
- -
Close circle
- -
-
- -
Close
- -
-
- -
Close square
- -
-
- -
Cloud download
- -
-
- -
Cloud upload
- -
-
- -
Code download
- -
-
- -
Code
- -
-
- -
Collapse
- -
-
- -
Color palette
- -
-
- -
Color picker
- -
-
- -
Compass
- -
-
- -
Copy
- -
-
- -
Corner down left
- -
-
- -
Corner down right
- -
-
- -
Corner left down
- -
-
- -
Corner left up
- -
-
- -
Corner right down
- -
-
- -
Corner right up
- -
-
- -
Corner up left
- -
-
- -
Corner up right
- -
-
- -
Credit card
- -
-
- -
Crop
- -
-
- -
Cube
- -
-
- -
Diagonal arrow left down
- -
-
- -
Diagonal arrow left up
- -
-
- -
Diagonal arrow right down
- -
-
- -
Diagonal arrow right up
- -
-
- -
Done all
- -
-
- -
Download
- -
-
- -
Droplet off
- -
-
- -
Droplet
- -
-
- -
Edit 2
- -
-
- -
Edit
- -
-
- -
Email
- -
-
- -
Expand
- -
-
- -
External link
- -
-
- -
Eye off 2
- -
-
- -
Eye off
- -
-
- -
Eye
- -
-
- -
Facebook
- -
-
- -
File add
- -
-
- -
File
- -
-
- -
File remove
- -
-
- -
File text
- -
-
- -
Film
- -
-
- -
Flag
- -
-
- -
Flash off
- -
-
- -
Flash
- -
-
- -
Flip 2
- -
-
- -
Flip
- -
-
- -
Folder add
- -
-
- -
Folder
- -
-
- -
Folder remove
- -
-
- -
Funnel
- -
-
- -
Gift
- -
-
- -
Github
- -
-
- -
Globe 2
- -
-
- -
Globe
- -
-
- -
Google
- -
-
- -
Grid
- -
-
- -
Hard drive
- -
-
- -
Hash
- -
-
- -
Headphones
- -
-
- -
Heart
- -
-
- -
Home
- -
-
- -
Image
- -
-
- -
Inbox
- -
-
- -
Info
- -
-
- -
Keypad
- -
-
- -
Layers
- -
-
- -
Layout
- -
-
- -
Link 2
- -
-
- -
Link
- -
-
- -
Linkedin
- -
-
- -
List
- -
-
- -
Loader
- -
-
- -
Lock
- -
-
- -
Log in
- -
-
- -
Log out
- -
-
- -
Map
- -
-
- -
Maximize
- -
-
- -
Menu 2
- -
-
- -
Menu arrow
- -
-
- -
Menu
- -
-
- -
Message circle
- -
-
- -
Message square
- -
-
- -
Mic off
- -
-
- -
Mic
- -
-
- -
Minimize
- -
-
- -
Minus circle
- -
-
- -
Minus
- -
-
- -
Minus square
- -
-
- -
Monitor
- -
-
- -
Moon
- -
-
- -
More horizontal
- -
-
- -
More vertical
- -
-
- -
Move
- -
-
- -
Music
- -
-
- -
Navigation 2
- -
-
- -
Navigation
- -
-
- -
Npm
- -
-
- -
Options 2
- -
-
- -
Options
- -
-
- -
Pantone
- -
-
- -
Paper plane
- -
-
- -
Pause circle
- -
-
- -
People
- -
-
- -
Percent
- -
-
- -
user add
- -
-
- -
user delete
- -
-
- -
user done
- -
-
- -
user
- -
-
- -
user remove
- -
-
- -
Phone call
- -
-
- -
Phone missed
- -
-
- -
Phone off
- -
-
- -
Phone
- -
-
- -
Pie chart
- -
-
- -
Pin
- -
-
- -
Play circle
- -
-
- -
Plus circle
- -
-
- -
Plus
- -
-
- -
Plus square
- -
-
- -
Power
- -
-
- -
Pricetags
- -
-
- -
Printer
- -
-
- -
Question mark circle
- -
-
- -
Question mark
- -
-
- -
Radio button off
- -
-
- -
Radio button on
- -
-
- -
Radio
- -
-
- -
Recording
- -
-
- -
Refresh
- -
-
- -
Repeat
- -
-
- -
Rewind left
- -
-
- -
Rewind right
- -
-
- -
Save
- -
-
- -
Scissors
- -
-
- -
Search
- -
-
- -
Settings 2
- -
-
- -
Settings
- -
-
- -
Shake
- -
-
- -
Share
- -
-
- -
Shield off
- -
-
- -
Shield
- -
-
- -
Shopping bag
- -
-
- -
Shopping cart
- -
-
- -
Shuffle 2
- -
-
- -
Shuffle
- -
-
- -
Step backwards
- -
-
- -
Step forward
- -
-
- -
Slash
- -
-
- -
Smartphone
- -
-
- -
Smiling face
- -
-
- -
Speaker
- -
-
- -
Square
- -
-
- -
Star
- -
-
- -
Stop circle
- -
-
- -
Sun
- -
-
- -
Swap
- -
-
- -
Sync
- -
-
- -
Text
- -
-
- -
Thermometer minus
- -
-
- -
Thermometer
- -
-
- -
Thermometer plus
- -
-
- -
Toggle left
- -
-
- -
Toggle right
- -
-
- -
Trash 2
- -
-
- -
Trash
- -
-
- -
Trending down
- -
-
- -
Trending up
- -
-
- -
Tv
- -
-
- -
Twitter
- -
-
- -
Umbrella
- -
-
- -
Undo
- -
-
- -
Unlock
- -
-
- -
Upload
- -
-
- -
Video off
- -
-
- -
Video
- -
-
- -
Volume down
- -
-
- -
Volume mute
- -
-
- -
Volume off
- -
-
- -
Volume up
- -
-
- -
Wifi off
- -
-
- -
Wifi
- -
-
-
+ +
Input with default value
+ +
+ + +
Input with actions
+ { + + }} + actions={[ + + ]} + valid={true} + /> +
+ + +
Invalid input
+ + + Input is invalid + +
+ + +
Valid input
+ +
+ +
Valid input
+ + + + + + + +
+ + + + + + + + +
Activity
+ +
+
+ + +
Alert circle
+ +
+
+ + +
Alert triangle
+ +
+
+ + +
Archive
+ +
+
+ + +
Arrow left
+ +
+
+ + +
Arrow circle down
+ +
+
+ + +
Arrow circle left
+ +
+
+ + +
Arrow circle right
+ +
+
+ + +
Arrow circle up
+ +
+
+ + +
Sort down
+ +
+
+ + +
Sort down
+ +
+
+ + +
Arrow right
+ +
+
+ + +
Arrow alt left
+ +
+
+ + +
Arrow alt down
+ +
+
+ + +
Arrow alt right
+ +
+
+ + +
Arrow alt up
+ +
+
+ + +
Sort left
+ +
+
+ + +
Sort right
+ +
+
+ + +
Sort up
+ +
+
+ + +
Arrow up
+ +
+
+ + +
chevron-double down
+ +
+
+ + +
chevron-double left
+ +
+
+ + +
chevron-double right
+ +
+
+ + +
chevron-double up
+ +
+
+ + +
At
+ +
+
+ + +
Attach 2
+ +
+
+ + +
Attach
+ +
+
+ + +
Award
+ +
+
+ + +
Backspace
+ +
+
+ + +
Bar chart 2
+ +
+
+ + +
Bar chart
+ +
+
+ + +
Battery
+ +
+
+ + +
Behance
+ +
+
+ + +
Bell off
+ +
+
+ + +
Bell
+ +
+
+ + +
Bluetooth
+ +
+
+ + +
Book open
+ +
+
+ + +
Book
+ +
+
+ + +
Bookmark
+ +
+
+ + +
Briefcase
+ +
+
+ + +
Browser
+ +
+
+ + +
Brush
+ +
+
+ + +
Bulb
+ +
+
+ + +
Calendar
+ +
+
+ + +
Camera
+ +
+
+ + +
Car
+ +
+
+ + +
Cast
+ +
+
+ + +
Charging
+ +
+
+ + +
Checkmark circle 2
+ +
+
+ + +
Checkmark circle
+ +
+
+ + +
Checkmark
+ +
+
+ + +
Checkmark square 2
+ +
+
+ + +
Checkmark square
+ +
+
+ + +
Chevron down
+ +
+
+ + +
Chevron left
+ +
+
+ + +
Chevron right
+ +
+
+ + +
Chevron up
+ +
+
+ + +
Clipboard
+ +
+
+ + +
Clock
+ +
+
+ + +
Close circle
+ +
+
+ + +
Close
+ +
+
+ + +
Close square
+ +
+
+ + +
Cloud download
+ +
+
+ + +
Cloud upload
+ +
+
+ + +
Code download
+ +
+
+ + +
Code
+ +
+
+ + +
Collapse
+ +
+
+ + +
Color palette
+ +
+
+ + +
Color picker
+ +
+
+ + +
Compass
+ +
+
+ + +
Copy
+ +
+
+ + +
Corner down left
+ +
+
+ + +
Corner down right
+ +
+
+ + +
Corner left down
+ +
+
+ + +
Corner left up
+ +
+
+ + +
Corner right down
+ +
+
+ + +
Corner right up
+ +
+
+ + +
Corner up left
+ +
+
+ + +
Corner up right
+ +
+
+ + +
Credit card
+ +
+
+ + +
Crop
+ +
+
+ + +
Cube
+ +
+
+ + +
Diagonal arrow left down
+ +
+
+ + +
Diagonal arrow left up
+ +
+
+ + +
Diagonal arrow right down
+ +
+
+ + +
Diagonal arrow right up
+ +
+
+ + +
Done all
+ +
+
+ + +
Download
+ +
+
+ + +
Droplet off
+ +
+
+ + +
Droplet
+ +
+
+ + +
Edit 2
+ +
+
+ + +
Edit
+ +
+
+ + +
Email
+ +
+
+ + +
Expand
+ +
+
+ + +
External link
+ +
+
+ + +
Eye off 2
+ +
+
+ + +
Eye off
+ +
+
+ + +
Eye
+ +
+
+ + +
Facebook
+ +
+
+ + +
File add
+ +
+
+ + +
File
+ +
+
+ + +
File remove
+ +
+
+ + +
File text
+ +
+
+ + +
Film
+ +
+
+ + +
Flag
+ +
+
+ + +
Flash off
+ +
+
+ + +
Flash
+ +
+
+ + +
Flip 2
+ +
+
+ + +
Flip
+ +
+
+ + +
Folder add
+ +
+
+ + +
Folder
+ +
+
+ + +
Folder remove
+ +
+
+ + +
Funnel
+ +
+
+ + +
Gift
+ +
+
+ + +
Github
+ +
+
+ + +
Globe 2
+ +
+
+ + +
Globe
+ +
+
+ + +
Google
+ +
+
+ + +
Grid
+ +
+
+ + +
Hard drive
+ +
+
+ + +
Hash
+ +
+
+ + +
Headphones
+ +
+
+ + +
Heart
+ +
+
+ + +
Home
+ +
+
+ + +
Image
+ +
+
+ + +
Inbox
+ +
+
+ + +
Info
+ +
+
+ + +
Keypad
+ +
+
+ + +
Layers
+ +
+
+ + +
Layout
+ +
+
+ + +
Link 2
+ +
+
+ + +
Link
+ +
+
+ + +
Linkedin
+ +
+
+ + +
List
+ +
+
+ + +
Loader
+ +
+
+ + +
Lock
+ +
+
+ + +
Log in
+ +
+
+ + +
Log out
+ +
+
+ + +
Map
+ +
+
+ + +
Maximize
+ +
+
+ + +
Menu 2
+ +
+
+ + +
Menu arrow
+ +
+
+ + +
Menu
+ +
+
+ + +
Message circle
+ +
+
+ + +
Message square
+ +
+
+ + +
Mic off
+ +
+
+ + +
Mic
+ +
+
+ + +
Minimize
+ +
+
+ + +
Minus circle
+ +
+
+ + +
Minus
+ +
+
+ + +
Minus square
+ +
+
+ + +
Monitor
+ +
+
+ + +
Moon
+ +
+
+ + +
More horizontal
+ +
+
+ + +
More vertical
+ +
+
+ + +
Move
+ +
+
+ + +
Music
+ +
+
+ + +
Navigation 2
+ +
+
+ + +
Navigation
+ +
+
+ + +
Npm
+ +
+
+ + +
Options 2
+ +
+
+ + +
Options
+ +
+
+ + +
Pantone
+ +
+
+ + +
Paper plane
+ +
+
+ + +
Pause circle
+ +
+
+ + +
People
+ +
+
+ + +
Percent
+ +
+
+ + +
user add
+ +
+
+ + +
user delete
+ +
+
+ + +
user done
+ +
+
+ + +
user
+ +
+
+ + +
user remove
+ +
+
+ + +
Phone call
+ +
+
+ + +
Phone missed
+ +
+
+ + +
Phone off
+ +
+
+ + +
Phone
+ +
+
+ + +
Pie chart
+ +
+
+ + +
Pin
+ +
+
+ + +
Play circle
+ +
+
+ + +
Plus circle
+ +
+
+ + +
Plus
+ +
+
+ + +
Plus square
+ +
+
+ + +
Power
+ +
+
+ + +
Pricetags
+ +
+
+ + +
Printer
+ +
+
+ + +
Question mark circle
+ +
+
+ + +
Question mark
+ +
+
+ + +
Radio button off
+ +
+
+ + +
Radio button on
+ +
+
+ + +
Radio
+ +
+
+ + +
Recording
+ +
+
+ + +
Refresh
+ +
+
+ + +
Repeat
+ +
+
+ + +
Rewind left
+ +
+
+ + +
Rewind right
+ +
+
+ + +
Save
+ +
+
+ + +
Scissors
+ +
+
+ + +
Search
+ +
+
+ + +
Settings 2
+ +
+
+ + +
Settings
+ +
+
+ + +
Shake
+ +
+
+ + +
Share
+ +
+
+ + +
Shield off
+ +
+
+ + +
Shield
+ +
+
+ + +
Shopping bag
+ +
+
+ + +
Shopping cart
+ +
+
+ + +
Shuffle 2
+ +
+
+ + +
Shuffle
+ +
+
+ + +
Step backwards
+ +
+
+ + +
Step forward
+ +
+
+ + +
Slash
+ +
+
+ + +
Smartphone
+ +
+
+ + +
Smiling face
+ +
+
+ + +
Speaker
+ +
+
+ + +
Square
+ +
+
+ + +
Star
+ +
+
+ + +
Stop circle
+ +
+
+ + +
Sun
+ +
+
+ + +
Swap
+ +
+
+ + +
Sync
+ +
+
+ + +
Text
+ +
+
+ + +
Thermometer minus
+ +
+
+ + +
Thermometer
+ +
+
+ + +
Thermometer plus
+ +
+
+ + +
Toggle left
+ +
+
+ + +
Toggle right
+ +
+
+ + +
Trash 2
+ +
+
+ + +
Trash
+ +
+
+ + +
Trending down
+ +
+
+ + +
Trending up
+ +
+
+ + +
Tv
+ +
+
+ + +
Twitter
+ +
+
+ + +
Umbrella
+ +
+
+ + +
Undo
+ +
+
+ + +
Unlock
+ +
+
+ + +
Upload
+ +
+
+ + +
Video off
+ +
+
+ + +
Video
+ +
+
+ + +
Volume down
+ +
+
+ + +
Volume mute
+ +
+
+ + +
Volume off
+ +
+
+ + +
Volume up
+ +
+
+ + +
Wifi off
+ +
+
+ + +
Wifi
+ +
+
+
+
+
, document.getElementById('root')