diff --git a/packages/gestalt/src/SheetMobile/PartialPage.tsx b/packages/gestalt/src/SheetMobile/PartialPage.tsx index 9a5f82aa92..dbb3483add 100644 --- a/packages/gestalt/src/SheetMobile/PartialPage.tsx +++ b/packages/gestalt/src/SheetMobile/PartialPage.tsx @@ -2,6 +2,7 @@ import { ComponentProps, ReactNode, useCallback, useEffect, useId, useLayoutEffe import classnames from 'classnames'; import ContentContainer from './ContentContainer'; import Header from './Header'; +import useIsOnScreenKeyboardOpen from './useIsOnScreenKeyboardOpen'; import animation from '../animation/animation.css'; import { ANIMATION_STATE, useAnimation } from '../animation/AnimationContext'; import { useRequestAnimationFrame } from '../animation/RequestAnimationFrameContext'; @@ -107,6 +108,9 @@ export default function PartialPage({ // Consumes AnimationProvider & RequestAnimationFrameProvider const { animationState, handleAnimationEnd } = useAnimation(); const { handleRequestAnimationFrame, onExternalDismiss } = useRequestAnimationFrame(); + const isOnScreenKeyboardOpen = useIsOnScreenKeyboardOpen(); + // eslint-disable-next-line no-console + console.log(isOnScreenKeyboardOpen); const handleOnAnimationEnd = useCallback(() => { handleAnimationEnd(); @@ -140,18 +144,49 @@ export default function PartialPage({ if (window && window.body?.style?.overflow) { // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. prevOverflowStyle = window.body.style.overflow; + // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. + prevHeightStyle = window.body.style.height; + // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. window.body.style.overflow = 'hidden'; + // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. + window.body.style.height = '100%'; } return () => { // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. if (window && window.body?.style?.overflow) { // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. window.body.style.overflow = prevOverflowStyle; + // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. + window.body.style.height = prevHeightStyle; } }; }, []); + // When SheetMobile is full page displayed in mobile browser, the body scroll is still accessible. Here we disable to just allow the scrolling within Modal + // useEffect(() => { + // const disableScroll = () => { + // // Get the current page scroll position + // const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + // const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + // // if any scroll is attempted, + // // set this to the previous value + // window.onscroll = () => { + // window.scrollTo(scrollLeft, scrollTop); + // }; + // }; + + // const enableScroll = () => { + // window.onscroll = () => {}; + // }; + + // if (isOnScreenKeyboardOpen) { + // disableScroll(); + // } else { + // enableScroll(); + // } + // }, [isOnScreenKeyboardOpen]); + // Use useLayoutEffect instead of useEffect as we need to close the component synchronously after all DOM mutations, useEffect was needed to prevent changing state while still rendering but useEffect will create a ms blink of the full OverlayPanel after closing which gets prevented with useLayoutEffect useLayoutEffect(() => { if (animationState === ANIMATION_STATE.unmount) { @@ -170,6 +205,20 @@ export default function PartialPage({ }, [closeOnOutsideClick, onExternalDismiss, onOutsideClick], ); + // // @ts-expect-error - TS2339 + // function findScroller(element) { + // // eslint-disable-next-line no-console + // console.log('scrollstart'); + // // eslint-disable-next-line no-param-reassign, func-names + // element.onscroll = function () { + // // eslint-disable-next-line no-console + // console.log('scrolls', element); + // }; + + // Array.from(element.children).forEach(findScroller); + // } + + // findScroller(document.body); return ( diff --git a/packages/gestalt/src/SheetMobile/useIsOnScreenKeyboardOpen.tsx b/packages/gestalt/src/SheetMobile/useIsOnScreenKeyboardOpen.tsx new file mode 100644 index 0000000000..34ac2e9515 --- /dev/null +++ b/packages/gestalt/src/SheetMobile/useIsOnScreenKeyboardOpen.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; +import { useDeviceType } from '../contexts/DeviceTypeProvider'; + +const isKeyboardInput = (elem: HTMLElement) => { + // eslint-disable-next-line no-console + console.log(elem); + return ( + (elem.tagName === 'INPUT' && + ['date', 'number', 'email', 'password', 'tel', 'text', 'url'].includes( + (elem as HTMLInputElement).type, + )) || + elem.hasAttribute('contenteditable') + ); +}; + +// This hooks detects the on-screen keyboard. In the future we can use the VirtualKeyboard API but it's still experimental and not supported in many browsers. https://developer.mozilla.org/en-US/docs/Web/API/VirtualKeyboard/geometrychange_event +const useIsOnScreenKeyboardOpen = () => { + const deviceType = useDeviceType(); + const isMobile = deviceType === 'mobile'; + + const [isOpen, setOpen] = useState(false); + + useEffect(() => { + const handleFocusIn = (e: FocusEvent) => { + if (!e.target) { + return; + } + const target = e.target as HTMLElement; + if (isMobile && isKeyboardInput(target)) { + setOpen(true); + } + }; + + document.addEventListener('focusin', handleFocusIn); + + const handleFocusOut = (e: FocusEvent) => { + if (!e.target) { + return; + } + const target = e.target as HTMLElement; + if (isMobile && isKeyboardInput(target)) { + setOpen(false); + } + }; + + document.addEventListener('focusout', handleFocusOut); + + return () => { + document.removeEventListener('focusin', handleFocusIn); + document.removeEventListener('focusout', handleFocusOut); + }; + }, [isMobile]); + + return isOpen; +}; + +export default useIsOnScreenKeyboardOpen;