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); +}