Skip to content

Commit

Permalink
feat(editor): 🚸 Arrow & Enter commands for dropdowns
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Jun 30, 2022
1 parent 6c1d9d4 commit bc803fc
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 22 deletions.
39 changes: 35 additions & 4 deletions apps/builder/components/shared/SearchableDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -96,6 +100,7 @@ export const SearchableDropdown = ({
const handleItemClick = (item: string) => () => {
setInputValue(item)
debounced(item)
setKeyboardFocusIndex(undefined)
onClose()
}

Expand Down Expand Up @@ -125,9 +130,31 @@ export const SearchableDropdown = ({
}, 100)
}

const handleKeyUp = () => {
if (!inputRef.current?.selectionStart) return
setCarretPosition(inputRef.current.selectionStart)
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 (
Expand Down Expand Up @@ -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)}
Expand All @@ -179,6 +207,9 @@ export const SearchableDropdown = ({
colorScheme="gray"
role="menuitem"
variant="ghost"
bgColor={
keyboardFocusIndex === idx ? 'gray.200' : 'transparent'
}
justifyContent="flex-start"
>
{item}
Expand Down
85 changes: 67 additions & 18 deletions apps/builder/components/shared/VariableSearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -93,6 +98,7 @@ export const VariableSearchInput = ({
const handleVariableNameClick = (variable: Variable) => () => {
setInputValue(variable.name)
onSelectVariable(variable)
setKeyboardFocusIndex(undefined)
onClose()
}

Expand Down Expand Up @@ -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
Expand All @@ -144,6 +182,7 @@ export const VariableSearchInput = ({
value={inputValue}
onChange={onInputChange}
onClick={onOpen}
onKeyUp={handleKeyUp}
placeholder={inputProps.placeholder ?? 'Select a variable'}
{...inputProps}
/>
Expand All @@ -155,28 +194,33 @@ export const VariableSearchInput = ({
w="inherit"
shadow="lg"
>
{(inputValue?.length ?? 0) > 0 &&
isNotDefined(variables.find((v) => v.name === inputValue)) && (
<Button
role="menuitem"
minH="40px"
onClick={handleCreateNewVariableClick}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PlusIcon />}
>
Create "{inputValue}"
</Button>
)}
{isCreateVariableButtonDisplayed && (
<Button
ref={createVariableItemRef}
role="menuitem"
minH="40px"
onClick={handleCreateNewVariableClick}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PlusIcon />}
bgColor={keyboardFocusIndex === 0 ? 'gray.200' : 'transparent'}
>
Create "{inputValue}"
</Button>
)}
{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}
Expand All @@ -187,6 +231,11 @@ export const VariableSearchInput = ({
colorScheme="gray"
variant="ghost"
justifyContent="space-between"
bgColor={
keyboardFocusIndex === indexInList
? 'gray.200'
: 'transparent'
}
>
{item.name}
<HStack>
Expand Down

0 comments on commit bc803fc

Please sign in to comment.