diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js
index dbde7ca775..0a2d3c02fc 100644
--- a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js
@@ -1,19 +1,29 @@
import { useEditorContext } from '../../../shared/context/editor-context'
import { useTranslation } from 'react-i18next'
+import PropTypes from 'prop-types'
export default function SymbolPaletteCloseButton() {
const { toggleSymbolPalette } = useEditorContext()
const { t } = useTranslation()
+ const handleClick = () => {
+ toggleSymbolPalette()
+ window.dispatchEvent(new CustomEvent('editor:focus'))
+ }
+
return (
)
}
+
+SymbolPaletteCloseButton.propTypes = {
+ focusInput: PropTypes.func,
+}
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js
index 1369534106..400da81b1e 100644
--- a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js
@@ -1,16 +1,27 @@
-import { useEffect, useRef } from 'react'
+import { useEffect, useRef, forwardRef } from 'react'
import PropTypes from 'prop-types'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
-export default function SymbolPaletteItem({
+const SymbolPaletteItem = forwardRef(function ({
focused,
handleSelect,
handleKeyDown,
symbol,
-}) {
+}, ref) {
const buttonRef = useRef(null)
- // call focus() on this item when appropriate
+ // Forward internal ref to parent
+ useEffect(() => {
+ if (ref) {
+ if (typeof ref === 'function') {
+ ref(buttonRef.current)
+ } else {
+ ref.current = buttonRef.current
+ }
+ }
+ }, [ref])
+
+ // Focus the item when it becomes focused
useEffect(() => {
if (
focused &&
@@ -56,7 +67,7 @@ export default function SymbolPaletteItem({
)
-}
+})
SymbolPaletteItem.propTypes = {
symbol: PropTypes.shape({
@@ -70,3 +81,4 @@ SymbolPaletteItem.propTypes = {
handleSelect: PropTypes.func.isRequired,
focused: PropTypes.bool,
}
+export default SymbolPaletteItem
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js
index 44835261f5..8d95439f5a 100644
--- a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js
@@ -1,5 +1,6 @@
-import { useCallback, useEffect, useState } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
+import { useEditorContext } from '../../../shared/context/editor-context'
import SymbolPaletteItem from './symbol-palette-item'
export default function SymbolPaletteItems({
@@ -8,54 +9,80 @@ export default function SymbolPaletteItems({
focusInput,
}) {
const [focusedIndex, setFocusedIndex] = useState(0)
+ const itemRefs = useRef([])
- // reset the focused item when the list of items changes
useEffect(() => {
+ itemRefs.current = items.map((_, i) => itemRefs.current[i] || null)
setFocusedIndex(0)
}, [items])
- // navigate through items with left and right arrows
+ const getItemRects = () => {
+ return itemRefs.current.map(ref => ref?.getBoundingClientRect?.() ?? null)
+ }
+ const { toggleSymbolPalette } = useEditorContext()
+
const handleKeyDown = useCallback(
event => {
- if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
- return
- }
+ if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return
+
+ const rects = getItemRects()
+ const currentRect = rects[focusedIndex]
+ if (!currentRect) return
+
+ let newIndex = focusedIndex
switch (event.key) {
- // focus previous item
case 'ArrowLeft':
- case 'ArrowUp':
- setFocusedIndex(index => (index > 0 ? index - 1 : items.length - 1))
+ newIndex = focusedIndex > 0 ? focusedIndex - 1 : items.length - 1
break
-
- // focus next item
case 'ArrowRight':
- case 'ArrowDown':
- setFocusedIndex(index => (index < items.length - 1 ? index + 1 : 0))
+ newIndex = focusedIndex < items.length - 1 ? focusedIndex + 1 : 0
break
+ case 'ArrowUp':
+ case 'ArrowDown': {
+ const direction = event.key === 'ArrowUp' ? -1 : 1
+ const candidates = rects
+ .map((rect, i) => ({ rect, i }))
+ .filter(({ rect }, i) =>
+ i !== focusedIndex &&
+ rect &&
+ Math.abs(rect.x - currentRect.x) < currentRect.width * 0.8 &&
+ (direction === -1 ? rect.y < currentRect.y : rect.y > currentRect.y)
+ )
- // focus first item
+ if (candidates.length > 0) {
+ const closest = candidates.reduce((a, b) =>
+ Math.abs(b.rect.y - currentRect.y) < Math.abs(a.rect.y - currentRect.y) ? b : a
+ )
+ newIndex = closest.i
+ }
+ break
+ }
case 'Home':
- setFocusedIndex(0)
+ newIndex = 0
break
-
- // focus last item
case 'End':
- setFocusedIndex(items.length - 1)
+ newIndex = items.length - 1
break
-
- // allow the default action
case 'Enter':
case ' ':
+ handleSelect(items[focusedIndex])
+ toggleSymbolPalette()
+ break
+ case 'Escape':
+ toggleSymbolPalette()
+ window.dispatchEvent(new CustomEvent('editor:focus'))
break
- // any other key returns focus to the input
default:
focusInput()
- break
+ return
}
+
+ event.preventDefault()
+ setFocusedIndex(newIndex)
},
- [focusInput, items.length]
+ [focusedIndex, items, focusInput, handleSelect]
)
return (
@@ -70,11 +97,15 @@ export default function SymbolPaletteItems({
}}
handleKeyDown={handleKeyDown}
focused={index === focusedIndex}
+ ref={el => {
+ itemRefs.current[index] = el
+ }}
/>
))}
)
}
+
SymbolPaletteItems.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
@@ -84,3 +115,4 @@ SymbolPaletteItems.propTypes = {
handleSelect: PropTypes.func.isRequired,
focusInput: PropTypes.func.isRequired,
}
+