feat: create clear button for ControlledCustomAutocomplete (#1222)

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
This commit is contained in:
kattoczko
2023-08-21 21:52:59 +01:00
committed by GitHub
parent 163aca6179
commit 9f9ee0bb58
5 changed files with 148 additions and 90 deletions

View File

@@ -20,7 +20,7 @@ interface CustomOptionsProps {
onTabChange: (tabIndex: 0 | 1) => void;
label?: string;
initialTabIndex?: 0 | 1;
};
}
const CustomOptions = (props: CustomOptionsProps) => {
const {
@@ -34,17 +34,23 @@ const CustomOptions = (props: CustomOptionsProps) => {
label,
initialTabIndex,
} = props;
const [activeTabIndex, setActiveTabIndex] = React.useState<number | undefined>(undefined);
React.useEffect(function applyInitialActiveTabIndex() {
setActiveTabIndex((currentActiveTabIndex) => {
if (currentActiveTabIndex === undefined) {
return initialTabIndex;
}
const [activeTabIndex, setActiveTabIndex] = React.useState<
number | undefined
>(undefined);
return currentActiveTabIndex;
});
}, [initialTabIndex]);
React.useEffect(
function applyInitialActiveTabIndex() {
setActiveTabIndex((currentActiveTabIndex) => {
if (currentActiveTabIndex === undefined) {
return initialTabIndex;
}
return currentActiveTabIndex;
});
},
[initialTabIndex]
);
return (
<Popper
@@ -75,22 +81,15 @@ const CustomOptions = (props: CustomOptionsProps) => {
</Tabs>
<TabPanel value={activeTabIndex ?? 0} index={0}>
<Options
data={options}
onOptionClick={onOptionClick}
/>
<Options data={options} onOptionClick={onOptionClick} />
</TabPanel>
<TabPanel value={activeTabIndex ?? 0} index={1}>
<Suggestions
data={data}
onSuggestionClick={onSuggestionClick}
/>
<Suggestions data={data} onSuggestionClick={onSuggestionClick} />
</TabPanel>
</Paper>
</Popper>
);
};
export default CustomOptions;

View File

@@ -1,15 +1,17 @@
import * as React from 'react';
import { useController, useFormContext } from 'react-hook-form';
import { IconButton } from '@mui/material';
import FormHelperText from '@mui/material/FormHelperText';
import { AutocompleteProps } from '@mui/material/Autocomplete';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ClearIcon from '@mui/icons-material/Clear';
import type { IFieldDropdownOption } from '@automatisch/types';
import { FakeDropdownButton } from './style';
import { ActionButtonsWrapper } from './style';
import ClickAwayListener from '@mui/base/ClickAwayListener';
import InputLabel from '@mui/material/InputLabel';
import { createEditor } from 'slate';
import { Editable, ReactEditor,} from 'slate-react';
import { Editable, ReactEditor } from 'slate-react';
import Slate from 'components/Slate';
import Element from 'components/Slate/Element';
@@ -23,7 +25,11 @@ import {
overrideEditorValue,
focusEditor,
} from 'components/Slate/utils';
import { FakeInput, InputLabelWrapper, ChildrenWrapper, } from 'components/PowerInput/style';
import {
FakeInput,
InputLabelWrapper,
ChildrenWrapper,
} from 'components/PowerInput/style';
import { VariableElement } from 'components/Slate/types';
import CustomOptions from './CustomOptions';
import { processStepWithExecutions } from 'components/PowerInput/data';
@@ -75,9 +81,11 @@ function ControlledCustomAutocomplete(
onChange: controllerOnChange,
onBlur: controllerOnBlur,
} = field;
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
const [isInitialValueSet, setInitialValue] = React.useState(false);
const [isSingleChoice, setSingleChoice] = React.useState<boolean | undefined>(undefined);
const [isSingleChoice, setSingleChoice] = React.useState<boolean | undefined>(
undefined
);
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
const editorRef = React.useRef<HTMLDivElement | null>(null);
const renderElement = React.useCallback(
@@ -104,12 +112,12 @@ function ControlledCustomAutocomplete(
const promoteValue = () => {
const serializedValue = serialize(editor.children);
controllerOnChange(serializedValue);
}
};
const resizeObserver = React.useMemo(function syncCustomOptionsPosition() {
return new ResizeObserver(() => {
forceUpdate();
})
});
}, []);
React.useEffect(() => {
@@ -121,24 +129,37 @@ function ControlledCustomAutocomplete(
}
}, dependsOnValues);
React.useEffect(function updateInitialValue() {
const hasOptions = options.length;
const isOptionsLoaded = loading === false;
if (!isInitialValueSet && hasOptions && isOptionsLoaded) {
setInitialValue(true);
React.useEffect(
function updateInitialValue() {
const hasOptions = options.length;
const isOptionsLoaded = loading === false;
if (!isInitialValueSet && hasOptions && isOptionsLoaded) {
setInitialValue(true);
const option: IFieldDropdownOption | undefined = options.find((option) => option.value === value);
const option: IFieldDropdownOption | undefined = options.find(
(option) => option.value === value
);
if (option) {
overrideEditorValue(editor, { option, focus: false });
setSingleChoice(true);
} else if (value) {
setSingleChoice(false);
if (option) {
overrideEditorValue(editor, { option, focus: false });
setSingleChoice(true);
} else if (value) {
setSingleChoice(false);
}
}
}
}, [isInitialValueSet, options, loading]);
},
[isInitialValueSet, options, loading]
);
const hideSuggestionsOnShift = (event: React.KeyboardEvent<HTMLInputElement>) => {
React.useEffect(() => {
if (!showVariableSuggestions && value !== serialize(editor.children)) {
promoteValue();
}
}, [showVariableSuggestions]);
const hideSuggestionsOnShift = (
event: React.KeyboardEvent<HTMLInputElement>
) => {
if (event.code === 'Tab') {
setShowVariableSuggestions(false);
}
@@ -170,21 +191,26 @@ function ControlledCustomAutocomplete(
(event: React.MouseEvent, option: IFieldDropdownOption) => {
event.stopPropagation();
overrideEditorValue(editor, { option, focus: false });
setShowVariableSuggestions(false);
promoteValue();
setSingleChoice(true);
},
[stepsWithVariables]
);
const handleClearButtonClick = (event: React.MouseEvent) => {
event.stopPropagation();
resetEditor(editor);
promoteValue();
setSingleChoice(undefined);
};
const reset = (tabIndex: 0 | 1) => {
const isOptions = tabIndex === 0;
setSingleChoice(isOptions);
resetEditor(editor, { focus: true });
}
};
return (
<Slate
@@ -193,11 +219,7 @@ function ControlledCustomAutocomplete(
>
<ClickAwayListener
mouseEvent="onMouseDown"
onClickAway={() => {
promoteValue();
setShowVariableSuggestions(false);
}}
onClickAway={() => setShowVariableSuggestions(false)}
>
{/* ref-able single child for ClickAwayListener */}
<ChildrenWrapper style={{ width: '100%' }} data-test="power-input">
@@ -232,14 +254,27 @@ function ControlledCustomAutocomplete(
}}
/>
<FakeDropdownButton
disabled={disabled}
edge="end"
size="small"
tabIndex={-1}
>
<ArrowDropDownIcon />
</FakeDropdownButton>
<ActionButtonsWrapper direction="row" mr={1.5}>
{isSingleChoice && serialize(editor.children) && (
<IconButton
disabled={disabled}
edge="end"
size="small"
tabIndex={-1}
onClick={handleClearButtonClick}
>
<ClearIcon />
</IconButton>
)}
<IconButton
disabled={disabled}
edge="end"
size="small"
tabIndex={-1}
>
<ArrowDropDownIcon />
</IconButton>
</ActionButtonsWrapper>
</FakeInput>
{/* ghost placer for the variables popover */}
<div
@@ -247,14 +282,16 @@ function ControlledCustomAutocomplete(
style={{
position: 'absolute',
right: 16,
left: 16
left: 16,
}}
/>
<CustomOptions
label={label}
open={showVariableSuggestions}
initialTabIndex={isSingleChoice === undefined ? undefined : (isSingleChoice ? 0 : 1)}
initialTabIndex={
isSingleChoice === undefined ? undefined : isSingleChoice ? 0 : 1
}
anchorEl={editorRef.current}
data={stepsWithVariables}
options={options}

View File

@@ -1,10 +1,10 @@
import { styled } from '@mui/material/styles';
import MuiIconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import MuiTabs from '@mui/material/Tabs';
export const FakeDropdownButton = styled(MuiIconButton)`
export const ActionButtonsWrapper = styled(Stack)`
position: absolute;
right: ${({ theme }) => theme.spacing(1)};
right: 0;
top: 50%;
transform: translateY(-50%);
`;

View File

@@ -9,4 +9,4 @@ export default function Element(props: any) {
default:
return <p {...attributes}>{children}</p>;
}
};
}

View File

@@ -3,7 +3,13 @@ import { withHistory } from 'slate-history';
import { ReactEditor, withReact } from 'slate-react';
import { IFieldDropdownOption } from '@automatisch/types';
import type { CustomEditor, CustomElement, CustomText, ParagraphElement, VariableElement } from './types';
import type {
CustomEditor,
CustomElement,
CustomText,
ParagraphElement,
VariableElement,
} from './types';
type StepWithVariables = {
id: string;
@@ -13,7 +19,7 @@ type StepWithVariables = {
sampleValue: string;
value: string;
}[];
}
};
type StepsWithVariables = StepWithVariables[];
@@ -26,10 +32,7 @@ function isCustomText(value: any): value is CustomText {
return false;
}
function getStepPosition(
id: string,
stepsWithVariables: StepsWithVariables
) {
function getStepPosition(id: string, stepsWithVariables: StepsWithVariables) {
const stepIndex = stepsWithVariables.findIndex((stepWithVariables) => {
return stepWithVariables.id === id;
});
@@ -48,29 +51,36 @@ function getVariableStepId(variable: string) {
return stepId;
}
function getVariableSampleValue(variable: string, stepsWithVariables: StepsWithVariables) {
function getVariableSampleValue(
variable: string,
stepsWithVariables: StepsWithVariables
) {
const variableStepId = getVariableStepId(variable);
const stepWithVariables = stepsWithVariables.find(({ id }: { id: string }) => id === variableStepId);
const stepWithVariables = stepsWithVariables.find(
({ id }: { id: string }) => id === variableStepId
);
if (!stepWithVariables) return null;
const variableName = getVariableName(variable);
const variableData = stepWithVariables.output.find(({ value }) => variableName === value);
const variableData = stepWithVariables.output.find(
({ value }) => variableName === value
);
if (!variableData) return null;
return variableData.sampleValue;
}
function getVariableDetails(variable: string, stepsWithVariables: StepsWithVariables) {
function getVariableDetails(
variable: string,
stepsWithVariables: StepsWithVariables
) {
const variableName = getVariableName(variable);
const stepId = getVariableStepId(variableName);
const stepPosition = getStepPosition(stepId, stepsWithVariables);
const sampleValue = getVariableSampleValue(variable, stepsWithVariables);
const label = variableName.replace(
`step.${stepId}.`,
`step${stepPosition}.`
);
const label = variableName.replace(`step.${stepId}.`, `step${stepPosition}.`);
return {
sampleValue,
@@ -114,7 +124,10 @@ export const deserialize = (
type: 'paragraph',
children: nodes.map((node) => {
if (node.match(variableRegExp)) {
const variableDetails = getVariableDetails(node, stepsWithVariables);
const variableDetails = getVariableDetails(
node,
stepsWithVariables
);
return {
type: 'variable',
@@ -199,7 +212,10 @@ export const insertVariable = (
variableData: Record<string, unknown>,
stepsWithVariables: StepsWithVariables
) => {
const variableDetails = getVariableDetails(`{{${variableData.value}}}`, stepsWithVariables);
const variableDetails = getVariableDetails(
`{{${variableData.value}}}`,
stepsWithVariables
);
const variable: VariableElement = {
type: 'variable',
@@ -217,15 +233,18 @@ export const insertVariable = (
export const focusEditor = (editor: CustomEditor) => {
ReactEditor.focus(editor);
editor.move();
}
};
export const resetEditor = (editor: CustomEditor, options?: { focus: boolean }) => {
export const resetEditor = (
editor: CustomEditor,
options?: { focus: boolean }
) => {
const focus = options?.focus || false;
editor.removeNodes({
at: {
anchor: editor.start([]),
focus: editor.end([])
focus: editor.end([]),
},
});
@@ -235,9 +254,12 @@ export const resetEditor = (editor: CustomEditor, options?: { focus: boolean })
if (focus) {
focusEditor(editor);
}
}
};
export const overrideEditorValue = (editor: CustomEditor, options: { option: IFieldDropdownOption, focus: boolean }) => {
export const overrideEditorValue = (
editor: CustomEditor,
options: { option: IFieldDropdownOption; focus: boolean }
) => {
const { option, focus } = options;
const variable: ParagraphElement = {
@@ -245,8 +267,8 @@ export const overrideEditorValue = (editor: CustomEditor, options: { option: IFi
children: [
{
value: option.value as string,
text: option.label as string
}
text: option.label as string,
},
],
};
@@ -254,7 +276,7 @@ export const overrideEditorValue = (editor: CustomEditor, options: { option: IFi
editor.removeNodes({
at: {
anchor: editor.start([]),
focus: editor.end([])
focus: editor.end([]),
},
});
@@ -270,9 +292,9 @@ export const createTextNode = (text: string): ParagraphElement => ({
type: 'paragraph',
children: [
{
text
}
]
text,
},
],
});
export const customizeEditor = (editor: CustomEditor): CustomEditor => {