mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Symbol Palette: improve keyboard input experience
This commit is contained in:
+11
-1
@@ -1,19 +1,29 @@
|
|||||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
export default function SymbolPaletteCloseButton() {
|
export default function SymbolPaletteCloseButton() {
|
||||||
const { toggleSymbolPalette } = useEditorContext()
|
const { toggleSymbolPalette } = useEditorContext()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
toggleSymbolPalette()
|
||||||
|
window.dispatchEvent(new CustomEvent('editor:focus'))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="symbol-palette-close-button-outer">
|
<div className="symbol-palette-close-button-outer">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn-close symbol-palette-close-button"
|
className="btn-close symbol-palette-close-button"
|
||||||
onClick={toggleSymbolPalette}
|
onClick={handleClick}
|
||||||
aria-label={t('close')}
|
aria-label={t('close')}
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 PropTypes from 'prop-types'
|
||||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||||
|
|
||||||
export default function SymbolPaletteItem({
|
const SymbolPaletteItem = forwardRef(function ({
|
||||||
focused,
|
focused,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
symbol,
|
symbol,
|
||||||
}) {
|
}, ref) {
|
||||||
const buttonRef = useRef(null)
|
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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
focused &&
|
focused &&
|
||||||
@@ -56,7 +67,7 @@ export default function SymbolPaletteItem({
|
|||||||
</button>
|
</button>
|
||||||
</OLTooltip>
|
</OLTooltip>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
SymbolPaletteItem.propTypes = {
|
SymbolPaletteItem.propTypes = {
|
||||||
symbol: PropTypes.shape({
|
symbol: PropTypes.shape({
|
||||||
@@ -70,3 +81,4 @@ SymbolPaletteItem.propTypes = {
|
|||||||
handleSelect: PropTypes.func.isRequired,
|
handleSelect: PropTypes.func.isRequired,
|
||||||
focused: PropTypes.bool,
|
focused: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
export default SymbolPaletteItem
|
||||||
|
|||||||
+55
-23
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||||
import SymbolPaletteItem from './symbol-palette-item'
|
import SymbolPaletteItem from './symbol-palette-item'
|
||||||
|
|
||||||
export default function SymbolPaletteItems({
|
export default function SymbolPaletteItems({
|
||||||
@@ -8,54 +9,80 @@ export default function SymbolPaletteItems({
|
|||||||
focusInput,
|
focusInput,
|
||||||
}) {
|
}) {
|
||||||
const [focusedIndex, setFocusedIndex] = useState(0)
|
const [focusedIndex, setFocusedIndex] = useState(0)
|
||||||
|
const itemRefs = useRef([])
|
||||||
|
|
||||||
// reset the focused item when the list of items changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
itemRefs.current = items.map((_, i) => itemRefs.current[i] || null)
|
||||||
setFocusedIndex(0)
|
setFocusedIndex(0)
|
||||||
}, [items])
|
}, [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(
|
const handleKeyDown = useCallback(
|
||||||
event => {
|
event => {
|
||||||
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
|
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return
|
||||||
return
|
|
||||||
}
|
const rects = getItemRects()
|
||||||
|
const currentRect = rects[focusedIndex]
|
||||||
|
if (!currentRect) return
|
||||||
|
|
||||||
|
let newIndex = focusedIndex
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
// focus previous item
|
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
case 'ArrowUp':
|
newIndex = focusedIndex > 0 ? focusedIndex - 1 : items.length - 1
|
||||||
setFocusedIndex(index => (index > 0 ? index - 1 : items.length - 1))
|
|
||||||
break
|
break
|
||||||
|
|
||||||
// focus next item
|
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
case 'ArrowDown':
|
newIndex = focusedIndex < items.length - 1 ? focusedIndex + 1 : 0
|
||||||
setFocusedIndex(index => (index < items.length - 1 ? index + 1 : 0))
|
|
||||||
break
|
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':
|
case 'Home':
|
||||||
setFocusedIndex(0)
|
newIndex = 0
|
||||||
break
|
break
|
||||||
|
|
||||||
// focus last item
|
|
||||||
case 'End':
|
case 'End':
|
||||||
setFocusedIndex(items.length - 1)
|
newIndex = items.length - 1
|
||||||
break
|
break
|
||||||
|
|
||||||
// allow the default action
|
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case ' ':
|
case ' ':
|
||||||
|
handleSelect(items[focusedIndex])
|
||||||
|
toggleSymbolPalette()
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
toggleSymbolPalette()
|
||||||
|
window.dispatchEvent(new CustomEvent('editor:focus'))
|
||||||
break
|
break
|
||||||
|
|
||||||
// any other key returns focus to the input
|
|
||||||
default:
|
default:
|
||||||
focusInput()
|
focusInput()
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
setFocusedIndex(newIndex)
|
||||||
},
|
},
|
||||||
[focusInput, items.length]
|
[focusedIndex, items, focusInput, handleSelect]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -70,11 +97,15 @@ export default function SymbolPaletteItems({
|
|||||||
}}
|
}}
|
||||||
handleKeyDown={handleKeyDown}
|
handleKeyDown={handleKeyDown}
|
||||||
focused={index === focusedIndex}
|
focused={index === focusedIndex}
|
||||||
|
ref={el => {
|
||||||
|
itemRefs.current[index] = el
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SymbolPaletteItems.propTypes = {
|
SymbolPaletteItems.propTypes = {
|
||||||
items: PropTypes.arrayOf(
|
items: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
@@ -84,3 +115,4 @@ SymbolPaletteItems.propTypes = {
|
|||||||
handleSelect: PropTypes.func.isRequired,
|
handleSelect: PropTypes.func.isRequired,
|
||||||
focusInput: PropTypes.func.isRequired,
|
focusInput: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user