Refactor form fields (#636)

This commit is contained in:
Kyle Knight
2016-12-16 09:54:49 -06:00
committed by GitHub
parent c8e6405220
commit 8ad731e8fb
27 changed files with 400 additions and 170 deletions
@@ -110,10 +110,10 @@ $base-class: 'button';
padding: $medium;
width: 100%;
height: auto;
box-shadow: inset 0 -3px 0px 0 rgba(0, 0, 0, 0.25), 0 -2px 3px 0 rgba(0, 0, 0, 0.15);
box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.25), 0 -2px 3px 0 rgba(0, 0, 0, 0.15);
&:active {
box-shadow: 0 1px 0 #734893, 0 -2px 0 #D1D9E9;
box-shadow: 0 1px 0 #734893, 0 -2px 0 #d1d9e9;
}
}
+27 -16
View File
@@ -8,43 +8,54 @@ class Rocker extends Component {
static propTypes = {
className: PropTypes.string,
handleChange: PropTypes.func,
name: PropTypes.string,
onChange: PropTypes.func,
options: PropTypes.shape({
aText: PropTypes.string,
aIcon: PropTypes.string,
bText: PropTypes.string,
bIcon: PropTypes.string,
rightText: PropTypes.string,
rightIcon: PropTypes.string,
leftText: PropTypes.string,
leftIcon: PropTypes.string,
}),
value: PropTypes.string,
};
static defaultProps = {
handleChange: noop,
onChange: noop,
};
handleChange = (evt) => {
const { onChange, options: { rightText, leftText }, value } = this.props;
evt.preventDefault();
const newOption = value === leftText ? rightText : leftText;
onChange(newOption);
};
render () {
const { className, handleChange, name, options, value } = this.props;
const { aText, aIcon, bText, bIcon } = options;
const { handleChange } = this;
const { className, options, value } = this.props;
const { rightText, rightIcon, leftText, leftIcon } = options;
const baseClass = 'kolide-rocker';
const rockerClasses = classnames(baseClass, className);
const buttonClasses = classnames(`${baseClass}__button`, 'button', 'button--unstyled', {
[`${baseClass}__button--checked`]: value === leftText,
});
return (
<div className={rockerClasses}>
<label className={`${baseClass}__label`} htmlFor={name}>
<input className={`${baseClass}__checkbox`} type="checkbox" value={value} name={name} id={name} onChange={handleChange} checked={value === bText} />
<span className={`${baseClass}__switch ${baseClass}__switch--opt-b`}>
<button className={buttonClasses} onClick={handleChange}>
<span className={`${baseClass}__switch ${baseClass}__switch--left`}>
<span className={`${baseClass}__text`}>
<Icon name={bIcon} /> {bText}
<Icon name={leftIcon} /> {leftText}
</span>
</span>
<span className={`${baseClass}__switch ${baseClass}__switch--opt-a`}>
<span className={`${baseClass}__switch ${baseClass}__switch--right`}>
<span className={`${baseClass}__text`}>
<Icon name={aIcon} /> {aText}
<Icon name={rightIcon} /> {rightText}
</span>
</span>
</label>
</button>
</div>
);
}
@@ -1,7 +1,8 @@
.kolide-rocker {
box-shadow: 0 0 9px 0 rgba(72, 81, 109, 0.1);
&__label {
&__button {
@include transform(translateY(-19px));
position: relative;
cursor: pointer;
width: 180px;
@@ -10,13 +11,10 @@
background-color: #9fa5ab;
display: block;
padding: 0 2px;
box-shadow: inset 0 -3px 3px #aab3bd;
}
background: transparent;
border: 0;
&__checkbox {
display: none;
&:checked ~ .kolide-rocker__switch--opt-b {
&--checked > .kolide-rocker__switch--left {
span {
@include transform(skewX(0) rotateZ(0));
top: 3px;
@@ -31,7 +29,7 @@
}
}
&:checked ~ .kolide-rocker__switch--opt-a {
&--checked > .kolide-rocker__switch--right {
span {
@include transform(skewX(-6deg) rotateZ(-6deg));
top: -6px;
@@ -92,7 +90,7 @@
}
}
&--opt-b {
&--left {
@include linear-gradient(-180deg, #eaedfb 81%, #aab3bd 100%);
left: 2px;
@@ -113,7 +111,7 @@
}
}
&--opt-a {
&--right {
@include linear-gradient(-180deg, #9651ca 81%, #6e3c93 100%);
right: 2px;
@@ -1,29 +0,0 @@
import React, { PropTypes } from 'react';
import classnames from 'classnames';
const Slider = ({ onClick, engaged }) => {
const baseClass = 'slider-wrap';
const sliderBtnClass = classnames(
baseClass,
{ [`${baseClass}--active`]: engaged }
);
const sliderDotClass = classnames(
`${baseClass}__dot`,
{ [`${baseClass}__dot--active`]: engaged }
);
return (
<button className={`button button--unstyled ${sliderBtnClass}`} onClick={onClick}>
<div className={sliderDotClass} />
</button>
);
};
Slider.propTypes = {
engaged: PropTypes.bool,
onClick: PropTypes.func,
};
export default Slider;
@@ -1,29 +0,0 @@
.slider-wrap {
@include transition(background-color 400ms ease-in-out);
background-color: $text-medium;
border-radius: 12px;
border: 1px solid #eaeaea;
cursor: pointer;
display: inline-block;
height: 22px;
min-width: 40px;
position: relative;
width: 40px;
&--active {
background-color: $brand;
}
&__dot {
@include transition(lest 300ms ease-in-out);
@include size(14px);
@include position(absolute, 0 null null 5px);
margin-top: 3px;
border-radius: 50%;
background-color: $white;
&--active {
left: 21px;
}
}
}
@@ -0,0 +1,63 @@
import React, { Component, PropTypes } from 'react';
import classnames from 'classnames';
const baseClass = 'form-field';
class FormField extends Component {
static propTypes = {
children: PropTypes.element,
className: PropTypes.string,
error: PropTypes.string,
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
label: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
};
renderLabel = () => {
const { error, label, name } = this.props;
const labelWrapperClasses = classnames(
`${baseClass}__label`,
{ [`${baseClass}__label--error`]: error }
);
if (!label) {
return false;
}
return (
<label className={labelWrapperClasses} htmlFor={name}>
{error || label}
</label>
);
}
renderHint = () => {
const { hint } = this.props;
if (hint) {
return <span className={`${baseClass}__hint`}>{hint}</span>;
}
return false;
}
render () {
const { renderLabel, renderHint } = this;
const { children, className, type } = this.props;
const formFieldClass = classnames(baseClass, className, {
[`${baseClass}--${type}`]: type,
});
return (
<div className={formFieldClass}>
{renderLabel()}
{children}
{renderHint()}
</div>
);
}
}
export default FormField;
@@ -0,0 +1,33 @@
.form-field {
margin-bottom: $pad-small;
&__label {
font-size: 16px;
font-weight: $bold;
font-style: normal;
font-stretch: normal;
letter-spacing: -0.5px;
color: $text-dark;
display: block;
margin-bottom: 4px;
&--error {
color: $alert;
}
}
&__hint {
font-size: 14px;
font-weight: $normal;
line-height: 1.57;
letter-spacing: 1px;
color: $accent-text;
code {
color: $brand-light;
background-color: $accent-light;
padding: 2px;
font-family: 'SourceCodePro', $monospace;
}
}
}
@@ -0,0 +1 @@
export default from './FormField';
@@ -135,6 +135,7 @@ class InviteUserForm extends Component {
autofocus
error={errors.name}
name="name"
iconName="username"
onChange={onInputChange('name')}
placeholder="Name"
value={name}
@@ -142,6 +143,7 @@ class InviteUserForm extends Component {
<InputFieldWithIcon
error={errors.email}
name="email"
iconName="email"
onChange={onInputChange('email')}
placeholder="Email"
value={email}
@@ -1,33 +1,50 @@
import React, { Component, PropTypes } from 'react';
import classnames from 'classnames';
import { noop } from 'lodash';
import { noop, pick } from 'lodash';
import FormField from 'components/forms/FormField';
const baseClass = 'kolide-checkbox';
class InputField extends Component {
class Checkbox extends Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
name: PropTypes.string,
onChange: PropTypes.func,
error: PropTypes.string,
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
label: PropTypes.string,
value: PropTypes.bool,
};
static defaultProps = {
onChange: noop,
};
handleChange = () => {
const { onChange, value } = this.props;
return onChange(!value);
};
render () {
const { children, className, name, onChange } = this.props;
const { handleChange } = this;
const { children, className, name, value } = this.props;
const checkBoxClass = classnames(baseClass, className);
const formFieldProps = pick(this.props, ['hint', 'label', 'error', 'name']);
return (
<label htmlFor={name} className={checkBoxClass}>
<input type="checkbox" name={name} id={name} className={`${checkBoxClass}__input`} onChange={onChange} />
<span className={`${checkBoxClass}__tick`} />
{children}
</label>
<FormField {...formFieldProps} type="checkbox">
<label htmlFor={name} className={checkBoxClass}>
<input type="checkbox" name={name} id={name} className={`${checkBoxClass}__input`} onChange={handleChange} checked={value} />
<span className={`${checkBoxClass}__tick`} />
<div className={`${checkBoxClass}__label`}>{children}</div>
</label>
</FormField>
);
}
}
export default InputField;
export default Checkbox;
@@ -1,7 +1,11 @@
.kolide-checkbox {
@include clearfix;
position: relative;
&__input {
visibility: hidden;
margin: 0;
position: absolute;
&:checked + .kolide-checkbox__tick {
&::after {
@@ -42,4 +46,15 @@
visibility: visible;
}
}
&__label {
font-size: 13px;
font-weight: $normal;
line-height: 20px;
letter-spacing: 0.5px;
color: $text-medium;
display: inline-block;
padding-left: 15px;
float: left;
}
}
@@ -1,38 +1,55 @@
import React, { Component, PropTypes } from 'react';
import Select from 'react-select';
import { noop } from 'lodash';
import { noop, pick } from 'lodash';
import dropdownOptionInterface from 'interfaces/dropdownOption';
import FormField from 'components/forms/FormField';
const baseClass = 'input-dropdown';
class Dropdown extends Component {
static propTypes = {
options: PropTypes.arrayOf(dropdownOptionInterface).isRequired,
onSelect: PropTypes.func,
onChange: PropTypes.func,
className: PropTypes.string,
error: PropTypes.string,
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
label: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.string,
clearable: PropTypes.bool,
};
static defaultProps = {
onSelect: noop,
onChange: noop,
clearable: false,
placeholder: 'Select One...',
};
handleChange = ({ value }) => {
const { onChange } = this.props;
return onChange(value);
};
render () {
const { options, className, placeholder, value, clearable, onSelect } = this.props;
const { handleChange } = this;
const { options, className, placeholder, value, clearable } = this.props;
const formFieldProps = pick(this.props, ['hint', 'label', 'error', 'name']);
return (
<Select
className={className}
name="targets"
options={options}
onChange={onSelect}
placeholder={placeholder}
value={value}
clearable={clearable}
/>
<FormField {...formFieldProps} type="dropdown">
<Select
className={`${baseClass}__select ${className}`}
name="targets"
options={options}
onChange={handleChange}
placeholder={placeholder}
value={value}
clearable={clearable}
/>
</FormField>
);
}
}
@@ -87,3 +87,24 @@
}
}
}
.input-dropdown {
&__wrapper {
margin-bottom: $pad-base;
}
&__hint {
font-size: 14px;
font-weight: $normal;
line-height: 1.57;
letter-spacing: 1px;
color: $accent-text;
code {
color: $brand-light;
background-color: $accent-light;
padding: 2px;
font-family: 'SourceCodePro', $monospace;
}
}
}
@@ -1,5 +1,8 @@
import React, { Component, PropTypes } from 'react';
import classnames from 'classnames';
import { pick } from 'lodash';
import FormField from 'components/forms/FormField';
const baseClass = 'input-field';
@@ -7,6 +10,7 @@ class InputField extends Component {
static propTypes = {
autofocus: PropTypes.bool,
error: PropTypes.string,
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
inputClassName: PropTypes.string, // eslint-disable-line react/forbid-prop-types
inputWrapperClass: PropTypes.string,
inputOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types
@@ -49,43 +53,21 @@ class InputField extends Component {
return onChange(value);
}
renderLabel = () => {
const { error, label, labelClassName, name } = this.props;
const labelWrapperClasses = classnames(
`${baseClass}__label`,
labelClassName,
{ [`${baseClass}__label--error`]: error }
);
if (!label) {
return false;
}
return (
<label
className={labelWrapperClasses}
htmlFor={name}
>
{error || label}
</label>
);
}
render () {
const { error, inputClassName, inputOptions, inputWrapperClass, name, placeholder, type, value } = this.props;
const { onInputChange, renderLabel } = this;
const { onInputChange } = this;
const shouldShowPasswordClass = type === 'password';
const inputClasses = classnames(baseClass, inputClassName, {
[`${baseClass}--password`]: shouldShowPasswordClass,
[`${baseClass}--error`]: error,
[`${baseClass}__textarea`]: type === 'textarea',
});
const inputWrapperClasses = classnames(`${baseClass}__wrapper`, inputWrapperClass);
const formFieldProps = pick(this.props, ['hint', 'label', 'error', 'name']);
if (type === 'textarea') {
return (
<div className={inputWrapperClasses}>
{renderLabel()}
<FormField {...formFieldProps} type="textarea" className={inputWrapperClass}>
<textarea
name={name}
onChange={onInputChange}
@@ -96,13 +78,12 @@ class InputField extends Component {
{...inputOptions}
value={value}
/>
</div>
</FormField>
);
}
return (
<div className={inputWrapperClasses}>
{renderLabel()}
<FormField {...formFieldProps} type="input" className={inputWrapperClass}>
<input
name={name}
onChange={onInputChange}
@@ -113,7 +94,7 @@ class InputField extends Component {
{...inputOptions}
value={value}
/>
</div>
</FormField>
);
}
}
@@ -5,7 +5,7 @@
background-color: $white;
border: solid 1px $accent-medium;
font-size: 16px;
padding: $pad-xsmall $pad-base;
padding: $pad-xsmall 12px;
color: $text-dark;
font-family: 'Oxygen', sans-serif;
box-sizing: border-box;
@@ -18,6 +18,7 @@
&:focus {
outline: none;
box-shadow: inset 0 0 8px 0 rgba($black, 0.1);
border-color: $link-light;
border-bottom-color: $brand;
}
@@ -60,4 +61,19 @@
&__wrapper {
margin-bottom: $pad-base;
}
&__hint {
font-size: 14px;
font-weight: $normal;
line-height: 1.57;
letter-spacing: 1px;
color: $accent-text;
code {
color: $brand-light;
background-color: $accent-light;
padding: 2px;
font-family: 'SourceCodePro', $monospace;
}
}
}
@@ -0,0 +1,50 @@
import React, { PropTypes } from 'react';
import classnames from 'classnames';
import { pick } from 'lodash';
import FormField from 'components/forms/FormField';
const Slider = ({ onChange, value, inactiveText = 'Off', activeText = 'On' }) => {
const baseClass = 'kolide-slider';
const sliderBtnClass = classnames(
baseClass,
{ [`${baseClass}--active`]: value }
);
const sliderDotClass = classnames(
`${baseClass}__dot`,
{ [`${baseClass}__dot--active`]: value }
);
const handleClick = (evt) => {
evt.preventDefault();
return onChange(!value);
};
const formFieldProps = pick(this.props, ['hint', 'label', 'error', 'name']);
return (
<FormField {...formFieldProps} type="slider">
<span className={`${baseClass}__label ${baseClass}__label--inactive`}>{inactiveText}</span>
<button className={`button button--unstyled ${sliderBtnClass}`} onClick={handleClick}>
<div className={sliderDotClass} />
</button>
<span className={`${baseClass}__label ${baseClass}__label--active`}>{activeText}</span>
</FormField>
);
};
Slider.propTypes = {
value: PropTypes.bool,
onChange: PropTypes.func,
inactiveText: PropTypes.string,
activeText: PropTypes.string,
error: PropTypes.string,
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
label: PropTypes.string,
name: PropTypes.string,
};
export default Slider;
@@ -0,0 +1,60 @@
.kolide-slider {
@include transition(background-color 150ms ease-in-out);
background-color: $text-medium;
border-radius: 12px;
border: 1px solid #eaeaea;
cursor: pointer;
display: inline-block;
height: 22px;
min-width: 40px;
position: relative;
width: 40px;
box-shadow: inset 0 1px 6px 0 rgba(0, 0, 0, 0.2);
&:hover {
background-color: $text-medium;
box-shadow: inset 0 1px 6px 0 rgba(0, 0, 0, 0.2);
}
&--active {
background-color: $brand-light;
&:hover {
background-color: $brand-light;
}
}
&__wrapper {
margin-bottom: $pad-base;
}
&__dot {
@include transition(left 150ms ease-in-out);
@include size(14px);
@include position(absolute, 0 null null 5px);
margin-top: 3px;
border-radius: 50%;
background-color: $white;
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.25);
&--active {
left: 21px;
}
}
&__label {
font-size: 18px;
font-weight: $light;
letter-spacing: 1.3px;
text-align: left;
vertical-align: text-bottom;
text-transform: uppercase;
margin: 0 10px;
color: $text-light;
&--active {
color: $brand;
}
}
}
@@ -226,7 +226,7 @@ class QueryForm extends Component {
return (
<Dropdown
options={platformOptions}
onSelect={onFieldChange('platform')}
onChange={onFieldChange('platform')}
value={platform.value}
/>
);
@@ -9,20 +9,25 @@
}
&__select-targets {
font-size: $base;
float: left;
}
&__target-label {
margin: 0;
font-size: $base;
font-weight: $light;
font-size: 16px;
font-weight: $bold;
font-style: normal;
font-stretch: normal;
letter-spacing: -0.5px;
color: $text-dark;
margin-bottom: $pad-xsmall;
display: block;
margin-bottom: 4px;
text-align: right;
}
&__targets-count {
font-size: $small;
margin-left: $xsmall;
color: $brand;
}
&__text-editor-wrapper {
@@ -129,7 +129,7 @@ class QuerySidePanel extends Component {
<Dropdown
options={tableNames}
value={selectedOsqueryTable.name}
onSelect={onSelectTable}
onChange={onSelectTable}
placeholder="Choose Table..."
/>
);
@@ -91,7 +91,7 @@ class UserBlock extends Component {
<Dropdown
options={userActionOptions}
placeholder="Actions..."
onSelect={onUserActionSelect}
onChange={onUserActionSelect}
className={invite ? 'revoke-invite' : ''}
/>
);
@@ -2,16 +2,16 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { first, isEqual, size } from 'lodash';
import entityGetter from '../../../redux/utilities/entityGetter';
import Button from '../../../components/buttons/Button';
import inviteActions from '../../../redux/nodes/entities/invites/actions';
import inviteInterface from '../../../interfaces/invite';
import InviteUserForm from '../../../components/forms/InviteUserForm';
import Modal from '../../../components/modals/Modal';
import userActions from '../../../redux/nodes/entities/users/actions';
import entityGetter from 'redux/utilities/entityGetter';
import Button from 'components/buttons/Button';
import inviteActions from 'redux/nodes/entities/invites/actions';
import inviteInterface from 'interfaces/invite';
import InviteUserForm from 'components/forms/InviteUserForm';
import Modal from 'components/modals/Modal';
import userActions from 'redux/nodes/entities/users/actions';
import userInterface from 'interfaces/user';
import { renderFlash } from 'redux/nodes/notifications/actions';
import UserBlock from './UserBlock';
import userInterface from '../../../interfaces/user';
import { renderFlash } from '../../../redux/nodes/notifications/actions';
class UserManagementPage extends Component {
static propTypes = {
+1 -1
View File
@@ -18,7 +18,7 @@ export class HomePage extends Component {
const baseClass = 'home-page';
return (
<div className={baseClass}>
<div className={`${baseClass} body-wrap`}>
{user && <Avatar size="small" className={`${baseClass}__avatar`} user={user} />}
<span>You are successfully logged in! </span>
{user && <Link to={LOGOUT}>Logout</Link>}
@@ -156,11 +156,10 @@ export class ManageHostsPage extends Component {
return false;
}
onToggleDisplay = () => {
const { dispatch, display } = this.props;
const newDisplay = display === 'Grid' ? 'List' : 'Grid';
onToggleDisplay = (val) => {
const { dispatch } = this.props;
dispatch(setDisplay(newDisplay));
dispatch(setDisplay(val));
return false;
}
@@ -175,10 +174,10 @@ export class ManageHostsPage extends Component {
const { count, description, display_text: displayText, query } = selectedLabel;
const { onToggleDisplay } = this;
const buttonOptions = {
aIcon: 'grid-select',
aText: 'Grid',
bIcon: 'list-select',
bText: 'List',
rightIcon: 'grid-select',
rightText: 'Grid',
leftIcon: 'list-select',
leftText: 'List',
};
return (
@@ -214,8 +213,7 @@ export class ManageHostsPage extends Component {
<div className={`${baseClass}__topper`}>
<p className={`${baseClass}__host-count`}>{count} Hosts Total</p>
<Rocker
handleChange={onToggleDisplay}
name="host-display-toggle"
onChange={onToggleDisplay}
options={buttonOptions}
value={display}
/>
@@ -91,7 +91,7 @@ describe('ManageHostsPage - component', () => {
const ownProps = { location: {}, params: {} };
const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore });
const page = mount(component);
const button = page.find('Rocker').find('input');
const button = page.find('Rocker').find('button');
const toggleDisplayAction = {
type: 'SET_DISPLAY',
payload: {
@@ -99,7 +99,7 @@ describe('ManageHostsPage - component', () => {
},
};
button.simulate('change');
button.simulate('click');
expect(mockStore.getActions()).toInclude(toggleDisplayAction);
});
+1 -1
View File
@@ -76,7 +76,7 @@ button {
font-family: 'Oxygen', sans-serif;
&:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset;
-webkit-box-shadow: 0 0 0 1000px #fff inset;
-webkit-text-fill-color: $text-dark !important;
}
}