@boomerang-io/carbon-addons-boomerang-react
Version:
Carbon Addons for Boomerang apps
110 lines (107 loc) • 5.02 kB
JavaScript
import React, { Component } from 'react';
import AutoSuggest from 'react-autosuggest';
import { matchSorter } from 'match-sorter';
import { prefix } from '../../internal/settings.js';
/*
IBM Confidential
694970X, 69497O0
© Copyright IBM Corp. 2022, 2024
*/
const SELECT_METHODS = ["up", "down", "click"];
class AutoSuggestBmrg extends Component {
inputRef = React.createRef();
state = {
// Used if we want to have some of the functions external instead of
// the default ones in the wrapper
value: this.props.initialValue ?? "",
caretIndex: 0,
suggestions: [],
};
// Each time the component updates we want to refocus the input and keep the cursor in the correct place
// Needed for when cycling through mutliple suggestions with the arrow keys and the cursor resets to the end of the input. We don't want that.
componentDidUpdate(_, prevState) {
if (this.state.value && prevState.value !== this.state.value) {
// prevent it from focusing on initial render / empty
this.inputRef.current?.focus();
this.inputRef.current?.setSelectionRange(this.state.caretIndex, this.state.caretIndex);
}
}
renderSuggestion = (suggestion) => suggestion.label;
onSuggestionsFetchRequested = () => {
this.setState(() => ({
suggestions: this.getSuggestions(),
}));
};
/**
* More logic here for handling a user cycling through suggestions
* Move the caret to the new suggestion location or use the reference to the DOM element.
* Shift based on the change in length of the value b/c of different length suggestions
*/
onInputChange = (event, { newValue, method }) => {
this.setState((prevState) => ({
value: newValue,
caretIndex: SELECT_METHODS.includes(method)
? prevState.caretIndex + (newValue.length - prevState.value.length)
: this.inputRef.current?.selectionStart ?? 0,
}));
this.props.onChange(newValue);
};
/**
* Return the new value for the input
* Find the current caret position
* get the string up to that point
* find the last word (space-delimited) and replace it in input
*/
getSuggestionValue = (suggestion) => {
const substringWordList = this.findWordsBeforeCurrentLocation();
/*
* Find the position of the caret, get the string up to that point
* and find the index of the last word in that substring
* This gives use the word to suggest matches for
*/
const closestWord = substringWordList.at(-1);
const position = this.state.value.slice(0, this.state.caretIndex).lastIndexOf(closestWord);
// Sub in the new property suggestion
return (this.state.value.substring(0, position) +
suggestion.value +
this.state.value.substring(position + closestWord.length));
};
getSuggestions = () => {
const substringWordList = this.findWordsBeforeCurrentLocation();
// Prevent empty string from matching everyhing
const closestWord = substringWordList.at(-1);
return !closestWord
? []
: matchSorter(this.props.autoSuggestions, closestWord, {
// Use match-sorter for matching inputs
keys: [{ key: "value" }],
});
};
// Get array of distinct words prior to the current location of entered text
// Use the inputRef instead of state becuase of asnychronous updating of state and calling of these functions :(
findWordsBeforeCurrentLocation = () => {
return this.inputRef.current?.value
.slice(0, this.inputRef.current?.selectionStart ?? undefined)
.split(" ");
};
onSuggestionsClearRequested = () => {
this.setState({
suggestions: [],
});
};
render() {
const { inputProps, children, ...rest } = this.props;
const finalInputProps = {
...inputProps,
onChange: this.onInputChange,
value: this.state.value,
};
return (React.createElement("div", { className: `${prefix}--bmrg-auto-suggest` },
React.createElement(AutoSuggest, { getSuggestionValue: this.getSuggestionValue, inputProps: finalInputProps, onSuggestionsClearRequested: this.onSuggestionsClearRequested, onSuggestionsFetchRequested: this.onSuggestionsFetchRequested, renderInputComponent: (props) => React.cloneElement(children, { ...props, ref: this.inputRef }), renderSuggestion: this.renderSuggestion, renderSuggestionsContainer: renderSuggestionsContainer, suggestions: this.state.suggestions, ...rest })));
}
}
// Needed to add aria-label for a11y
function renderSuggestionsContainer({ containerProps, children, ...rest }) {
return (React.createElement("div", { "aria-label": "AutoSuggest listbox", ...containerProps, ...rest }, children));
}
export { AutoSuggestBmrg as default };