feat(editor): 🚸 Arrow & Enter commands for dropdowns
This commit is contained in:
@ -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}
|
||||||
|
@ -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,28 +194,33 @@ 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}
|
||||||
fontSize="16px"
|
fontSize="16px"
|
||||||
fontWeight="normal"
|
fontWeight="normal"
|
||||||
rounded="none"
|
rounded="none"
|
||||||
colorScheme="gray"
|
colorScheme="gray"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
leftIcon={<PlusIcon />}
|
leftIcon={<PlusIcon />}
|
||||||
>
|
bgColor={keyboardFocusIndex === 0 ? 'gray.200' : 'transparent'}
|
||||||
Create "{inputValue}"
|
>
|
||||||
</Button>
|
Create "{inputValue}"
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
{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>
|
||||||
|
Reference in New Issue
Block a user