Skip to content

Instantly share code, notes, and snippets.

@MartijnHols
Last active July 16, 2024 21:57
Show Gist options
  • Save MartijnHols/e9f4f787efa9190885a708468f63c5bb to your computer and use it in GitHub Desktop.
Save MartijnHols/e9f4f787efa9190885a708468f63c5bb to your computer and use it in GitHub Desktop.
React hooks for getting the document height that updates when the On Screen Keyboard/Virtual Keyboard toggles
The latest version is available at https://martijnhols.nl/gists/how-to-get-document-height-ios-safari-osk
import { useEffect } from 'react'
const useOnScreenKeyboardScrollFix = () => {
useEffect(() => {
const handleScroll = () => {
window.scrollTo(0, 0)
}
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
}
export default useOnScreenKeyboardScrollFix
@kvyatek83
Copy link

Hi @MartijnHols,
First of all, thank you so much for your work!!!
I have the same issue on my app and I implemented your solution and it works but most of the time when the keyboard is open and the screen resizes + the scroll to top emitting, there is a glitch that the app slides from the bottom to the top...
Here is an example, first focus has the glitch and the second don't:

Screen.Recording.2024-06-23.at.12.45.24.mov

@komagroup
Copy link

komagroup commented Jun 28, 2024

@MartijnHols Thank you very much for the gists.

I also can't figure out a scroll issue.
I did everything as you suggested but this keeps happening ( testing on android for now )

GIF about the problem

FullViewportContainer

import { ElementType, HTMLAttributes } from "react";
import useViewportSize from "@/hooks/useViewportSize";
import useOnScreenKeyboardScrollFix from "@/hooks/useOnScreenKeyboardScrollFix";
import usePreventOverScrolling from "@/hooks/usePreventOverScrolling";
import useIsOnScreenKeyboardOpen from "@/hooks/useOnScreenKeyboardOpen";

interface Props extends HTMLAttributes<HTMLDivElement> {
  element?: ElementType;
}

export function FullViewportContainer({
  element: Element = "div",
  ...others
}: Props) {
  useOnScreenKeyboardScrollFix();
  const [, viewportHeight] = useViewportSize()  ?? [];
  const ref = usePreventOverScrolling();
  const isOnScreenKeyboardOpen = useIsOnScreenKeyboardOpen();
  return (
    <Element
      {...others}
      ref={ref}
      style={{
        height: viewportHeight,
        padding: isOnScreenKeyboardOpen
          ? "env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left)"
          : "env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)",
        transition: "padding 100ms, height 100ms",
      }}
    />
  );
}

The only thing I left out is the css variables because I couldn't find them anywhere.
The problem is when I focus on the textarea, it drops the whole viewport up. If I scroll up manually it works.

@MartijnHols
Copy link
Author

MartijnHols commented Jun 28, 2024

Hi @MartijnHols, First of all, thank you so much for your work!!! I have the same issue on my app and I implemented your solution and it works but most of the time when the keyboard is open and the screen resizes + the scroll to top emitting, there is a glitch that the app slides from the bottom to the top... Here is an example, first focus has the glitch and the second don't:

If I understand the issue correctly, it's subtle and fixes itself a few frames later. Mine did the same, but the issue was hard to notice in production so I never did anything to fix it. The scroll-hack is kind of an ugly solution for squashing anything to do with scrolling, and I fear it would get too complex with too many edge cases if it were to be made any smarter.

@komagroup I mostly worked on the keyboard opening scroll hack in iOS, so most of my knowledge is about that. If I remember correctly, I noticed that the keyboard-scroll is kind of weird and not really a standard browser scroll so it's hard to work with. In your case it seems the scroll event might not be fired at all? You may have to do some logging to ensure the scroll effect is triggered. If it is, the event may be triggered before the browser actually scrolls and you may have to add a requestAnimationFrame(() => window.scrollTo(0, 0)) to delay the scroll position fix by a frame. If there's no scroll events, your browser is crazy and you'll have to do some digging. Worst case you will need to set a timer when the OSK opens, or poll the window scroll position (if the scroll is even happening to the window, because as I said, it doesn't appear to be a standard scroll).

The scroll hack of this gist isn't the prettiest and if I'm honest just one of the first things I threw at the problem that stuck. It worked perfectly fine on the devices I tested (mostly iOS but also a cheap and slow Motorola), but with the amount of (mobile) browsers there are, I would not be surprised there are some that misbehave (and browsers may change their behavior over time).

@komagroup
Copy link

Yap. It doesn't even fire. It behaves very strange when I inspect it in devtools with a usb cable attached

image

Even the full html is resized but no event happens

image

import { useEffect } from "react";

const useOnScreenKeyboardScrollFix = () => {
  useEffect(() => {
    const handleScroll = () => {
      console.log("FIRED");
      window.scrollTo(0, 0);
    };

    window.addEventListener("scroll", handleScroll);

    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);
};

export default useOnScreenKeyboardScrollFix;

@komagroup
Copy link

komagroup commented Jun 28, 2024

I have added this line

useEffect(() => {
    window.scrollTo(0, 0)
  }, [viewportHeight]);

So it always scrolls to 0 if the viewportHeight changes but it also doesnt work.
Browser is standard android google chrome btw

@MartijnHols
Copy link
Author

You might need a few timeouts like I did in the useViewportSize hook, but that will be kind of ugly for the user. Luckily it will only be ugly for the crazy browsers.

      // Closing the OSK in iOS does not immediately update the visual viewport
      // size :<
      setTimeout(updateViewportSize, 1000)

One nice thing is that since you're setting scroll position to 0 every time, you can fairly easily start multiple timeouts without the user noticing.

@komagroup
Copy link

komagroup commented Jun 28, 2024

It seems to work if I adjust the maxHeight instead of the height and it has a h-full class from tailwind ( which is just height:100% )

import { ElementType, HTMLAttributes } from "react";
import useViewportSize from "@/hooks/useViewportSize";
import useOnScreenKeyboardScrollFix from "@/hooks/useOnScreenKeyboardScrollFix";
import usePreventOverScrolling from "@/hooks/usePreventOverScrolling";
import useIsOnScreenKeyboardOpen from "@/hooks/useOnScreenKeyboardOpen";

interface Props extends HTMLAttributes<HTMLDivElement> {
  element?: ElementType;
}

export function FullViewportContainer({
  element: Element = "div",
  ...others
}: Props) {
  useOnScreenKeyboardScrollFix();
  const [, viewportHeight] = useViewportSize()  ?? [];
  const ref = usePreventOverScrolling();
  const isOnScreenKeyboardOpen = useIsOnScreenKeyboardOpen();
  return (
    <Element
      {...others}
      ref={ref}
      className="w-full h-full"
      style={{
        maxHeight: viewportHeight,
        padding: isOnScreenKeyboardOpen
          ? "env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left)"
          : "env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)",
        transition: "padding 100ms, height 100ms",
      }}
    />
  );
}

Aaaand it must be the direct children of the root it seems

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment