fix: editor content being hidden under keyboard on mobile (#1410)
This commit is contained in:
@@ -34,18 +34,13 @@ import { ApplicationGroup } from './Application/ApplicationGroup'
|
||||
import { WebOrDesktopDevice } from './Application/Device/WebOrDesktopDevice'
|
||||
import { WebApplication } from './Application/Application'
|
||||
import { createRoot, Root } from 'react-dom/client'
|
||||
import { ElementIds } from './Constants/ElementIDs'
|
||||
|
||||
let keyCount = 0
|
||||
const getKey = () => {
|
||||
return keyCount++
|
||||
}
|
||||
|
||||
const RootId = 'app-group-root'
|
||||
|
||||
const setViewportHeight = () => {
|
||||
document.documentElement.style.setProperty('--viewport-height', `${window.innerHeight}px`)
|
||||
}
|
||||
|
||||
const startApplication: StartApplication = async function startApplication(
|
||||
defaultSyncServerHost: string,
|
||||
device: WebOrDesktopDevice,
|
||||
@@ -58,24 +53,18 @@ const startApplication: StartApplication = async function startApplication(
|
||||
let root: Root
|
||||
|
||||
const onDestroy = () => {
|
||||
const rootElement = document.getElementById(RootId) as HTMLElement
|
||||
const rootElement = document.getElementById(ElementIds.RootId) as HTMLElement
|
||||
root.unmount()
|
||||
rootElement.remove()
|
||||
window.removeEventListener('resize', setViewportHeight)
|
||||
window.removeEventListener('orientationchange', setViewportHeight)
|
||||
renderApp()
|
||||
}
|
||||
|
||||
const renderApp = () => {
|
||||
const rootElement = document.createElement('div')
|
||||
rootElement.id = RootId
|
||||
rootElement.id = ElementIds.RootId
|
||||
const appendedRootNode = document.body.appendChild(rootElement)
|
||||
root = createRoot(appendedRootNode)
|
||||
|
||||
setViewportHeight()
|
||||
window.addEventListener('resize', setViewportHeight)
|
||||
window.addEventListener('orientationchange', setViewportHeight)
|
||||
|
||||
disableIosTextFieldZoom()
|
||||
|
||||
root.render(
|
||||
|
||||
@@ -33,7 +33,7 @@ const VisibilityChangeKey = 'visibilitychange'
|
||||
const MSToWaitAfterIframeLoadToAvoidFlicker = 35
|
||||
|
||||
const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, componentViewer, requestReload }) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
||||
const [loadTimeout, setLoadTimeout] = useState<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
|
||||
const [hasIssueLoading, setHasIssueLoading] = useState(false)
|
||||
@@ -200,6 +200,7 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={component.displayName} />}
|
||||
{component.uuid && isComponentValid && (
|
||||
<iframe
|
||||
className="min-h-[40rem]"
|
||||
ref={iframeRef}
|
||||
onLoad={onIframeLoad}
|
||||
data-component-viewer-id={componentViewer.identifier}
|
||||
|
||||
@@ -68,8 +68,8 @@ const ContentList: FunctionComponent<Props> = ({
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'infinite-scroll overflow-y-auto overflow-x-hidden focus:shadow-none focus:outline-none',
|
||||
'md:overflow-y-hidden md:hover:overflow-y-auto',
|
||||
'infinite-scroll max-h-[75vh] overflow-y-auto overflow-x-hidden focus:shadow-none focus:outline-none',
|
||||
'md:max-h-full md:overflow-y-hidden md:hover:overflow-y-auto',
|
||||
'md:hover:[overflow-y:_overlay]',
|
||||
)}
|
||||
id={ElementIds.ContentList}
|
||||
|
||||
@@ -191,7 +191,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
aria-label={'Notes & Files'}
|
||||
ref={itemsViewPanelRef}
|
||||
>
|
||||
<ResponsivePaneContent paneId={AppPaneId.Items}>
|
||||
<ResponsivePaneContent paneId={AppPaneId.Items} contentClassName="min-h-[85vh]">
|
||||
<div id="items-title-bar" className="section-title-bar border-b border-solid border-border">
|
||||
<div id="items-title-bar-container">
|
||||
<input
|
||||
|
||||
@@ -51,7 +51,11 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
className={'sn-component section app-column w-[220px] xsm-only:!w-full sm-only:!w-full'}
|
||||
ref={ref}
|
||||
>
|
||||
<ResponsivePaneContent paneId={AppPaneId.Navigation} contentElementId="navigation-content">
|
||||
<ResponsivePaneContent
|
||||
paneId={AppPaneId.Navigation}
|
||||
contentElementId="navigation-content"
|
||||
contentClassName="min-h-[85vh]"
|
||||
>
|
||||
<div className={'section-title-bar'}>
|
||||
<div className="section-title-bar-header">
|
||||
<div className="title text-sm">
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { ComponentPropsWithoutRef, ForwardedRef, forwardRef } from 'react'
|
||||
|
||||
// Based on: https://css-tricks.com/auto-growing-inputs-textareas/#aa-other-ideas
|
||||
const AutoresizingNoteViewTextarea = forwardRef(
|
||||
({ value, ...textareaProps }: ComponentPropsWithoutRef<'textarea'>, ref: ForwardedRef<HTMLTextAreaElement>) => {
|
||||
return (
|
||||
<div className="relative inline-grid min-h-[75vh] w-full grid-rows-1 items-stretch md:block md:flex-grow">
|
||||
<pre
|
||||
id="textarea-mobile-resizer"
|
||||
className={classNames(
|
||||
'editable font-editor break-word whitespace-pre-wrap',
|
||||
'invisible [grid-area:1_/_1] md:hidden',
|
||||
)}
|
||||
aria-hidden
|
||||
>
|
||||
{value}{' '}
|
||||
</pre>
|
||||
<textarea
|
||||
value={value}
|
||||
className="editable font-editor [grid-area:1_/_1] md:h-full md:min-h-0"
|
||||
{...textareaProps}
|
||||
ref={ref}
|
||||
></textarea>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default AutoresizingNoteViewTextarea
|
||||
@@ -37,6 +37,7 @@ import { reloadFont } from './FontFunctions'
|
||||
import { NoteViewProps } from './NoteViewProps'
|
||||
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import AutoresizingNoteViewTextarea from './AutoresizingTextarea'
|
||||
|
||||
const MINIMUM_STATUS_DURATION = 400
|
||||
const TEXTAREA_DEBOUNCE = 100
|
||||
@@ -889,7 +890,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
|
||||
return (
|
||||
<div aria-label="Note" className="section editor sn-component">
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="flex-grow flex-col md:flex">
|
||||
{this.state.noteLocked && (
|
||||
<EditingDisabledBanner
|
||||
onMouseLeave={() => {
|
||||
@@ -1021,9 +1022,8 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
)}
|
||||
|
||||
{this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading && (
|
||||
<textarea
|
||||
<AutoresizingNoteViewTextarea
|
||||
autoComplete="off"
|
||||
className="editable font-editor"
|
||||
dir="auto"
|
||||
id={ElementIds.NoteTextEditor}
|
||||
onChange={this.onTextAreaChange}
|
||||
@@ -1032,7 +1032,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
onFocus={this.onContentFocus}
|
||||
spellCheck={this.state.spellcheck}
|
||||
ref={(ref) => ref && this.onSystemEditorLoad(ref)}
|
||||
></textarea>
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
|
||||
|
||||
@@ -9,6 +9,8 @@ import { getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
|
||||
import { PopoverContentProps } from './Types'
|
||||
import { getPopoverMaxHeight, getAppRect } from './Utils/Rect'
|
||||
import { usePopoverCloseOnClickOutside } from './Utils/usePopoverCloseOnClickOutside'
|
||||
import { fitNodeToMobileScreen } from '@/Utils'
|
||||
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
|
||||
|
||||
const PositionedPopoverContent = ({
|
||||
align = 'end',
|
||||
@@ -49,6 +51,8 @@ const PositionedPopoverContent = ({
|
||||
childPopovers,
|
||||
})
|
||||
|
||||
useDisableBodyScrollOnMobile()
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div
|
||||
@@ -63,6 +67,7 @@ const PositionedPopoverContent = ({
|
||||
}}
|
||||
ref={(node) => {
|
||||
setPopoverElement(node)
|
||||
fitNodeToMobileScreen(node)
|
||||
}}
|
||||
data-popover={id}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { observer } from 'mobx-react-lite'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import PreferencesCanvas from './PreferencesCanvas'
|
||||
import { PreferencesProps } from './PreferencesProps'
|
||||
import { fitNodeToMobileScreen } from '@/Utils'
|
||||
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
|
||||
|
||||
const PreferencesView: FunctionComponent<PreferencesProps> = (props) => {
|
||||
const menu = useMemo(
|
||||
@@ -25,8 +27,13 @@ const PreferencesView: FunctionComponent<PreferencesProps> = (props) => {
|
||||
}
|
||||
}, [props, menu])
|
||||
|
||||
useDisableBodyScrollOnMobile()
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 z-preferences flex h-full w-full flex-col bg-contrast">
|
||||
<div
|
||||
className="absolute top-0 left-0 z-preferences flex h-full max-h-screen w-full flex-col bg-contrast"
|
||||
ref={fitNodeToMobileScreen}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between border-b border-solid border-border bg-default px-3 py-2 md:p-3">
|
||||
<div className="hidden h-8 w-8 md:block" />
|
||||
<h1 className="text-base font-bold md:text-lg">Your preferences for Standard Notes</h1>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { Fragment, FunctionComponent } from 'react'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
@@ -11,10 +11,10 @@ const ModalDialogButtons: FunctionComponent<Props> = ({ children, className }) =
|
||||
<div className={classNames('flex items-center justify-end px-4 py-4', className)}>
|
||||
{children != undefined && Array.isArray(children)
|
||||
? children.map((child, idx, arr) => (
|
||||
<>
|
||||
<Fragment key={idx}>
|
||||
{child}
|
||||
{idx < arr.length - 1 ? <div className="min-w-3" /> : undefined}
|
||||
</>
|
||||
</Fragment>
|
||||
))
|
||||
: children}
|
||||
</div>
|
||||
|
||||
@@ -8,4 +8,5 @@ export const ElementIds = {
|
||||
NavigationColumn: 'navigation',
|
||||
NoteTextEditor: 'note-text-editor',
|
||||
NoteTitleEditor: 'note-title-editor',
|
||||
}
|
||||
RootId: 'app-group-root',
|
||||
} as const
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Used to disable scroll on document.body when opening popovers or preferences view
|
||||
* on mobile so that user can only scroll within the popover or prefs view
|
||||
*/
|
||||
export const useDisableBodyScrollOnMobile = () => {
|
||||
const styleElementRef = useRef<HTMLStyleElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const isMobile = isMobileScreen()
|
||||
|
||||
if (isMobile && !styleElementRef.current) {
|
||||
const styleElement = document.createElement('style')
|
||||
styleElement.textContent = 'body { overflow: hidden; }'
|
||||
document.body.appendChild(styleElement)
|
||||
styleElementRef.current = styleElement
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (isMobile && styleElementRef.current) {
|
||||
styleElementRef.current.remove()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* source: https://github.com/juliangruber/is-mobile
|
||||
*
|
||||
* (MIT)
|
||||
* Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
const mobileRE =
|
||||
/(android|bb\d+|meego).+mobile|armv7l|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i
|
||||
|
||||
const tabletRE = /android|ipad|playbook|silk/i
|
||||
|
||||
export type Opts = {
|
||||
tablet?: boolean
|
||||
}
|
||||
|
||||
export const isMobile = (opts: Opts = {}) => {
|
||||
const ua = navigator.userAgent || navigator.vendor
|
||||
|
||||
if (typeof ua !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return mobileRE.test(ua) || (!!opts.tablet && tabletRE.test(ua))
|
||||
}
|
||||
@@ -2,8 +2,6 @@ import { Platform, platformFromString } from '@standardnotes/snjs'
|
||||
import { IsDesktopPlatform, IsWebPlatform } from '@/Constants/Version'
|
||||
import { EMAIL_REGEX } from '../Constants/Constants'
|
||||
|
||||
export { isMobile } from './IsMobile'
|
||||
|
||||
declare const process: {
|
||||
env: {
|
||||
NODE_ENV: string | null | undefined
|
||||
@@ -203,3 +201,14 @@ export const disableIosTextFieldZoom = () => {
|
||||
addMaximumScaleToMetaViewport()
|
||||
}
|
||||
}
|
||||
|
||||
export const isMobileScreen = () => !window.matchMedia('(min-width: 768px)').matches
|
||||
|
||||
export const fitNodeToMobileScreen = (node: HTMLElement | null) => {
|
||||
if (!node || !isMobileScreen()) {
|
||||
return
|
||||
}
|
||||
node.style.height = `${visualViewport.height}px`
|
||||
node.style.position = 'absolute'
|
||||
node.style.top = `${document.documentElement.scrollTop}px`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './ConcatenateUint8Arrays'
|
||||
export * from './IsMobile'
|
||||
export * from './StringUtils'
|
||||
export * from './Utils'
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
flex-grow: 1;
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ $heading-height: 75px;
|
||||
.section.editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-y: hidden;
|
||||
background-color: var(--editor-background-color);
|
||||
color: var(--editor-foreground-color);
|
||||
@@ -95,8 +96,6 @@ $heading-height: 75px;
|
||||
.editor-content,
|
||||
#editor-content {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
tab-size: 2;
|
||||
background-color: var(--editor-pane-background-color);
|
||||
|
||||
@@ -41,8 +41,6 @@ body {
|
||||
-moz-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -107,16 +105,30 @@ p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
.main-ui-view {
|
||||
height: max-content;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
|
||||
display: block;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.main-ui-view {
|
||||
// Fallbacks
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
// Mobile-corrected viewport height
|
||||
min-height: var(--viewport-height);
|
||||
height: var(--viewport-height);
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
background-color: var(--editor-header-bar-background-color);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
$footer-height: 2rem;
|
||||
@@ -139,8 +151,6 @@ $footer-height: 2rem;
|
||||
|
||||
.section {
|
||||
padding-bottom: 0px;
|
||||
height: 100%;
|
||||
max-height: calc(100vh - #{$footer-height});
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user