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

View File

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

View File

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

View File

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

View File

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