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

View File

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