Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | 1x 1x 1x 1x 1x 76x 76x 76x 76x 76x 48x 38x 48x 48x 48x 11x 27x 48x 9x 10x 10x 10x 10x 10x 10x 10x 10x 2x 2x 2x 8x 8x 8x 10x 10x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 76x 76x | import { useEffect, type RefObject } from 'react';
const MOBILE_MEDIA = '(max-width: 767px)';
// Gap kept between the menu and the top of the keyboard / top of the screen.
const VISIBLE_MARGIN = 16;
// Only reposition once the keyboard covers a meaningful slice of the viewport.
// Smaller shrinkages are just browser UI chrome (address bar, etc.) and should
// leave the menu at its natural resting position.
const KEYBOARD_MIN_HEIGHT = 120;
/**
* Keep the tracked content sitting just above the mobile virtual keyboard.
* Uses the VisualViewport API to measure the keyboard and applies a translateY
* transform to `containerRef` (which must be an ancestor of `contentRef`) so it
* never clashes with the content's own enter/exit animation.
*
* While the keyboard is open the menu is anchored `VISIBLE_MARGIN` above it in
* both directions: lifted when the keyboard would cover it, and pulled back
* down when the layout leaves it floating too far above (e.g. a short comment
* that rests high on screen). With the keyboard closed the transform is
* cleared so the menu keeps its natural position.
*
* Measurement is layout-based (`offsetTop`/`offsetHeight`) rather than
* `getBoundingClientRect()`, so it is immune to both the entrance slide
* animation and to the transform this hook itself applies. That lets the lift
* begin immediately and track the keyboard as it rises, so opening the menu and
* clearing the keyboard read as a single, continuous motion instead of two
* separate steps.
*/
export const useKeyboardInset = (
containerRef: RefObject<HTMLElement | null>,
contentRef: RefObject<HTMLElement | null>,
enabled: boolean,
): void => {
useEffect(() => {
if (!enabled) return;
const viewport =
typeof window !== 'undefined' ? window.visualViewport : null;
const isMobile =
typeof window !== 'undefined' && window.matchMedia
? window.matchMedia(MOBILE_MEDIA).matches
: false;
if (!viewport || !isMobile) return;
const update = (): void => {
const container = containerRef.current;
const content = contentRef.current;
if (!container || !content) return;
// Layout offsets describe the content's *resting* position relative to
// the positioned overlay (pinned at the viewport top), so they stay
// constant while the entrance animation slides the content in and while
// this transform lifts it. That means no mid-animation mis-measurement,
// so the offset can be applied right away.
const naturalTop = content.offsetTop;
const naturalBottom = naturalTop + content.offsetHeight;
const visibleBottom = viewport.height + viewport.offsetTop;
const keyboardHeight = window.innerHeight - visibleBottom;
// Keyboard closed: leave the menu at its natural resting position.
if (keyboardHeight < KEYBOARD_MIN_HEIGHT) {
container.style.transform = '';
return;
}
// Distance to move so the content bottom sits VISIBLE_MARGIN above the
// keyboard. Positive = lift up (keyboard covers content); negative = pull
// down (content is floating too far above the keyboard).
let offset = naturalBottom + VISIBLE_MARGIN - visibleBottom;
// Never push the top of the content off the top of the screen.
offset = Math.min(offset, Math.max(0, naturalTop - VISIBLE_MARGIN));
offset = Math.round(offset);
container.style.transform = offset ? `translateY(${-offset}px)` : '';
};
viewport.addEventListener('resize', update);
viewport.addEventListener('scroll', update);
update();
return () => {
viewport.removeEventListener('resize', update);
viewport.removeEventListener('scroll', update);
if (containerRef.current) {
containerRef.current.style.transform = '';
}
};
}, [enabled, containerRef, contentRef]);
};
|