Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create <TextInput> row component #41

Merged
merged 4 commits into from
May 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .storybook/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import propsTableAddon from './propsTable-addon';
setOptions({
name: 'iCHEF gypcrete',
url: 'https://github.com/iCHEF/gypcrete',
showDownPanel: false
showDownPanel: true,
});

setDefaults({
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Add `<SwitchIcon>` to be used as a 64x32 icon.
- Add `<Switch>` row component.
- Add `<EditableText>` visual element which has an `<input type="text" />` inside.
- Add `<TextInput>` which contains `<EditableText>` and ignores normal text props like `basic`, `tag` and `aside`.

### Changed
- `<IconLayout>` can now be tooltip-free by turning it off.
Expand Down
8 changes: 6 additions & 2 deletions examples/EditableText/BasicUsage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { action } from '@kadira/storybook';

import EditableText from 'src/EditableText';
import DebugBox from '../DebugBox';
Expand All @@ -11,11 +12,14 @@ function BasicUsage() {
</DebugBox>

<DebugBox>
<EditableText value="Controlled input" />
<EditableText
value="Controlled input"
onChange={action('change')} />
</DebugBox>

<DebugBox>
<EditableText defaultValue="Uncontrolled input" />
<EditableText
defaultValue="Uncontrolled input" />
</DebugBox>
</div>
);
Expand Down
27 changes: 27 additions & 0 deletions examples/TextInput/BasicUsage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { action } from '@kadira/storybook';

import TextInput from 'src/TextInput';
import DebugBox from '../DebugBox';

function BasicUsage() {
return (
<div>
<DebugBox>
<TextInput />
</DebugBox>

<DebugBox>
<TextInput
value="Controlled input"
onChange={action('change')} />
</DebugBox>

<DebugBox>
<TextInput defaultValue="Uncontrolled input" />
</DebugBox>
</div>
);
}

export default BasicUsage;
32 changes: 32 additions & 0 deletions examples/TextInput/WithStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { action } from '@kadira/storybook';

import TextInput from 'src/TextInput';
import DebugBox from '../DebugBox';

function WithStatus() {
return (
<div>
<DebugBox>
<TextInput status="loading" />
</DebugBox>

<DebugBox>
<TextInput
defaultValue="Living room TV"
status="success"
statusOptions={{ autohide: false }} />
</DebugBox>

<DebugBox>
<TextInput
value="Kitchen Printer"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here also.

主要是避免 console 會噴奇怪的 warning XD

status="error"
errorMsg="Network failure"
onChange={action('change')} />
</DebugBox>
</div>
);
}

export default WithStatus;
14 changes: 14 additions & 0 deletions examples/TextInput/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { storiesOf } from '@kadira/storybook';

// For props table
import TextInput, { PureTextInput } from 'src/TextInput';

import BasicUsage from './BasicUsage';
import WithStatus from './WithStatus';

storiesOf('TextInput', module)
.addWithInfo('Basic usage', BasicUsage)
.addWithInfo('With status', WithStatus)
// Props table
.addPropsTable(() => <TextInput />, [PureTextInput]);
7 changes: 5 additions & 2 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src": ["src"]
}
},
"lib": [
"es2017"
]
},
"typeAcquisition": {
"include": [
Expand Down
75 changes: 75 additions & 0 deletions src/TextInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import prefixClass from './utils/prefixClass';
import rowComp, { getTextLayoutProps, ROW_COMP_ALIGN } from './mixins/rowComp';
import './styles/TextLabel.scss';

import EditableText, { PureEditableText } from './EditableText';

export const COMPONENT_NAME = prefixClass('text-input');

const filterOutStatusProps = (props) => {
const {
statusIcon,
errorMsg,
...filteredProps
} = props;

return filteredProps;
};

function TextInput(props, { align }) {
const {
// <EditableText> props
value,
defaultValue,
placeholder,
disabled,
onFocus,
onBlur,
onChange,
input,
// React props
className,
...otherProps
} = props;

const editableTextProps = {
value,
defaultValue,
placeholder,
disabled,
onFocus,
onBlur,
onChange,
input,
};

const rootClassName = classNames(className, COMPONENT_NAME);
const textLayoutProps = getTextLayoutProps(align, false);

return (
<div className={rootClassName} {...otherProps}>
<EditableText
{...editableTextProps}
{...textLayoutProps} />
</div>
);
}

TextInput.propTypes =
filterOutStatusProps(PureEditableText.propTypes);

TextInput.defaultProps =
filterOutStatusProps(PureEditableText.defaultProps);

TextInput.contextTypes = {
align: PropTypes.oneOf(Object.values(ROW_COMP_ALIGN)),
};

// export for tests
export { TextInput as PureTextInput };

export default rowComp({ defaultAlign: ROW_COMP_ALIGN.REVERSE })(TextInput);
85 changes: 85 additions & 0 deletions src/__tests__/TextInput.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { shallow } from 'enzyme';

import { getTextLayoutProps, ROW_COMP_ALIGN } from '../mixins/rowComp';
import EditableText from '../EditableText';
import TextInput, { PureTextInput } from '../TextInput';

describe('rowComp(TextInput)', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
const element = <TextInput />;

ReactDOM.render(element, div);
});

it('reverse-aligns by default', () => {
const wrapper = shallow(<div><TextInput /></div>);

expect(wrapper.childAt(0).prop('align')).toBe('reverse');
});
});

describe('pure <TextInput>', () => {
it('renders with a proper-named wrapper', () => {
const wrapper = shallow(<PureTextInput />);

expect(wrapper.hasClass('gyp-text-input')).toBeTruthy();
});

it('renders <EditableText> and ignores chidlren from parent mixin', () => {
const wrapper = shallow(
<PureTextInput>
<span data-foo />
Bar content
</PureTextInput>
);

expect(wrapper.text()).toBe('<withStatus(EditableText) />');
expect(wrapper.containsMatchingElement(<span data-foo />)).toBeFalsy();
});

it('renders <EditableText> with layout props the same as rowComp()', () => {
const wrapper = shallow(<PureTextInput />, { context: { align: 'left' } });

Object.values(ROW_COMP_ALIGN).forEach((alignment) => {
const layoutProps = getTextLayoutProps(alignment, false);
wrapper.setContext({ align: alignment });

expect(wrapper.find(EditableText).prop('align')).toBe(layoutProps.align);
expect(wrapper.find(EditableText).prop('noGrow')).toBe(layoutProps.noGrow);
});
});

it('passes only white-listed props to <EditableText>', () => {
const handleFocus = jest.fn();
const handleBlur = jest.fn();
const handleChange = jest.fn();

const whitelistedProps = {
value: 'Foo',
defaultValue: 'Bar',
placeholder: 'john.appleseed@example.com',
disabled: false,
onFocus: handleFocus,
onBlur: handleBlur,
onChange: handleChange,
};

const wrapper = shallow(
<PureTextInput
{...whitelistedProps}
input={{ id: 'foo-dom-id' }}
data-unsupported-prop />
);
const textWrapper = wrapper.childAt(0);

Object.entries(whitelistedProps).forEach(([key, value]) => {
expect(textWrapper.prop(key)).toBe(value);
});

expect(textWrapper.prop('input')).toEqual({ id: 'foo-dom-id' });
expect(textWrapper.prop('data-unsupported-prop')).toBeUndefined();
});
});
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import IconButton from './IconButton';
import Checkbox from './Checkbox';
import IconCheckbox from './IconCheckbox';
import Switch from './Switch';
import TextInput from './TextInput';
import SearchInput from './SearchInput';

export {
Expand All @@ -46,5 +47,6 @@ export {
Checkbox,
IconCheckbox,
Switch,
TextInput,
SearchInput,
};
27 changes: 21 additions & 6 deletions src/mixins/rowComp.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ function determineTextAlign(compAlign, hasIcon) {
}
}

/**
* Get 'align' and 'noGrow' layout props for <Text>
* as it would receive if rendered by rowComp().
*
* @param {String} compAlign
* @param {Bool} hasIcon
* @return {Object} layoutProps
*/
export function getTextLayoutProps(compAlign, hasIcon) {
return {
align: determineTextAlign(compAlign, hasIcon),
noGrow: compAlign === CENTER,
};
}

const rowComp = ({
defaultMinified = false,
defaultAlign = 'left'
Expand Down Expand Up @@ -132,16 +147,17 @@ const rowComp = ({
};

static childContextTypes = {
align: RowComp.propTypes.align,
...statusPropTypes,
// status,
// statusOptions,
// errorMsg,
};

getChildContext() {
const { status, statusOptions, errorMsg } = this.props;
const { align, status, statusOptions, errorMsg } = this.props;

return { status, statusOptions, errorMsg };
return { align, status, statusOptions, errorMsg };
}

renderContent() {
Expand All @@ -154,15 +170,14 @@ const rowComp = ({
} = this.props;

const textProps = { basic, aside, tag };
const textAlign = determineTextAlign(align, !!icon);
const textLayoutProps = getTextLayoutProps(align, !!icon);

return [
icon && <Icon key="comp-icon" type={icon} />,
<Text
key="comp-text"
align={textAlign}
noGrow={align === CENTER}
{...textProps} />
{...textProps}
{...textLayoutProps} />
];
}

Expand Down