mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Symbol Palette: improve keyboard input experience
This commit is contained in:
@@ -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 (
|
||||
<div className="symbol-palette-close-button-outer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close symbol-palette-close-button"
|
||||
onClick={toggleSymbolPalette}
|
||||
onClick={handleClick}
|
||||
aria-label={t('close')}
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SymbolPaletteCloseButton.propTypes = {
|
||||
focusInput: PropTypes.func,
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
</button>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
SymbolPaletteItem.propTypes = {
|
||||
symbol: PropTypes.shape({
|
||||
@@ -70,3 +81,4 @@ SymbolPaletteItem.propTypes = {
|
||||
handleSelect: PropTypes.func.isRequired,
|
||||
focused: PropTypes.bool,
|
||||
}
|
||||
export default SymbolPaletteItem
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SymbolPaletteItems.propTypes = {
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
@@ -84,3 +115,4 @@ SymbolPaletteItems.propTypes = {
|
||||
handleSelect: PropTypes.func.isRequired,
|
||||
focusInput: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user