fix: focused elements could be hidden by the header on top

Massimo Melina committed Sep 16, 2025 at 15:44 UTC 1fa8f8fd04ee2106cf5fc04f5a00b0655fa50b59
3 files changed +17 -7
admin/src/App.ts
+2 -2
@@ -14,7 +14,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
14 import ConfigFilePage from './ConfigFilePage'
15 import { useSnapState } from './state'
16 import { useEventListener } from 'usehooks-ts'
17 -import { AriaOnly, isMac, xlate } from './misc'
17 +import { AriaOnly, isMac, useFixSticky, xlate } from './misc'
18 import { getLocale } from './locale'
19 import { fillFlexParentSx } from './DataTable'
20
@@ -130,7 +130,7 @@ function itemTitle(idx: number) {
130 }
131
132 function StickyBar({ title, titleSide, openMenu, props }: { props?: BoxProps, titleSide?: ReactNode, title?: string, openMenu: ()=>void }) {
133 - return h(AppBar, { position: 'sticky', sx: { mb: 1 } },
133 + return h(AppBar, { ref: useFixSticky(), position: 'sticky', sx: { mb: 1 }, },
134 h(Toolbar, {},
135 h(IconButton, {
136 size: 'large',
frontend/src/Head.ts
+2 -2
@@ -1,7 +1,7 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import { createElement as h, Fragment, useMemo } from 'react'
4 -import { formatBytes, hIcon } from './misc'
4 +import { formatBytes, hIcon, useFixSticky } from './misc'
5 import { CustomCode, Spinner } from './components'
6 import { useSnapState } from './state'
7 import { MenuPanel } from './menu'
@@ -11,7 +11,7 @@ import i18n from './i18n'
11 const { useI18N } = i18n
12
13 export function Head() {
14 - return h('header', {},
14 + return h('header', { ref: useFixSticky() },
15 h(MenuPanel),
16 h(CustomCode, { name: 'afterMenuBar' }),
17 h(Breadcrumbs),
shared/react.ts
+13 -3
@@ -126,14 +126,24 @@ export function useIsMobile() {
126 return useMediaQuery('(pointer:coarse)')
127 }
128
129 +// workaround for the usability problem caused by sticky headers/footers. Just assign the returned value as ref prop of your sticky element.
130 +export function useFixSticky() {
131 + return useOnResize((_w, h, _el, style) => {
132 + Object.assign(document.documentElement.style, {
133 + scrollPaddingTop: `${h + (parseFloat(style.top) || 0)}px`,
134 + scrollPaddingBottom: `${h + (parseFloat(style.bottom) || 0)}px`,
135 + })
136 + }).refToPass
137 +}
138 +
139 // returns props to assign to your component, and a copy of the ref; calls back with [width, height]
130 -export function useOnResize(cb: Callback<[number, number]>) {
140 +export function useOnResize(cb: (width: number, height: number, target: HTMLElement, style: CSSStyleDeclaration) => any) {
141 const observer = useMemo(() =>
142 new ResizeObserver(_.debounce(([{ contentRect: r, target }]) => {
143 const style = getComputedStyle(target)
144 const pw = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)
145 const ph = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)
136 - cb([r.width + pw, r.height + ph])
146 + cb(r.width + pw, r.height + ph, target, style)
147 }, 10)),
148 [])
149 const ref = useRef<HTMLElement>()
@@ -150,7 +160,7 @@ export function useOnResize(cb: Callback<[number, number]>) {
160
161 export function useGetSize() {
162 const [size, setSize] = useState<[number,number]>()
153 - const { refToPass, ref } = useOnResize(setSize)
163 + const { refToPass, ref } = useOnResize((w, h) => setSize([w, h]))
164 return useMemo(() => ({
165 w: size?.[0],
166 h: size?.[1],