mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
fix(mobile): the 3D grade legend goes compact on the side, and a field under the keyboard gets room to scroll above it
This commit is contained in:
@@ -1,32 +1,73 @@
|
||||
/**
|
||||
* The field being typed into stays above the keyboard, on every screen.
|
||||
*
|
||||
* The app resizes the page body for the keyboard, but a field low in a
|
||||
* scrolling pane (the settings, a long form) is not moved by that: it ends up
|
||||
* under the keyboard and the user types blind. Once the keyboard is up, the
|
||||
* focused field is scrolled to the middle of what is left of the window.
|
||||
* Screens that keep their composer above the keyboard themselves (the chat,
|
||||
* useKeyboardInset) are unaffected: a field already in view does not move.
|
||||
* The app is told to resize the page body for the keyboard, but on an iPad the
|
||||
* panes keep their full height (they are sized to the window, not the body),
|
||||
* so a field low in a scrolling pane (the settings, the tour editor's breaks,
|
||||
* a long form) ends up under the keyboard and the user types blind.
|
||||
*
|
||||
* Once the keyboard is up, the pane the field scrolls in gets room below its
|
||||
* content as tall as the keyboard, and is scrolled until the field sits above
|
||||
* the keyboard with a little air. The room goes again when the keyboard does.
|
||||
* A field already in view does not move, so screens that keep their composer
|
||||
* above the keyboard themselves (the chat, useKeyboardInset) are unaffected.
|
||||
*/
|
||||
|
||||
const TYPING_TARGET = 'input, textarea, select, [contenteditable="true"], [contenteditable=""]';
|
||||
/** What is left between the field and the keyboard's top edge. */
|
||||
const AIR = 24;
|
||||
|
||||
interface KeyboardEvents {
|
||||
addListener(event: 'keyboardDidShow', listener: () => void): Promise<unknown>;
|
||||
interface KeyboardInfo {
|
||||
keyboardHeight: number;
|
||||
}
|
||||
|
||||
/** Scrolls the focused field into view when it is one the user types into and it sits outside the window. */
|
||||
export function revealFocusedField(doc: Document = document): void {
|
||||
interface KeyboardEvents {
|
||||
addListener(event: 'keyboardDidShow', listener: (info: KeyboardInfo) => void): Promise<unknown>;
|
||||
addListener(event: 'keyboardWillHide', listener: () => void): Promise<unknown>;
|
||||
}
|
||||
|
||||
/** The pane given room for the keyboard, and what its bottom padding was before. */
|
||||
let padded: { pane: HTMLElement; before: string } | null = null;
|
||||
|
||||
function scrollingPane(field: HTMLElement): HTMLElement | null {
|
||||
for (let node = field.parentElement; node; node = node.parentElement) {
|
||||
const overflow = getComputedStyle(node).overflowY;
|
||||
if ((overflow === 'auto' || overflow === 'scroll') && node.scrollHeight > node.clientHeight) return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Gives the field's pane room for the keyboard and scrolls the field above it. */
|
||||
export function revealFocusedField(keyboardHeight: number, doc: Document = document): void {
|
||||
const field = doc.activeElement;
|
||||
if (!(field instanceof HTMLElement) || !field.matches(TYPING_TARGET)) return;
|
||||
const box = field.getBoundingClientRect();
|
||||
const view = doc.defaultView;
|
||||
const height = view?.visualViewport?.height ?? view?.innerHeight ?? 0;
|
||||
if (box.top >= 0 && box.bottom <= height) return;
|
||||
field.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
const visibleBottom = (view?.innerHeight ?? 0) - keyboardHeight;
|
||||
const box = field.getBoundingClientRect();
|
||||
if (box.top >= 0 && box.bottom <= visibleBottom - AIR) return;
|
||||
|
||||
const pane = scrollingPane(field);
|
||||
if (!pane) {
|
||||
field.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
return;
|
||||
}
|
||||
if (!padded || padded.pane !== pane) {
|
||||
releaseRoom();
|
||||
padded = { pane, before: pane.style.paddingBottom };
|
||||
pane.style.paddingBottom = `${keyboardHeight}px`;
|
||||
}
|
||||
pane.scrollBy({ top: box.bottom - (visibleBottom - AIR), behavior: 'smooth' });
|
||||
}
|
||||
|
||||
/** Takes the room for the keyboard away again. */
|
||||
export function releaseRoom(): void {
|
||||
if (!padded) return;
|
||||
padded.pane.style.paddingBottom = padded.before;
|
||||
padded = null;
|
||||
}
|
||||
|
||||
/** Wires it to the keyboard of the app. */
|
||||
export function keepFieldInView(keyboard: KeyboardEvents): void {
|
||||
void keyboard.addListener('keyboardDidShow', () => revealFocusedField());
|
||||
void keyboard.addListener('keyboardDidShow', ({ keyboardHeight }) => revealFocusedField(keyboardHeight));
|
||||
void keyboard.addListener('keyboardWillHide', () => releaseRoom());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user