Skip to content

Commit

Permalink
ui-components: integration of modal in native
Browse files Browse the repository at this point in the history
Integration of a simple modal component in the native part, so it can be later reused in the different apps.

Fixes #16
  • Loading branch information
Borislav Gadjev authored and mgenov committed Mar 13, 2018
1 parent 26702c2 commit 5996216
Show file tree
Hide file tree
Showing 8 changed files with 416 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
TEST-REACT-NATIVE.xml
package-lock.json
TEST-RESULTS-NATIVE.xml
1 change: 1 addition & 0 deletions native/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as Alert } from './src/Alert'
export { default as Icon } from './src/Icon'
export { default as Modal } from './src/Modal'
2 changes: 1 addition & 1 deletion native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
},
"jest-junit": {
"suiteName": "ui-components-native",
"output": "./TEST-REACT-NATIVE.xml",
"output": "./TEST-RESULTS-NATIVE.xml",
"classNameTemplate": "{classname}-{title}",
"titleTemplate": "{classname}-{title}",
"usePathForSuiteName": "true"
Expand Down
138 changes: 138 additions & 0 deletions native/src/Modal/Modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from 'react'
import { Modal as RNModal, View, ScrollView, TouchableOpacity } from 'react-native'
import PropTypes from 'prop-types'
import Icon from '../Icon'

const Body = ({ children }) => children

Body.propTypes = {
children: PropTypes.any.isRequired
}

class Trigger extends React.Component {
render() {
return React.Children.map(this.props.children, child => {
const onPressCall = child.props.onPress
return React.cloneElement(child, {
onPress: () => {
if (typeof onPressCall === 'function') {
if (onPressCall.then !== undefined) {
onPressCall()
.then(result => {
this.props.onShowDialog()
return Promise.resolve(result)
})
.catch(error => {
return Promise.reject(error)
})
} else {
onPressCall()
this.props.onShowDialog()
}
} else {
this.props.onShowDialog()
}
}
})
})
}
}
Trigger.propTypes = {
children: PropTypes.any.isRequired,
onShowDialog: PropTypes.func
}

class Modal extends React.Component {
static Body = Body
static Trigger = Trigger

constructor(props) {
super(props)
this.state = {
visible: false
}
}

open() {
this.setState({ visible: true })
}

close() {
this.setState({ visible: false })
}

componentWillReceiveProps(newProps) {
if (newProps.visible !== this.props.visible) {
this.setState({ visible: newProps.visible })
}
}

render() {
const triggerButton = React.Children.map(this.props.children, child => {
if (child.type === Trigger) {
return React.cloneElement(child, {
onShowDialog: () => {
this.open()
}
})
}
})

const children = React.Children.map(this.props.children, child => {
if (child.type !== Trigger) {
return React.cloneElement(child)
}
})

const { visible } = this.state
return (
<View>
<RNModal
animationType={'fade'}
transparent={true}
visible={visible}
onRequestClose={this.close}
>
<View style={this.props.containerStyles}>
<View style={this.props.bodyStyles}>
{this.props.hasCloseHeader && (
<View style={this.props.headerStyles}>
<TouchableOpacity onPress={() => this.close()}>
<Icon
name={'close'}
/>
</TouchableOpacity>
</View>
)}
<ScrollView>{children}</ScrollView>
</View>
</View>
</RNModal>
{triggerButton}
</View>
)
}
}

Modal.propTypes = {
containerStyles: PropTypes.any,
bodyStyles: PropTypes.any,
children: PropTypes.any,
headerStyles: PropTypes.any,
hasCloseHeader: PropTypes.bool.isRequired,
visible: PropTypes.bool
}

Modal.defaultProps = {
visible: false,
hasCloseHeader: false,
headerStyles: {
flexDirection: 'row',
alignItems: 'flex-end',
margin: 0,
padding: 0,
borderBottomWidth: 0
}
}

export default Modal
71 changes: 71 additions & 0 deletions native/src/Modal/Modal.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react'
import { TouchableOpacity, Text } from 'react-native'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import Modal from './Modal'

describe('(Modal) rendering', () => {
it('should render Modal component', () => {
const wrapper = shallow(
<Modal>
<Modal.Trigger>
<TouchableOpacity onPress={() => {}}>
<Text>trigger</Text>
</TouchableOpacity>
</Modal.Trigger>
<Modal.Body>
<Text>body</Text>
</Modal.Body>
</Modal>
)
expect(toJson(wrapper)).toMatchSnapshot()
})

it('should render Modal component with header', () => {
const wrapper = shallow(
<Modal hasCloseHeader={true} >
<Modal.Trigger>
<TouchableOpacity onPress={() => {}}>
<Text>trigger</Text>
</TouchableOpacity>
</Modal.Trigger>
<Modal.Body>
<Text>body</Text>
</Modal.Body>
</Modal>
)
expect(toJson(wrapper)).toMatchSnapshot()
})

it('should hide modal when close header is pressed', () => {
const wrapper = shallow(
<Modal hasCloseHeader={true} />
)
wrapper.setState({ visible: true })
wrapper
.find(TouchableOpacity)
.first()
.props()
.onPress()
expect(wrapper.state().visible).toBe(false)
})

it('should open modal dialog on press trigger', () => {
const wrapper = shallow(
<Modal>
<Modal.Trigger>
<TouchableOpacity onPress={() => {}}>
<Text>test</Text>
</TouchableOpacity>
</Modal.Trigger>
</Modal>
)
wrapper.setState({ visible: false })
wrapper
.find(Modal.Trigger)
.first()
.props()
.onShowDialog()
expect(wrapper.state().visible).toBe(true)
})
})
84 changes: 84 additions & 0 deletions native/src/Modal/Modal.story.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react'
import Modal from './Modal'
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native'
import { storiesOf } from '@storybook/react-native'
import { action } from '@storybook/addon-actions'
import { linkTo } from '@storybook/addon-links'

storiesOf('Modal', module)
.add('Example Modal with Trigger', () => (
<Modal
containerStyles={styles.containerStyles}
bodyStyles={styles.bodyStyles}
hasCloseHeader={true}
>
<Modal.Trigger>
<TouchableOpacity style={styles.trigger} onPress={() => {}}>
<Text style={styles.triggerText} >Open</Text>
</TouchableOpacity>
</Modal.Trigger>
<Modal.Body>
<Text>body</Text>
</Modal.Body>
</Modal>
))
.add('Example Modal with reference', () => (
<View>
<Modal
containerStyles={styles.containerStyles}
bodyStyles={styles.bodyStyles}
ref={ref => (this.modal = ref)}
>
<Modal.Body>
<Text>body</Text>
<TouchableOpacity style={styles.trigger} onPress={() => this.modal.close()}>
<Text style={styles.triggerText} >Close</Text>
</TouchableOpacity>
</Modal.Body>
</Modal>
<View style={styles.containerColumn}>
<TouchableOpacity style={styles.trigger} onPress={() => this.modal.show()}>
<Text style={styles.triggerText} >Open</Text>
</TouchableOpacity>
</View>
</View>
))


const styles = StyleSheet.create({
containerColumn: {
flex: 1,
alignItems: 'center',
marginTop: 'auto',
marginBottom: 'auto'
},
containerStyles: {
flex: 1,
backgroundColor: 'rgb(236, 240, 241)'
},
bodyStyles: {
marginTop: 'auto',
marginBottom: 'auto',
marginLeft: 20,
marginRight: 20,
backgroundColor: 'rgb(255, 255, 255)',
borderTopWidth: 5,
borderRadius: 5,
borderColor: 'rgb(34, 167, 240)',
alignItems: 'center'
},
trigger: {
width: 100,
height: 50,
alignItems: 'center',
justifyContent: 'space-around',
margin: 5,
borderRadius: 5,
backgroundColor: 'rgb(34, 167, 240)'
},
triggerText: {
color: 'rgb(228, 241, 254)'
}
})

export default styles
Loading

0 comments on commit 5996216

Please sign in to comment.