From 0362cb6c85849086a08a9d8cb13bf891dc2a9e5d Mon Sep 17 00:00:00 2001
From: Alexander Harding <2166114+aeharding@users.noreply.github.com>
Date: Sat, 8 Jul 2023 17:48:36 -0500
Subject: [PATCH] Fix create comment, edit comment, create post text area being
cut off (#332)
* Fix create comment, edit comment, create post text area being cut off
This is really fragile logic, but I've tried to abstract it to
IonModalAutosizedForOnScreenKeyboard to keep it modular at least...
* Abstract TextareaAutosizedForOnScreenKeyboard
---
src/features/comment/reply/CommentReply.tsx | 11 +-
src/features/post/new/NewPostText.tsx | 8 +-
.../shared/DynamicDismissableModal.tsx | 7 +-
.../IonModalAutosizedForOnScreenKeyboard.tsx | 108 ++++++++++++++++++
.../TextareaAutosizedForOnScreenKeyboard.tsx | 29 +++++
src/helpers/safari.ts | 13 +++
6 files changed, 164 insertions(+), 12 deletions(-)
create mode 100644 src/features/shared/IonModalAutosizedForOnScreenKeyboard.tsx
create mode 100644 src/features/shared/TextareaAutosizedForOnScreenKeyboard.tsx
create mode 100644 src/helpers/safari.ts
diff --git a/src/features/comment/reply/CommentReply.tsx b/src/features/comment/reply/CommentReply.tsx
index 5e54f9ff0b..b499920fdd 100644
--- a/src/features/comment/reply/CommentReply.tsx
+++ b/src/features/comment/reply/CommentReply.tsx
@@ -23,24 +23,25 @@ import { Centered, Spinner } from "../../auth/Login";
import { handleSelector, jwtSelector } from "../../auth/authSlice";
import { css } from "@emotion/react";
import { preventPhotoswipeGalleryFocusTrap } from "../../gallery/GalleryImg";
+import TextareaAutosizedForOnScreenKeyboard from "../../shared/TextareaAutosizedForOnScreenKeyboard";
export const Container = styled.div`
- position: absolute;
- inset: 0;
+ min-height: 100%;
display: flex;
flex-direction: column;
`;
-export const Textarea = styled.textarea`
+export const Textarea = styled(TextareaAutosizedForOnScreenKeyboard)`
border: 0;
background: none;
resize: none;
outline: 0;
padding: 1rem;
- flex: 1 0 0;
- min-height: 7rem;
+ min-height: 200px;
+
+ flex: 1 0 auto;
${({ theme }) =>
!theme.dark &&
diff --git a/src/features/post/new/NewPostText.tsx b/src/features/post/new/NewPostText.tsx
index 92a831f8bc..d0c770ab1b 100644
--- a/src/features/post/new/NewPostText.tsx
+++ b/src/features/post/new/NewPostText.tsx
@@ -12,23 +12,23 @@ import {
import { useState } from "react";
import { Centered, Spinner } from "../../auth/Login";
import { css } from "@emotion/react";
+import TextareaAutosizedForOnScreenKeyboard from "../../shared/TextareaAutosizedForOnScreenKeyboard";
const Container = styled.div`
- position: absolute;
- inset: 0;
+ min-height: 100%;
display: flex;
flex-direction: column;
`;
-const Textarea = styled.textarea`
+const Textarea = styled(TextareaAutosizedForOnScreenKeyboard)`
border: 0;
background: none;
resize: none;
outline: 0;
padding: 1rem;
- flex: 1 0 0;
+ flex: 1 0 auto;
min-height: 7rem;
${({ theme }) =>
diff --git a/src/features/shared/DynamicDismissableModal.tsx b/src/features/shared/DynamicDismissableModal.tsx
index 2545d73e00..a904322886 100644
--- a/src/features/shared/DynamicDismissableModal.tsx
+++ b/src/features/shared/DynamicDismissableModal.tsx
@@ -5,9 +5,10 @@ import React, {
useRef,
useState,
} from "react";
-import { IonModal, useIonActionSheet } from "@ionic/react";
+import { useIonActionSheet } from "@ionic/react";
import { PageContext } from "../auth/PageContext";
import { Prompt, useLocation } from "react-router";
+import IonModalAutosizedForOnScreenKeyboard from "./IonModalAutosizedForOnScreenKeyboard";
export interface DismissableProps {
dismiss: () => void;
@@ -82,7 +83,7 @@ export function DynamicDismissableModal({
when={!canDismiss}
message="Are you sure you want to discard your work?"
/>
- setIsOpen(false)}
@@ -99,7 +100,7 @@ export function DynamicDismissableModal({
onDismissAttemptCb();
},
})}
-
+
>
);
}
diff --git a/src/features/shared/IonModalAutosizedForOnScreenKeyboard.tsx b/src/features/shared/IonModalAutosizedForOnScreenKeyboard.tsx
new file mode 100644
index 0000000000..89795a8f39
--- /dev/null
+++ b/src/features/shared/IonModalAutosizedForOnScreenKeyboard.tsx
@@ -0,0 +1,108 @@
+import { IonModal } from "@ionic/react";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import usePageVisibility from "../../helpers/usePageVisibility";
+import styled from "@emotion/styled";
+
+// TODO it's a bit buggy trying to compute this
+// in realtime with the new post dialog + comment dialogs
+// So hardcode for now
+const FIXED_HEADER_HEIGHT = 56;
+
+const StyledIonModal = styled(IonModal)<{ viewportHeight: number }>`
+ ion-content::part(scroll) {
+ max-height: ${({ viewportHeight }) => viewportHeight}px;
+ }
+`;
+
+export default function IonModalAutosizedForOnScreenKeyboard(
+ props: React.ComponentProps
+) {
+ const [viewportHeight, setViewportHeight] = useState(
+ document.documentElement.clientHeight
+ );
+ const isVisible = usePageVisibility();
+ // eslint-disable-next-line no-undef
+ const modalRef = useRef(null);
+
+ const updateViewport = useCallback(() => {
+ if (!props.isOpen) return;
+
+ // For the rare legacy browsers that don't support it
+ if (!window.visualViewport) {
+ return;
+ }
+
+ const page = modalRef.current?.querySelector(
+ ".ion-page:not(.ion-page-hidden)"
+ );
+
+ setViewportHeight(
+ window.visualViewport.height -
+ (page instanceof HTMLElement ? cumulativeOffset(page).top : 0) -
+ FIXED_HEADER_HEIGHT
+ );
+ }, [props.isOpen]);
+
+ const onScroll = useCallback(() => {
+ setTimeout(() => {
+ window.scrollTo(0, 0);
+ }, 100);
+ }, []);
+
+ // Turning iPhone on/off can mess up the scrolling to top again
+ useEffect(() => {
+ if (!props.isOpen) return;
+
+ updateViewport();
+ }, [isVisible, updateViewport, props.isOpen]);
+
+ useEffect(() => {
+ if (!props.isOpen) return;
+
+ document.addEventListener("scroll", onScroll);
+
+ return () => {
+ document.removeEventListener("scroll", onScroll);
+ };
+ }, [onScroll, props.isOpen]);
+
+ useEffect(() => {
+ if (!props.isOpen) return;
+
+ const onResize = () => {
+ updateViewport();
+ };
+
+ window.visualViewport?.addEventListener("resize", onResize);
+
+ return () => {
+ window.visualViewport?.removeEventListener("resize", onResize);
+ };
+ }, [updateViewport, props.isOpen]);
+
+ return (
+ {
+ window.scrollTo(0, 0);
+ }}
+ {...props}
+ />
+ );
+}
+
+function cumulativeOffset(element: HTMLElement) {
+ let top = 0,
+ left = 0;
+ do {
+ top += element.offsetTop || 0;
+ left += element.offsetLeft || 0;
+ element = element.offsetParent as HTMLElement;
+ } while (element instanceof HTMLElement);
+
+ return {
+ top: top,
+ left: left,
+ };
+}
diff --git a/src/features/shared/TextareaAutosizedForOnScreenKeyboard.tsx b/src/features/shared/TextareaAutosizedForOnScreenKeyboard.tsx
new file mode 100644
index 0000000000..ffc8e480b9
--- /dev/null
+++ b/src/features/shared/TextareaAutosizedForOnScreenKeyboard.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { isAppleDeviceInstalledToHomescreen } from "../../helpers/device";
+import { fixSafariAutoscroll } from "../../helpers/safari";
+import TextareaAutosize, {
+ TextareaAutosizeProps,
+} from "react-textarea-autosize";
+
+export default function TextareaAutosizedForOnScreenKeyboard(
+ props: Omit<
+ TextareaAutosizeProps & React.RefAttributes,
+ "onFocus"
+ >
+) {
+ return (
+ {
+ if (!isAppleDeviceInstalledToHomescreen()) return;
+
+ // https://stackoverflow.com/a/74902393/1319878
+ const target = e.currentTarget;
+ target.style.opacity = "0";
+ setTimeout(() => (target.style.opacity = "1"));
+
+ fixSafariAutoscroll();
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/src/helpers/safari.ts b/src/helpers/safari.ts
new file mode 100644
index 0000000000..16bd4be759
--- /dev/null
+++ b/src/helpers/safari.ts
@@ -0,0 +1,13 @@
+export function fixSafariAutoscroll() {
+ let checkAttempts = 0;
+
+ const interval = setInterval(() => {
+ window.scrollTo(0, 0);
+
+ if (window.scrollY === 0) {
+ checkAttempts++;
+
+ if (checkAttempts > 10) clearInterval(interval);
+ }
+ }, 100);
+}