@samitouri / QOSami-HFS / commits / 6f0800e1

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