-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Autocomplete] getOptionLabel, onInputChange, onChange are super confusing when used together #19426
Comments
@jdoklovic Good luck with the task! |
Actually, let's keep it open in case somebody has time to look at it (I might look at it later on.). It seems that we have a good signal/noise ratio here: https://www.linkedin.com/in/jdoklovic/. Thanks for opening it. |
@oliviertassinari here's the code I have that's mostly working. Still suffers from #19705 interface JQLInputProps {
jqlAutocompleteRestData: JqlAutocompleteRestData;
suggestionFetcher: FieldSuggestionFetcher;
onJqlChange?: (newValue: string) => void;
value?: string;
disabled?: boolean;
}
const loadingOption: Suggestion = { displayName: 'Loading...', value: '__-loading-__' };
export const JQLInput: React.FunctionComponent<JQLInputProps & TextFieldProps> = ({ jqlAutocompleteRestData, suggestionFetcher, value, onJqlChange, disabled, ...tfProps }) => {
const [inputValue, setInputValue] = useState((value) ? value : '');
const [selStart, setSelStart] = useState<number>(0);
const [fieldPositionDirty, setFieldPositionDirty] = useState<boolean>(false);
const inputField = useRef<HTMLInputElement>();
const [open, setOpen] = React.useState<boolean>(false);
const [fetching, setFetching] = React.useState<boolean>(false);
const suggestor = useMemo<JqlSuggestor>(() => new JqlSuggestor(jqlAutocompleteRestData, suggestionFetcher), [jqlAutocompleteRestData, suggestionFetcher]);
// get initial data
const suggestions: Suggestion[] = [{ displayName: 'NOT', value: 'NOT' }, { displayName: 'start group (', value: '(' }];
suggestions.push(...jqlAutocompleteRestData.visibleFieldNames.map<Suggestion>(f => ({ displayName: f.displayName, value: f.value })));
const [options, setOptions] = useState<Suggestions>(suggestions);
const optionsLoading = open && fetching;
const fetch = React.useMemo(
() =>
async (input: string, startIndex: number) => {
setOptions([loadingOption]);
if (!open) {
setOpen(true);
}
var suggestions: Suggestions = [];
setFetching(true);
suggestions = await suggestor.getSuggestions(input, startIndex);
setFetching(false);
setOptions(suggestions);
},
[open, suggestor],
);
const handleTextFieldChange = (event: React.ChangeEvent<HTMLInputElement>) => { // fires on typing, but not when option is selected
// update our local state
if (inputField.current) {
var newStart = (inputField.current.selectionStart) ? inputField.current.selectionStart : 0;
fetch(event.target.value, newStart);
setSelStart(newStart);
}
setInputValue(event.target.value);
};
const handleAutocompleteChange = (event: React.ChangeEvent<HTMLInputElement>, value: any) => { //only fires when an option is selected with the option value
var insertText: string = (value) ? value.value : "";
var newCursorPos = 0;
if (insertText === '' || insertText === loadingOption.value) {
return;
}
if (inputField.current) {
newCursorPos = selStart;
[insertText, newCursorPos] = suggestor.calculateNewValueForSelected(inputValue, insertText, selStart);
}
setInputValue(insertText);
setSelStart(newCursorPos);
// if we're not at the end, we're editing and need to update the cursor position later when the text field catches up
if (newCursorPos !== insertText.length) {
setFieldPositionDirty(true);
}
};
const getOptionLabel = (option: Suggestion): string => { //fires when option is selected and should return the new input box value in it's entirety. Also fires on focus events, and randomly.
// if we recently edited a value, we need to set the new cursor position manually
if (fieldPositionDirty && inputField.current) {
inputField.current.value = inputValue; // have to reset the value to get the right sel index
inputField.current.selectionStart = selStart;
inputField.current.selectionEnd = selStart;
setFieldPositionDirty(false);
}
return inputValue;
};
const handleTextFieldKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
// update our local cursorPosition on arrow keys so we fetch the proper options
switch (event.key) {
case 'ArrowRight': {
if (inputField.current && inputField.current.selectionStart !== null && inputField.current.selectionStart !== inputField.current.value.length) {
const newStart = (inputField.current.selectionStart < inputField.current.value.length) ? inputField.current.selectionStart + 1 : inputField.current.value.length;
fetch(inputField.current.value, newStart);
setSelStart(newStart);
}
break;
}
case 'ArrowLeft': {
if (inputField.current && inputField.current.selectionStart && inputField.current.selectionStart !== 0) {
const newStart = (inputField.current.selectionStart > 0) ? inputField.current.selectionStart - 1 : 0;
fetch(inputField.current.value, newStart);
setSelStart(newStart);
}
break;
}
case 'Enter': {
break;
}
};
};
useEffect(() => {
if (onJqlChange) {
onJqlChange(inputValue);
}
}, [inputValue, onJqlChange]);
return (
<Autocomplete
id="jql-editor"
disableOpenOnFocus={false}
getOptionLabel={getOptionLabel}
filterOptions={x => x}
options={options}
value={inputValue}
includeInputInList
freeSolo
autoHighlight
onChange={handleAutocompleteChange}
loading={optionsLoading}
open={open}
disabled={disabled}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
renderInput={params => (
<TextField
{...params}
{...tfProps}
inputRef={inputField}
onChange={handleTextFieldChange}
onKeyDown={handleTextFieldKeyDown}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{optionsLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
renderOption={option => {
const matches = match(option.displayName, suggestor.getCurrentToken().value);
const parts = parse(option.displayName, matches);
return (
<div key={option.value}>
{parts.map((part, index) => (
<span key={index} style={{ fontWeight: part.highlight ? 700 : 400 }}>
{part.text}
</span>
))}
</div>
);
}}
/>
);
}; |
I'm closing as I don't think there is much leverage to apply. |
Any ideas on how to use |
Current Behavior 😯
After working through some of the things in #19267, I'm still having problems implementing a freesolo query editor with autocomplete.
Example of what I'm trying to accomplish:
Essentially, I put together a few examples from the demos page and thought I could use getOptionLabel to update the input field with the correct value after a user picks an option, however this doesn't exactly work properly because of the way events are fired and state is managed.
The biggest problem is that I currently update the local inputValue custom state with a TextField onChange handler and that function as well as useEffect are not always fired as expected. Also, getOptionLabel is fired multiple times and not always the same number of times.
Expected Behavior 🤔
When a user makes any change (typing, selecting an option, editing the value) there should always be a handler (the same one) that can update state.
Steps to Reproduce 🕹
Component code:
Notes:
handleTextFieldChange updates the local 'inputValue' state from event.target.value
handleAutocompleteChange & handleAutocompleteInputChange are doing nothing but logging the current state.
getOptionLabel takes the passed option, the selectionStart and the inputValue and calculates the new complete query string.
useEffect fetches options using an async function and sets the new options as local state which should force the Autocomplete to re-render if there are new options. This is dependent on the inputValue state being correct to properly fetch new options which is currently failing and in a lot of cases, returning the same options even though the textField value has changed because the local state isn't correct.
Context 🔦
After debugging, It looks like the events are fired as such:
handleTextFieldChange: fires on typing, but not when option is selected
handleAutocompleteChange: only fires when an option is selected with the option value
handleAutocompleteInputChange: fires on typing as 'input' and on option selected as 'reset'
getOptionLabel: fires when option is selected. Also fires on focus events, and "randomly".
Here's a screenshot of the events that happen when typing a single letter and then selecting an option. They are separated by a "click" log entry:
After that, here' are the events that fire when the input is focused again and then a single backspace is pressed. Note: when backspace is pressed:
Your Environment 🌎
The text was updated successfully, but these errors were encountered: