focus-typing: visual clue when the end of tab-cycle is reached
Massimo Melina committed
Sep 16, 2025 at 10:37 UTC
6f0800e15ed255c0320817e803c1888360897318
2 files changed
+15
-6
frontend/src/BrowseFiles.ts
+14
-5
@@ -5,7 +5,9 @@ import {
5
createElement as h, Fragment, memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState, useId
6
} from 'react'
7
import { useEventListener, useMediaQuery, useWindowSize } from 'usehooks-ts'
8
-import { domOn, ErrorMsg, hIcon, onlyTruthy, prefix, isMac, isCtrlKey, hfsEvent, formatTimestamp } from './misc'
8
+import {
9
+ domOn, ErrorMsg, hIcon, onlyTruthy, prefix, isMac, isCtrlKey, hfsEvent, formatTimestamp, restartAnimation
10
+} from './misc'
11
import { Checkbox, CustomCode, Bytes, iconBtn, Spinner } from './components'
12
import { Head } from './Head'
13
import { DirEntry, state, useSnapState } from './state'
@@ -109,6 +111,8 @@ function FilesList() {
111
const [focus, setFocus] = useState('')
112
const [focusSkip, setFocusSkip] = useState(0)
113
useEffect(() => setFocus(''), [theList]) // reset
114
+ const focusTypingId = 'focus-typing'
115
+ const endReached = () => restartAnimation(document.getElementById(focusTypingId), 'spin .3s')
116
const navigate = useNavigate()
117
const timeout = useRef()
118
useEventListener('keydown', ev => {
@@ -119,7 +123,12 @@ function FilesList() {
123
const { key } = ev
124
if (key === 'Tab' && focus) {
125
const go = ev.shiftKey ? -1 : 1
122
- setFocusSkip(x => x + (go > 0 || x ? go : 0)) // we always try to go forward, and skip more, and if no enough matching items are found, we adjust "focusSkip" back
126
+ setFocusSkip(was => {
127
+ if (go > 0 || was)
128
+ return was + go
129
+ endReached()
130
+ return was
131
+ }) // we always try to go forward, and skip more, and if no enough matching items are found, we adjust "focusSkip" back
132
renewTimeout()
133
ev.preventDefault()
134
return
@@ -149,7 +158,7 @@ function FilesList() {
158
}
159
let ret = search(offset) // first attempt within this page
160
if (offset && (ret < 0 || ret > pageEnd))
152
- ret = search(0) // search again on whole list
161
+ ret = search(0) // search again on the whole list
162
if (ret >= 0)
163
setPage(Math.floor(ret / pageSize))
164
return ret
@@ -164,7 +173,7 @@ function FilesList() {
173
return -1
174
if (focusSkip)
175
setFocusSkip(x => x - leftToSkip - 1) // prev is a result, but let's lessen the skip
167
- console.log(prev, theList[prev]?.name, leftToSkip)
176
+ endReached()
177
return prev
178
}
179
if (! leftToSkip--)
@@ -206,7 +215,7 @@ function FilesList() {
215
216
const focusHint = `${t('focus_hint', "By typing on your keyboard, you search and focus elements of the list.")}\n\nESC: ${t`Cancel`}`
217
return h(Fragment, {},
209
- focus && h('div', { id: 'focus-typing', className: focusIndex < 0 ? 'focus-typing-mismatch' : '' }, focus,
218
+ focus && h('div', { id: focusTypingId, className: focusIndex < 0 ? 'focus-typing-mismatch' : '' }, focus,
219
hIcon('info', { style: { cursor: 'default', marginLeft: '.3em' }, title: focusHint, onClick: () => alertDialog(focusHint) }) ),
220
h('ul', { ref, className: 'dir' },
221
msgInstead ? h('p', {}, msgInstead)
shared/index.ts
+1
-1
@@ -60,7 +60,7 @@ export function domOn<K extends keyof WindowEventMap>(eventName: K, cb: (ev: Win
60
return () => target.removeEventListener(eventName, cb)
61
}
62
63
-export function restartAnimation(e: HTMLElement | null, animation: string) {
63
+export function restartAnimation(e: HTMLElement | null | undefined, animation: string) {
64
if (!e) return
65
e.style.animation = ''
66
void e.offsetWidth