2
0

🐛 (editor) Improve variables dropdown auto focus

This commit is contained in:
Baptiste Arnaud
2023-01-11 10:53:24 +01:00
parent 4c2eaf9b79
commit b65ffe8c53
9 changed files with 28 additions and 39 deletions

View File

@@ -11,21 +11,20 @@ import {
useColorModeValue, useColorModeValue,
PopoverAnchor, PopoverAnchor,
Portal, Portal,
Tag,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons' import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import cuid from 'cuid' import cuid from 'cuid'
import { Variable } from 'models' import { Variable } from 'models'
import React, { useState, useRef, ChangeEvent, useEffect } from 'react' import React, { useState, useRef, ChangeEvent, useEffect } from 'react'
import { useDebouncedCallback } from 'use-debounce' import { byId, isDefined, isNotDefined } from 'utils'
import { byId, env, isDefined, isNotDefined } from 'utils'
import { useOutsideClick } from '@/hooks/useOutsideClick' import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph' import { useParentModal } from '@/features/graph'
type Props = { type Props = {
initialVariableId?: string initialVariableId?: string
debounceTimeout?: number autoFocus?: boolean
isDefaultOpen?: boolean
onSelectVariable: ( onSelectVariable: (
variable: Pick<Variable, 'id' | 'name'> | undefined variable: Pick<Variable, 'id' | 'name'> | undefined
) => void ) => void
@@ -34,8 +33,7 @@ type Props = {
export const VariableSearchInput = ({ export const VariableSearchInput = ({
initialVariableId, initialVariableId,
onSelectVariable, onSelectVariable,
isDefaultOpen, autoFocus,
debounceTimeout = 1000,
...inputProps ...inputProps
}: Props) => { }: Props) => {
const bg = useColorModeValue('gray.200', 'gray.700') const bg = useColorModeValue('gray.200', 'gray.700')
@@ -46,13 +44,6 @@ export const VariableSearchInput = ({
const [inputValue, setInputValue] = useState( const [inputValue, setInputValue] = useState(
variables.find(byId(initialVariableId))?.name ?? '' variables.find(byId(initialVariableId))?.name ?? ''
) )
const debounced = useDebouncedCallback(
(value) => {
const variable = variables.find((v) => v.name === value)
if (variable) onSelectVariable(variable)
},
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
)
const [filteredItems, setFilteredItems] = useState<Variable[]>( const [filteredItems, setFilteredItems] = useState<Variable[]>(
variables ?? [] variables ?? []
) )
@@ -67,25 +58,17 @@ export const VariableSearchInput = ({
useOutsideClick({ useOutsideClick({
ref: dropdownRef, ref: dropdownRef,
handler: onClose, handler: () => onClose,
}) })
useEffect(() => { useEffect(() => {
if (isDefaultOpen) onOpen() if (autoFocus) onOpen()
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(
() => () => {
debounced.flush()
},
[debounced]
)
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => { const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value) setInputValue(e.target.value)
debounced(e.target.value)
if (e.target.value === '') { if (e.target.value === '') {
onSelectVariable(undefined) onSelectVariable(undefined)
setFilteredItems([...variables.slice(0, 50)]) setFilteredItems([...variables.slice(0, 50)])
@@ -104,6 +87,7 @@ export const VariableSearchInput = ({
setInputValue(variable.name) setInputValue(variable.name)
onSelectVariable(variable) onSelectVariable(variable)
setKeyboardFocusIndex(undefined) setKeyboardFocusIndex(undefined)
inputRef.current?.blur()
onClose() onClose()
} }
@@ -112,6 +96,7 @@ export const VariableSearchInput = ({
const id = 'v' + cuid() const id = 'v' + cuid()
onSelectVariable({ id, name: inputValue }) onSelectVariable({ id, name: inputValue })
createVariable({ id, name: inputValue }) createVariable({ id, name: inputValue })
inputRef.current?.blur()
onClose() onClose()
} }
@@ -122,7 +107,6 @@ export const VariableSearchInput = ({
setFilteredItems(filteredItems.filter((item) => item.id !== variable.id)) setFilteredItems(filteredItems.filter((item) => item.id !== variable.id))
if (variable.name === inputValue) { if (variable.name === inputValue) {
setInputValue('') setInputValue('')
debounced('')
} }
} }
@@ -147,7 +131,12 @@ export const VariableSearchInput = ({
if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) { if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
if (keyboardFocusIndex === 0 && isCreateVariableButtonDisplayed) if (keyboardFocusIndex === 0 && isCreateVariableButtonDisplayed)
handleCreateNewVariableClick() handleCreateNewVariableClick()
else handleVariableNameClick(filteredItems[keyboardFocusIndex])() else
handleVariableNameClick(
filteredItems[
keyboardFocusIndex - (isCreateVariableButtonDisplayed ? 1 : 0)
]
)()
return setKeyboardFocusIndex(undefined) return setKeyboardFocusIndex(undefined)
} }
if (e.key === 'ArrowDown') { if (e.key === 'ArrowDown') {
@@ -199,6 +188,7 @@ export const VariableSearchInput = ({
role="menu" role="menu"
w="inherit" w="inherit"
shadow="lg" shadow="lg"
onMouseDown={(e) => e.stopPropagation()}
> >
{isCreateVariableButtonDisplayed && ( {isCreateVariableButtonDisplayed && (
<Button <Button
@@ -215,7 +205,10 @@ export const VariableSearchInput = ({
leftIcon={<PlusIcon />} leftIcon={<PlusIcon />}
bgColor={keyboardFocusIndex === 0 ? bg : 'transparent'} bgColor={keyboardFocusIndex === 0 ? bg : 'transparent'}
> >
Create &quot;{inputValue}&quot; Create
<Tag colorScheme="orange" ml="1">
{inputValue}
</Tag>
</Button> </Button>
)} )}
{filteredItems.length > 0 && ( {filteredItems.length > 0 && (
@@ -230,7 +223,6 @@ export const VariableSearchInput = ({
role="menuitem" role="menuitem"
minH="40px" minH="40px"
key={idx} key={idx}
onMouseDown={(e) => e.stopPropagation()}
onClick={handleVariableNameClick(item)} onClick={handleVariableNameClick(item)}
fontSize="16px" fontSize="16px"
fontWeight="normal" fontWeight="normal"

View File

@@ -163,7 +163,7 @@ const TextBubbleEditorContent = ({
<VariableSearchInput <VariableSearchInput
onSelectVariable={handleVariableSelected} onSelectVariable={handleVariableSelected}
placeholder="Search for a variable" placeholder="Search for a variable"
isDefaultOpen autoFocus
/> />
</Flex> </Flex>
)} )}

View File

@@ -27,6 +27,7 @@ type Props = {
export const ToolBar = ({ onVariablesButtonClick, ...props }: Props) => { export const ToolBar = ({ onVariablesButtonClick, ...props }: Props) => {
const editor = usePlateEditorRef() const editor = usePlateEditorRef()
const handleVariablesButtonMouseDown = (e: React.MouseEvent) => { const handleVariablesButtonMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault() e.preventDefault()
onVariablesButtonClick() onVariablesButtonClick()
} }

View File

@@ -48,7 +48,7 @@ test.describe('Text bubble block', () => {
await page.press('div[role="textbox"]', 'Shift+Enter') await page.press('div[role="textbox"]', 'Shift+Enter')
await page.click('button[aria-label="Insert variable"]') await page.click('button[aria-label="Insert variable"]')
await page.fill('[data-testid="variables-input"]', 'test') await page.fill('[data-testid="variables-input"]', 'test')
await page.click('text=Create "test"') await page.getByRole('menuitem', { name: 'Create test' }).click()
await page.click('text=Preview') await page.click('text=Preview')
await expect( await expect(

View File

@@ -169,5 +169,5 @@ const fillInSpreadsheetInfo = async (page: Page) => {
const createNewVar = async (page: Page, name: string) => { const createNewVar = async (page: Page, name: string) => {
await page.fill('input[placeholder="Select a variable"] >> nth=-1', name) await page.fill('input[placeholder="Select a variable"] >> nth=-1', name)
await page.click(`text=Create "${name}"`) await page.getByRole('menuitem', { name: `Create ${name}` }).click()
} }

View File

@@ -8,7 +8,6 @@ export const DataVariableInputs = ({
item, item,
onItemChange, onItemChange,
dataItems, dataItems,
debounceTimeout,
}: TableListItemProps<ResponseVariableMapping> & { dataItems: string[] }) => { }: TableListItemProps<ResponseVariableMapping> & { dataItems: string[] }) => {
const handleBodyPathChange = (bodyPath: string) => const handleBodyPathChange = (bodyPath: string) =>
onItemChange({ ...item, bodyPath }) onItemChange({ ...item, bodyPath })
@@ -24,7 +23,6 @@ export const DataVariableInputs = ({
value={item.bodyPath} value={item.bodyPath}
onValueChange={handleBodyPathChange} onValueChange={handleBodyPathChange}
placeholder="Select the data" placeholder="Select the data"
debounceTimeout={debounceTimeout}
withVariableButton withVariableButton
/> />
</FormControl> </FormControl>
@@ -34,7 +32,6 @@ export const DataVariableInputs = ({
onSelectVariable={handleVariableChange} onSelectVariable={handleVariableChange}
placeholder="Search for a variable" placeholder="Search for a variable"
initialVariableId={item.variableId} initialVariableId={item.variableId}
debounceTimeout={debounceTimeout}
/> />
</FormControl> </FormControl>
</Stack> </Stack>

View File

@@ -23,7 +23,6 @@ export const VariableForTestInputs = ({
id={'name' + item.id} id={'name' + item.id}
initialVariableId={item.variableId} initialVariableId={item.variableId}
onSelectVariable={handleVariableSelect} onSelectVariable={handleVariableSelect}
debounceTimeout={debounceTimeout}
/> />
</FormControl> </FormControl>
<FormControl> <FormControl>

View File

@@ -18,11 +18,11 @@ test.describe('Set variable block', () => {
await page.goto(`/typebots/${typebotId}/edit`) await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Type a number...') await page.click('text=Type a number...')
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Num') await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Num')
await page.click('text=Create "Num"') await page.getByRole('menuitem', { name: 'Create Num' }).click()
await page.click('text=Click to edit... >> nth = 0') await page.click('text=Click to edit... >> nth = 0')
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total') await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total')
await page.click('text=Create "Total"') await page.getByRole('menuitem', { name: 'Create Total' }).click()
await page.fill('textarea', '1000 * {{Num}}') await page.fill('textarea', '1000 * {{Num}}')
await page.click('text=Click to edit...', { force: true }) await page.click('text=Click to edit...', { force: true })
@@ -30,7 +30,7 @@ test.describe('Set variable block', () => {
'input[placeholder="Select a variable"] >> nth=-1', 'input[placeholder="Select a variable"] >> nth=-1',
'Custom var' 'Custom var'
) )
await page.click('text=Create "Custom var"') await page.getByRole('menuitem', { name: 'Create Custom var' }).click()
await page.fill('textarea', 'Custom value') await page.fill('textarea', 'Custom value')
await page.click('text=Click to edit...', { force: true }) await page.click('text=Click to edit...', { force: true })
@@ -38,7 +38,7 @@ test.describe('Set variable block', () => {
'input[placeholder="Select a variable"] >> nth=-1', 'input[placeholder="Select a variable"] >> nth=-1',
'Addition' 'Addition'
) )
await page.click('text=Create "Addition"') await page.getByRole('menuitem', { name: 'Create Addition' }).click()
await page.fill('textarea', '1000 + {{Total}}') await page.fill('textarea', '1000 + {{Total}}')
await page.click('text=Preview') await page.click('text=Preview')

View File

@@ -38,7 +38,7 @@ export const VariablesButton = ({ onSelectVariable, ...props }: Props) => {
onSelectVariable={onSelectVariable} onSelectVariable={onSelectVariable}
placeholder="Search for a variable" placeholder="Search for a variable"
shadow="lg" shadow="lg"
isDefaultOpen autoFocus
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>