ionic-framework icon indicating copy to clipboard operation
ionic-framework copied to clipboard

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

Open Marius-Romanus opened this issue 2 years ago • 40 comments

Prerequisites

Ionic Framework Version

  • [ ] v4.x
  • [ ] v5.x
  • [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

Marius-Romanus avatar Jan 23 '22 21:01 Marius-Romanus

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 avatar Jan 25 '22 14:01 liamdebeasi

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.

Marius-Romanus avatar Jan 25 '22 17:01 Marius-Romanus

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 avatar Jan 25 '22 17:01 liamdebeasi

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%.

Marius-Romanus avatar Jan 25 '22 17:01 Marius-Romanus

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

liamdebeasi avatar Feb 01 '22 14:02 liamdebeasi

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.

https://user-images.githubusercontent.com/18282247/152017356-c1ea8d05-28fc-48ab-8b0c-6a2ef3a93ad5.mp4

Marius-Romanus avatar Feb 01 '22 17:02 Marius-Romanus

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

Marius-Romanus avatar Feb 03 '22 14:02 Marius-Romanus

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

sean-perkins avatar Feb 07 '22 18:02 sean-perkins

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 avatar Feb 22 '22 22:02 liamdebeasi

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

snimavat avatar Apr 10 '22 12:04 snimavat

+1 Would be great to have this feature!:)

aklen avatar Apr 18 '22 22:04 aklen

Would be amazing to have this feature. To be able to scroll and click de page behind the modal when modal is not fully expanded

jldlxs avatar May 12 '22 03:05 jldlxs

Workaround:

Create a directive like

import { Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: '[coreSuppressScrollEvent]'
})
export class SuppressScrollEventDirective implements OnInit {
  public constructor(private el: ElementRef) {}

  public ngOnInit(): void {
    (this.el.nativeElement as HTMLElement).ontouchmove = function (e) {
      e.stopPropagation();
    };
  }
}

and appliy it to the elements that should be scrollable

EinfachHans avatar May 17 '22 13:05 EinfachHans

I wanted to accomplish the same thing ... This is the CSS i used and the result:

<IonModal isOpen={showModal} onDidDismiss={handleDismiss} className="auto-height" breakpoints={[0, 1.0]} initialBreakpoint={1.0}>

ion-modal { --border-radius: 10px; padding-left: 15px; padding-right: 15px; }

ion-modal.auto-height::part(content) { position: relative; top: 80px; bottom: 0px; height: 80%; }

Screen Recording 2022-06-07 at 9 46 04 PM

peterwb avatar Jun 08 '22 02:06 peterwb

Improving the previous solution by making top the half of modals height works for any percentage:

ion-modal.auto-height::part(content) {
    position: relative;
    --varPer: 50%;
    bottom: 0px;
    height: var(--varPer);
    top: calc(var(--varPer) / 2);
}

heavy-matill avatar Jun 13 '22 16:06 heavy-matill

+1 I'm also missing this exact feature :)

chris-si avatar Jul 17 '22 14:07 chris-si

+1 Is there a timing on this already? I don't want to sound "pushy" but a short feedback would help me a lot to know if I need to implement a workaround myself. Thank you so much for your great work and efforts dear @liamdebeasi

derWebdesigner avatar Jul 21 '22 15:07 derWebdesigner

This feature is not in active development at the moment. When the feature has been added, we will close the issue and post a comment here.

liamdebeasi avatar Jul 21 '22 15:07 liamdebeasi

@EinfachHans Did you try your directive? For me, this doesn't work when for example applying it to the ion-content or elements within the DOM in the modal.

derWebdesigner avatar Jul 21 '22 16:07 derWebdesigner

@EinfachHans Did you try your directive? For me, this doesn't work when for example applying it to the ion-content or elements within the DOM in the modal.

It works perfectly for me, and on the ion-content in my component.

Make sure you've declared your directive in a common module, and imported that into your common components module too. It took a minute to get mine working as this was the first directive in the app so I didn't have some of the modules setup yet.

philkonick avatar Jul 28 '22 14:07 philkonick

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 >
)

molleahahs avatar Nov 15 '22 10:11 molleahahs

@molleahahs Thanks for that code! I am still getting this problem despite having e.stopPropagation() fire on modal touch events unfortunately. Would love to see this option built in to ionic soon.

reidwatson avatar Jan 20 '23 00:01 reidwatson

Found a way to make the modal sheet scrollable no matter on what breakpoint by reverse engineering the CSS that is applied by the scroll-y class that is added on the ion-content inside the ion-modal when last breakpoint is reached.

ion-modal ion-content::part(scroll) {
  overflow-y: var(--overflow);
  overscroll-behavior-y: contain;
}

Ion_Modal_Sheet_Scrollable

jfcere avatar Feb 16 '23 21:02 jfcere

still waiting for the feature

Sovai avatar Jul 13 '23 08:07 Sovai

Would also love to see this feature.

danielmalmros avatar Jul 17 '23 09:07 danielmalmros

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);
}

andybonner avatar Jul 31 '23 19:07 andybonner

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

RRGT19 avatar Aug 06 '23 12:08 RRGT19

up Is it possible to integrate prefersScrollingExpandsWhenScrolledToEdge?

ludonoel1 avatar Aug 14 '23 12:08 ludonoel1

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

  • https://stackoverflow.com/questions/54379721/detect-swipe-direction-without-scrolling
  • https://stackoverflow.com/questions/4770025/how-to-disable-scrolling-temporarily

thor9n avatar Aug 16 '23 12:08 thor9n

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.

leonkosterss avatar Nov 15 '23 21:11 leonkosterss