Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add native component HTMLTextInput #16226

Merged
merged 3 commits into from
Jun 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/components/src/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export { default as withSpokenMessages } from './higher-order/with-spoken-messag

// Mobile Components
export { default as BottomSheet } from './mobile/bottom-sheet';
export { default as Picker } from './mobile/picker';
export { default as HTMLTextInput } from './mobile/html-text-input';
export { default as KeyboardAvoidingView } from './mobile/keyboard-avoiding-view';
export { default as KeyboardAwareFlatList } from './mobile/keyboard-aware-flat-list';
export { default as Picker } from './mobile/picker';
export { default as ReadableContentView } from './mobile/readable-content-view';
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { ScrollView } from 'react-native';

/**
* Internal dependencies
*/
import KeyboardAvoidingView from '../keyboard-avoiding-view';
import styles from './style.android.scss';

const HTMLInputContainer = ( { children, parentHeight } ) => (
<KeyboardAvoidingView style={ styles.keyboardAvoidingView } parentHeight={ parentHeight }>
<ScrollView style={ styles.scrollView } >
{ children }
</ScrollView>
</KeyboardAvoidingView>
);

HTMLInputContainer.scrollEnabled = false;

export default HTMLInputContainer;
50 changes: 50 additions & 0 deletions packages/components/src/mobile/html-text-input/container.ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* External dependencies
*/
import { UIManager, PanResponder } from 'react-native';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';

/**
* Internal dependencies
*/
import KeyboardAvoidingView from '../keyboard-avoiding-view';
import styles from './style.ios.scss';

class HTMLInputContainer extends Component {
constructor() {
super( ...arguments );

this.panResponder = PanResponder.create( {
onStartShouldSetPanResponderCapture: () => true,

onPanResponderMove: ( e, gestureState ) => {
if ( gestureState.dy > 100 && gestureState.dy < 110 ) {
//Keyboard.dismiss() and this.textInput.blur() are not working here
//They require to know the currentlyFocusedID under the hood but
//during this gesture there's no currentlyFocusedID
UIManager.blur( e.target );
}
},
} );
}

render() {
return (
<KeyboardAvoidingView
style={ styles.keyboardAvoidingView }
{ ...this.panResponder.panHandlers }
parentHeight={ this.props.parentHeight }
>
{ this.props.children }
</KeyboardAvoidingView>
);
}
}

HTMLInputContainer.scrollEnabled = true;

export default HTMLInputContainer;
115 changes: 115 additions & 0 deletions packages/components/src/mobile/html-text-input/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* External dependencies
*/
import { TextInput } from 'react-native';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { parse } from '@wordpress/blocks';
import { withDispatch, withSelect } from '@wordpress/data';
import { withInstanceId, compose } from '@wordpress/compose';

/**
* Internal dependencies
*/
import HTMLInputContainer from './container';
import styles from './style.scss';

export class HTMLTextInput extends Component {
constructor() {
super( ...arguments );

this.edit = this.edit.bind( this );
this.stopEditing = this.stopEditing.bind( this );

this.state = {
isDirty: false,
value: '',
};
}

static getDerivedStateFromProps( props, state ) {
if ( state.isDirty ) {
return null;
}

return {
value: props.value,
isDirty: false,
};
}

componentWillUnmount() {
//TODO: Blocking main thread
this.stopEditing();
}

edit( html ) {
this.props.onChange( html );
this.setState( { value: html, isDirty: true } );
}

stopEditing() {
if ( this.state.isDirty ) {
this.props.onPersist( this.state.value );
this.setState( { isDirty: false } );
}
}

render() {
return (
<HTMLInputContainer parentHeight={ this.props.parentHeight }>
<TextInput
autoCorrect={ false }
accessibilityLabel="html-view-title"
textAlignVertical="center"
numberOfLines={ 1 }
style={ styles.htmlViewTitle }
value={ this.props.title }
placeholder={ __( 'Add title' ) }
onChangeText={ this.props.setTitleAction }
/>
<TextInput
autoCorrect={ false }
accessibilityLabel="html-view-content"
textAlignVertical="top"
multiline
style={ styles.htmlView }
value={ this.state.value }
onChangeText={ this.edit }
onBlur={ this.stopEditing }
placeholder={ __( 'Start writing…' ) }
scrollEnabled={ HTMLInputContainer.scrollEnabled }
/>
</HTMLInputContainer>
);
}
}

export default compose( [
withSelect( ( select ) => {
const {
getEditedPostContent,
} = select( 'core/editor' );

return {
value: getEditedPostContent(),
};
} ),
withDispatch( ( dispatch ) => {
const { resetBlocks } = dispatch( 'core/block-editor' );
const { editPost } = dispatch( 'core/editor' );
return {
onChange( content ) {
editPost( { content } );
},
onPersist( content ) {
resetBlocks( parse( content ) );
},
};
} ),
withInstanceId,
] )( HTMLTextInput );
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
$padding: 8;
$backgroundColor: $white;
$htmlFont: $default-monospace-font;

.keyboardAvoidingView {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
}

.container {
flex: 1;
}
23 changes: 23 additions & 0 deletions packages/components/src/mobile/html-text-input/style.android.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@import "./style-common.scss";

.htmlView {
font-family: $htmlFont;
background-color: $backgroundColor;
padding-left: $padding;
padding-right: $padding;
padding-top: $padding;
padding-bottom: $padding + 16;
}

.htmlViewTitle {
font-family: $htmlFont;
background-color: $backgroundColor;
padding-left: $padding;
padding-right: $padding;
padding-top: $padding;
padding-bottom: $padding;
}

.scrollView {
flex: 1;
}
21 changes: 21 additions & 0 deletions packages/components/src/mobile/html-text-input/style.ios.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@import "./style-common.scss";

$title-height: 32;

.htmlView {
font-family: $htmlFont;
background-color: $backgroundColor;
padding-left: $padding;
padding-right: $padding;
padding-bottom: $title-height + $padding;
}

.htmlViewTitle {
font-family: $htmlFont;
background-color: $backgroundColor;
padding-left: $padding;
padding-right: $padding;
padding-top: $padding;
padding-bottom: $padding;
height: $title-height;
}
116 changes: 116 additions & 0 deletions packages/components/src/mobile/html-text-input/test/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* External dependencies
*/
import { shallow } from 'enzyme';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { HTMLTextInput } from '..';

// Utility to find a TextInput in a ShallowWrapper
const findTextInputInWrapper = ( wrapper, matchingProps ) => {
return wrapper.dive().findWhere( ( node ) => {
return node.name() === 'TextInput' && node.is( matchingProps );
} ).first();
};

// Finds the Content TextInput in our HTMLInputView
const findContentTextInput = ( wrapper ) => {
const placeholder = __( 'Start writing…' );
const matchingProps = { multiline: true, placeholder };
return findTextInputInWrapper( wrapper, matchingProps );
};

// Finds the Title TextInput in our HTMLInputView
const findTitleTextInput = ( wrapper ) => {
const placeholder = __( 'Add title' );
return findTextInputInWrapper( wrapper, { placeholder } );
};

describe( 'HTMLTextInput', () => {
it( 'HTMLTextInput renders', () => {
const wrapper = shallow(
<HTMLTextInput />
);
expect( wrapper ).toBeTruthy();
} );

it( 'HTMLTextInput updates store and state on HTML text change', () => {
const onChange = jest.fn();

const wrapper = shallow(
<HTMLTextInput
onChange={ onChange }
/>
);

expect( wrapper.instance().state.isDirty ).toBeFalsy();

// Simulate user typing text
const htmlTextInput = findContentTextInput( wrapper );
htmlTextInput.simulate( 'changeText', 'text' );

//Check if the onChange is called and the state is updated
expect( onChange ).toHaveBeenCalledTimes( 1 );
expect( onChange ).toHaveBeenCalledWith( 'text' );

expect( wrapper.instance().state.isDirty ).toBeTruthy();
expect( wrapper.instance().state.value ).toEqual( 'text' );
} );

it( 'HTMLTextInput persists changes in HTML text input on blur', () => {
const onPersist = jest.fn();

const wrapper = shallow(
<HTMLTextInput
onPersist={ onPersist }
onChange={ jest.fn() }
/>
);

// Simulate user typing text
const htmlTextInput = findContentTextInput( wrapper );
htmlTextInput.simulate( 'changeText', 'text' );

//Simulate blur event
htmlTextInput.simulate( 'blur' );

//Normally prop.value is updated with the help of withSelect
//But we don't have it in tests so we just simulate it
wrapper.setProps( { value: 'text' } );

//Check if the onPersist is called and the state is updated
expect( onPersist ).toHaveBeenCalledTimes( 1 );
expect( onPersist ).toHaveBeenCalledWith( 'text' );

expect( wrapper.instance().state.isDirty ).toBeFalsy();

//We expect state.value is getting propagated from prop.value
expect( wrapper.instance().state.value ).toEqual( 'text' );
} );

it( 'HTMLTextInput propagates title changes to store', () => {
const setTitleAction = jest.fn();

const wrapper = shallow(
<HTMLTextInput
setTitleAction={ setTitleAction }
/>
);

// Simulate user typing text
const textInput = findTitleTextInput( wrapper );
textInput.simulate( 'changeText', 'text' );

//Check if the setTitleAction is called
expect( setTitleAction ).toHaveBeenCalledTimes( 1 );
expect( setTitleAction ).toHaveBeenCalledWith( 'text' );
} );
} );