From 30180f95a8f78dc578c4c9244a67ef24341d603b Mon Sep 17 00:00:00 2001 From: Alexandru Buliga Date: Mon, 8 Oct 2018 17:20:08 +0200 Subject: [PATCH 1/4] feat(slot): create generic slot component --- CHANGELOG.md | 1 + .../Input/Types/InputExample.shorthand.tsx | 2 +- .../InputExampleClearable.shorthand.tsx | 4 +- .../InputExampleFluid.shorthand.tsx | 6 +- ...utExampleInlineIconClearable.shorthand.tsx | 11 + .../InputExampleWrapper.shorthand.tsx | 24 ++ .../components/Input/Variations/index.tsx | 10 + .../Types/RadioGroupExample.shorthand.tsx | 3 +- .../RadioGroupVerticalExample.shorthand.tsx | 3 +- .../prototypes/chatPane/chatPaneHeader.tsx | 3 +- .../prototypes/chatPane/composeMessage.tsx | 1 + src/components/Input/Input.tsx | 205 ++++++++---------- src/components/Input/InputBase.tsx | 109 ++++++++++ src/index.ts | 21 +- src/themes/teams/componentStyles.ts | 1 + src/themes/teams/componentVariables.ts | 1 + .../teams/components/Input/inputBaseStyles.ts | 22 ++ .../components/Input/inputBaseVariables.ts | 27 +++ .../teams/components/Input/inputStyles.ts | 54 ++--- .../teams/components/Input/inputVariables.ts | 26 +-- .../specs/components/Input/InputBase-test.tsx | 23 ++ 21 files changed, 365 insertions(+), 192 deletions(-) create mode 100644 docs/src/examples/components/Input/Variations/InputExampleInlineIconClearable.shorthand.tsx create mode 100644 docs/src/examples/components/Input/Variations/InputExampleWrapper.shorthand.tsx create mode 100644 src/components/Input/InputBase.tsx create mode 100644 src/themes/teams/components/Input/inputBaseStyles.ts create mode 100644 src/themes/teams/components/Input/inputBaseVariables.ts create mode 100644 test/specs/components/Input/InputBase-test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 2acb5718fc..43f33124ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Features - Add `target` prop to `Popup` @kuzhelog ([#356](https://github.com/stardust-ui/react/pull/356)) +- Add new `Input` component with `wrapper` prop @Bugaa92 ([#326](https://github.com/stardust-ui/react/pull/326)) ## [v0.9.1](https://github.com/stardust-ui/react/tree/v0.9.1) (2018-10-11) diff --git a/docs/src/examples/components/Input/Types/InputExample.shorthand.tsx b/docs/src/examples/components/Input/Types/InputExample.shorthand.tsx index 9b812e77ee..f823f513c5 100644 --- a/docs/src/examples/components/Input/Types/InputExample.shorthand.tsx +++ b/docs/src/examples/components/Input/Types/InputExample.shorthand.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Input } from '@stardust-ui/react' -const InputExample = () => +const InputExample = () => export default InputExample diff --git a/docs/src/examples/components/Input/Variations/InputExampleClearable.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleClearable.shorthand.tsx index d8a61b7f16..ee6267fadf 100644 --- a/docs/src/examples/components/Input/Variations/InputExampleClearable.shorthand.tsx +++ b/docs/src/examples/components/Input/Variations/InputExampleClearable.shorthand.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Input } from '@stardust-ui/react' -const InputExampleClearableShorthand = () => +const InputExampleClearable = () => -export default InputExampleClearableShorthand +export default InputExampleClearable diff --git a/docs/src/examples/components/Input/Variations/InputExampleFluid.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleFluid.shorthand.tsx index 2c1c9bcd0b..e830db1dcd 100644 --- a/docs/src/examples/components/Input/Variations/InputExampleFluid.shorthand.tsx +++ b/docs/src/examples/components/Input/Variations/InputExampleFluid.shorthand.tsx @@ -1,6 +1,8 @@ import React from 'react' import { Input } from '@stardust-ui/react' -const InputExample = () => +const InputExampleFluid = () => ( + +) -export default InputExample +export default InputExampleFluid diff --git a/docs/src/examples/components/Input/Variations/InputExampleInlineIconClearable.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleInlineIconClearable.shorthand.tsx new file mode 100644 index 0000000000..55e7e29704 --- /dev/null +++ b/docs/src/examples/components/Input/Variations/InputExampleInlineIconClearable.shorthand.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Input } from '@stardust-ui/react' + +const InputExampleInline = () => ( +
+ Some text inline with the and + more text. +
+) + +export default InputExampleInline diff --git a/docs/src/examples/components/Input/Variations/InputExampleWrapper.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleWrapper.shorthand.tsx new file mode 100644 index 0000000000..7d382de281 --- /dev/null +++ b/docs/src/examples/components/Input/Variations/InputExampleWrapper.shorthand.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { Grid, Input, Text } from '@stardust-ui/react' + +const wrapperStyles = { padding: '5px', background: 'red' } +const InputExampleWrapper = () => ( + + + + + + + + + + + + } /> + + + } /> + +) + +export default InputExampleWrapper diff --git a/docs/src/examples/components/Input/Variations/index.tsx b/docs/src/examples/components/Input/Variations/index.tsx index 5b2f69527d..1fa4b73a6c 100644 --- a/docs/src/examples/components/Input/Variations/index.tsx +++ b/docs/src/examples/components/Input/Variations/index.tsx @@ -29,6 +29,16 @@ const Variations = () => ( description="An input can be used inline with text." examplePath="components/Input/Variations/InputExampleInline" /> + + ) diff --git a/docs/src/examples/components/RadioGroup/Types/RadioGroupExample.shorthand.tsx b/docs/src/examples/components/RadioGroup/Types/RadioGroupExample.shorthand.tsx index ff1580da8e..12e3254edb 100644 --- a/docs/src/examples/components/RadioGroup/Types/RadioGroupExample.shorthand.tsx +++ b/docs/src/examples/components/RadioGroup/Types/RadioGroupExample.shorthand.tsx @@ -34,7 +34,8 @@ class RadioGroupVerticalExample extends React.Component { key: 'Custom', label: ( - Choose your own + Choose your own{' '} + ), value: 'custom', diff --git a/docs/src/examples/components/RadioGroup/Types/RadioGroupVerticalExample.shorthand.tsx b/docs/src/examples/components/RadioGroup/Types/RadioGroupVerticalExample.shorthand.tsx index c771af972e..a3a2877226 100644 --- a/docs/src/examples/components/RadioGroup/Types/RadioGroupVerticalExample.shorthand.tsx +++ b/docs/src/examples/components/RadioGroup/Types/RadioGroupVerticalExample.shorthand.tsx @@ -35,7 +35,8 @@ class RadioGroupVerticalExample extends React.Component { key: 'Custom', label: ( - Choose your own + Choose your own{' '} + ), value: 'custom', diff --git a/docs/src/prototypes/chatPane/chatPaneHeader.tsx b/docs/src/prototypes/chatPane/chatPaneHeader.tsx index ca3f1ef893..3a0e94ddb0 100644 --- a/docs/src/prototypes/chatPane/chatPaneHeader.tsx +++ b/docs/src/prototypes/chatPane/chatPaneHeader.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { Avatar, Button, Divider, Icon, Layout, Segment, Text } from '@stardust-ui/react' -import { pxToRem } from 'src/lib' import { IChat } from './services' import { Sizes } from 'src/lib/enums' @@ -87,7 +86,7 @@ class ChatPaneHeader extends React.PureComponent { styles={{ fontWeight: 100, ...(!index && { marginRight: '1.6rem' }), - marginTop: pxToRem(8), + marginTop: '8px', }} variables={siteVars => ({ color: siteVars.gray04 })} /> diff --git a/docs/src/prototypes/chatPane/composeMessage.tsx b/docs/src/prototypes/chatPane/composeMessage.tsx index 248ebdc3b7..036f1cf543 100644 --- a/docs/src/prototypes/chatPane/composeMessage.tsx +++ b/docs/src/prototypes/chatPane/composeMessage.tsx @@ -21,6 +21,7 @@ class ComposeMessage extends React.Component { ({ backgroundColor: siteVars.white })} /> ) diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 0f91ded013..72c5b51eeb 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -1,41 +1,26 @@ -import * as PropTypes from 'prop-types' import * as React from 'react' +import * as PropTypes from 'prop-types' import * as _ from 'lodash' -import { - AutoControlledComponent, - createHTMLInput, - customPropTypes, - getUnhandledProps, - partitionHTMLProps, -} from '../../lib' +import { AutoControlledComponent, customPropTypes } from '../../lib' +import { Extendable, ShorthandValue, ShorthandRenderFunction } from '../../../types/utils' +import InputBase, { IInputBaseProps } from './InputBase' import Icon from '../Icon' -import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme' -import { - ComponentEventHandler, - Extendable, - ReactChildren, - ShorthandRenderFunction, - ShorthandValue, -} from '../../../types/utils' - -export interface IInputProps { - as?: any - children?: ReactChildren - className?: string +import Slot from '../Slot' +import Ref from '../Ref' + +export interface IInputProps extends IInputBaseProps { clearable?: boolean - defaultValue?: string | number - fluid?: boolean icon?: ShorthandValue inline?: boolean - input?: ShorthandValue - onChange?: ComponentEventHandler - value?: string | number - type?: string renderIcon?: ShorthandRenderFunction renderInput?: ShorthandRenderFunction - styles?: ComponentPartStyle - variables?: ComponentVariablesInput + renderWrapper?: ShorthandRenderFunction + wrapper?: ShorthandValue +} + +export interface IInputState { + value?: React.ReactText } /** @@ -43,12 +28,12 @@ export interface IInputProps { * @accessibility * For good screen reader experience set aria-label or aria-labelledby attribute for input. * - * * Other considerations: * - if input is search, then use "role='search'" - * */ -class Input extends AutoControlledComponent, any> { +class Input extends AutoControlledComponent, IInputState> { + private inputRef: HTMLInputElement + static className = 'ui-input' static displayName = 'Input' @@ -83,6 +68,9 @@ class Input extends AutoControlledComponent, any> { */ onChange: PropTypes.func, + /** The HTML input placeholder. */ + placeholder: PropTypes.string, + /** The HTML input type. */ type: PropTypes.string, @@ -104,6 +92,15 @@ class Input extends AutoControlledComponent, any> { */ renderInput: PropTypes.func, + /** + * A custom render function the wrapper slot. + * + * @param { React.ReactType } Component - The computed component for this slot. + * @param { object } props - The computed props for this slot. + * @param { ReactNode | ReactNodeArray } children - The computed children for this slot. + */ + renderWrapper: PropTypes.func, + /** Additional CSS styles to apply to the component instance. */ styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), @@ -112,112 +109,94 @@ class Input extends AutoControlledComponent, any> { /** Override for theme site variables to allow modifications of component styling via themes. */ variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + + /** Shorthand for the wrapper component */ + wrapper: customPropTypes.itemShorthand, } static defaultProps = { - as: 'div', + as: 'input', type: 'text', + wrapper: 'div', } static autoControlledProps = ['value'] - inputRef: any - - state: any = { value: this.props.value || this.props.defaultValue || '' } - - handleChange = e => { - const value = _.get(e, 'target.value') + state = { value: this.props.value || this.props.defaultValue || '' } + + renderComponent({ classes, styles, variables }) { + const { + clearable, + icon, + inline, + renderIcon, + renderInput, + renderWrapper, + wrapper, + ...rest + } = this.props + const { value } = this.state - _.invoke(this.props, 'onChange', e, { ...this.props, value }) + const inputComponent = InputBase.create(this.props.type, { + defaultProps: { className: classes.input, ...rest, value }, + overrideProps: { onChange: this.handleChange }, + render: renderInput, + }) - this.trySetState({ value }) + return wrapper + ? Slot.create(wrapper, { + defaultProps: { className: classes.root }, + overrideProps: { + children: ( + <> + {inputComponent} + {Icon.create(this.computeIcon(), { + defaultProps: { + styles: styles.icon, + variables: variables.icon, + }, + overrideProps: this.handleIconOverrides, + render: renderIcon, + })} + + ), + }, + render: renderWrapper, + }) + : inputComponent } - handleChildOverrides = (child, defaultProps) => ({ - ...defaultProps, - ...child.props, - }) + private handleInputRef = (c: HTMLInputElement) => (this.inputRef = c) - handleInputRef = c => (this.inputRef = c) + private handleIconOverrides = predefinedProps => ({ + onClick: (e: React.SyntheticEvent) => { + this.handleOnClear() + this.inputRef.focus() + _.invoke(predefinedProps, 'onClick', e, this.props) + }, + ...(predefinedProps.onClick && { tabIndex: '0' }), + }) - handleOnClear = e => { - const { clearable } = this.props + private handleChange = (e: React.SyntheticEvent, { value }: { value: React.ReactText }) => { + _.invoke(this.props, 'onChange', e, { ...this.props, value }) + this.trySetState({ value }) + } - if (clearable) { + private handleOnClear = () => { + if (this.props.clearable) { this.trySetState({ value: '' }) } } - partitionProps = () => { - const { type } = this.props - const { value } = this.state - - const unhandled = getUnhandledProps(Input, this.props) - const [htmlInputProps, rest] = partitionHTMLProps(unhandled) - - return [ - { - ...htmlInputProps, - onChange: this.handleChange, - type, - value: value || '', - }, - rest, - ] - } - - computeIcon = () => { + private computeIcon = (): ShorthandValue => { const { clearable, icon } = this.props const { value } = this.state - if (clearable && value.length !== 0) { + if (clearable && (value as string).length !== 0) { return 'close' } - if (!_.isNil(icon)) return icon - - return null - } - - handleIconOverrides = predefinedProps => { - return { - onClick: e => { - this.handleOnClear(e) - - this.inputRef.focus() - _.invoke(predefinedProps, 'onClick', e, this.props) - }, - ...(predefinedProps.onClick && { tabIndex: '0' }), - } - } - - renderComponent({ ElementType, classes, styles, variables }) { - const { renderIcon, renderInput, type } = this.props - const [htmlInputProps, restProps] = this.partitionProps() - - const inputClasses = classes.input - - return ( - - {createHTMLInput(type, { - defaultProps: htmlInputProps, - overrideProps: { - className: inputClasses, - ref: this.handleInputRef, - }, - render: renderInput, - })} - {this.computeIcon() && - Icon.create(this.computeIcon(), { - defaultProps: { - styles: styles.icon, - variables: variables.icon, - }, - overrideProps: this.handleIconOverrides, - render: renderIcon, - })} - - ) + return icon || null } } diff --git a/src/components/Input/InputBase.tsx b/src/components/Input/InputBase.tsx new file mode 100644 index 0000000000..c302d4fb75 --- /dev/null +++ b/src/components/Input/InputBase.tsx @@ -0,0 +1,109 @@ +import * as PropTypes from 'prop-types' +import * as React from 'react' +import * as _ from 'lodash' + +import { AutoControlledComponent, customPropTypes, createShorthandFactory } from '../../lib' +import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme' +import { Extendable, ComponentEventHandler } from '../../../types/utils' + +export interface IInputBaseProps { + as?: any + className?: string + defaultValue?: React.ReactText + fluid?: boolean + onChange?: ComponentEventHandler + placeholder?: string + type?: string + styles?: ComponentPartStyle + value?: React.ReactText + variables?: ComponentVariablesInput +} + +export interface IInputState { + value?: React.ReactText +} + +/** + * A basic Input + * @accessibility + * For good screen reader experience set aria-label or aria-labelledby attribute for input. + */ +class InputBase extends AutoControlledComponent, IInputState> { + static create: Function + + static className = 'ui-input__base' + + static displayName = 'InputBase' + + static propTypes = { + /** An element type to render as (string or function). */ + as: customPropTypes.as, + + /** Additional CSS class name(s) to apply. */ + className: PropTypes.string, + + /** The default value of the input. */ + defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + /** An input can take the width of its container. */ + fluid: PropTypes.bool, + + /** + * Called on change. + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props and proposed value. + */ + onChange: PropTypes.func, + + /** The HTML input placeholder. */ + placeholder: PropTypes.string, + + /** The HTML input type. */ + type: PropTypes.string, + + /** Additional CSS styles to apply to the component instance. */ + styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + + /** The value of the input. */ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + /** Override for theme site variables to allow modifications of component styling via themes. */ + variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + } + + static defaultProps = { + as: 'input', + type: 'text', + } + + static autoControlledProps = ['value'] + + state = { value: this.props.value || this.props.defaultValue || '' } + + renderComponent({ ElementType, classes, rest }) { + const { placeholder, type } = this.props + + return ( + + ) + } + + private handleChange = (e: React.SyntheticEvent) => { + const value = _.get(e, 'target.value') + + _.invoke(this.props, 'onChange', e, { ...this.props, value }) + + this.trySetState({ value }) + } +} + +InputBase.create = createShorthandFactory(InputBase, type => ({ type })) + +export default InputBase diff --git a/src/index.ts b/src/index.ts index 28c50034b1..de86865a17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,29 +5,23 @@ export { themes } export { default as Accordion } from './components/Accordion' export { default as Attachment } from './components/Attachment' export { default as Avatar } from './components/Avatar' -export { default as Button } from './components/Button' -export { ButtonGroup } from './components/Button' +export { default as Button, ButtonGroup } from './components/Button' export { default as Chat } from './components/Chat' -export { ChatItem } from './components/Chat' -export { ChatMessage } from './components/Chat' +export { ChatItem, ChatMessage } from './components/Chat' export { default as Divider } from './components/Divider' export { default as Grid } from './components/Grid' -export { default as Header } from './components/Header' -export { HeaderDescription } from './components/Header' +export { default as Header, HeaderDescription } from './components/Header' export { default as Icon } from './components/Icon' export { default as Image } from './components/Image' export { default as Input } from './components/Input' export { default as ItemLayout } from './components/ItemLayout' export { default as Label } from './components/Label' export { default as Layout } from './components/Layout' -export { default as List } from './components/List' -export { ListItem } from './components/List' -export { default as Menu } from './components/Menu' -export { MenuItem } from './components/Menu' +export { default as List, ListItem } from './components/List' +export { default as Menu, MenuItem } from './components/Menu' export { default as Provider } from './components/Provider' export { default as ProviderConsumer } from './components/Provider/ProviderConsumer' -export { default as RadioGroup } from './components/RadioGroup' -export { RadioGroupItem } from './components/RadioGroup' +export { default as RadioGroup, RadioGroupItem } from './components/RadioGroup' export { default as Segment } from './components/Segment' export { default as Status } from './components/Status' export { default as TabBehavior } from './lib/accessibility/Behaviors/Tab/tabBehavior' @@ -55,5 +49,4 @@ export { } from './lib/accessibility/Behaviors/Chat/chatMessageEnterEscBehavior' export { default as Portal } from './components/Portal' -export { default as Popup } from './components/Popup' -export { PopupContent } from './components/Popup' +export { default as Popup, PopupContent } from './components/Popup' diff --git a/src/themes/teams/componentStyles.ts b/src/themes/teams/componentStyles.ts index b136c379f4..aa6bef1aca 100644 --- a/src/themes/teams/componentStyles.ts +++ b/src/themes/teams/componentStyles.ts @@ -24,6 +24,7 @@ export { default as Icon } from './components/Icon/iconStyles' export { default as Image } from './components/Image/imageStyles' +export { default as InputBase } from './components/Input/inputBaseStyles' export { default as Input } from './components/Input/inputStyles' export { default as Label } from './components/Label/labelStyles' diff --git a/src/themes/teams/componentVariables.ts b/src/themes/teams/componentVariables.ts index df10e0a02a..04a7466c86 100644 --- a/src/themes/teams/componentVariables.ts +++ b/src/themes/teams/componentVariables.ts @@ -24,6 +24,7 @@ export { default as Icon } from './components/Icon/iconVariables' export { default as Image } from './components/Image/imageVariables' +export { default as InputBase } from './components/Input/inputBaseVariables' export { default as Input } from './components/Input/inputVariables' export { default as Label } from './components/Label/labelVariables' diff --git a/src/themes/teams/components/Input/inputBaseStyles.ts b/src/themes/teams/components/Input/inputBaseStyles.ts new file mode 100644 index 0000000000..bd59f18edb --- /dev/null +++ b/src/themes/teams/components/Input/inputBaseStyles.ts @@ -0,0 +1,22 @@ +import { IComponentPartStylesInput, ICSSInJSStyle } from '../../../../../types/theme' +import { IInputBaseProps } from '../../../../components/Input/InputBase' +import { IInputBaseVariables } from './inputBaseVariables' + +const inputBaseStyles: IComponentPartStylesInput = { + root: ({ props: p, variables: v }): ICSSInJSStyle => ({ + outline: 0, + border: 0, + borderRadius: v.borderRadius, + borderBottom: v.borderBottom, + color: v.fontColor, + backgroundColor: v.backgroundColor, + padding: v.inputPadding, + ...(p.fluid && { width: '100%' }), + ':focus': { + borderColor: v.inputFocusBorderColor, + borderRadius: v.inputFocusBorderRadius, + }, + }), +} + +export default inputBaseStyles diff --git a/src/themes/teams/components/Input/inputBaseVariables.ts b/src/themes/teams/components/Input/inputBaseVariables.ts new file mode 100644 index 0000000000..3e1b3b30ac --- /dev/null +++ b/src/themes/teams/components/Input/inputBaseVariables.ts @@ -0,0 +1,27 @@ +import { pxToRem } from '../../../../lib' + +export interface IInputBaseVariables { + borderRadius: string + borderBottom: string + backgroundColor: string + fontColor: string + fontSize: string + inputPadding: string + inputFocusBorderColor: string + inputFocusBorderRadius: string +} + +const [px2asRem, px3asRem, px6asRem, px12asRem, px24asRem] = [2, 3, 6, 12, 24].map(v => pxToRem(v)) + +export default (siteVars): IInputBaseVariables => ({ + borderRadius: px3asRem, + borderBottom: `${px2asRem} solid transparent`, + backgroundColor: siteVars.gray10, + + fontColor: siteVars.bodyColor, + fontSize: siteVars.fontSizes.medium, + + inputPadding: `${px6asRem} ${px24asRem} ${px6asRem} ${px12asRem}`, + inputFocusBorderColor: siteVars.brand, + inputFocusBorderRadius: `${px3asRem} ${px3asRem} ${px2asRem} ${px2asRem}`, +}) diff --git a/src/themes/teams/components/Input/inputStyles.ts b/src/themes/teams/components/Input/inputStyles.ts index f37ad16842..2d978088da 100644 --- a/src/themes/teams/components/Input/inputStyles.ts +++ b/src/themes/teams/components/Input/inputStyles.ts @@ -1,46 +1,24 @@ import { IComponentPartStylesInput, ICSSInJSStyle } from '../../../../../types/theme' import { IInputProps } from '../../../../components/Input/Input' +import { IInputVariables } from './inputVariables' +import { PositionProperty } from 'csstype' -const inputStyles: IComponentPartStylesInput = { - root: ({ props, variables }): ICSSInJSStyle => { - const { fluid } = props +const inputStyles: IComponentPartStylesInput = { + root: ({ props: p }): ICSSInJSStyle => ({ + display: 'inline-flex', + position: 'relative', + alignItems: 'center', + outline: 0, + ...(p.fluid && { width: '100%' }), + }), - return { - display: 'inline-flex', - position: 'relative', - alignItems: 'center', - outline: 0, - ...(fluid && { width: '100%' }), - } - }, + input: ({ props: p }): ICSSInJSStyle => p.inline && { float: 'left' }, - input: ({ props, variables }): ICSSInJSStyle => { - const { fluid, inline } = props - - return { - outline: 0, - border: 0, - borderRadius: variables.borderRadius, - borderBottom: variables.borderBottom, - color: variables.fontColor, - backgroundColor: variables.backgroundColor, - padding: variables.inputPadding, - ...(fluid && { width: '100%' }), - ...(inline && { float: 'left' }), - ':focus': { - borderColor: variables.inputFocusBorderColor, - borderRadius: variables.inputFocusBorderRadius, - }, - } - }, - - icon: ({ props, variables }): ICSSInJSStyle => { - return { - position: variables.iconPosition, - right: variables.iconRight, - outline: 0, - } - }, + icon: ({ variables: v }): ICSSInJSStyle => ({ + position: v.iconPosition as PositionProperty, + right: v.iconRight, + outline: 0, + }), } export default inputStyles diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index 060bd6b50d..ebae82aa84 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -1,21 +1,11 @@ import { pxToRem } from '../../../../lib' -export default (siteVars: any) => { - const vars: any = {} - - vars.borderRadius = `${pxToRem(3)}` - vars.borderBottom = `${pxToRem(2)} solid transparent` - vars.backgroundColor = siteVars.gray10 - - vars.fontColor = siteVars.bodyColor - vars.fontSize = siteVars.fontSizes.medium - - vars.inputPadding = `${pxToRem(6)} ${pxToRem(24)} ${pxToRem(6)} ${pxToRem(12)}` - vars.inputFocusBorderColor = siteVars.brand - vars.inputFocusBorderRadius = `${pxToRem(3)} ${pxToRem(3)} ${pxToRem(2)} ${pxToRem(2)}` - - vars.iconPosition = 'absolute' - vars.iconRight = `${pxToRem(2)}` - - return vars +export interface IInputVariables { + iconPosition: string + iconRight: string } + +export default (): IInputVariables => ({ + iconPosition: 'absolute', + iconRight: pxToRem(2), +}) diff --git a/test/specs/components/Input/InputBase-test.tsx b/test/specs/components/Input/InputBase-test.tsx new file mode 100644 index 0000000000..021b0ec575 --- /dev/null +++ b/test/specs/components/Input/InputBase-test.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' + +import { isConformant } from 'test/specs/commonTests' + +import InputBase from 'src/components/Input/InputBase' +import { mountWithProvider } from 'test/utils' + +describe('InputBase', () => { + isConformant(InputBase, { + eventTargets: { + onChange: 'input', + }, + }) + + describe('input', () => { + it('renders a text by default', () => { + const input = mountWithProvider().find( + 'input[type="text"]', + ) + expect(input).not.toBe(undefined) + }) + }) +}) From 854eb32c687b5b1dae50d998ff141cb6bf39e64d Mon Sep 17 00:00:00 2001 From: Alexandru Buliga Date: Wed, 10 Oct 2018 17:30:47 +0200 Subject: [PATCH 2/4] refactored input and removed InputBase --- .../Input/Types/InputExample.shorthand.tsx | 2 +- .../InputExampleFluid.shorthand.tsx | 4 +- .../InputExampleInputSlot.shorthand.tsx | 24 ++++ .../InputExampleTargeting.shorthand.tsx | 41 ++++++ .../InputExampleWrapper.shorthand.tsx | 24 ---- .../InputExampleWrapperSlot.shorthand.tsx | 29 ++++ .../components/Input/Variations/index.tsx | 16 ++- .../Types/RadioGroupExample.shorthand.tsx | 2 +- .../RadioGroupVerticalExample.shorthand.tsx | 2 +- .../prototypes/chatPane/composeMessage.tsx | 2 +- package.json | 1 + src/components/Input/Input.tsx | 132 +++++++++++------- src/components/Input/InputBase.tsx | 109 --------------- src/components/Slot/Slot.tsx | 27 +++- src/lib/factories.tsx | 5 +- src/themes/teams/componentStyles.ts | 1 - src/themes/teams/componentVariables.ts | 1 - .../teams/components/Input/inputBaseStyles.ts | 22 --- .../components/Input/inputBaseVariables.ts | 27 ---- .../teams/components/Input/inputStyles.ts | 16 ++- .../teams/components/Input/inputVariables.ts | 24 +++- .../commonTests/implementsShorthandProp.tsx | 43 ++++-- .../commonTests/implementsWrapperProp.tsx | 60 ++++++++ test/specs/commonTests/index.ts | 1 + test/specs/commonTests/isConformant.tsx | 6 +- test/specs/components/Input/Input-test.tsx | 95 +++++++++++-- .../specs/components/Input/InputBase-test.tsx | 23 --- test/specs/components/Slot/Slot-test.ts | 67 ++++++++- yarn.lock | 11 ++ 29 files changed, 503 insertions(+), 314 deletions(-) create mode 100644 docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx create mode 100644 docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx delete mode 100644 docs/src/examples/components/Input/Variations/InputExampleWrapper.shorthand.tsx create mode 100644 docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx delete mode 100644 src/components/Input/InputBase.tsx delete mode 100644 src/themes/teams/components/Input/inputBaseStyles.ts delete mode 100644 src/themes/teams/components/Input/inputBaseVariables.ts create mode 100644 test/specs/commonTests/implementsWrapperProp.tsx delete mode 100644 test/specs/components/Input/InputBase-test.tsx diff --git a/docs/src/examples/components/Input/Types/InputExample.shorthand.tsx b/docs/src/examples/components/Input/Types/InputExample.shorthand.tsx index f823f513c5..9b812e77ee 100644 --- a/docs/src/examples/components/Input/Types/InputExample.shorthand.tsx +++ b/docs/src/examples/components/Input/Types/InputExample.shorthand.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Input } from '@stardust-ui/react' -const InputExample = () => +const InputExample = () => export default InputExample diff --git a/docs/src/examples/components/Input/Variations/InputExampleFluid.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleFluid.shorthand.tsx index e830db1dcd..daee65cd4c 100644 --- a/docs/src/examples/components/Input/Variations/InputExampleFluid.shorthand.tsx +++ b/docs/src/examples/components/Input/Variations/InputExampleFluid.shorthand.tsx @@ -1,8 +1,6 @@ import React from 'react' import { Input } from '@stardust-ui/react' -const InputExampleFluid = () => ( - -) +const InputExampleFluid = () => export default InputExampleFluid diff --git a/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx new file mode 100644 index 0000000000..4d2c25e624 --- /dev/null +++ b/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { Grid, Input, Text } from '@stardust-ui/react' + +const inputProps = { placeholder: 'Search...', role: 'presentation' } +const inputStyles = { color: 'blue', background: 'yellow' } +const inputOverrides = { placeholder: 'Placeholder Override...', role: 'checkbox' } + +const InputExampleInputSlot = () => ( + + + + + + + + + } /> + + + } /> + +) + +export default InputExampleInputSlot diff --git a/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx new file mode 100644 index 0000000000..5a54b37eb3 --- /dev/null +++ b/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { Grid, Input, Text } from '@stardust-ui/react' + +const inputStyles = { color: 'blue', background: 'yellow' } +const wrapperStyles = { padding: '5px', background: 'red' } +const partitionedProps = { + placeholder: 'Search...', + dir: 'ltr', + tabIndex: 2, + disabled: false, + role: 'presentation', + styles: { padding: '5px', background: 'green' }, +} + +const InputExampleTargeting = () => ( + + + + + + + + + + + + + + + + +) + +export default InputExampleTargeting diff --git a/docs/src/examples/components/Input/Variations/InputExampleWrapper.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleWrapper.shorthand.tsx deleted file mode 100644 index 7d382de281..0000000000 --- a/docs/src/examples/components/Input/Variations/InputExampleWrapper.shorthand.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import { Grid, Input, Text } from '@stardust-ui/react' - -const wrapperStyles = { padding: '5px', background: 'red' } -const InputExampleWrapper = () => ( - - - - - - - - - - - - } /> - - - } /> - -) - -export default InputExampleWrapper diff --git a/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx new file mode 100644 index 0000000000..763e05cd05 --- /dev/null +++ b/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { Grid, Input, Text } from '@stardust-ui/react' + +const inputProps = { + placeholder: 'Search...', + dir: 'ltr', + tabIndex: 2, + styles: { color: 'blue', backgroundColor: 'yellow' }, +} +const wrapperStyles = { padding: '5px', backgroundColor: 'red' } +const wrapperOverrides = { dir: 'rtl', tabIndex: 0 } + +const InputExampleWrapperSlot = () => ( + + + + + + + + + } /> + + + } /> + +) + +export default InputExampleWrapperSlot diff --git a/docs/src/examples/components/Input/Variations/index.tsx b/docs/src/examples/components/Input/Variations/index.tsx index 1fa4b73a6c..553f6b7e2b 100644 --- a/docs/src/examples/components/Input/Variations/index.tsx +++ b/docs/src/examples/components/Input/Variations/index.tsx @@ -35,9 +35,19 @@ const Variations = () => ( examplePath="components/Input/Variations/InputExampleInlineIconClearable" /> + + ) diff --git a/docs/src/examples/components/RadioGroup/Types/RadioGroupExample.shorthand.tsx b/docs/src/examples/components/RadioGroup/Types/RadioGroupExample.shorthand.tsx index 12e3254edb..8bf469347c 100644 --- a/docs/src/examples/components/RadioGroup/Types/RadioGroupExample.shorthand.tsx +++ b/docs/src/examples/components/RadioGroup/Types/RadioGroupExample.shorthand.tsx @@ -35,7 +35,7 @@ class RadioGroupVerticalExample extends React.Component { label: ( Choose your own{' '} - + ), value: 'custom', diff --git a/docs/src/examples/components/RadioGroup/Types/RadioGroupVerticalExample.shorthand.tsx b/docs/src/examples/components/RadioGroup/Types/RadioGroupVerticalExample.shorthand.tsx index a3a2877226..7ed24ff27d 100644 --- a/docs/src/examples/components/RadioGroup/Types/RadioGroupVerticalExample.shorthand.tsx +++ b/docs/src/examples/components/RadioGroup/Types/RadioGroupVerticalExample.shorthand.tsx @@ -36,7 +36,7 @@ class RadioGroupVerticalExample extends React.Component { label: ( Choose your own{' '} - + ), value: 'custom', diff --git a/docs/src/prototypes/chatPane/composeMessage.tsx b/docs/src/prototypes/chatPane/composeMessage.tsx index 036f1cf543..2ccc970f2c 100644 --- a/docs/src/prototypes/chatPane/composeMessage.tsx +++ b/docs/src/prototypes/chatPane/composeMessage.tsx @@ -21,7 +21,7 @@ class ComposeMessage extends React.Component { ({ backgroundColor: siteVars.white })} /> ) diff --git a/package.json b/package.json index 70a75f1979..b0c383a9ab 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ }, "devDependencies": { "@types/classnames": "^2.2.4", + "@types/enzyme": "^3.1.14", "@types/faker": "^4.1.3", "@types/gulp-load-plugins": "^0.0.31", "@types/jest": "^23.1.0", diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 72c5b51eeb..5e6dc23810 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -1,21 +1,42 @@ import * as React from 'react' import * as PropTypes from 'prop-types' +import * as cx from 'classnames' import * as _ from 'lodash' -import { AutoControlledComponent, customPropTypes } from '../../lib' -import { Extendable, ShorthandValue, ShorthandRenderFunction } from '../../../types/utils' -import InputBase, { IInputBaseProps } from './InputBase' +import { + AutoControlledComponent, + customPropTypes, + IRenderResultConfig, + partitionHTMLProps, +} from '../../lib' +import { + Extendable, + ShorthandValue, + ShorthandRenderFunction, + ComponentEventHandler, +} from '../../../types/utils' +import { ComponentPartStyle, ComponentVariablesInput } from 'theme' import Icon from '../Icon' import Slot from '../Slot' import Ref from '../Ref' -export interface IInputProps extends IInputBaseProps { +export interface IInputProps { + as?: any + className?: string clearable?: boolean + defaultValue?: React.ReactText + fluid?: boolean icon?: ShorthandValue inline?: boolean + input?: ShorthandValue + onChange?: ComponentEventHandler renderIcon?: ShorthandRenderFunction renderInput?: ShorthandRenderFunction renderWrapper?: ShorthandRenderFunction + styles?: ComponentPartStyle + type?: string + value?: React.ReactText + variables?: ComponentVariablesInput wrapper?: ShorthandValue } @@ -57,6 +78,9 @@ class Input extends AutoControlledComponent, IInputState /** Optional Icon to display inside the Input. */ icon: customPropTypes.itemShorthand, + /** Shorthand for the input component */ + input: customPropTypes.itemShorthand, + /** An input can be used inline with text */ inline: PropTypes.bool, @@ -68,12 +92,6 @@ class Input extends AutoControlledComponent, IInputState */ onChange: PropTypes.func, - /** The HTML input placeholder. */ - placeholder: PropTypes.string, - - /** The HTML input type. */ - type: PropTypes.string, - /** * A custom render function the icon slot. * @@ -104,6 +122,9 @@ class Input extends AutoControlledComponent, IInputState /** Additional CSS styles to apply to the component instance. */ styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + /** The HTML input type. */ + type: PropTypes.string, + /** The value of the input. */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), @@ -115,7 +136,7 @@ class Input extends AutoControlledComponent, IInputState } static defaultProps = { - as: 'input', + as: 'div', type: 'text', wrapper: 'div', } @@ -124,46 +145,52 @@ class Input extends AutoControlledComponent, IInputState state = { value: this.props.value || this.props.defaultValue || '' } - renderComponent({ classes, styles, variables }) { - const { - clearable, - icon, - inline, - renderIcon, - renderInput, - renderWrapper, - wrapper, - ...rest - } = this.props - const { value } = this.state - - const inputComponent = InputBase.create(this.props.type, { - defaultProps: { className: classes.input, ...rest, value }, - overrideProps: { onChange: this.handleChange }, - render: renderInput, + renderComponent({ + ElementType, + classes, + rest: restProps, + styles, + variables, + }: IRenderResultConfig) { + const { className, input, renderIcon, renderInput, renderWrapper, type, wrapper } = this.props + const { value = '' } = this.state + const [htmlInputProps, rest] = partitionHTMLProps(restProps) + + return Slot.create(wrapper, { + defaultProps: { + as: ElementType, + className: cx(Input.className, className), + styles: styles.root, + ...rest, + }, + overrideProps: { + children: ( + <> + + {Slot.createHTMLInput(input || type, { + defaultProps: { + ...htmlInputProps, + type, + value, + className: classes.input, + onChange: this.handleChange, + }, + render: renderInput, + })} + + {Icon.create(this.computeIcon(), { + defaultProps: { + styles: styles.icon, + variables: variables.icon, + }, + overrideProps: this.handleIconOverrides, + render: renderIcon, + })} + + ), + }, + render: renderWrapper, }) - - return wrapper - ? Slot.create(wrapper, { - defaultProps: { className: classes.root }, - overrideProps: { - children: ( - <> - {inputComponent} - {Icon.create(this.computeIcon(), { - defaultProps: { - styles: styles.icon, - variables: variables.icon, - }, - overrideProps: this.handleIconOverrides, - render: renderIcon, - })} - - ), - }, - render: renderWrapper, - }) - : inputComponent } private handleInputRef = (c: HTMLInputElement) => (this.inputRef = c) @@ -177,8 +204,11 @@ class Input extends AutoControlledComponent, IInputState ...(predefinedProps.onClick && { tabIndex: '0' }), }) - private handleChange = (e: React.SyntheticEvent, { value }: { value: React.ReactText }) => { + private handleChange = (e: React.SyntheticEvent) => { + const value = _.get(e, 'target.value') + _.invoke(this.props, 'onChange', e, { ...this.props, value }) + this.trySetState({ value }) } diff --git a/src/components/Input/InputBase.tsx b/src/components/Input/InputBase.tsx deleted file mode 100644 index c302d4fb75..0000000000 --- a/src/components/Input/InputBase.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import * as PropTypes from 'prop-types' -import * as React from 'react' -import * as _ from 'lodash' - -import { AutoControlledComponent, customPropTypes, createShorthandFactory } from '../../lib' -import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme' -import { Extendable, ComponentEventHandler } from '../../../types/utils' - -export interface IInputBaseProps { - as?: any - className?: string - defaultValue?: React.ReactText - fluid?: boolean - onChange?: ComponentEventHandler - placeholder?: string - type?: string - styles?: ComponentPartStyle - value?: React.ReactText - variables?: ComponentVariablesInput -} - -export interface IInputState { - value?: React.ReactText -} - -/** - * A basic Input - * @accessibility - * For good screen reader experience set aria-label or aria-labelledby attribute for input. - */ -class InputBase extends AutoControlledComponent, IInputState> { - static create: Function - - static className = 'ui-input__base' - - static displayName = 'InputBase' - - static propTypes = { - /** An element type to render as (string or function). */ - as: customPropTypes.as, - - /** Additional CSS class name(s) to apply. */ - className: PropTypes.string, - - /** The default value of the input. */ - defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - - /** An input can take the width of its container. */ - fluid: PropTypes.bool, - - /** - * Called on change. - * @param {SyntheticEvent} event - React's original SyntheticEvent. - * @param {object} data - All props and proposed value. - */ - onChange: PropTypes.func, - - /** The HTML input placeholder. */ - placeholder: PropTypes.string, - - /** The HTML input type. */ - type: PropTypes.string, - - /** Additional CSS styles to apply to the component instance. */ - styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - - /** The value of the input. */ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - - /** Override for theme site variables to allow modifications of component styling via themes. */ - variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - } - - static defaultProps = { - as: 'input', - type: 'text', - } - - static autoControlledProps = ['value'] - - state = { value: this.props.value || this.props.defaultValue || '' } - - renderComponent({ ElementType, classes, rest }) { - const { placeholder, type } = this.props - - return ( - - ) - } - - private handleChange = (e: React.SyntheticEvent) => { - const value = _.get(e, 'target.value') - - _.invoke(this.props, 'onChange', e, { ...this.props, value }) - - this.trySetState({ value }) - } -} - -InputBase.create = createShorthandFactory(InputBase, type => ({ type })) - -export default InputBase diff --git a/src/components/Slot/Slot.tsx b/src/components/Slot/Slot.tsx index 0228b1fd61..48121b842c 100644 --- a/src/components/Slot/Slot.tsx +++ b/src/components/Slot/Slot.tsx @@ -1,7 +1,13 @@ import * as React from 'react' import * as PropTypes from 'prop-types' -import { customPropTypes, UIComponent, childrenExist, createShorthandFactory } from '../../lib' -import { Extendable } from '../../../types/utils' +import { + customPropTypes, + UIComponent, + childrenExist, + IRenderResultConfig, + createShorthand, +} from '../../lib' +import { Extendable, MapValueToProps, IProps } from '../../../types/utils' import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme' export interface ISlotProps { @@ -12,12 +18,18 @@ export interface ISlotProps { variables?: ComponentVariablesInput } +export const createSlotFactory = (as: any, mapValueToProps: MapValueToProps) => ( + val, + options: IProps = {}, +) => { + options.defaultProps = { as, ...options.defaultProps } + return createShorthand(Slot, mapValueToProps, val, options) +} + /** * A Slot is a basic component (no default styles) */ class Slot extends UIComponent, any> { - static create: Function - static className = 'ui-slot' static displayName = 'Slot' @@ -43,7 +55,10 @@ class Slot extends UIComponent, any> { as: 'div', } - renderComponent({ ElementType, classes, rest }) { + static create = createSlotFactory(Slot.defaultProps.as, content => ({ content })) + static createHTMLInput = createSlotFactory('input', type => ({ type })) + + renderComponent({ ElementType, classes, rest }: IRenderResultConfig) { const { children, content } = this.props return ( @@ -54,6 +69,4 @@ class Slot extends UIComponent, any> { } } -Slot.create = createShorthandFactory(Slot, content => ({ content })) - export default Slot diff --git a/src/lib/factories.tsx b/src/lib/factories.tsx index 1204293550..eaaf09eb11 100644 --- a/src/lib/factories.tsx +++ b/src/lib/factories.tsx @@ -6,12 +6,9 @@ import { ShorthandPrimitive, ShorthandRenderFunction, ShorthandValue, + IProps, } from '../../types/utils' -interface IProps { - [key: string]: any -} - interface ICreateShorthandOptions { /** Override the default render implementation. */ render?: ShorthandRenderFunction diff --git a/src/themes/teams/componentStyles.ts b/src/themes/teams/componentStyles.ts index aa6bef1aca..b136c379f4 100644 --- a/src/themes/teams/componentStyles.ts +++ b/src/themes/teams/componentStyles.ts @@ -24,7 +24,6 @@ export { default as Icon } from './components/Icon/iconStyles' export { default as Image } from './components/Image/imageStyles' -export { default as InputBase } from './components/Input/inputBaseStyles' export { default as Input } from './components/Input/inputStyles' export { default as Label } from './components/Label/labelStyles' diff --git a/src/themes/teams/componentVariables.ts b/src/themes/teams/componentVariables.ts index 04a7466c86..df10e0a02a 100644 --- a/src/themes/teams/componentVariables.ts +++ b/src/themes/teams/componentVariables.ts @@ -24,7 +24,6 @@ export { default as Icon } from './components/Icon/iconVariables' export { default as Image } from './components/Image/imageVariables' -export { default as InputBase } from './components/Input/inputBaseVariables' export { default as Input } from './components/Input/inputVariables' export { default as Label } from './components/Label/labelVariables' diff --git a/src/themes/teams/components/Input/inputBaseStyles.ts b/src/themes/teams/components/Input/inputBaseStyles.ts deleted file mode 100644 index bd59f18edb..0000000000 --- a/src/themes/teams/components/Input/inputBaseStyles.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IComponentPartStylesInput, ICSSInJSStyle } from '../../../../../types/theme' -import { IInputBaseProps } from '../../../../components/Input/InputBase' -import { IInputBaseVariables } from './inputBaseVariables' - -const inputBaseStyles: IComponentPartStylesInput = { - root: ({ props: p, variables: v }): ICSSInJSStyle => ({ - outline: 0, - border: 0, - borderRadius: v.borderRadius, - borderBottom: v.borderBottom, - color: v.fontColor, - backgroundColor: v.backgroundColor, - padding: v.inputPadding, - ...(p.fluid && { width: '100%' }), - ':focus': { - borderColor: v.inputFocusBorderColor, - borderRadius: v.inputFocusBorderRadius, - }, - }), -} - -export default inputBaseStyles diff --git a/src/themes/teams/components/Input/inputBaseVariables.ts b/src/themes/teams/components/Input/inputBaseVariables.ts deleted file mode 100644 index 3e1b3b30ac..0000000000 --- a/src/themes/teams/components/Input/inputBaseVariables.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { pxToRem } from '../../../../lib' - -export interface IInputBaseVariables { - borderRadius: string - borderBottom: string - backgroundColor: string - fontColor: string - fontSize: string - inputPadding: string - inputFocusBorderColor: string - inputFocusBorderRadius: string -} - -const [px2asRem, px3asRem, px6asRem, px12asRem, px24asRem] = [2, 3, 6, 12, 24].map(v => pxToRem(v)) - -export default (siteVars): IInputBaseVariables => ({ - borderRadius: px3asRem, - borderBottom: `${px2asRem} solid transparent`, - backgroundColor: siteVars.gray10, - - fontColor: siteVars.bodyColor, - fontSize: siteVars.fontSizes.medium, - - inputPadding: `${px6asRem} ${px24asRem} ${px6asRem} ${px12asRem}`, - inputFocusBorderColor: siteVars.brand, - inputFocusBorderRadius: `${px3asRem} ${px3asRem} ${px2asRem} ${px2asRem}`, -}) diff --git a/src/themes/teams/components/Input/inputStyles.ts b/src/themes/teams/components/Input/inputStyles.ts index 2d978088da..37051a4b64 100644 --- a/src/themes/teams/components/Input/inputStyles.ts +++ b/src/themes/teams/components/Input/inputStyles.ts @@ -12,7 +12,21 @@ const inputStyles: IComponentPartStylesInput = { ...(p.fluid && { width: '100%' }), }), - input: ({ props: p }): ICSSInJSStyle => p.inline && { float: 'left' }, + input: ({ props: p, variables: v }): ICSSInJSStyle => ({ + outline: 0, + border: 0, + borderRadius: v.borderRadius, + borderBottom: v.borderBottom, + color: v.fontColor, + backgroundColor: v.backgroundColor, + padding: v.inputPadding, + ...(p.fluid && { width: '100%' }), + ...(p.inline && { float: 'left' }), + ':focus': { + borderColor: v.inputFocusBorderColor, + borderRadius: v.inputFocusBorderRadius, + }, + }), icon: ({ variables: v }): ICSSInJSStyle => ({ position: v.iconPosition as PositionProperty, diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index ebae82aa84..90da36ab0d 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -1,11 +1,31 @@ import { pxToRem } from '../../../../lib' - export interface IInputVariables { + borderRadius: string + borderBottom: string + backgroundColor: string + fontColor: string + fontSize: string iconPosition: string iconRight: string + inputPadding: string + inputFocusBorderColor: string + inputFocusBorderRadius: string } -export default (): IInputVariables => ({ +const [px2asRem, px3asRem, px6asRem, px12asRem, px24asRem] = [2, 3, 6, 12, 24].map(v => pxToRem(v)) + +export default (siteVars): IInputVariables => ({ + borderRadius: px3asRem, + borderBottom: `${px2asRem} solid transparent`, + backgroundColor: siteVars.gray10, + + fontColor: siteVars.bodyColor, + fontSize: siteVars.fontSizes.medium, + iconPosition: 'absolute', iconRight: pxToRem(2), + + inputPadding: `${px6asRem} ${px24asRem} ${px6asRem} ${px12asRem}`, + inputFocusBorderColor: siteVars.brand, + inputFocusBorderRadius: `${px3asRem} ${px3asRem} ${px2asRem} ${px2asRem}`, }) diff --git a/test/specs/commonTests/implementsShorthandProp.tsx b/test/specs/commonTests/implementsShorthandProp.tsx index 5873f2ca62..5d72926e1f 100644 --- a/test/specs/commonTests/implementsShorthandProp.tsx +++ b/test/specs/commonTests/implementsShorthandProp.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { mount } from './isConformant' +import { mount, ReactWrapper } from 'enzyme' export type ShorthandTestOptions = { mapsValueToProp?: string @@ -18,34 +18,47 @@ export default Component => { const { mapsValueToProp } = options const { displayName } = ShorthandComponent + /** + * @param {ReactWrapper} wrapper [mounted Component] + * @param {(n: ReactWrapper) => boolean} propsCheckFn [boolean function that applies a boolean check on a mounted ShorthandComponent] + * return true if the ShorthandComponent that matches propsCheckFn is found + */ + const testShorthandComponentProps = ( + wrapper: ReactWrapper, + propsCheckFn: (n: ReactWrapper) => boolean, + ) => { + const shorthandComponent = wrapper.findWhere( + n => n.type() === ShorthandComponent && propsCheckFn(n), + ) + expect(shorthandComponent.length).toEqual(1) + } + describe(`shorthand property '${shorthandProp}' with default value of '${displayName}' component`, () => { test(`is defined`, () => { expect(Component.propTypes[shorthandProp]).toBeTruthy() }) test(`string value is handled as ${displayName}'s ${mapsValueToProp}`, () => { - const props = { [shorthandProp]: 'some value' } - const wrapper = mount() + const shorthandPropValue = 'shorthand prop value' + const props = { [shorthandProp]: shorthandPropValue } - const shorthandComponentProps = wrapper.find(displayName).props() - expect(shorthandComponentProps[mapsValueToProp]).toEqual('some value') + testShorthandComponentProps( + mount(), + n => n.prop(mapsValueToProp) === shorthandPropValue, + ) }) test(`object value is spread as ${displayName}'s props`, () => { const ShorthandValue = { foo: 'foo value', bar: 'bar value' } - const props = { [shorthandProp]: ShorthandValue } - const wrapper = mount() + const checkPropsAreSpread = (props: {}) => + Object.keys(ShorthandValue).every( + propName => ShorthandValue[propName] === props[propName], + ) - const shorthandComponentProps = wrapper.find(displayName).props() - - const allShorthandPropertiesArePassedToShorthandComponent = Object.keys( - ShorthandValue, - ).every( - propertyName => ShorthandValue[propertyName] === shorthandComponentProps[propertyName], + testShorthandComponentProps(mount(), n => + checkPropsAreSpread(n.props()), ) - - expect(allShorthandPropertiesArePassedToShorthandComponent).toBe(true) }) }) } diff --git a/test/specs/commonTests/implementsWrapperProp.tsx b/test/specs/commonTests/implementsWrapperProp.tsx new file mode 100644 index 0000000000..c0593fc6c4 --- /dev/null +++ b/test/specs/commonTests/implementsWrapperProp.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { mount, ReactWrapper } from 'enzyme' + +import Slot from 'src/components/Slot' +import Segment from 'src/components/Segment' +import { ShorthandValue } from 'utils' + +export interface ImplementsWrapperPropOptions { + wrapppedComponentSelector: any + wrappperComponentSelector?: any +} + +const implementsWrapperProp =

( + Component: React.ReactType

, + options: ImplementsWrapperPropOptions, +) => { + const { wrapppedComponentSelector, wrappperComponentSelector = Slot.defaultProps.as } = options + + const wrapperTests = (wrapper: ReactWrapper) => { + expect(wrapper.length).toBeGreaterThan(0) + expect(wrapper.find(wrapppedComponentSelector).length).toBeGreaterThan(0) + } + + describe('"wrapper" prop', () => { + it('does not wrap the component when set to false', () => { + const wrapper = mount().find(wrappperComponentSelector) + + expect(wrapper.length).toEqual(0) + }) + + it('wraps the component by default', () => { + wrapperTests(mount().find(wrappperComponentSelector)) + }) + + it('wraps the component with a custom element', () => { + wrapperTests(mount(} />).find('span')) + }) + + it('wraps the component with a custom element using "as" prop', () => { + const customElem = 'p' + wrapperTests(mount().find(customElem)) + }) + + it('wraps the component with another stardust component as wrapper', () => { + wrapperTests(mount(} />).find(Segment)) + }) + + it('wraps the component with a custom component as wrapper', () => { + class MyComponent extends React.Component { + render() { + return

{this.props.children}
+ } + } + + wrapperTests(mount(} />).find(MyComponent)) + }) + }) +} + +export default implementsWrapperProp diff --git a/test/specs/commonTests/index.ts b/test/specs/commonTests/index.ts index 9234d5fe96..cc420b2043 100644 --- a/test/specs/commonTests/index.ts +++ b/test/specs/commonTests/index.ts @@ -4,6 +4,7 @@ export { default as hasUIClassName } from './hasUIClassName' export * from './implementsClassNameProps' export { default as implementsCreateMethod } from './implementsCreateMethod' export { default as implementsShorthandProp } from './implementsShorthandProp' +export { default as implementsWrapperProp } from './implementsWrapperProp' export { default as handlesAccessibility, getRenderedAttribute } from './handlesAccessibility' diff --git a/test/specs/commonTests/isConformant.tsx b/test/specs/commonTests/isConformant.tsx index cae25f0cc5..e6a0f4ad52 100644 --- a/test/specs/commonTests/isConformant.tsx +++ b/test/specs/commonTests/isConformant.tsx @@ -1,6 +1,6 @@ import * as _ from 'lodash' import * as React from 'react' -import { mount as enzymeMount } from 'enzyme' +import { mount as enzymeMount, ReactWrapper } from 'enzyme' import * as ReactDOMServer from 'react-dom/server' import { ThemeProvider } from 'react-fela' @@ -32,7 +32,7 @@ export const mount = (node, options?) => { * @param {React.Component|Function} Component A component that should conform. * @param {Object} [options={}] * @param {Object} [options.eventTargets={}] Map of events and the child component to target. - * @param {boolean} [options.exportedAtTopLevel=false] Is this component exported as top level API + * @param {boolean} [options.exportedAtTopLevel=false] Is this component exported as top level API? * @param {boolean} [options.rendersPortal=false] Does this component render a Portal powered component? * @param {Object} [options.requiredProps={}] Props required to render Component without errors or warnings. */ @@ -48,7 +48,7 @@ export default (Component, options: IConformant = {}) => { const componentType = typeof Component // This is added because the component is mounted - const getComponent = wrapper => { + const getComponent = (wrapper: ReactWrapper) => { // FelaTheme wrapper and the component itself: let component = wrapper .childAt(0) diff --git a/test/specs/components/Input/Input-test.tsx b/test/specs/components/Input/Input-test.tsx index 12eb1675ab..595d13e1d8 100644 --- a/test/specs/components/Input/Input-test.tsx +++ b/test/specs/components/Input/Input-test.tsx @@ -1,30 +1,101 @@ import * as React from 'react' -import { isConformant, implementsShorthandProp } from 'test/specs/commonTests' +import { mount, ReactWrapper } from 'enzyme' +import { + isConformant, + implementsShorthandProp, + implementsWrapperProp, +} from 'test/specs/commonTests' import Input from 'src/components/Input/Input' import Icon from 'src/components/Icon/Icon' -import { mountWithProvider } from 'test/utils' +import Slot from 'src/components/Slot' + +const testValue = 'test value' +const htmlInputAttrs = ['id', 'name', 'pattern', 'placeholder', 'type', 'value'] + +const getInputAttrsObject = (testValue: string) => + htmlInputAttrs.reduce((acc, attr) => { + acc[attr] = testValue + return acc + }, {}) + +const getInputDomNode = (inputComp: ReactWrapper): HTMLInputElement => + inputComp.find('input').getDOMNode() as HTMLInputElement + +const setUserInputValue = (inputComp: ReactWrapper, value: string) => { + inputComp.find('input').simulate('change', { target: { value } }) +} describe('Input', () => { - isConformant(Input, { - eventTargets: { - onChange: 'input', - }, + describe('conformance', () => { + isConformant(Input, { + eventTargets: { onChange: 'input' }, + }) }) + + implementsShorthandProp(Input)('input', Slot, { mapsValueToProp: 'type' }) implementsShorthandProp(Input)('icon', Icon, { mapsValueToProp: 'name' }) - describe('input', () => { - it('renders a text by default', () => { - const input = mountWithProvider().find('input[type="text"]') - expect(input).not.toBe(undefined) + describe('wrapper', () => { + implementsShorthandProp(Input)('wrapper', Slot, { mapsValueToProp: 'content' }) + implementsWrapperProp(Input, { wrapppedComponentSelector: 'input' }) + }) + + it('renders a text by default', () => { + const inputComp = mount() + expect(inputComp.find('input[type="text"]').length).toBeGreaterThan(0) + }) + + describe('input related HTML attribute', () => { + const inputAttrsObject = getInputAttrsObject(testValue) + const domNode = getInputDomNode(mount()) + + htmlInputAttrs.forEach(attr => { + it(`'${attr}' is set correctly to '${testValue}'`, () => { + expect(domNode[attr]).toEqual(testValue) + }) + }) + }) + + describe('auto-controlled', () => { + it('sets input value from user when the value prop is not set (non-controlled mode)', () => { + const inputComp = mount() + const domNode = getInputDomNode(inputComp) + setUserInputValue(inputComp, testValue) + + expect(domNode.value).toEqual(testValue) + }) + + it('cannot set input value from user when the value prop is already set (controlled mode)', () => { + const controlledInputValue = 'controlled input value' + const inputComp = mount() + const domNode = getInputDomNode(inputComp) + setUserInputValue(inputComp, testValue) + + expect(domNode.value).toEqual(controlledInputValue) }) }) describe('icon', () => { it('creates the Icon component when the icon shorthand is provided', () => { - const input = mountWithProvider().find('Icon[name="close"]') - expect(input).not.toBe(undefined) + const inputComp = mount() + expect(inputComp.find('Icon[name="search"]').length).toBeGreaterThan(0) + }) + + it('creates the "close" Icon component when the clearable prop is provided and the input has content, removes the icon and value when the icon is clicked', () => { + const inputComp = mount() + const domNode = getInputDomNode(inputComp) + setUserInputValue(inputComp, testValue) // user types into the input + const iconComp = inputComp.find('Icon[name="close"]') + + expect(domNode.value).toEqual(testValue) // input value is the one typed by the user + expect(iconComp.length).toBeGreaterThan(0) // the 'x' icon appears + + iconComp.simulate('click') // user clicks on 'x' icon + + expect(domNode.value).toEqual('') // input value gets cleared + expect(inputComp.find('Icon[name="close"]').length).toEqual(0) // the 'x' icon disappears }) }) }) diff --git a/test/specs/components/Input/InputBase-test.tsx b/test/specs/components/Input/InputBase-test.tsx deleted file mode 100644 index 021b0ec575..0000000000 --- a/test/specs/components/Input/InputBase-test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' - -import { isConformant } from 'test/specs/commonTests' - -import InputBase from 'src/components/Input/InputBase' -import { mountWithProvider } from 'test/utils' - -describe('InputBase', () => { - isConformant(InputBase, { - eventTargets: { - onChange: 'input', - }, - }) - - describe('input', () => { - it('renders a text by default', () => { - const input = mountWithProvider().find( - 'input[type="text"]', - ) - expect(input).not.toBe(undefined) - }) - }) -}) diff --git a/test/specs/components/Slot/Slot-test.ts b/test/specs/components/Slot/Slot-test.ts index 6354dcac11..545ddf03a5 100644 --- a/test/specs/components/Slot/Slot-test.ts +++ b/test/specs/components/Slot/Slot-test.ts @@ -1,6 +1,69 @@ -import { isConformant } from 'test/specs/commonTests' +import { mount } from 'enzyme' + +import * as lib from 'src/lib' import Slot from 'src/components/Slot' +import { createSlotFactory } from 'src/components/Slot/Slot' +import { isConformant } from 'test/specs/commonTests' describe('Slot', () => { - isConformant(Slot, { exportedAtTopLevel: false }) + const createSlot = (factoryFn: Function, val, options?) => + mount(factoryFn(val, options)).find(Slot) + + describe('is conformant', () => { + isConformant(Slot, { exportedAtTopLevel: false }) + }) + + describe('createSlotFactory', () => { + it('calls createShorthand from lib', () => { + const mapValueToProp = mappedProp => ({ mappedProp }) + const value = 'testValue' + const createShorthandSpy = spyOn(lib, 'createShorthand') + createSlotFactory('span', mapValueToProp)(value) + + expect(createShorthandSpy).toHaveBeenCalledWith( + Slot, + mapValueToProp, + value, + expect.any(Object), + ) + }) + + it('sets correct "as" prop in defaultProps', () => { + const as = 'span' + const options = { testOption: 'test option value' } + const createShorthandSpy = spyOn(lib, 'createShorthand') + createSlotFactory(as, () => ({}))('testValue', { ...options }) // clone of the options + + const optionsArg = createShorthandSpy.calls.mostRecent().args[3] + expect(optionsArg).toEqual({ defaultProps: { as }, ...options }) + }) + + it('overrides "as" prop in defaultProps', () => { + const as = 'span' + const asOverride = 'p' + const createShorthandSpy = spyOn(lib, 'createShorthand') + createSlotFactory(as, () => ({}))('testValue', { defaultProps: { as: asOverride } }) // clone of the options + + const optionsArg = createShorthandSpy.calls.mostRecent().args[3] + expect(optionsArg).toEqual({ defaultProps: { as: asOverride } }) + }) + }) + + it(`create renders a ${Slot.defaultProps.as} element with content prop`, () => { + const testContent = 'test content' + const slot = createSlot(Slot.create, testContent) + const { as, content } = slot.props() + + expect(as).toEqual(Slot.defaultProps.as) + expect(content).toEqual(testContent) + }) + + it(`createHTMLInput renders an input element with type prop`, () => { + const testType = 'test type' + const slot = createSlot(Slot.createHTMLInput, testType) + const { as, type } = slot.props() + + expect(as).toEqual('input') + expect(type).toEqual(testType) + }) }) diff --git a/yarn.lock b/yarn.lock index 61ca87d589..e61fac43ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -52,11 +52,22 @@ dependencies: any-observable "^0.3.0" +"@types/cheerio@*": + version "0.22.9" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.9.tgz#b5990152604c2ada749b7f88cab3476f21f39d7b" + "@types/classnames@^2.2.4": version "2.2.4" resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.4.tgz#d3ee9ebf714aa34006707b8f4a58fd46b642305a" integrity sha512-UWUmNYhaIGDx8Kv0NSqFRwP6HWnBMXam4nBacOrjIiPBKKCdWMCe77+Nbn6rI9+Us9c+BhE26u84xeYQv2bKeA== +"@types/enzyme@^3.1.14": + version "3.1.14" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.14.tgz#379c26205f6e0e272f3a51d6bbdd50071a9d03a6" + dependencies: + "@types/cheerio" "*" + "@types/react" "*" + "@types/faker@^4.1.3": version "4.1.3" resolved "https://registry.yarnpkg.com/@types/faker/-/faker-4.1.3.tgz#544398268b37248300dc428316daa6a7521bbd19" From d8d15f30dbf65af3c33661661491268c06a40211 Mon Sep 17 00:00:00 2001 From: Alexandru Buliga Date: Wed, 17 Oct 2018 15:09:46 +0200 Subject: [PATCH 3/4] addressed PR comments --- .../InputExampleInputSlot.shorthand.tsx | 39 ++++++++++++--- .../InputExampleTargeting.shorthand.tsx | 48 +++++++++--------- .../InputExampleWrapperSlot.shorthand.tsx | 49 ++++++++++++++----- .../prototypes/chatPane/chatPaneHeader.tsx | 4 +- src/components/Input/Input.tsx | 22 ++++----- 5 files changed, 102 insertions(+), 60 deletions(-) diff --git a/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx index 4d2c25e624..c5f8aa79e6 100644 --- a/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx +++ b/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx @@ -1,23 +1,48 @@ import React from 'react' import { Grid, Input, Text } from '@stardust-ui/react' -const inputProps = { placeholder: 'Search...', role: 'presentation' } const inputStyles = { color: 'blue', background: 'yellow' } -const inputOverrides = { placeholder: 'Placeholder Override...', role: 'checkbox' } - const InputExampleInputSlot = () => ( - + - + - } /> + + } + /> - } /> + } + /> ) diff --git a/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx index 5a54b37eb3..65ea0be674 100644 --- a/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx +++ b/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx @@ -1,39 +1,37 @@ import React from 'react' import { Grid, Input, Text } from '@stardust-ui/react' -const inputStyles = { color: 'blue', background: 'yellow' } -const wrapperStyles = { padding: '5px', background: 'red' } -const partitionedProps = { - placeholder: 'Search...', - dir: 'ltr', - tabIndex: 2, - disabled: false, +const propsForInput = { placeholder: 'Search...', id: 'inputId', role: 'checkbox' } +const propsTargettingWrapper = { + placeholder: 'Wrapper placeholder...', + id: 'wrapperId', role: 'presentation', - styles: { padding: '5px', background: 'green' }, } -const InputExampleTargeting = () => ( - - - +const propsForWrapper = { dir: 'ltr', tabIndex: 2, styles: { padding: '5px', background: 'red' } } +const propsTargettingInput = { + dir: 'rtl', + tabIndex: 0, + styles: { color: 'blue', background: 'yellow' }, +} - - +const InputExampleTargeting = () => ( + + + - - + + - - + + - + ) diff --git a/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx index 763e05cd05..96dcc49f36 100644 --- a/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx +++ b/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx @@ -1,28 +1,51 @@ import React from 'react' import { Grid, Input, Text } from '@stardust-ui/react' -const inputProps = { - placeholder: 'Search...', - dir: 'ltr', - tabIndex: 2, - styles: { color: 'blue', backgroundColor: 'yellow' }, -} -const wrapperStyles = { padding: '5px', backgroundColor: 'red' } -const wrapperOverrides = { dir: 'rtl', tabIndex: 0 } - const InputExampleWrapperSlot = () => ( - + - + - } /> + } + /> - } /> + } + /> ) diff --git a/docs/src/prototypes/chatPane/chatPaneHeader.tsx b/docs/src/prototypes/chatPane/chatPaneHeader.tsx index 3a0e94ddb0..441a51ed30 100644 --- a/docs/src/prototypes/chatPane/chatPaneHeader.tsx +++ b/docs/src/prototypes/chatPane/chatPaneHeader.tsx @@ -85,8 +85,8 @@ class ChatPaneHeader extends React.PureComponent { tabIndex={0} styles={{ fontWeight: 100, - ...(!index && { marginRight: '1.6rem' }), - marginTop: '8px', + margin: 'auto', + ...(!index && { margin: 'auto 1.6rem auto auto' }), }} variables={siteVars => ({ color: siteVars.gray04 })} /> diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 5e6dc23810..1583804f85 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -15,7 +15,7 @@ import { ShorthandRenderFunction, ComponentEventHandler, } from '../../../types/utils' -import { ComponentPartStyle, ComponentVariablesInput } from 'theme' +import { ComponentPartStyle, ComponentVariablesInput } from '../../../types/theme' import Icon from '../Icon' import Slot from '../Slot' import Ref from '../Ref' @@ -63,10 +63,10 @@ class Input extends AutoControlledComponent, IInputState /** An element type to render as (string or function). */ as: customPropTypes.as, - /** Additional CSS class name(s) to apply. */ + /** Additional CSS class name(s) to apply. */ className: PropTypes.string, - /** A property that will change the icon on the input and clear the input on click on Cancel */ + /** A property that will change the icon on the input and clear the input on click on Cancel. */ clearable: PropTypes.bool, /** The default value of the input. */ @@ -78,10 +78,10 @@ class Input extends AutoControlledComponent, IInputState /** Optional Icon to display inside the Input. */ icon: customPropTypes.itemShorthand, - /** Shorthand for the input component */ + /** Shorthand for the input component. */ input: customPropTypes.itemShorthand, - /** An input can be used inline with text */ + /** An input can be used inline with text. */ inline: PropTypes.bool, /** @@ -119,7 +119,7 @@ class Input extends AutoControlledComponent, IInputState */ renderWrapper: PropTypes.func, - /** Additional CSS styles to apply to the component instance. */ + /** Additional CSS styles to apply to the component instance. */ styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), /** The HTML input type. */ @@ -131,7 +131,7 @@ class Input extends AutoControlledComponent, IInputState /** Override for theme site variables to allow modifications of component styling via themes. */ variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** Shorthand for the wrapper component */ + /** Shorthand for the wrapper component. */ wrapper: customPropTypes.itemShorthand, } @@ -143,8 +143,6 @@ class Input extends AutoControlledComponent, IInputState static autoControlledProps = ['value'] - state = { value: this.props.value || this.props.defaultValue || '' } - renderComponent({ ElementType, classes, @@ -160,10 +158,6 @@ class Input extends AutoControlledComponent, IInputState defaultProps: { as: ElementType, className: cx(Input.className, className), - styles: styles.root, - ...rest, - }, - overrideProps: { children: ( <> @@ -188,6 +182,8 @@ class Input extends AutoControlledComponent, IInputState })} ), + styles: styles.root, + ...rest, }, render: renderWrapper, }) From 93156c3aa4dfea55d914ea03fae296b54a0f2fa9 Mon Sep 17 00:00:00 2001 From: Alexandru Buliga Date: Thu, 18 Oct 2018 20:18:28 +0200 Subject: [PATCH 4/4] addressed comments again --- .../InputExampleInputSlot.shorthand.tsx | 6 +-- .../InputExampleTargeting.shorthand.tsx | 4 +- .../InputExampleWrapperSlot.shorthand.tsx | 23 +++------ .../components/Input/Variations/index.tsx | 2 +- .../prototypes/chatPane/composeMessage.tsx | 2 +- src/components/Input/Input.tsx | 14 +++--- src/lib/customPropTypes.tsx | 6 +++ .../teams/components/Input/inputVariables.ts | 14 +++--- .../commonTests/implementsShorthandProp.tsx | 49 +++++++------------ .../commonTests/implementsWrapperProp.tsx | 24 +-------- test/specs/components/Slot/Slot-test.ts | 38 -------------- 11 files changed, 58 insertions(+), 124 deletions(-) diff --git a/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx index c5f8aa79e6..f492c13d3a 100644 --- a/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx +++ b/docs/src/examples/components/Input/Variations/InputExampleInputSlot.shorthand.tsx @@ -12,13 +12,13 @@ const InputExampleInputSlot = () => ( placeholder="Search..." role="presentation" input={{ - // override component's 'placeholder' attribute + // will override component's 'placeholder' attribute placeholder: 'Placeholder Override...', - // override component's 'role' attribute + // will override component's 'role' attribute role: 'checkbox', - // set custom styles for input DOM element + // will set custom styles for input DOM element styles: inputStyles, }} /> diff --git a/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx index 65ea0be674..798b1dca3f 100644 --- a/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx +++ b/docs/src/examples/components/Input/Variations/InputExampleTargeting.shorthand.tsx @@ -1,6 +1,7 @@ import React from 'react' import { Grid, Input, Text } from '@stardust-ui/react' +// This object contains properties that will be applied to the input const propsForInput = { placeholder: 'Search...', id: 'inputId', role: 'checkbox' } const propsTargettingWrapper = { placeholder: 'Wrapper placeholder...', @@ -8,7 +9,8 @@ const propsTargettingWrapper = { role: 'presentation', } -const propsForWrapper = { dir: 'ltr', tabIndex: 2, styles: { padding: '5px', background: 'red' } } +// This object contains properties that will be applied to the wrapper +const propsForWrapper = { dir: 'ltr', tabIndex: -1, styles: { padding: '5px', background: 'red' } } const propsTargettingInput = { dir: 'rtl', tabIndex: 0, diff --git a/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx b/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx index 96dcc49f36..d6a65cda35 100644 --- a/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx +++ b/docs/src/examples/components/Input/Variations/InputExampleWrapperSlot.shorthand.tsx @@ -6,25 +6,20 @@ const InputExampleWrapperSlot = () => ( @@ -32,19 +27,17 @@ const InputExampleWrapperSlot = () => ( } + wrapper={} /> } + wrapper={} /> ) diff --git a/docs/src/examples/components/Input/Variations/index.tsx b/docs/src/examples/components/Input/Variations/index.tsx index 553f6b7e2b..b43476677b 100644 --- a/docs/src/examples/components/Input/Variations/index.tsx +++ b/docs/src/examples/components/Input/Variations/index.tsx @@ -31,7 +31,7 @@ const Variations = () => ( /> ({ backgroundColor: siteVars.white })} /> ) diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 1583804f85..7db66e2cb2 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -53,7 +53,7 @@ export interface IInputState { * - if input is search, then use "role='search'" */ class Input extends AutoControlledComponent, IInputState> { - private inputRef: HTMLInputElement + private inputDomElement: HTMLInputElement static className = 'ui-input' @@ -132,7 +132,7 @@ class Input extends AutoControlledComponent, IInputState variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), /** Shorthand for the wrapper component. */ - wrapper: customPropTypes.itemShorthand, + wrapper: customPropTypes.wrapperShorthand, } static defaultProps = { @@ -160,7 +160,11 @@ class Input extends AutoControlledComponent, IInputState className: cx(Input.className, className), children: ( <> - + + (this.inputDomElement = inputDomElement as HTMLInputElement) + } + > {Slot.createHTMLInput(input || type, { defaultProps: { ...htmlInputProps, @@ -189,12 +193,10 @@ class Input extends AutoControlledComponent, IInputState }) } - private handleInputRef = (c: HTMLInputElement) => (this.inputRef = c) - private handleIconOverrides = predefinedProps => ({ onClick: (e: React.SyntheticEvent) => { this.handleOnClear() - this.inputRef.focus() + this.inputDomElement.focus() _.invoke(predefinedProps, 'onClick', e, this.props) }, ...(predefinedProps.onClick && { tabIndex: '0' }), diff --git a/src/lib/customPropTypes.tsx b/src/lib/customPropTypes.tsx index 82a15a7c7b..396c55c90c 100644 --- a/src/lib/customPropTypes.tsx +++ b/src/lib/customPropTypes.tsx @@ -336,6 +336,12 @@ export const multipleProp = possible => (props, propName, componentName) => { */ export const contentShorthand = every([disallow(['children']), PropTypes.node]) +export const wrapperShorthand = PropTypes.oneOfType([ + PropTypes.node, + PropTypes.object, + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.node, PropTypes.object])), +]) + /** * Item shorthand is a description of a component that can be a literal, * a props object, or an element. diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index 90da36ab0d..5f65f7188d 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -12,20 +12,22 @@ export interface IInputVariables { inputFocusBorderRadius: string } -const [px2asRem, px3asRem, px6asRem, px12asRem, px24asRem] = [2, 3, 6, 12, 24].map(v => pxToRem(v)) +const [_2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem, _24px_asRem] = [2, 3, 6, 12, 24].map(v => + pxToRem(v), +) export default (siteVars): IInputVariables => ({ - borderRadius: px3asRem, - borderBottom: `${px2asRem} solid transparent`, + borderRadius: _3px_asRem, + borderBottom: `${_2px_asRem} solid transparent`, backgroundColor: siteVars.gray10, fontColor: siteVars.bodyColor, fontSize: siteVars.fontSizes.medium, iconPosition: 'absolute', - iconRight: pxToRem(2), + iconRight: _2px_asRem, - inputPadding: `${px6asRem} ${px24asRem} ${px6asRem} ${px12asRem}`, + inputPadding: `${_6px_asRem} ${_24px_asRem} ${_6px_asRem} ${_12px_asRem}`, inputFocusBorderColor: siteVars.brand, - inputFocusBorderRadius: `${px3asRem} ${px3asRem} ${px2asRem} ${px2asRem}`, + inputFocusBorderRadius: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`, }) diff --git a/test/specs/commonTests/implementsShorthandProp.tsx b/test/specs/commonTests/implementsShorthandProp.tsx index 5d72926e1f..5a8fea42c4 100644 --- a/test/specs/commonTests/implementsShorthandProp.tsx +++ b/test/specs/commonTests/implementsShorthandProp.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { mount, ReactWrapper } from 'enzyme' +import { IProps } from '../../../types/utils' export type ShorthandTestOptions = { mapsValueToProp?: string @@ -18,19 +19,22 @@ export default Component => { const { mapsValueToProp } = options const { displayName } = ShorthandComponent - /** - * @param {ReactWrapper} wrapper [mounted Component] - * @param {(n: ReactWrapper) => boolean} propsCheckFn [boolean function that applies a boolean check on a mounted ShorthandComponent] - * return true if the ShorthandComponent that matches propsCheckFn is found - */ - const testShorthandComponentProps = ( - wrapper: ReactWrapper, - propsCheckFn: (n: ReactWrapper) => boolean, - ) => { - const shorthandComponent = wrapper.findWhere( - n => n.type() === ShorthandComponent && propsCheckFn(n), - ) - expect(shorthandComponent.length).toEqual(1) + const checkPropsMatch = (props: IProps, matchedProps: IProps) => + Object.keys(matchedProps).every(propName => matchedProps[propName] === props[propName]) + + const expectContainsSingleShorthandElement = (wrapper: ReactWrapper, withProps: IProps) => + expect( + wrapper.findWhere( + node => node.type() === ShorthandComponent && checkPropsMatch(node.props(), withProps), + ).length, + ).toEqual(1) + + const expectShorthandPropsAreHandled = (withProps: IProps | string) => { + const props = { [shorthandProp]: withProps } + const matchedProps = + typeof withProps === 'string' ? { [mapsValueToProp]: withProps } : withProps + + expectContainsSingleShorthandElement(mount(), matchedProps) } describe(`shorthand property '${shorthandProp}' with default value of '${displayName}' component`, () => { @@ -39,26 +43,11 @@ export default Component => { }) test(`string value is handled as ${displayName}'s ${mapsValueToProp}`, () => { - const shorthandPropValue = 'shorthand prop value' - const props = { [shorthandProp]: shorthandPropValue } - - testShorthandComponentProps( - mount(), - n => n.prop(mapsValueToProp) === shorthandPropValue, - ) + expectShorthandPropsAreHandled('shorthand prop value') }) test(`object value is spread as ${displayName}'s props`, () => { - const ShorthandValue = { foo: 'foo value', bar: 'bar value' } - const props = { [shorthandProp]: ShorthandValue } - const checkPropsAreSpread = (props: {}) => - Object.keys(ShorthandValue).every( - propName => ShorthandValue[propName] === props[propName], - ) - - testShorthandComponentProps(mount(), n => - checkPropsAreSpread(n.props()), - ) + expectShorthandPropsAreHandled({ foo: 'foo value', bar: 'bar value' }) }) }) } diff --git a/test/specs/commonTests/implementsWrapperProp.tsx b/test/specs/commonTests/implementsWrapperProp.tsx index c0593fc6c4..3efbccaa78 100644 --- a/test/specs/commonTests/implementsWrapperProp.tsx +++ b/test/specs/commonTests/implementsWrapperProp.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { mount, ReactWrapper } from 'enzyme' import Slot from 'src/components/Slot' -import Segment from 'src/components/Segment' import { ShorthandValue } from 'utils' export interface ImplementsWrapperPropOptions { @@ -22,12 +21,6 @@ const implementsWrapperProp =

( } describe('"wrapper" prop', () => { - it('does not wrap the component when set to false', () => { - const wrapper = mount().find(wrappperComponentSelector) - - expect(wrapper.length).toEqual(0) - }) - it('wraps the component by default', () => { wrapperTests(mount().find(wrappperComponentSelector)) }) @@ -37,22 +30,7 @@ const implementsWrapperProp =

( }) it('wraps the component with a custom element using "as" prop', () => { - const customElem = 'p' - wrapperTests(mount().find(customElem)) - }) - - it('wraps the component with another stardust component as wrapper', () => { - wrapperTests(mount(} />).find(Segment)) - }) - - it('wraps the component with a custom component as wrapper', () => { - class MyComponent extends React.Component { - render() { - return

{this.props.children}
- } - } - - wrapperTests(mount(} />).find(MyComponent)) + wrapperTests(mount().find('p')) }) }) } diff --git a/test/specs/components/Slot/Slot-test.ts b/test/specs/components/Slot/Slot-test.ts index 545ddf03a5..1241df7774 100644 --- a/test/specs/components/Slot/Slot-test.ts +++ b/test/specs/components/Slot/Slot-test.ts @@ -1,8 +1,6 @@ import { mount } from 'enzyme' -import * as lib from 'src/lib' import Slot from 'src/components/Slot' -import { createSlotFactory } from 'src/components/Slot/Slot' import { isConformant } from 'test/specs/commonTests' describe('Slot', () => { @@ -13,42 +11,6 @@ describe('Slot', () => { isConformant(Slot, { exportedAtTopLevel: false }) }) - describe('createSlotFactory', () => { - it('calls createShorthand from lib', () => { - const mapValueToProp = mappedProp => ({ mappedProp }) - const value = 'testValue' - const createShorthandSpy = spyOn(lib, 'createShorthand') - createSlotFactory('span', mapValueToProp)(value) - - expect(createShorthandSpy).toHaveBeenCalledWith( - Slot, - mapValueToProp, - value, - expect.any(Object), - ) - }) - - it('sets correct "as" prop in defaultProps', () => { - const as = 'span' - const options = { testOption: 'test option value' } - const createShorthandSpy = spyOn(lib, 'createShorthand') - createSlotFactory(as, () => ({}))('testValue', { ...options }) // clone of the options - - const optionsArg = createShorthandSpy.calls.mostRecent().args[3] - expect(optionsArg).toEqual({ defaultProps: { as }, ...options }) - }) - - it('overrides "as" prop in defaultProps', () => { - const as = 'span' - const asOverride = 'p' - const createShorthandSpy = spyOn(lib, 'createShorthand') - createSlotFactory(as, () => ({}))('testValue', { defaultProps: { as: asOverride } }) // clone of the options - - const optionsArg = createShorthandSpy.calls.mostRecent().args[3] - expect(optionsArg).toEqual({ defaultProps: { as: asOverride } }) - }) - }) - it(`create renders a ${Slot.defaultProps.as} element with content prop`, () => { const testContent = 'test content' const slot = createSlot(Slot.create, testContent)