tab to navigate the focus-typing
Massimo Melina committed
Feb 7, 2025 at 17:12 UTC
7e653ac59aa515185cd6c8998c8c48c5df43548c
1 file changed
+49
-17
frontend/src/BrowseFiles.ts
+49
-17
@@ -76,8 +76,8 @@ function FilesList() {
76
const theList = filteredList || list
77
const total = theList.length
78
const nPages = Math.ceil(total / pageSize)
79
- const pageEnd = offset + pageSize * (1+extraPages)
80
- const thisPage = theList.slice(offset, pageEnd)
79
+ const pageEnd = offset + pageSize * (1+extraPages) - 1
80
+ const thisPage = theList.slice(offset, pageEnd + 1)
81
82
useEffect(() => setPage(0), [theList[0]]) // reset page if the list changes
83
// reset scrolling if the page changes
@@ -106,6 +106,7 @@ function FilesList() {
106
107
// type to focus
108
const [focus, setFocus] = useState('')
109
+ const [focusSkip, setFocusSkip] = useState(0)
110
useEffect(() => setFocus(''), [theList]) // reset
111
const navigate = useNavigate()
112
const timeout = useRef()
@@ -115,29 +116,60 @@ function FilesList() {
116
return navigate(location.pathname + '..')
117
const { key } = ev
118
if (ev.metaKey || ev.ctrlKey || ev.altKey) return
119
+ if (key === 'Tab' && focus) {
120
+ const go = ev.shiftKey ? -1 : 1
121
+ 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
122
+ renewTimeout()
123
+ ev.preventDefault()
124
+ return
125
+ }
126
setFocus(was => {
127
const will = key === 'Backspace' ? was.slice(0, -1)
120
- : key === 'Escape' || key === 'Tab' ? ''
128
+ : key === 'Escape' ? ''
129
: key.length === 1 ? was + key.toLocaleLowerCase()
130
: was
123
- if (will !== was) {
124
- clearTimeout(timeout.current)
125
- timeout.current = setTimeout(() => setFocus(''), 5_000) as any
126
- }
131
+ if (will !== was)
132
+ renewTimeout()
133
return will
134
})
135
+
136
+ function renewTimeout() {
137
+ clearTimeout(timeout.current)
138
+ timeout.current = setTimeout(() => setFocus(''), 5_000) as any
139
+ }
140
})
141
const focusIndex = useMemo(() => {
131
- if (!focus) return -1
132
- const match = (x: typeof theList[0]) => x.name.toLocaleLowerCase().normalize().startsWith(focus)
133
- const inThisPage = thisPage.findIndex(match) // first attempt within this page
134
- if (inThisPage >= 0)
135
- return inThisPage + offset
136
- const i = theList.findIndex(match) // search again on whole list
137
- if (i >= 0)
138
- setPage(Math.floor(i / pageSize))
139
- return i
140
- }, [focus])
142
+ if (!focus) {
143
+ setFocusSkip(0)
144
+ return -1
145
+ }
146
+ let ret = search(offset) // first attempt within this page
147
+ if (offset && (ret < 0 || ret > pageEnd))
148
+ ret = search(0) // search again on whole list
149
+ if (ret >= 0)
150
+ setPage(Math.floor(ret / pageSize))
151
+ return ret
152
+
153
+ function search(offset: number) {
154
+ let prev = offset - 1
155
+ let leftToSkip = focusSkip
156
+ while (1) {
157
+ const ret = _.findIndex(theList, x => x.name.toLocaleLowerCase().normalize().startsWith(focus), prev + 1)
158
+ if (ret < 0) {
159
+ if (offset || prev < offset) // this is not our last search, or prev is not a result
160
+ return -1
161
+ if (focusSkip)
162
+ setFocusSkip(x => x - leftToSkip - 1) // prev is a result, but let's lessen the skip
163
+ console.log(prev, theList[prev]?.name, leftToSkip)
164
+ return prev
165
+ }
166
+ if (! leftToSkip--)
167
+ return ret
168
+ prev = ret
169
+ }
170
+ throw 'unreachable' // shut up ts
171
+ }
172
+ }, [focus, focusSkip])
173
useEffect(() => { // wait for possible page-change before focusing
174
if (focusIndex >= 0)
175
(document.querySelector(`a[href="${theList[focusIndex]?.uri}"]`) as HTMLElement)?.focus()