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()