Skip to content

Commit 47ddeab

Browse files
authored
Feat/caret position (#35)
* feat: added options for output * fix: fixed flow * fix: eslint * test: added cypress tests for the new feats * fix: lint * chore(deps): updated deps * docs: updated readme * fix: lint * test
1 parent 59e6644 commit 47ddeab

File tree

13 files changed

+576
-252
lines changed

13 files changed

+576
-252
lines changed

.eslintignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
dist/
1+
example/

.eslintrc

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
{
2-
"parser": "babel-eslint",
3-
"extends": ["airbnb", "plugin:flowtype/recommended"],
4-
"plugins": ["react", "jsx-a11y", "flowtype"],
2+
"extends": ["callstack-io"],
53
"rules": {
64
"react/jsx-filename-extension": [
75
1,
@@ -10,8 +8,14 @@
108
}
119
],
1210
"no-underscore-dangle": 0,
11+
"no-console": 0,
12+
"flowtype/no-weak-types": 0,
1313
"react/forbid-prop-types": [0, { "forbid": ["object"] }]
1414
},
15+
"globals": {
16+
"cy": true,
17+
"context": true
18+
},
1519
"env": {
1620
"es6": true,
1721
"browser": true,

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ export default App;
100100

101101
```javascript
102102
{
103-
[triggerChar: string]: {}
104-
?output: (item: Object | string, trigger?: string) => string,
103+
[triggerChar: string]: {|
104+
?output: (item: Object | string, trigger?: string) => {| text: string, caretPosition: "start" | "end" | number |} | string,
105105
dataProvider: (token: string) => Promise<Array<Object | string>> | Array<Object | string>,
106106
component: ReactClass<*>,
107107
|},
@@ -112,7 +112,9 @@ export default App;
112112
- **component** is the component for render the item in suggestion list. It has `selected` and `entity` props provided by React Textarea Autocomplete
113113
- **output** (Optional for string based item. If the item is an object this method is *required*) This function defines text which will be placed into textarea after the user makes a selection.
114114

115-
Default behavior for string based item is string: `<TRIGGER><ITEM><TRIGGER>`). This method should **always** return a unique string.
115+
You can also specify the behavior of caret if you return object `{text: "item", caretPosition: "start"}` the caret will be before the word once the user confirms his selection. Other possible value is "end" and number, which is absolute number in contex of textarea. (0 is equal position before the first char);
116+
117+
Default behavior for string based item is string: `<TRIGGER><ITEM><TRIGGER>`). This method should **always** return a unique string.
116118

117119
## [Example of usage](http://react-textarea-autocomplete.surge.sh/)
118120
`create-react-app example && cd example && yarn add @jukben/emoji-search @webscopeio/react-textarea-autocomplete`

__tests__/__snapshots__/index.spec.js.snap

+6
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ ShallowWrapper {
276276
minChar={1}
277277
onCaretPositionChange={[Function]}
278278
onChange={[Function]}
279+
onSelect={undefined}
279280
placeholder="Write a message."
280281
style={
281282
Object {
@@ -315,6 +316,7 @@ ShallowWrapper {
315316
"minChar": 1,
316317
"onCaretPositionChange": [Function],
317318
"onChange": [Function],
319+
"onSelect": undefined,
318320
"placeholder": "Write a message.",
319321
"style": Object {
320322
"background": "red",
@@ -413,6 +415,7 @@ ShallowWrapper {
413415
minChar={1}
414416
onCaretPositionChange={[Function]}
415417
onChange={[Function]}
418+
onSelect={undefined}
416419
placeholder="Write a message."
417420
style={
418421
Object {
@@ -489,6 +492,7 @@ ShallowWrapper {
489492
minChar={1}
490493
onCaretPositionChange={[Function]}
491494
onChange={[Function]}
495+
onSelect={undefined}
492496
placeholder="Write a message."
493497
style={
494498
Object {
@@ -529,6 +533,7 @@ ShallowWrapper {
529533
"minChar": 1,
530534
"onCaretPositionChange": [Function],
531535
"onChange": [Function],
536+
"onSelect": undefined,
532537
"placeholder": "Write a message.",
533538
"style": Object {
534539
"background": "red",
@@ -628,6 +633,7 @@ ShallowWrapper {
628633
minChar={1}
629634
onCaretPositionChange={[Function]}
630635
onChange={[Function]}
636+
onSelect={undefined}
631637
placeholder="Write a message."
632638
style={
633639
Object {

cypress/integration/simple.js

-32
This file was deleted.

cypress/integration/textarea.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
describe('React Textarea Autocomplete', () => {
2+
it('server is reachable', () => {
3+
cy.visit('http://localhost:8080');
4+
});
5+
6+
it('textarea exists', () => {
7+
cy.get('.rta__textarea');
8+
});
9+
10+
context('basic', () => {
11+
beforeEach(() => {
12+
cy.get('.rta__textarea').clear();
13+
});
14+
15+
it('basic test with keyboard', () => {
16+
cy
17+
.get('.rta__textarea')
18+
.type('This is test :ro{downarrow}{enter}')
19+
.should('have.value', 'This is test 🤣');
20+
});
21+
22+
it('basic test with mouse', () => {
23+
cy
24+
.get('.rta__textarea')
25+
.type('This is test :ro')
26+
.get('li:nth-child(2)')
27+
.click();
28+
29+
cy.get('.rta__textarea').should('have.value', 'This is test 🤣');
30+
});
31+
});
32+
33+
context('advanced', () => {
34+
beforeEach(() => {
35+
cy.get('.rta__textarea').clear();
36+
});
37+
38+
it('should have place caret before outputed word', () => {
39+
cy.get('[data-test="changeCaretOption"]').click();
40+
41+
cy.get('.rta__textarea').type('This is test :ro{downarrow}{downarrow}');
42+
43+
cy.get('.rta__item:nth-child(3)').click();
44+
45+
cy.get('[data-test="actualCaretPosition"]').contains('13');
46+
});
47+
48+
it('should have place caret after word', () => {
49+
cy.get('[data-test="changeCaretOption"]').click();
50+
51+
cy.get('.rta__textarea').type('This is test :ro{downarrow}{downarrow}');
52+
53+
cy.get('.rta__item:nth-child(3)').click();
54+
55+
cy.get('[data-test="actualCaretPosition"]').contains('15'); // emoji is 2 bytes
56+
});
57+
58+
it('set caret position', () => {
59+
cy.get('.rta__textarea').type('This is test :ro{uparrow}{enter}');
60+
cy.get('[data-test="setCaretPosition"]').click();
61+
cy.get('[data-test="actualCaretPosition"]').contains('1');
62+
});
63+
64+
it('get caret position', () => {
65+
const stub = cy.stub();
66+
67+
cy.on('window:alert', stub);
68+
69+
cy.get('.rta__textarea').type('This is test :ro{uparrow}{enter}');
70+
cy
71+
.get('[data-test="getCaretPosition"]')
72+
.click()
73+
.then(() => {
74+
expect(stub.getCall(0)).to.be.calledWith(15);
75+
});
76+
});
77+
});
78+
});

example/App.jsx

+89-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,96 @@
1-
import React, { Component } from 'react';
1+
import React from 'react';
22
import ReactTextareaAutocomplete from '@webscopeio/react-textarea-autocomplete';
33
import emoji from '@jukben/emoji-search';
44

55
// import '@webscopeio/react-textarea-autocomplete/style.css'
66
import '../style.css';
77

8-
const Item = ({ entity: { name, char } }) => <div>{`${name}: ${char}`}</div>;
9-
const Loading = ({ data }) => <div>Loading</div>;
10-
class App extends Component {
8+
type ItemProps = {
9+
entity: {
10+
char: string,
11+
name: string,
12+
},
13+
};
14+
15+
const Item = ({ entity: { name, char } }: ItemProps) => (
16+
<div>{`${name}: ${char}`}</div>
17+
);
18+
19+
type LoadingProps = {
20+
data: Array<{ name: string, char: string }>,
21+
};
22+
23+
const Loading = ({ data }: LoadingProps) => <div>Loading</div>;
24+
25+
class App extends React.Component {
26+
state = {
27+
optionsCaretStart: false,
28+
caretPosition: 0,
29+
text: '',
30+
};
31+
32+
_handleOptionsCaretStart = () => {
33+
this.setState(({ optionsCaretStart }) => ({
34+
optionsCaretStart: !optionsCaretStart,
35+
}));
36+
};
37+
38+
_onChangeHandle = ({ target: { value } }) => {
39+
this.setState({
40+
text: value,
41+
});
42+
};
43+
44+
_onCaretPositionChangeHandle = (position: number) => {
45+
this.setState({
46+
caretPosition: position,
47+
});
48+
};
49+
50+
_setCaretPosition = () => {
51+
this.rtaRef.setCaretPosition(1);
52+
};
53+
54+
_getCaretPosition = () => {
55+
alert(this.rtaRef.getCaretPosition());
56+
};
57+
58+
_getCarePosition = () => {};
59+
60+
_outputCaretEnd = (item, trigger) => item.char;
61+
62+
_outputCaretStart = item => ({ text: item.char, caretPosition: 'start' });
63+
1164
render() {
65+
const { optionsCaretStart, caretPosition, text } = this.state;
66+
1267
return (
1368
<div>
69+
<label>
70+
Place caret before word
71+
<input
72+
data-test="changeCaretOption"
73+
type="checkbox"
74+
defaultChecked={optionsCaretStart}
75+
onChange={this._handleOptionsCaretStart}
76+
/>
77+
</label>
78+
<div>
79+
Actual caret position:{' '}
80+
<span data-test="actualCaretPosition">{caretPosition}</span>
81+
</div>
82+
<button data-test="setCaretPosition" onClick={this._setCaretPosition}>
83+
setCaretPosition(1);
84+
</button>
85+
<button data-test="getCaretPosition" onClick={this._getCaretPosition}>
86+
getCaretPosition();
87+
</button>
88+
1489
<ReactTextareaAutocomplete
15-
className="my-textarea"
90+
className="one"
91+
ref={ref => {
92+
this.rtaRef = ref;
93+
}}
1694
loadingComponent={Loading}
1795
style={{
1896
fontSize: '18px',
@@ -25,15 +103,20 @@ class App extends Component {
25103
height: 100,
26104
margin: '20px auto',
27105
}}
106+
onCaretPositionChange={this._onCaretPositionChangeHandle}
28107
minChar={0}
108+
value={text}
109+
onChange={this._onChangeHandle}
29110
trigger={{
30111
':': {
31112
dataProvider: token =>
32113
emoji(token)
33114
.slice(0, 10)
34115
.map(({ name, char }) => ({ name, char })),
35116
component: Item,
36-
output: (item, trigger) => item.char,
117+
output: optionsCaretStart
118+
? this._outputCaretStart
119+
: this._outputCaretEnd,
37120
},
38121
}}
39122
/>

package.json

+15-14
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"test:watch": "jest --watch",
3838
"flow": "flow",
3939
"flow:check": "flow check",
40-
"cypress": "yarn run dev & cypress run",
40+
"cypress": "yarn run dev & cypress run --headed",
4141
"cypress:open": "cypress open",
4242
"precommit": "lint-staged"
4343
},
@@ -46,37 +46,38 @@
4646
"babel-core": "^6.26.0",
4747
"babel-loader": "^7.1.2",
4848
"babel-polyfill": "^6.26.0",
49-
"babel-preset-react-app": "^3.0.3",
50-
"babel-runtime": "^6.26.0",
49+
"babel-preset-react-app": "^3.1.0",
50+
"babel-runtime": "^6.23.0",
5151
"css-loader": "^0.28.7",
52-
"cypress": "^1.0.2",
52+
"cypress": "^1.1.2",
5353
"enzyme": "^2.9.1",
54-
"eslint": "^4.10.0",
54+
"eslint": "^4.12.1",
5555
"eslint-config-callstack-io": "^1.0.1",
56-
"flow-bin": "^0.57.3",
56+
"flow-bin": "^0.60.1",
5757
"html-webpack-plugin": "^2.30.1",
5858
"husky": "^0.14.3",
5959
"jest": "^21.2.1",
60-
"lint-staged": "^4.3.0",
61-
"prettier": "^1.7.4",
60+
"lint-staged": "^6.0.0",
61+
"prettier": "^1.8.2",
6262
"react": "^15.6.1",
6363
"react-dom": "^15.6.1",
6464
"react-test-renderer": "^15.6.1",
65-
"rollup": "^0.50.0",
65+
"rollup": "^0.52.0",
6666
"rollup-plugin-babel": "^3.0.2",
67-
"rollup-plugin-commonjs": "^8.2.5",
67+
"rollup-plugin-commonjs": "^8.2.6",
6868
"rollup-plugin-license": "^0.5.0",
6969
"rollup-plugin-node-resolve": "^3.0.0",
7070
"rollup-plugin-uglify": "^2.0.1",
7171
"style-loader": "^0.19.0",
72-
"webpack": "^3.8.1",
73-
"webpack-dev-server": "^2.9.3"
72+
"webpack": "^3.9.1",
73+
"webpack-dev-server": "^2.9.5"
7474
},
7575
"peerDependencies": {
76-
"prop-types": ">=15",
77-
"react": ">=15"
76+
"prop-types": "^15.0.0 || ^16.0.0",
77+
"react": "^15.0.0 || ^16.0.0"
7878
},
7979
"dependencies": {
80+
"eslint-plugin-prettier": "^2.3.1",
8081
"textarea-caret": "3.0.2"
8182
},
8283
"lint-staged": {

0 commit comments

Comments
 (0)