티스토리 뷰

이건 안드로이드 운영체제에서 되지 않습니다, 안드로이드 브라우저에서는 키보드가 열리면 window.visualViewport.height와 window.innerHeight가 함께 줄어듭니다.

오늘은 그런 걸 만들었습니다. 일전에 사용자가 HTMLTextAreaElement에 글을 작성할 때, 컨텐츠의 길이에 따라 <textarea>의 높이를 조절하는 Vue 지시자를 만든 적이 있는데, 우리가 만든 앱은 모바일 레이아웃에 바텀 고정인 내비게이션이 있어, 온스크린 키보드가 있으면 사용자 입력 창의 커서와 컨텐츠를 가리게 됩니다. 요구사항은 화면에 키보드가 열려있는 경우에는 바텀 내비게이션을 없애라.

 

안타깝게도 키보드 토글에 대한 Javascript 이벤트는 없습니다. (이 포스트를 쓰는 지금도 나는 믿을 수가 없습니다.) 다만 웹 브라우저들은 VisualViewport API를 제공하고, iOS에서는 키보드가 열리고 닫힐 때에 이 값이 변하므로 그것을 빌려 쓸 수 있을 것만 같습니다. 참고로 상단 툴바가 사라지고 나타날 때에도 이 비쥬얼 뷰포트의 크기가 변하지만, 그때에는 윈도우의 innerHeight와 visualViewport.height가 다르지 않습니다. 앗, 그렇습니까? 그러면

if ("VisualViewport" in window) {
  visualViewport.addEventListener("resize", handleResize);
  function handleResize(event) {
    const {height: visualViewportHeight} = event.target;
    const eventName =
      Math.ceil(visualViewportHeight) < window.innerHeight
        ? "keyboardopen"
        : "keyboardclose";
    emitEvent.call(event, eventName);
  }
  function emitEvent(name) {
    window.dispatchEvent(
      new CustomEvent(name, {
        detail: {
          originalEvent: this,
        },
      })
    );
  }
}

visualViewport의 높이가 window의 내부 높이보다 작을 때에 키보드가 열린 것이라 가정합시다.

  1. (주의) 온스크린 키보드(혹은 가상 키보드라 부르는)만이 비쥬얼 뷰포트의 높이를 더 작게 만드는 UI 요소라는 것을 장담할 수는 없습니다.

  2. (정보) visualViewport의 높이는 double이고, window의 높이는 integer이기 때문에, visualViewport의 높이에 올림 처리를 합니다.

resize 이벤트에는 적당한 값의 debounce 처리를 하여, 불필요한 중복 트리거링을 방지하는 게 좋겠죠? 그러면

if ("VisualViewport" in window) {
  const debouncedHandleResize = debounce(handleResize, 100);
  visualViewport.addEventListener("resize", debouncedHandleResize);
  function handleResize(event) {
    const {height: visualViewportHeight} = event.target;
    const eventName =
      Math.ceil(visualViewportHeight) < window.innerHeight
        ? "keyboardopen"
        : "keyboardclose";
    emitEvent.call(event, eventName);
  }
  function emitEvent(name) {
    window.dispatchEvent(
      new CustomEvent(name, {
        detail: {
          originalEvent: this,
        },
      })
    );
  }
  function debounce(fn, wait) {
    let cancelId = null;
    return function debounced(...args) {
      clearTimeout(cancelId);
      cancelId = setTimeout(fn.bind(this, ...args), wait);
    }
  }
}

100ms 정도 여지를 줍니다. 잘 동작하나요? 터치 디바이스가 아니면 대체로 필요없는 코드일 테니 맨 앞에 조건을 더 추가합니다.

const {matches} = window.matchMedia("(hover: none), (pointer: coarse)");
if (matches && "VisualViewport" in window) {
  // ...
}
  1. 주 입력 장치가 hover를 허용하지 않거나, 불편하게 허용(롱탭 등), 혹은 주 입력 장치가 대략적인 정확도를 제공하는 포인터일 것을 요구합니다.

  2. 터치 디바이스 여부를 판단하는 방법은 몇 가지가 더 있겠지만 이번 포스트의 주제가 아닙니다.

(문서의 끝)

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크