Skip to content

Commit 2294bdb

Browse files
authored
Fix correct event propagation (#114)
* Fix correct event propagation * Update readme to reflect the change * Be more defensive for tests
1 parent 3f822ab commit 2294bdb

File tree

5 files changed

+66
-63
lines changed

5 files changed

+66
-63
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ or there is UMD build available. [Check out this pen as example](https://codepen
5050
| scrollToItem | boolean \| (container: HTMLDivElement, item: HTMLDivElement) => void) | Defaults to true. With default implementation it will scroll the dropdown every time when the item gets out of the view. |
5151
| minChar | Number | Number of characters that user should type for trigger a suggestion. Defaults to 1. |
5252
| onCaretPositionChange | Function: (number) => void | Listener called every time the textarea's caret position is changed. The listener is called with one attribute - caret position denoted by an integer number. |
53-
| closeOnClickOutside | boolean | When it's true autocomplete will close when use click outside. Defaults to false. |
5453
| movePopupAsYouType | boolean | When it's true the textarea will move along with a caret as a user continues to type. Defaults to false. |
5554
| boundariesElement | string \| HTMLElement | Element which should prevent autocomplete to overflow. Defaults to _body_. |
5655
| style | Style Object | Style's of textarea |

cypress/integration/textarea.js

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -202,26 +202,11 @@ describe("React Textarea Autocomplete", () => {
202202
});
203203

204204
it("should close the textarea when click outside happens", () => {
205-
cy.get('[data-test="clickoutsideOption"]').click();
206-
207205
cy.get(".rta__textarea").type("This is test :ro{uparrow}{uparrow}");
208206

209207
cy.get(".rta__autocomplete").should("be.visible");
210208

211-
cy.get('[data-test="clickoutsideOption"]').click();
212-
213-
cy.get(".rta__autocomplete").should("not.be.visible");
214-
});
215-
216-
it("should be possible to select item with click with closeOnClickOutside option enabled", () => {
217-
cy.get('[data-test="clickoutsideOption"]').click();
218-
219-
cy.get(".rta__textarea")
220-
.type("This is test :ro")
221-
.get("li:nth-child(2)")
222-
.click();
223-
224-
cy.get(".rta__textarea").should("have.value", "This is test 🙄");
209+
cy.get('[data-test="dummy"]').click();
225210

226211
cy.get(".rta__autocomplete").should("not.be.visible");
227212
});
@@ -309,5 +294,19 @@ describe("React Textarea Autocomplete", () => {
309294
cy.get(".rta__textarea").type("somethingelse");
310295
cy.get(".rta__autocomplete").should("not.be.visible");
311296
});
297+
298+
it("event is successfully blocked", () => {
299+
cy.window().then(async win => {
300+
const spy = cy.spy(win.console, "log");
301+
302+
await cy
303+
.get(".rta__textarea")
304+
.type(":ro{uparrow}{uparrow}{enter}")
305+
.then(e => {
306+
// the last console.log call should not be `pressed "enter"` because that event is blocked because it's happening in autocomplete.
307+
expect(spy.lastCall.args).to.eql([`pressed "o"`]);
308+
});
309+
});
310+
});
312311
});
313312
});

example/App.jsx

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ const Loading = ({ data }: LoadingProps) => <div>Loading</div>;
2525
class App extends React.Component {
2626
state = {
2727
optionsCaretStart: false,
28-
clickoutsideOption: false,
2928
caretPosition: 0,
3029
movePopupAsYouType: false,
3130
text: "",
@@ -52,12 +51,6 @@ class App extends React.Component {
5251
}));
5352
};
5453

55-
_handleClickoutsideOption = () => {
56-
this.setState(({ clickoutsideOption }) => ({
57-
clickoutsideOption: !clickoutsideOption
58-
}));
59-
};
60-
6154
_handleShowSecondTextarea = () => {
6255
this.setState(({ showSecondTextarea }) => ({
6356
showSecondTextarea: !showSecondTextarea
@@ -126,7 +119,6 @@ class App extends React.Component {
126119
const {
127120
optionsCaret,
128121
caretPosition,
129-
clickoutsideOption,
130122
movePopupAsYouType,
131123
actualTokenInProvider,
132124
showSecondTextarea,
@@ -166,17 +158,6 @@ class App extends React.Component {
166158
/>
167159
<label htmlFor="caretNext">Place caret after word with a space</label>
168160
</div>
169-
<div>
170-
<label>
171-
<input
172-
data-test="clickoutsideOption"
173-
type="checkbox"
174-
defaultChecked={clickoutsideOption}
175-
onChange={this._handleClickoutsideOption}
176-
/>
177-
Close when click outside
178-
</label>
179-
</div>
180161
<div>
181162
<label>
182163
<input
@@ -224,13 +205,16 @@ class App extends React.Component {
224205
<button data-test="changeValueTo" onClick={this._changeValueTo}>
225206
Change value to ":troph"
226207
</button>
208+
<button data-test="dummy">dummy</button>
227209
<div>
228210
Actual token in "[" provider:{" "}
229211
<span data-test="actualToken">{actualTokenInProvider}</span>
230212
</div>
231-
232213
<ReactTextareaAutocomplete
233214
className="one"
215+
onKeyDown={e => {
216+
console.log(`pressed "${e.key}"`);
217+
}}
234218
ref={ref => {
235219
this.rtaRef = ref;
236220
}}
@@ -248,7 +232,6 @@ class App extends React.Component {
248232
margin: "20px auto"
249233
}}
250234
movePopupAsYouType={movePopupAsYouType}
251-
closeOnClickOutside={clickoutsideOption}
252235
onCaretPositionChange={this._onCaretPositionChangeHandle}
253236
minChar={0}
254237
value={text}

src/Textarea.jsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ class ReactTextareaAutocomplete extends React.Component<
199199
TextareaState
200200
> {
201201
static defaultProps = {
202-
closeOnClickOutside: false,
203202
movePopupAsYouType: false,
204203
value: null,
205204
minChar: 1,
@@ -238,9 +237,22 @@ class ReactTextareaAutocomplete extends React.Component<
238237
textToReplace: null
239238
};
240239

240+
escListenerInit = () => {
241+
if (!this.escListener) {
242+
this.escListener = Listeners.add(KEY_CODES.ESC, this._closeAutocomplete);
243+
}
244+
};
245+
246+
escListenerDestroy = () => {
247+
if (this.escListener) {
248+
Listeners.remove(this.escListener);
249+
this.escListener = null;
250+
}
251+
};
252+
241253
componentDidMount() {
242254
Listeners.add(KEY_CODES.ESC, this._closeAutocomplete);
243-
Listeners.startListen();
255+
Listeners.startListen(this.textareaRef);
244256
}
245257

246258
componentDidUpdate({ trigger: oldTrigger, value: oldValue }: TextareaProps) {
@@ -264,7 +276,8 @@ class ReactTextareaAutocomplete extends React.Component<
264276
}
265277

266278
componentWillUnmount() {
267-
Listeners.stopListen();
279+
this.escListenerDestroy();
280+
Listeners.stopListen(this.textareaRef);
268281
}
269282

270283
getSelectionPosition = (): ?{|
@@ -567,6 +580,7 @@ class ReactTextareaAutocomplete extends React.Component<
567580
* Close autocomplete, also clean up trigger (to avoid slow promises)
568581
*/
569582
_closeAutocomplete = () => {
583+
this.escListenerDestroy();
570584
this.setState({
571585
data: null,
572586
dataLoading: false,
@@ -600,7 +614,6 @@ class ReactTextareaAutocomplete extends React.Component<
600614
"listClassName",
601615
"itemClassName",
602616
"loaderClassName",
603-
"closeOnClickOutside",
604617
"dropdownStyle",
605618
"dropdownClassName",
606619
"movePopupAsYouType"
@@ -750,6 +763,8 @@ class ReactTextareaAutocomplete extends React.Component<
750763
});
751764
}
752765

766+
this.escListenerInit();
767+
753768
this.setState(
754769
{
755770
selectionEnd,
@@ -785,7 +800,7 @@ class ReactTextareaAutocomplete extends React.Component<
785800
};
786801

787802
_onClickAndBlurHandler = (e: SyntheticFocusEvent<*>) => {
788-
const { closeOnClickOutside, onBlur } = this.props;
803+
const { onBlur } = this.props;
789804

790805
// If this is a click: e.target is the textarea, and e.relatedTarget is the thing
791806
// that was actually clicked. If we clicked inside the autoselect dropdown, then
@@ -799,9 +814,7 @@ class ReactTextareaAutocomplete extends React.Component<
799814
return;
800815
}
801816

802-
if (closeOnClickOutside) {
803-
this._closeAutocomplete();
804-
}
817+
this._closeAutocomplete();
805818

806819
if (onBlur) {
807820
e.persist();
@@ -832,6 +845,13 @@ class ReactTextareaAutocomplete extends React.Component<
832845
scrollToItem(this.dropdownRef, item);
833846
};
834847

848+
_isAutocompleteOpen = () => {
849+
const { dataLoading, currentTrigger } = this.state;
850+
const suggestionData = this._getSuggestions();
851+
852+
return (dataLoading || suggestionData) && currentTrigger;
853+
};
854+
835855
props: TextareaProps;
836856

837857
textareaRef: HTMLInputElement;
@@ -847,6 +867,8 @@ class ReactTextareaAutocomplete extends React.Component<
847867
// Last trigger index, to know when user selected the item and we should stop showing the autocomplete
848868
lastTrigger: number = 0;
849869

870+
escListener: ?number = null;
871+
850872
render() {
851873
const {
852874
loadingComponent: Loader,
@@ -869,12 +891,12 @@ class ReactTextareaAutocomplete extends React.Component<
869891
left,
870892
top,
871893
dataLoading,
872-
currentTrigger,
873894
component,
874895
value,
875896
textToReplace
876897
} = this.state;
877898

899+
const isAutocompleteOpen = this._isAutocompleteOpen();
878900
const suggestionData = this._getSuggestions();
879901

880902
return (
@@ -902,7 +924,7 @@ class ReactTextareaAutocomplete extends React.Component<
902924
value={value}
903925
style={style}
904926
/>
905-
{(dataLoading || suggestionData) && currentTrigger && (
927+
{isAutocompleteOpen && (
906928
<Autocomplete
907929
innerRef={ref => {
908930
// $FlowFixMe
@@ -1021,7 +1043,6 @@ ReactTextareaAutocomplete.propTypes = {
10211043
className: PropTypes.string,
10221044
containerStyle: PropTypes.object,
10231045
containerClassName: PropTypes.string,
1024-
closeOnClickOutside: PropTypes.bool,
10251046
style: PropTypes.object,
10261047
listStyle: PropTypes.object,
10271048
itemStyle: PropTypes.object,

src/listener.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,32 @@ class Listener {
2323
constructor() {
2424
this.index = 0;
2525
this.listeners = {};
26-
this.refCount = 0;
2726

2827
this.f = (e: KeyboardEvent) => {
28+
if (!e) return;
29+
2930
const code = e.keyCode || e.which;
3031
for (let i = 0; i < this.index; i += 1) {
3132
const { keyCode, fn } = this.listeners[i];
32-
if (keyCode.includes(code)) fn(e);
33+
if (keyCode.includes(code)) {
34+
e.stopPropagation();
35+
e.preventDefault();
36+
fn(e);
37+
}
3338
}
3439
};
3540
}
3641

37-
startListen = () => {
38-
if (!this.refCount) {
39-
// prevent multiple listeners in case of multiple TextareaAutocomplete components on page
40-
document.addEventListener("keydown", this.f);
41-
}
42-
this.refCount++;
42+
startListen = (ref: HTMLInputElement) => {
43+
if (!ref) return;
44+
45+
ref.addEventListener("keydown", this.f);
4346
};
4447

45-
stopListen = () => {
46-
this.refCount--;
47-
if (!this.refCount) {
48-
// prevent disable listening in case of multiple TextareaAutocomplete components on page
49-
document.removeEventListener("keydown", this.f);
50-
}
48+
stopListen = (ref: HTMLInputElement) => {
49+
if (!ref) return;
50+
51+
ref.removeEventListener("keydown", this.f);
5152
};
5253

5354
add = (keyCodes: Array<number> | number, fn: Function) => {

0 commit comments

Comments
 (0)