Skip to content

Commit 3f5c130

Browse files
authored
feat: added onItemSelected prop (#168)
1 parent 10b72f3 commit 3f5c130

File tree

6 files changed

+78
-36
lines changed

6 files changed

+78
-36
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ or there is UMD build available. [Check out this pen as example](https://codepen
5353
| movePopupAsYouType | boolean | When it's true the textarea will move along with a caret as a user continues to type. Defaults to false. |
5454
| boundariesElement | string \| HTMLElement | Element which should prevent autocomplete to overflow. Defaults to _body_. |
5555
| textAreaComponent | React.Component \| {component: React.Component, ref: string} | What component use for as textarea. Default is `textarea`. (You can combine this with [react-autosize-textarea](https://github.com/buildo/react-autosize-textarea) for instance) |
56-
| renderToBody | boolean | When set to `true` the autocomplete will be rendered at the end of the `<body>`. Default is `false`. |
56+
| renderToBody | boolean | When set to `true` the autocomplete will be rendered at the end of the `<body>`. Default is `false`. |
57+
| onItemSelected | ({currentTrigger: string, item: string \| Object}) => void | Callback get called everytime item is selected |
5758
| style | Style Object | Style's of textarea |
5859
| listStyle | Style Object | Styles of list's wrapper |
5960
| itemStyle | Style Object | Styles of item's wrapper |

cypress/integration/textarea.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ describe("React Textarea Autocomplete", () => {
134134
cy.get('[data-test="minChar"]').clear({ force: true });
135135
});
136136

137+
it("onSelectItem should return correct item and trigger", () => {
138+
cy.get(".rta__textarea").type(":ro{uparrow}{uparrow}{enter}");
139+
cy.window().then(async win => {
140+
const shouldSelectItem = {
141+
currentTrigger: ":",
142+
item: { name: "rofl", char: "🤣" }
143+
};
144+
145+
expect(win.__lastSelectedItem).to.deep.equal(shouldSelectItem);
146+
});
147+
});
148+
137149
it("should have place caret before outputted word", () => {
138150
/**
139151
* This is probably Cypress bug (1.0.2)

example/App.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ class App extends React.Component {
271271
}}
272272
movePopupAsYouType={movePopupAsYouType}
273273
onCaretPositionChange={this._onCaretPositionChangeHandle}
274+
onItemSelected={info => {
275+
// save selected item to window; use it later in E2E tests
276+
window.__lastSelectedItem = info;
277+
}}
274278
minChar={minChar}
275279
value={text}
276280
onChange={this._onChangeHandle}

src/List.jsx

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export default class List extends React.Component<ListProps, ListState> {
1111
selectedItem: null
1212
};
1313

14+
cachedIdOfItems: Map<Object | string, string> = new Map();
15+
1416
componentDidMount() {
1517
this.listeners.push(
1618
Listeners.add([KEY_CODES.DOWN, KEY_CODES.UP], this.scroll),
@@ -60,26 +62,39 @@ export default class List extends React.Component<ListProps, ListState> {
6062
};
6163

6264
getId = (item: Object | string): string => {
65+
if (this.cachedIdOfItems.has(item)) {
66+
// $FlowFixMe
67+
return this.cachedIdOfItems.get(item);
68+
}
69+
6370
const textToReplace = this.props.getTextToReplace(item);
6471

65-
if (textToReplace) {
66-
if (textToReplace.key) {
67-
return textToReplace.key;
72+
const computeId = (): string => {
73+
if (textToReplace) {
74+
if (textToReplace.key) {
75+
return textToReplace.key;
76+
}
77+
78+
if (typeof item === "string" || !item.key) {
79+
return textToReplace.text;
80+
}
6881
}
6982

70-
if (typeof item === "string" || !item.key) {
71-
return textToReplace.text;
83+
if (!item.key) {
84+
throw new Error(
85+
`Item ${JSON.stringify(item)} has to have defined "key" property`
86+
);
7287
}
73-
}
7488

75-
if (!item.key) {
76-
throw new Error(
77-
`Item ${JSON.stringify(item)} has to have defined "key" property`
78-
);
79-
}
89+
// $FlowFixMe
90+
return item.key;
91+
};
92+
93+
const id = computeId();
94+
95+
this.cachedIdOfItems.set(item, id);
8096

81-
// $FlowFixMe
82-
return item.key;
97+
return id;
8398
};
8499

85100
props: ListProps;
@@ -93,9 +108,9 @@ export default class List extends React.Component<ListProps, ListState> {
93108
modifyText = (value: Object | string) => {
94109
if (!value) return;
95110

96-
const { onSelect, getTextToReplace } = this.props;
111+
const { onSelect } = this.props;
97112

98-
onSelect(getTextToReplace(value));
113+
onSelect(value);
99114
};
100115

101116
selectItem = (item: Object | string, keyboard: boolean = false) => {

src/Textarea.jsx

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type {
1818
caretPositionType,
1919
outputType,
2020
triggerType,
21-
textToReplaceType,
2221
settingType
2322
} from "./types";
2423

@@ -358,16 +357,34 @@ class ReactTextareaAutocomplete extends React.Component<
358357
cleanLastTrigger();
359358
};
360359

361-
_onSelect = (newToken: textToReplaceType) => {
360+
_onSelect = (item: Object | string) => {
362361
const { selectionEnd, currentTrigger, value: textareaValue } = this.state;
363-
const { trigger } = this.props;
362+
const { trigger, onItemSelected } = this.props;
363+
364+
if (!currentTrigger) return;
365+
366+
const getTextToReplaceForCurrentTrigger = this._getTextToReplace(
367+
currentTrigger
368+
);
369+
370+
if (!getTextToReplaceForCurrentTrigger) {
371+
this._closeAutocomplete();
372+
return;
373+
}
374+
375+
const newToken = getTextToReplaceForCurrentTrigger(item);
364376

365377
if (!newToken) {
366378
this._closeAutocomplete();
367379
return;
368380
}
369381

370-
if (!currentTrigger) return;
382+
if (onItemSelected) {
383+
onItemSelected({
384+
currentTrigger,
385+
item
386+
});
387+
}
371388

372389
const computeCaretPosition = (
373390
position: caretPositionType,
@@ -451,13 +468,7 @@ class ReactTextareaAutocomplete extends React.Component<
451468
);
452469
};
453470

454-
_getTextToReplace = ({
455-
actualToken,
456-
currentTrigger
457-
}: {|
458-
actualToken: string,
459-
currentTrigger: string
460-
|}): ?outputType => {
471+
_getTextToReplace = (currentTrigger: string): ?outputType => {
461472
const triggerSettings = this.props.trigger[currentTrigger];
462473

463474
if (!currentTrigger || !triggerSettings) return null;
@@ -481,7 +492,7 @@ class ReactTextareaAutocomplete extends React.Component<
481492
throw new Error(
482493
`Output functor should return string or object in shape {text: string, caretPosition: string | number}.\nGot "${String(
483494
textToReplace
484-
)}". Check the implementation for trigger "${currentTrigger}" and its token "${actualToken}"\n\nSee https://github.com/webscopeio/react-textarea-autocomplete#trigger-type for more informations.\n`
495+
)}". Check the implementation for trigger "${currentTrigger}"\n\nSee https://github.com/webscopeio/react-textarea-autocomplete#trigger-type for more information.\n`
485496
);
486497
}
487498

@@ -496,13 +507,13 @@ class ReactTextareaAutocomplete extends React.Component<
496507

497508
if (!textToReplace.text) {
498509
throw new Error(
499-
`Output "text" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger "${currentTrigger}" and its token "${actualToken}"\n`
510+
`Output "text" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger "${currentTrigger}"\n`
500511
);
501512
}
502513

503514
if (!textToReplace.caretPosition) {
504515
throw new Error(
505-
`Output "caretPosition" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger "${currentTrigger}" and its token "${actualToken}"\n`
516+
`Output "caretPosition" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger "${currentTrigger}"\n`
506517
);
507518
}
508519

@@ -666,7 +677,8 @@ class ReactTextareaAutocomplete extends React.Component<
666677
"dropdownClassName",
667678
"movePopupAsYouType",
668679
"textAreaComponent",
669-
"renderToBody"
680+
"renderToBody",
681+
"onItemSelected"
670682
];
671683

672684
// eslint-disable-next-line
@@ -830,10 +842,7 @@ class ReactTextareaAutocomplete extends React.Component<
830842

831843
this.escListenerInit();
832844

833-
const textToReplace = this._getTextToReplace({
834-
actualToken,
835-
currentTrigger
836-
});
845+
const textToReplace = this._getTextToReplace(currentTrigger);
837846

838847
this.setState(
839848
{

src/types.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export type ListProps = {
3636
itemStyle: ?Object,
3737
className: ?string,
3838
itemClassName: ?string,
39-
onSelect: textToReplaceType => void,
39+
onSelect: (Object | string) => void,
4040
dropdownScroll: HTMLDivElement => void
4141
};
4242

@@ -78,6 +78,7 @@ export type TextareaProps = {
7878
| boolean
7979
| ((container: HTMLDivElement, item: HTMLDivElement) => void),
8080
closeOnClickOutside?: boolean,
81+
onItemSelected?: ({ currentTrigger: string, item: Object | string }) => void,
8182
movePopupAsYouType?: boolean,
8283
boundariesElement: string | HTMLElement,
8384
minChar: number,

0 commit comments

Comments
 (0)