huntabyte / vaul-svelte

An unstyled drawer component for Svelte.
https://vaul-svelte.com
MIT License
382 stars 16 forks source link

Drawer disappear on iOS when using an input with bind:value #68

Open tobiassern opened 3 months ago

tobiassern commented 3 months ago

Describe the bug

Drawer disappear on Safari on iPhone when having an input inside the drawer with a bind:value. I have added a minimal reproduction for this.

The same issue appears when using shadcn-svelte and the drawer component.

Video showing the reproduction of this bug

https://github.com/huntabyte/vaul-svelte/assets/7224143/fcd1c668-3982-4925-9a5a-a9bd77401e73

Reproduction

<script>
    import { Drawer } from 'vaul-svelte';

    let value = '';
</script>

<div data-vaul-drawer-wrapper style="background: #fff; height: 100dvh;">
    <h1>Welcome to SvelteKit</h1>
    <p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

    <Drawer.Root shouldScaleBackground>
        <Drawer.Trigger>Open Drawer</Drawer.Trigger>
        <Drawer.Portal>
            <Drawer.Overlay style="position: fixed; background: #00000040; inset: 0;" />
            <Drawer.Content style="position: fixed; bottom: 0px; left: 0px; right: 0px;">
                <div style="border-radius: 25px 25px 0 0; background: #fff; padding: 12px;">
                    <p>Content</p>
                    <label>
                        Without binded value
                        <input style="font-size: 16px;" />
                    </label>
                    <label>
                        With binded value
                        <input style="font-size: 16px;" bind:value />
                    </label>
                </div>
            </Drawer.Content>
            <Drawer.Overlay />
        </Drawer.Portal>
    </Drawer.Root>
</div>

Logs

No response

System Info

iPhone 12 Pro
iOS: 17.3.1

Severity

blocking all usage of vaul-svelte

devjume commented 2 months ago

https://github.com/huntabyte/vaul-svelte/assets/34103045/c08fb610-d3f0-43e1-bb01-035606ed0075

I can confirm same bug on Android Chrome.

As shown in the video, this is caused by some bug that resets style attribute, when user enters value to input field that has bind:value.

If users click out of the input field, styling is set to back to correct value.

Note: This can't be debugged or reproduced on browser dev tools. Must be done in real or virtual device. In the video I am using chrome "inspect remote devices" feature.

About severity: I agree with @tobiassern. This bug makes vaul-svelte totally unusable in any practical application where forms are used in the drawer/vaul.

System info:

Android: 13
Chrome: 124.0.6367.82
devjume commented 2 months ago

I tried to debug and fix this but couldn't even find out what might cause the bug. Feels like that I might not even be caused by this codebase but maybe something else like, svelte itself?

Only thing was able to identify is that everytime there is keydown event on input field with bind:value set, the height property of drawer content component is deleted/set to null/0px.

Screenshot from 2024-04-30 20-51-19

I am more than happy to try to fix this and make PR as debugging this with real device is kinda slow and painful.
So other people don't have to hassle with the debugging setup as I already have it done.

@huntabyte Any insight or idea about this? Where should I pay attention?

devjume commented 2 months ago

I think I found the solution. Could someone confirm if this fixes issue on your device?

Sidenote: I still get some styling issues and drawer jumping around but I think those bugs are not related to this. I have to do more debugging on those and probably open new issue.

Cause:

The problem is caused by svelte reactivity. Everytime the binded variable bind:value is updated by user input, it causes drawer to rerender, which then causes styling of the content component to be resetted to its original state: position: fixed; bottom: 0px; left: 0px; right: 0px;. This then hides the drawer behind the keyboard.

Fix:

Wrap everything inside <Drawer.Content> to new component. (Or atleast wrap <input> element to its own component) This prevents rerender from happening in the <Drawer>. Rerender will still happen but only in the newly create component.

Example:

// hero.svelte
<Drawer.Root direction="bottom">
   <Drawer.Trigger>Open Drawer 2</Drawer.Trigger>
   <Drawer.Portal>
      <Drawer.Overlay style="position: fixed; background: #00000040; inset: 0;" />
      <Drawer.Content style="position: fixed; bottom: 0px; left: 0px; right: 0px; max-height: 96%;">
         <div style="display: flex; flex-direction: column; gap: 10px; overflow: auto; padding: 1rem; background: #fff;">
            <label> Without binded value
              <input style="border: 1px green solid;" />
            </label>

            <label> With binded value
              <input bind:value={value01} type="text" style="border: 2px red solid;" /> 
            </label>

            <!-- Here is the new component-->
            <IsolatedInput />
         </div>
      </Drawer.Content>
      <Drawer.Overlay />
   </Drawer.Portal>
</Drawer.Root>

// isolated-input.svelte
<script lang="ts">
  let value = "";
</script>

<label>
  With isolated input component
  <input bind:value={value} style="border: 1px solid gray;"/>
</label>

Suggestion