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

feat: sheet modal, add option to prefer scrolling when not fully expanded #24631

Open
4 of 6 tasks
Marius-Romanus opened this issue Jan 23, 2022 · 50 comments · May be fixed by #30097
Open
4 of 6 tasks

feat: sheet modal, add option to prefer scrolling when not fully expanded #24631

Marius-Romanus opened this issue Jan 23, 2022 · 50 comments · May be fixed by #30097
Labels
package: core @ionic/core package type: feature request a new feature, enhancement, or improvement

Comments

@Marius-Romanus
Copy link

Prerequisites

Ionic Framework Version

  • v4.x
  • v5.x
  • v6.x

Current Behavior

Hi, in the Sheet Modal the scroll of the content doesn't appear unless it's at 100% size, in other breakpoints the scroll doesn't appear.

Expected Behavior

The scroll must adjust to the content at all times, regardless of the breakpoint.

Steps to Reproduce

<ion-modal [isOpen]="true" [breakpoints]="[0.1, 0.5, 1]" [initialBreakpoint]="0.5">
    <ng-template>
      <ion-content>
        <ion-list>
          <ion-item>
            <ion-label>Text</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Text</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Text</ion-label>
          </ion-item>
          ...
          <ion-item>
            <ion-label>Text</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Text</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Text</ion-label>
          </ion-item>
        </ion-list>
      </ion-content>
    </ng-template>
  </ion-modal>

Code Reproduction URL

No response

Ionic Info

Ionic:

Ionic CLI : 6.18.1

Utility:

cordova-res : not installed globally
native-run : 1.5.0

System:

NodeJS : v17.3.0
npm : 8.3.0
OS : Windows 10

Additional Information

No response

@ionitron-bot ionitron-bot bot added the triage label Jan 23, 2022
@liamdebeasi liamdebeasi self-assigned this Jan 25, 2022
@liamdebeasi
Copy link
Contributor

Thanks for the issue. This behavior is intentional and was designed to model how native iOS apps handle the sheet. Can you please explain why this behavior does not work for your use case?

@liamdebeasi liamdebeasi added the needs: reply the issue needs a response from the user label Jan 25, 2022
@liamdebeasi liamdebeasi removed their assignment Jan 25, 2022
@ionitron-bot ionitron-bot bot removed the triage label Jan 25, 2022
@Marius-Romanus
Copy link
Author

You are right, I have been looking at other apps and this is not a bug.

I propose a new parameter to choose if you want to slide the content and slide the modal only from the header, or slide the modal without considering the content as it currently is.

In my case, I have a map, with some items in the modal. Clicking on the item moves the map to the coordinates of the item. If I have the modal at 100% the movement is not seen, if I have the modal for example at 40%, I can't scroll through the list of items.

@ionitron-bot ionitron-bot bot added triage and removed needs: reply the issue needs a response from the user labels Jan 25, 2022
@liamdebeasi
Copy link
Contributor

Thanks! It sounds like in this case you want a swipeable modal that does not take up 100% of the screen. Would setting --height: 40% on the ion-modal work instead?

My concern is adding this functionality would make swiping up harder as it would limit where you can swipe to just the header. Additionally, if you chose not to use a header users would not be able to swipe at all. This is something the card-style modal does, but we are looking to change that since it has been a pain point for some.

@liamdebeasi liamdebeasi added the needs: reply the issue needs a response from the user label Jan 25, 2022
@ionitron-bot ionitron-bot bot removed the triage label Jan 25, 2022
@Marius-Romanus
Copy link
Author

Yes, I had also thought of it as a solution, but in that case you could not put the modal at 100% of the screen, and it greatly limits the content you can see. In my example it's not that useful, but there may be other cases where you need 40% scrolling content as well as 100%.

@ionitron-bot ionitron-bot bot added triage and removed needs: reply the issue needs a response from the user labels Jan 25, 2022
@liamdebeasi
Copy link
Contributor

liamdebeasi commented Feb 1, 2022

Thanks. Do you have any examples of native iOS/Android apps that provide the behavior you are looking to achieve?

@liamdebeasi liamdebeasi added the needs: reply the issue needs a response from the user label Feb 1, 2022
@ionitron-bot ionitron-bot bot removed the triage label Feb 1, 2022
@Marius-Romanus
Copy link
Author

Hello, I have been able to see the behavior in the youtube Android application, in the long descriptions. I don't have an iphone to check it.

video6039851876587932189.mp4

@ionitron-bot ionitron-bot bot added triage and removed needs: reply the issue needs a response from the user labels Feb 1, 2022
@Marius-Romanus
Copy link
Author

Hello @liamdebeasi, I have found a bug that has nothing to do with the petition we are talking about, but it coincides with the title :)

When the ion-modal contains ion-header, the bottom part of the ion-content cannot be read at all.
I give you an example, if you remove the header the content is seen correctly

https://stackblitz.com/edit/ionic-angular-v5-axhw6t?file=src%2Fapp%2Fapp.component.html

@sean-perkins
Copy link
Contributor

@Marius-Romanus I believe that specific side-issue has been reported here: #24706 and PR to address it here: #24723

@liamdebeasi
Copy link
Contributor

liamdebeasi commented Feb 22, 2022

Native iOS has this feature according to https://sarunw.com/posts/bottom-sheet-in-ios-15-with-uisheetpresentationcontroller/, so I think we should add it in Ionic as well.

See https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3801907-prefersscrollingexpandswhenscrol

@liamdebeasi liamdebeasi changed the title bug: Sheet Modal​ content scroll feat: sheet modal, add option to prefer scrolling when not fully expanded Feb 22, 2022
@liamdebeasi liamdebeasi added package: core @ionic/core package type: feature request a new feature, enhancement, or improvement labels Feb 22, 2022
@ionitron-bot ionitron-bot bot removed the triage label Feb 22, 2022
@snimavat
Copy link

snimavat commented Apr 10, 2022

+1 Looking for a way to make content inside sheet modal scrollable even when its not 100%

@Sovai
Copy link

Sovai commented Jul 13, 2023

still waiting for the feature

@danielmalmros
Copy link

danielmalmros commented Jul 17, 2023

Would also love to see this feature.

@andybonner
Copy link

I'd love this feature. Ideally it would align with Apple's native behavior, in which a swipe within modal content starts by scrolling, until we reach the top of scrollable content, at which point it turns into a drag action on the entire sheet and ultimately a dismissal if swiped to the bottom of the screen. Or, in fewer words: You can scroll and dismiss in the same gesture.

In the meantime I thought I'd share a workaround that I cobbled together from this thread and some SO sources. This doesn't provide what I just described—a swipe only scrolls, it can't drag or dismiss (unless you target the drag handle, in which case it's not a scroll). Basically I declare the breakpoint to be 1, making it scrollable, and then offset the positioning by a bit to make the background visible.

In my global scss file:

/**
HOW TO USE:
* Give your modal `[initialBreakpoint]="1"` and `[breakpoints]="[0, 1]"`. If you include other breakpoints
in the array, scrolling will not work on them. By calling the breakpoint 1, ionic provides the scrolling.
* Give your modal class `scrollable-sheet-modal-95`. This will take up 95% of the screen height. Rather
than hardcode this, the mixin below parses the "-95" suffix.
* If you'd like a different percentage, duplicate the "@include" line below with a new argument and use
a corresponding class, e.g. `@include custom-sheet-modal-height(90)` and `sfd-scrollable-sheet-modal-90`.
You can't just use any suffix you want in the class; there has to be a matching "@include".
* But if you want a much lower height, maybe question why you're making it scrollable rather than making
it bigger.
*/
@mixin custom-sheet-modal-height($argument) {
  $heightString: $argument;

  &-#{$heightString}::part(content) {
    height: unquote($argument + '%');
    bottom: unquote('-' + (100 - $argument) / 2 + '%');
  }
}

ion-modal.scrollable-sheet-modal {
  position: relative;
  @include custom-sheet-modal-height(95);
  // Add similar lines here to use other heights. Like:
  // @include custom-sheet-modal-height(90);
}

@RRGT19
Copy link

RRGT19 commented Aug 6, 2023

I would like this feature too, I have the same issue.

@ludonoel1
Copy link

up
Is it possible to integrate prefersScrollingExpandsWhenScrolledToEdge?

@thor9n
Copy link

thor9n commented Aug 16, 2023

I implemented a solution in Vue since i wanted the same behaviour as @andybonner:

Ideally it would align with Apple's native behavior, in which a swipe within modal content starts by scrolling, until we reach the top of scrollable content, at which point it turns into a drag action on the entire sheet and ultimately a dismissal if swiped to the bottom of the screen.

  • It's not an optimal or elegant solution, but it works for my use case. Basically it sets a new breakpoint if the user is scrolling down (swiping up) and the content is already at top.
  • It requires the top modal breakpoint to be 100% (== 1) since otherwise it wont catch the scroll event.
  • This only support touch on mobile devices and needs to be adapted to scrolling if needed (see resources below).

Hope it helps somebody!

Template

<ion-content ref="content" :fullscreen="true" :scrollEvents="true" @ionScroll="on_scroll($event)">

Script

let content = ref(null)
let content_scroll_start = ref(null)
let content_scroll_top = ref(null)
let content_scroll_start_from_top = ref(false)
let timer = ref(null)
let timer_timestamp = ref(null)

onMounted(async () => {
  content.value.$el.addEventListener("touchstart", on_native_scroll_start)
  content.value.$el.addEventListener("touchmove", on_native_scroll)
})

function on_native_scroll_start(event) {
  content_scroll_start.value = event
  content_scroll_start_from_top.value = content_scroll_is_top.value ? true : false
}
async function on_native_scroll(event) {
  let scroll = event.changedTouches[0]

  if (scroll.screenY - content_scroll_start.value.changedTouches[0].screenY > 0) {
    if (!content_scroll_is_top.value || !content_scroll_start_from_top.value) return

    if (timer.value || timer_timestamp.value === content_scroll_start.value.timeStamp) return

    timer.value = setTimeout(async () => {
      const modal = await modalController.getTop()
      const current_breakpoint = await modal.getCurrentBreakpoint()
      if (current_breakpoint === 1) modal.setCurrentBreakpoint(0.65)
      clearTimeout(timer.value)
      timer.value = null
      timer_timestamp.value = content_scroll_start.value.timeStamp
    }, 50)
  } else if (scroll.screenY - content_scroll_start.value.changedTouches[0].screenY < 0) {
    if (timer.value) clearTimeout(timer.value)
  }
}
function on_scroll(event) {
  // prettier-ignore
  const { detail: { scrollTop } } = event
  content_scroll_top.value = scrollTop
}
const content_scroll_is_top = computed(() => {
  return content_scroll_top.value <= 0 || content_scroll_top.value === null
})

Resources

@leonkosterss
Copy link

Would also like to see the iOS native way of scroll and swipe to next breakpoint with same gesture. Like Apple Maps. Now it’s very hard to close it when the modal sheet is fully opened.

@liamdebeasi
Copy link
Contributor

Would also like to see the iOS native way of scroll and swipe to next breakpoint with same gesture. Like Apple Maps. Now it’s very hard to close it when the modal sheet is fully opened.

Can you attach a video of this? What you're describing sounds different than what is described in the original post.

@adedayojs
Copy link

Still waiting for this feature

@leonkosterss
Copy link

@liamdebeasi The video for scrolling and dragging the Sheet in Apple Maps with 1 gesture:

RPReplay_Final1700683167.1.mp4

@liamdebeasi
Copy link
Contributor

liamdebeasi commented Nov 22, 2023

That's a different behavior and not something that will be solved by this feature. This feature is focused on allowing the content to scroll even when the sheet modal is not fully expanded.

We have a similar request for this in #23919, but we are unable to add it due to platform limitations. The way browsers handle scroll dispatching does not make this functionality possible at the moment. The linked thread has more context.

@leonkosterss
Copy link

Aaah, that's indeed different. Thank you for the great answer.

@cre8tiveit
Copy link

Hi @leonkosterss ,

The modal is still resizable? I'm looking for this behaviour. Can you help me out?

@joelutting
Copy link

Need this behaviour too. Have tried all the workarounds on this page but can't get any of them working - is there a complete solution anywhere? This is really necessary for my project.

@jldlxs
Copy link

jldlxs commented Jan 25, 2024 via email

@creazy231
Copy link

+1 also would love to see this feature soon 🔥

@beliven-davide-lorigliola

+1 also here

@i4innovationnet
Copy link

+1

1 similar comment
@bobbyg603
Copy link

+1

@rasheeek
Copy link

Would love this feature :)

@nikqig
Copy link

nikqig commented Sep 8, 2024

+1

@chopperdaddy
Copy link

It's absolutely ridiculous that this is still an issue in 2024. Devs pls do something.

@caoquy2000
Copy link

caoquy2000 commented Nov 1, 2024

Any update in 2024 ? (T_T)

@danielehrhardt
Copy link
Contributor

+1

1 similar comment
@stephan-fischer
Copy link

+1

@diegosystem32
Copy link

for react, maybe the following code will help you,give a ref attribute to the element you want to scroll,and then like code write in useEffect, make it stopPropagation.

const modalBoxRef: any = useRef()

 useEffect(() => {
    if (visible) {
      setTimeout(() => {
        modalBoxRef && modalBoxRef.current && (modalBoxRef.current.ontouchmove = function (e: any) {
          e.stopPropagation();
        })
      }, 0)
    }
  }, [visible])

return (
 <IonModal 
    isOpen={visible} 
    onDidDismiss={() => {onClose!() }} 
    breakpoints=[0, 0.3] 
    initialBreakpoint=[0.3]>
      <IonLoading
        isOpen={props.loading}
        message={'加载中,请稍等...'}
      />
      <div className="modal-box" style={props.hideModal ? { height: 0, overflow: 'hidden' } : props.bgColor ? { background: props.bgColor } : {}}
        ref={modalBoxRef}
      >
         // your list code
      </div>
 </IonModal >
)

It worked for me, but is auto height possible?

@diegosystem32
Copy link

diegosystem32 commented Dec 9, 2024

I found a workaround working for me in 08/12/2024, the content is scrollable in anyone breakpoints and subtract header height and footer height:

In reactJS Typescript component:

import { ReactNode, useEffect, useRef, useState } from "react";
import { IonModal, IonLoading, IonContent, IonHeader, IonTitle, IonToolbar } from "@ionic/react";

interface ModalProps {
  visible: boolean;
  onClose?: () => void;
  children: ReactNode;
}

const ModalSheetComponent: React.FC<ModalProps> = ({
  visible,
  onClose,
  children,
}) => {
  const modalBoxRef = useRef<HTMLIonContentElement | null>(null);

  const handleBreakpointChange = (
    event: any/*CustomEvent<{ breakpoint: number }>*/
  ) => {
    const ionModalElement = document.querySelector(".custom-modal-scroll > .ion-page");
    const ionModalContent: any = document.querySelector(".custom-modal-scroll > .ion-page > ion-content");
    const ionModalHeader: any = document.querySelector(".custom-modal-scroll > .ion-page > ion-header");
    const ionModalFooter: any = document.querySelector(".custom-modal-scroll > .ion-page > ion-footer");
    if (ionModalElement) {
      const modalTop = ionModalElement.getBoundingClientRect().top;
      console.log(modalTop, ionModalContent);
      const head = ionModalHeader ? ionModalHeader.getBoundingClientRect().height : 0;
      const foot = ionModalFooter ? ionModalFooter.getBoundingClientRect().height : 0;
      ionModalContent.style.maxHeight = `${(window.innerHeight - modalTop) - (head + foot)}px`
    }
  };

  useEffect(() => {
    if (visible) {
      setTimeout(() => {
        const ionModalContent: any = document.querySelector(".custom-modal-scroll > .ion-page > ion-content");
        if (ionModalContent) {
          ionModalContent.ontouchmove = (e: TouchEvent) => {
            e.stopPropagation();
          };
        }
      }, 0);
    }
  }, [visible]);

  return (
    <IonModal
      className="custom-modal-scroll"
      isOpen={visible}
      onDidPresent={handleBreakpointChange}
      onDidDismiss={onClose}
      initialBreakpoint={0.25}
      breakpoints={[0, 0.25, 0.5, 0.75]}
      onIonBreakpointDidChange={handleBreakpointChange}
    >
      {children}
    </IonModal>
  );
};

export default ModalSheetComponent;



//USAGE:
  <ModalSheetComponent visible={modalAddTextOpen}>
        <IonHeader>
            <IonToolbar>
              <IonTitle>Header</IonTitle>
            </IonToolbar>
            <IonSearchbar placeholder="Search"></IonSearchbar>
          </IonHeader>
          <IonContent>
            <IonList>
              {Array.from({ length: 300 }, (_, index) => index + 1).map(
                (i, index) => (
                  <IonItem key={index}>
                    <IonAvatar slot="start">
                      <IonImg src="https://i.pravatar.cc/300?u=b" />
                    </IonAvatar>
                    <IonLabel>
                      <h2>Connor Smith</h2>
                      <p>Sales Rep</p>
                    </IonLabel>
                  </IonItem>
                )
              )}
              </IonList>              
          </IonContent>
</ModalSheetComponent>


//Css code:
.custom-modal-scroll>.ion-page>ion-content {
  overflow-y: scroll;
  overscroll-behavior-y: contain;
}


/* CUSTOM HEADER REMOVE SHADOW AND OUTLINE */
ion-modal>.ion-page>ion-content>ion-header>ion-toolbar {
  border: none !important;
}

ion-modal>.ion-page>ion-content>ion-footer>ion-toolbar {
  border: 0px solid #ffffff00 !important;
}

ion-modal>.ion-page>ion-content>ion-footer {
  box-shadow: none !important;
  -webkit-box-shadow: none !important;
}

ion-modal>.ion-page>ion-content>ion-header {
  box-shadow: none !important;
  -webkit-box-shadow: none !important;
}

@kumibrr kumibrr linked a pull request Dec 22, 2024 that will close this issue
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package: core @ionic/core package type: feature request a new feature, enhancement, or improvement
Projects
None yet
Development

Successfully merging a pull request may close this issue.