Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Positioning issues with Inputs when the keyboard changes of type #521

Open
Fefedu973 opened this issue Dec 3, 2024 · 4 comments
Open

Positioning issues with Inputs when the keyboard changes of type #521

Fefedu973 opened this issue Dec 3, 2024 · 4 comments

Comments

@Fefedu973
Copy link

In an input when the keyboard changes size there is an issue where it is not handled well.
See:
https://github.com/user-attachments/assets/d4d5e69f-a958-4c2a-8c79-a410d741cd9e

@Fefedu973
Copy link
Author

I found a solution, i edited the mjs version and edited the onvisualviewportchange function to this and now it works flawlessly.

    React.useEffect(() => {
        function onVisualViewportChange() {
            if (!drawerRef.current || !repositionInputs) return;

            const focusedElement = document.activeElement;
            // If the focused element is an input OR we've detected the keyboard is open,
            // then proceed to measure and possibly reposition the drawer
            if (isInput(focusedElement) || keyboardIsOpen.current) {
                const visualViewportHeight = window.visualViewport?.height || 0;
                const totalHeight = window.innerHeight;
                const drawerHeight = drawerRef.current.getBoundingClientRect().height || 0;

                // This is the "keyboard height" difference
                let diffFromInitial = totalHeight - visualViewportHeight;

                // If we have snapPoints, adjust diffFromInitial accordingly
                if (snapPoints && snapPoints.length > 0 && snapPointsOffset && activeSnapPointIndex !== undefined) {
                    const activeSnapPointHeight = snapPointsOffset[activeSnapPointIndex] || 0;
                    diffFromInitial += activeSnapPointHeight;
                }

                // We treat a non-negligible diff as "keyboard open"
                // (for example, anything more than ~60px might indicate a fully opened keyboard,
                // but if we want to capture *any* keyboard shift, we can lower this threshold.)
                const KEYBOARD_OPEN_THRESHOLD = 20;
                keyboardIsOpen.current = diffFromInitial > KEYBOARD_OPEN_THRESHOLD;

                // Store the updated diff
                previousDiffFromInitial.current = diffFromInitial;

                // If we haven’t stored the initial drawer height yet, store it
                if (!initialDrawerHeight.current) {
                    initialDrawerHeight.current = drawerHeight;
                }

                const offsetFromTop = drawerRef.current.getBoundingClientRect().top;
                const isTallEnough = drawerHeight > totalHeight * 0.8;

                // Check if the drawer should be “shrunk” or repositioned
                // whenever the keyboard is open or the drawer is taller than the viewport
                if (drawerHeight > visualViewportHeight || keyboardIsOpen.current) {
                    const currentHeight = drawerRef.current.getBoundingClientRect().height;
                    let newDrawerHeight = currentHeight;

                    // If the drawer is taller than the visible viewport, shrink it
                    if (currentHeight > visualViewportHeight) {
                        // Use either a top offset-based calculation if tall enough,
                        // or a constant offset if not.
                        newDrawerHeight = visualViewportHeight - (isTallEnough ? offsetFromTop : WINDOW_TOP_OFFSET);
                    }

                    if (fixed) {
                        // “fixed” logic: only change the height, so the user can scroll inside
                        // the drawer if needed.
                        drawerRef.current.style.height = `${currentHeight - Math.max(diffFromInitial, 0)}px`;
                    } else {
                        // “absolute/relative” logic: set the drawer height so it remains
                        // fully within the viewport. We also ensure the drawer can’t become
                        // smaller than (viewport height - offsetFromTop)
                        drawerRef.current.style.height = `${Math.max(newDrawerHeight, visualViewportHeight - offsetFromTop)}px`;
                    }
                }
                // If the keyboard is closed and we’re not Firefox Mobile, reset back 
                // to original height
                else if (!isMobileFirefox()) {
                    drawerRef.current.style.height = `${initialDrawerHeight.current}px`;
                }

                // Update bottom offset
                if (snapPoints && snapPoints.length > 0 && !keyboardIsOpen.current) {
                    // If the keyboard is closed, align the drawer bottom to 0
                    drawerRef.current.style.bottom = `0px`;
                } else {
                    // If the keyboard is open, push the bottom up by diffFromInitial
                    // but never let it go negative
                    drawerRef.current.style.bottom = `${Math.max(diffFromInitial, 0)}px`;
                }
            }
        }

        // Attach the event listener
        window.visualViewport?.addEventListener('resize', onVisualViewportChange);

        return () => {
            // Cleanup
            window.visualViewport?.removeEventListener('resize', onVisualViewportChange);
        };
    }, [
        activeSnapPointIndex,
        snapPoints,
        snapPointsOffset,
        repositionInputs,
        fixed
    ]);

@airtonix
Copy link

@Fefedu973 Since the contents of that useEffect look different to the version i have, it looks like all you've changed is the threshold to detect if the keyboard is open?

                if (Math.abs(previousDiffFromInitial.current - diffFromInitial) > 60) {
                    keyboardIsOpen.current = !keyboardIsOpen.current;
                }

vs

                if (Math.abs(previousDiffFromInitial.current - diffFromInitial) > 20) {
                    keyboardIsOpen.current = !keyboardIsOpen.current;
                }

@airtonix
Copy link

airtonix commented Dec 15, 2024

It might have been better to provide a yarn/pnpm patch for your changes that we can just drop into our projects

@airtonix
Copy link

airtonix commented Dec 15, 2024

on android firefox: my solution was to anchor an element to all edges of the screen then flex align the drawercontent to the edges/corners I want:

import cn from 'classnames';
import tv from 'tailwind-variants';

const DrawerFrameStyles = tv({
  base: ['bg-background-base shadow-lg', 'max-w-full ', 'flex flex-col'],
  variants: {
    tone: {
      primary: ['text-text-base bg-background-overlay'],
    },
    rounded: { true: ['rounded-t-[10px] rounded-b-[10px]'] },
    size: {
      sm: ['w-full md:w-[320px]'],
      md: ['w-full md:w-[480px]'],
      lg: ['w-full md:w-[640px]'],
      xl: ['w-full md:w-[800px]'],
    },
  },
  defaultVariants: {
    size: 'md',
    tone: 'primary',
    rounded: true,
  },
});

const DrawerFrame = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement> &
  VariantProps<typeof DrawerFrameStyles>) => {
  const styles = DrawerFrameStyles(props);
  return (
    <div
      className={cn(styles, className)}
      {...props}
    />
  );
};
DrawerFrame.displayName = 'DrawerFrame';
const DrawerAnchorStyles = tv({
  base: 'drawer-anchored top-0 bottom-0 left-0 right-0 flex flex-col max-h-screen',
  variants: {
    anchor: {
      bottomleft: ['justify-end items-start'],
      bottomright: ['justify-end items-end'],
      bottom: ['justify-end items-center'],
      topleft: ['justify-start items-start'],
      topright: ['justify-start items-end'],
      top: ['justify-start items-center'],
      left: ['justify-center items-start'],
      right: ['justify-center items-end'],
    },
  },
  defaultVariants: {
    anchor: 'bottomright',
  },
});
const DrawerContent = forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> &
    VariantProps<typeof DrawerFrameStyles> &
    VariantProps<typeof DrawerAnchorStyles>
>(({ className, children, anchor, tone, rounded, size, ...props }, ref) => {
  const styles = DrawerAnchorStyles({ anchor });

  return (
    <DrawerPortal>
      <DrawerOverlay />
      <Box className={cn('fixed z-50 ', styles, className)}>
        <DrawerPrimitive.Content
          ref={ref}
          {...props}
        >
          <DrawerFrame
            tone={tone}
            rounded={rounded}
            size={size}
            className={cn('overflow-y-auto max-h-screen', className)}
            {...props}
          >
            <DrawerHandle
              edge={(anchor.startsWith('bottom') && 'top') || 'bottom'}
            />

            {children}
          </DrawerFrame>
        </DrawerPrimitive.Content>
      </Box>
    </DrawerPortal>
  );
});
DrawerContent.displayName = 'DrawerContent';

Now when focused, inputs are correctly moved into the screen, and when blurred the drawer correctly repositions. Additionally, drawers whose content is too tall for the screen can be scrolled to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants