2
0

feat(editor): 🚸 Arrow & Enter commands for dropdowns

This commit is contained in:
Baptiste Arnaud
2022-06-30 11:19:26 +02:00
parent 6c1d9d419b
commit bc803fc552
2 changed files with 102 additions and 22 deletions

View File

@ -13,7 +13,7 @@ import {
import { Variable } from 'models' import { Variable } from 'models'
import { useState, useRef, useEffect, ChangeEvent } from 'react' import { useState, useRef, useEffect, ChangeEvent } from 'react'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import { env } from 'utils' import { env, isDefined } from 'utils'
import { VariablesButton } from './buttons/VariablesButton' import { VariablesButton } from './buttons/VariablesButton'
type Props = { type Props = {
@ -47,7 +47,11 @@ export const SearchableDropdown = ({
) )
.slice(0, 50), .slice(0, 50),
]) ])
const [keyboardFocusIndex, setKeyboardFocusIndex] = useState<
number | undefined
>()
const dropdownRef = useRef(null) const dropdownRef = useRef(null)
const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
useEffect( useEffect(
@ -96,6 +100,7 @@ export const SearchableDropdown = ({
const handleItemClick = (item: string) => () => { const handleItemClick = (item: string) => () => {
setInputValue(item) setInputValue(item)
debounced(item) debounced(item)
setKeyboardFocusIndex(undefined)
onClose() onClose()
} }
@ -125,9 +130,31 @@ export const SearchableDropdown = ({
}, 100) }, 100)
} }
const handleKeyUp = () => { const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!inputRef.current?.selectionStart) return if (inputRef.current?.selectionStart)
setCarretPosition(inputRef.current.selectionStart) setCarretPosition(inputRef.current.selectionStart)
if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
handleItemClick(filteredItems[keyboardFocusIndex])()
return setKeyboardFocusIndex(undefined)
}
if (e.key === 'ArrowDown') {
if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0)
if (keyboardFocusIndex === filteredItems.length - 1) return
itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
return setKeyboardFocusIndex(keyboardFocusIndex + 1)
}
if (e.key === 'ArrowUp') {
if (keyboardFocusIndex === undefined) return
if (keyboardFocusIndex === 0) return setKeyboardFocusIndex(undefined)
itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
setKeyboardFocusIndex(keyboardFocusIndex - 1)
}
} }
return ( return (
@ -170,6 +197,7 @@ export const SearchableDropdown = ({
{filteredItems.map((item, idx) => { {filteredItems.map((item, idx) => {
return ( return (
<Button <Button
ref={(el) => (itemsRef.current[idx] = el)}
minH="40px" minH="40px"
key={idx} key={idx}
onClick={handleItemClick(item)} onClick={handleItemClick(item)}
@ -179,6 +207,9 @@ export const SearchableDropdown = ({
colorScheme="gray" colorScheme="gray"
role="menuitem" role="menuitem"
variant="ghost" variant="ghost"
bgColor={
keyboardFocusIndex === idx ? 'gray.200' : 'transparent'
}
justifyContent="flex-start" justifyContent="flex-start"
> >
{item} {item}

View File

@ -17,7 +17,7 @@ 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 { useDebouncedCallback } from 'use-debounce'
import { byId, env, isNotDefined } from 'utils' import { byId, env, isDefined, isNotDefined } from 'utils'
type Props = { type Props = {
initialVariableId?: string initialVariableId?: string
@ -52,8 +52,13 @@ export const VariableSearchInput = ({
const [filteredItems, setFilteredItems] = useState<Variable[]>( const [filteredItems, setFilteredItems] = useState<Variable[]>(
variables ?? [] variables ?? []
) )
const [keyboardFocusIndex, setKeyboardFocusIndex] = useState<
number | undefined
>()
const dropdownRef = useRef(null) const dropdownRef = useRef(null)
const inputRef = useRef(null) const inputRef = useRef(null)
const createVariableItemRef = useRef<HTMLButtonElement | null>(null)
const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
useOutsideClick({ useOutsideClick({
ref: dropdownRef, ref: dropdownRef,
@ -93,6 +98,7 @@ export const VariableSearchInput = ({
const handleVariableNameClick = (variable: Variable) => () => { const handleVariableNameClick = (variable: Variable) => () => {
setInputValue(variable.name) setInputValue(variable.name)
onSelectVariable(variable) onSelectVariable(variable)
setKeyboardFocusIndex(undefined)
onClose() onClose()
} }
@ -128,6 +134,38 @@ export const VariableSearchInput = ({
) )
} }
const isCreateVariableButtonDisplayed =
(inputValue?.length ?? 0) > 0 &&
isNotDefined(variables.find((v) => v.name === inputValue))
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
if (keyboardFocusIndex === 0 && isCreateVariableButtonDisplayed)
handleCreateNewVariableClick()
else handleVariableNameClick(filteredItems[keyboardFocusIndex])()
return setKeyboardFocusIndex(undefined)
}
if (e.key === 'ArrowDown') {
if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0)
if (keyboardFocusIndex >= filteredItems.length) return
itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
return setKeyboardFocusIndex(keyboardFocusIndex + 1)
}
if (e.key === 'ArrowUp') {
if (keyboardFocusIndex === undefined) return
if (keyboardFocusIndex <= 0) return setKeyboardFocusIndex(undefined)
itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
return setKeyboardFocusIndex(keyboardFocusIndex - 1)
}
return setKeyboardFocusIndex(undefined)
}
return ( return (
<Flex ref={dropdownRef} w="full"> <Flex ref={dropdownRef} w="full">
<Popover <Popover
@ -144,6 +182,7 @@ export const VariableSearchInput = ({
value={inputValue} value={inputValue}
onChange={onInputChange} onChange={onInputChange}
onClick={onOpen} onClick={onOpen}
onKeyUp={handleKeyUp}
placeholder={inputProps.placeholder ?? 'Select a variable'} placeholder={inputProps.placeholder ?? 'Select a variable'}
{...inputProps} {...inputProps}
/> />
@ -155,9 +194,9 @@ export const VariableSearchInput = ({
w="inherit" w="inherit"
shadow="lg" shadow="lg"
> >
{(inputValue?.length ?? 0) > 0 && {isCreateVariableButtonDisplayed && (
isNotDefined(variables.find((v) => v.name === inputValue)) && (
<Button <Button
ref={createVariableItemRef}
role="menuitem" role="menuitem"
minH="40px" minH="40px"
onClick={handleCreateNewVariableClick} onClick={handleCreateNewVariableClick}
@ -168,6 +207,7 @@ export const VariableSearchInput = ({
variant="ghost" variant="ghost"
justifyContent="flex-start" justifyContent="flex-start"
leftIcon={<PlusIcon />} leftIcon={<PlusIcon />}
bgColor={keyboardFocusIndex === 0 ? 'gray.200' : 'transparent'}
> >
Create "{inputValue}" Create "{inputValue}"
</Button> </Button>
@ -175,8 +215,12 @@ export const VariableSearchInput = ({
{filteredItems.length > 0 && ( {filteredItems.length > 0 && (
<> <>
{filteredItems.map((item, idx) => { {filteredItems.map((item, idx) => {
const indexInList = isCreateVariableButtonDisplayed
? idx + 1
: idx
return ( return (
<Button <Button
ref={(el) => (itemsRef.current[idx] = el)}
role="menuitem" role="menuitem"
minH="40px" minH="40px"
key={idx} key={idx}
@ -187,6 +231,11 @@ export const VariableSearchInput = ({
colorScheme="gray" colorScheme="gray"
variant="ghost" variant="ghost"
justifyContent="space-between" justifyContent="space-between"
bgColor={
keyboardFocusIndex === indexInList
? 'gray.200'
: 'transparent'
}
> >
{item.name} {item.name}
<HStack> <HStack>