For your first question, there may be two ways to do this. To do it the React way, you could use indexOf
to find the index of the user text in the suggestion and then split the text into multiple React elements with one of them being bolded. If you want to use replace
like you're currently doing then this may be a good opportunity to use dangerouslySetInnerHTML
:
<div className="SuggestionParent" id="Suggestion1">
{this.state.suggestions.map(suggestion => (
<div className="Suggestion" onClick={this.handleSuggestionClick} >
<div dangerouslySetInnerHTML={this.getSuggestionHtml(suggestion)} />
</div>
))}
</div>
The "dangerous" warning is because you need to make sure you don't allow the user to provide any of the potential values that can go in the inner HTML or else they could inject script tags. As long as your suggestions are being drawn from a fixed database and the data is secure then you might be okay. Otherwise you would have to sanitize the HTML and in that case it would probably be easier to just not use dangerouslySetInnerHTML
at all. If we do set the inner HTML, then we can use replace
to just directly apply HTML tags to the string:
getSuggestionHtml(suggestion) {
const lowerCaseSuggestion = suggestion.toLowerCase();
return {
__html: lowerCaseSuggestion.includes(this.state.suggestionTypedText) ? lowerCaseSuggestion
.replace(this.state.suggestionTypedText, `<b>${this.state.suggestionTypedText}</b>`) : lowerCaseSuggestion
};
}
For your second question, you said you've already solved it. I can see that you're using a Boolean switch to temporarily turn off the way you respond to the WEB_CHAT/SET_SEND_BOX action.
For your third question, there are a lot of design considerations that you have to ask yourself about when figuring out how your UI is going to work, like "What happens if the user mouses over the suggestions while they're using the arrow keys?" and "Should the highlighted suggestion be previewed in the send box before the user presses enter?" I was hoping to find a preexisting React autocomplete component that you could use instead of building your own because that would already address all these potential pitfalls. Unfortunately, the two prominent React autocomplete packages (here and here) both have the same two problems:
- They're not currently being maintained
- The targeted input is included in the component so you don't get to connect the component to a preexisting input.
However, they are both open source so we can model our own autocomplete functionality after them. I'll guide you through the basic functionality and you can expand on that as you please.
Keyboard events are generally handled in React using the onKeyDown
property. I've placed it on an element that contains both Web Chat and your suggestions parent:
<div className={ROOT_CSS} onKeyDown={this.handleKeyDown.bind(this)}>
<div className={WEB_CHAT_CSS + ''}>
<ReactWebChat
That will handle all key presses, so you'll need a way to route to the function for the correct key. You could use a switch
statement but the source code for react-autocomplete uses a lookup object and I think that's smart.
keyDownHandlers = {
ArrowDown(event) {
this.moveHighlight(event, 1);
},
ArrowUp(event) {
this.moveHighlight(event, -1);
},
Enter(event) {
const {suggestions} = this.state;
if (!suggestions.length) {
// menu is closed so there is no selection to accept -> do nothing
return
}
event.preventDefault()
this.applySuggestion(suggestions[this.state.highlightedIndex]);
},
}
handleKeyDown(event) {
if (this.keyDownHandlers[event.key])
this.keyDownHandlers[event.key].call(this, event)
}
I've centralized the functionality for the up and down arrows into one function: moveHighlight
. You will need to define a new property in your state to keep track of which suggestion has been selected by the keyboard. I'm keeping the name highlightedIndex
from react-autocomplete.
moveHighlight(event, direction) {
event.preventDefault();
const { highlightedIndex, suggestions } = this.state;
if (!suggestions.length) return;
let newIndex = (highlightedIndex + direction + suggestions.length) % suggestions.length;
if (newIndex !== highlightedIndex) {
this.setState({
highlightedIndex: newIndex,
});
}
}
For the enter key to apply a suggestion, you'll want to centralize your functionality so that it works the same way as a mouse click.
async handleSuggestionClick(event) {
await this.applySuggestion(event.currentTarget.textContent);
}
async applySuggestion(newValue) {
await this.setState({ typingChecking: "false", suggestions: [], highlightedIndex: 0 });
this.state.suggestionCallback.dispatch({
type: 'WEB_CHAT/SET_SEND_BOX',
payload: {
text: newValue,
}
});
await this.setState({ typingChecking: "true" });
}
Finally, make sure the highlightedIndex
property is used to render the highlighted index differently.
getSuggestionCss(index) {
return index === this.state.highlightedIndex ? HIGHLIGHTED_CSS : SUGGESTION_CSS;
}
. . .
<div className="SuggestionParent" id="Suggestion1">
{this.state.suggestions.map((suggestion, index) => (
<div className={this.getSuggestionCss(index)} key={index} onClick={this.handleSuggestionClick} >
<div dangerouslySetInnerHTML={this.getSuggestionHtml(suggestion)} />
</div>
))}
</div>