-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(popups): Add DraftJS Editor (#462)
* feat(popups): Add DraftJS Editor * feat(popups): Save Cursor position * feat(popups): Address comments * feat(popups): Address comments * feat(popups): Address comments * feat(popups): draft-js
- Loading branch information
Mingze
authored
Apr 29, 2020
1 parent
8ca58f9
commit ff35c5c
Showing
21 changed files
with
265 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
@import '~draft-js/dist/Draft.css'; | ||
|
||
.ba-ReplyField { | ||
div[contentEditable='true'] { | ||
&, | ||
&:disabled, | ||
&:hover, | ||
&:focus { | ||
padding: 0; | ||
border: none; | ||
box-shadow: none; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,92 @@ | ||
import * as React from 'react'; | ||
import classnames from 'classnames'; | ||
import { Editor, EditorState, ContentState, SelectionState } from 'draft-js'; | ||
|
||
import './ReplyField.scss'; | ||
|
||
export type Props = { | ||
className?: string; | ||
disabled?: boolean; | ||
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void; | ||
cursorPosition: number; | ||
isDisabled?: boolean; | ||
onChange: (text?: string) => void; | ||
onClick: (event: React.SyntheticEvent) => void; | ||
onMention?: (mention: string) => void; | ||
placeholder?: string; | ||
setCursorPosition: (cursorPosition: number) => void; | ||
value?: string; | ||
}; | ||
|
||
const ReplyField = (props: Props, ref: React.Ref<HTMLTextAreaElement>): JSX.Element => { | ||
const { className, ...rest } = props; | ||
|
||
return <textarea ref={ref} className={classnames('ba-TextArea', className)} {...rest} />; | ||
export type State = { | ||
editorState: EditorState; | ||
}; | ||
|
||
export default React.forwardRef(ReplyField); | ||
export default class ReplyField extends React.Component<Props, State> { | ||
editorRef: React.MutableRefObject<Editor | null> = React.createRef<Editor | null>(); | ||
|
||
constructor(props: Props) { | ||
super(props); | ||
|
||
const { value, cursorPosition } = props; | ||
const contentState = ContentState.createFromText(value || ''); | ||
let prevEditorState = EditorState.createWithContent(contentState); | ||
let selectionState = prevEditorState.getSelection(); | ||
|
||
selectionState = selectionState.merge({ | ||
anchorOffset: cursorPosition, | ||
focusOffset: cursorPosition, | ||
}) as SelectionState; | ||
prevEditorState = EditorState.forceSelection(prevEditorState, selectionState); | ||
|
||
this.state = { | ||
editorState: prevEditorState, | ||
}; | ||
} | ||
|
||
componentDidMount(): void { | ||
this.focusEditor(); | ||
} | ||
|
||
componentWillUnmount(): void { | ||
this.saveCursorPosition(); | ||
} | ||
|
||
focusEditor = (): void => { | ||
const { current: editor } = this.editorRef; | ||
if (editor) { | ||
editor.focus(); | ||
} | ||
}; | ||
|
||
saveCursorPosition = (): void => { | ||
const { setCursorPosition } = this.props; | ||
const { editorState } = this.state; | ||
|
||
setCursorPosition(editorState.getSelection().getFocusOffset()); | ||
}; | ||
|
||
handleChange = (nextEditorState: EditorState): void => { | ||
const { onChange } = this.props; | ||
|
||
this.setState({ editorState: nextEditorState }); | ||
onChange(nextEditorState.getCurrentContent().getPlainText()); | ||
}; | ||
|
||
render(): JSX.Element { | ||
const { className, isDisabled, placeholder, ...rest } = this.props; | ||
const { editorState } = this.state; | ||
|
||
return ( | ||
<div className={classnames(className, 'ba-ReplyField')}> | ||
<Editor | ||
{...rest} | ||
ref={this.editorRef} | ||
editorState={editorState} | ||
onChange={this.handleChange} | ||
placeholder={placeholder} | ||
readOnly={isDisabled} | ||
stripPastedStyles | ||
/> | ||
</div> | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { connect } from 'react-redux'; | ||
import { AppState, getCreatorCursor, setCursorAction } from '../../../store'; | ||
import ReplyField from './ReplyField'; | ||
|
||
export type Props = { | ||
cursorPosition: number; | ||
}; | ||
|
||
export const mapStateToProps = (state: AppState): Props => ({ | ||
cursorPosition: getCreatorCursor(state), | ||
}); | ||
|
||
export const mapDispatchToProps = { | ||
setCursorPosition: setCursorAction, | ||
}; | ||
|
||
export default connect(mapStateToProps, mapDispatchToProps)(ReplyField); |
72 changes: 59 additions & 13 deletions
72
src/components/Popups/ReplyField/__tests__/ReplyField-test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,82 @@ | ||
import React from 'react'; | ||
import { Editor, EditorState } from 'draft-js'; | ||
import { shallow, ShallowWrapper } from 'enzyme'; | ||
import ReplyField, { Props } from '../ReplyField'; | ||
import ReplyField, { Props, State } from '../ReplyField'; | ||
|
||
describe('components/Popups/ReplyField', () => { | ||
const defaults: Props = { | ||
className: 'ba-Popup-text', | ||
disabled: false, | ||
className: 'ba-Popup-field', | ||
cursorPosition: 0, | ||
isDisabled: false, | ||
onChange: jest.fn(), | ||
onClick: jest.fn(), | ||
setCursorPosition: jest.fn(), | ||
value: '', | ||
}; | ||
|
||
const getWrapper = (props = {}): ShallowWrapper => shallow(<ReplyField {...defaults} {...props} />); | ||
const getWrapper = (props = {}): ShallowWrapper<Props, State, ReplyField> => | ||
shallow(<ReplyField {...defaults} {...props} />); | ||
|
||
const mockEditorState = ({ | ||
getCurrentContent: () => ({ getPlainText: () => 'test' }), | ||
getSelection: () => ({ getFocusOffset: () => 0 }), | ||
} as unknown) as EditorState; | ||
|
||
const mockEditor = ({ | ||
focus: jest.fn(), | ||
} as unknown) as Editor; | ||
|
||
describe('render()', () => { | ||
test('should render the editor with right props', () => { | ||
const wrapper = getWrapper(); | ||
|
||
expect(wrapper.prop('className')).toBe('ba-Popup-field ba-ReplyField'); | ||
}); | ||
}); | ||
|
||
describe('event handlers', () => { | ||
test('should call onChange event', () => { | ||
test('should handle the editor onChange event', () => { | ||
const wrapper = getWrapper(); | ||
const editor = wrapper.find(Editor); | ||
|
||
editor.simulate('change', mockEditorState); | ||
|
||
expect(defaults.onChange).toBeCalledWith('test'); | ||
}); | ||
|
||
test('should handle the editor onClick event', () => { | ||
const wrapper = getWrapper(); | ||
const textarea = wrapper.find('textarea'); | ||
const editor = wrapper.find(Editor); | ||
|
||
textarea.simulate('change', 'test'); | ||
textarea.simulate('click', 'test'); | ||
editor.simulate('click', 'test'); | ||
|
||
expect(defaults.onChange).toHaveBeenCalledWith('test'); | ||
expect(defaults.onClick).toHaveBeenCalledWith('test'); | ||
expect(defaults.onClick).toBeCalledWith('test'); | ||
}); | ||
}); | ||
|
||
describe('render()', () => { | ||
test('should render the textarea with right props', () => { | ||
describe('focusEditor()', () => { | ||
test('should call editor ref focus', () => { | ||
const wrapper = getWrapper(); | ||
const instance = wrapper.instance(); | ||
|
||
instance.focusEditor(); | ||
expect(mockEditor.focus).not.toBeCalled(); | ||
|
||
instance.editorRef.current = mockEditor; | ||
instance.focusEditor(); | ||
|
||
expect(mockEditor.focus).toBeCalled(); | ||
}); | ||
}); | ||
|
||
describe('saveCursorPosition()', () => { | ||
test('should call setCursorPosition with cursor position', () => { | ||
const wrapper = getWrapper(); | ||
wrapper.setState({ editorState: mockEditorState }); | ||
|
||
wrapper.instance().saveCursorPosition(); | ||
|
||
expect(wrapper.prop('className')).toBe('ba-TextArea ba-Popup-text'); | ||
expect(defaults.setCursorPosition).toBeCalledWith(0); | ||
}); | ||
}); | ||
}); |
34 changes: 34 additions & 0 deletions
34
src/components/Popups/ReplyField/__tests__/ReplyFieldContainer-test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import * as React from 'react'; | ||
import { mount, ReactWrapper } from 'enzyme'; | ||
import ReplyField from '../ReplyField'; | ||
import ReplyFieldContainer, { Props } from '../ReplyFieldContainer'; | ||
import { createStore } from '../../../../store'; | ||
|
||
jest.mock('../ReplyField'); | ||
|
||
describe('components/Popups/ReplyFieldContainer', () => { | ||
const store = createStore({ | ||
creator: { | ||
cursor: 0, | ||
}, | ||
}); | ||
const defaults = { | ||
onChange: jest.fn(), | ||
onClick: jest.fn(), | ||
store, | ||
}; | ||
|
||
const getWrapper = (props = {}): ReactWrapper<Props> => mount(<ReplyFieldContainer {...defaults} {...props} />); | ||
|
||
describe('render', () => { | ||
test('should connect the underlying component', () => { | ||
const wrapper = getWrapper(); | ||
|
||
expect(wrapper.find(ReplyField).props()).toMatchObject({ | ||
cursorPosition: 0, | ||
onChange: defaults.onChange, | ||
onClick: defaults.onClick, | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export { default } from './ReplyField'; | ||
export { default } from './ReplyFieldContainer'; |
Oops, something went wrong.