Skip to content

feat(modal): clicking handle advances to the next breakpoint #25057

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

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/src/components/modal/gestures/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export const createSheetGesture = (
const processedStep =
isAttemptingDismissWithCanDismiss && secondToLastBreakpoint !== undefined
? secondToLastBreakpoint +
calculateSpringStep((step - secondToLastBreakpoint) / (maxStep - secondToLastBreakpoint))
calculateSpringStep((step - secondToLastBreakpoint) / (maxStep - secondToLastBreakpoint))
: step;

offset = clamp(0.0001, processedStep, maxStep);
Expand Down Expand Up @@ -371,5 +371,6 @@ export const createSheetGesture = (
return {
gesture,
moveSheetToBreakpoint,
animation
};
};
24 changes: 23 additions & 1 deletion core/src/components/modal/modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
contain: strict;
}

.modal-wrapper, ion-backdrop {
.modal-wrapper,
ion-backdrop {
pointer-events: auto;
}

Expand Down Expand Up @@ -124,9 +125,30 @@
*/
transform: translateZ(0);

border: 0;

background: var(--ion-color-step-350, #c0c0be);

cursor: pointer;

z-index: 11;

&::before {
/**
* Adds a 4px tap area to the perimeter
* of the handle.
*/
@include padding(4px, 4px, 4px, 4px);

position: absolute;

width: 36px;
height: 5px;

transform: translate(-50%, -50%);

content: "";
}
}

/**
Expand Down
41 changes: 39 additions & 2 deletions core/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
private sortedBreakpoints?: number[];
private keyboardOpenCallback?: () => void;
private moveSheetToBreakpoint?: (options: MoveSheetToBreakpointOptions) => void;
private sheetAnimation?: Animation;

private inline = false;
private workingDelegate?: FrameworkDelegate;
Expand Down Expand Up @@ -550,7 +551,7 @@ export class Modal implements ComponentInterface, OverlayInterface {

ani.progressStart(true, 1);

const { gesture, moveSheetToBreakpoint } = createSheetGesture(
const { gesture, moveSheetToBreakpoint, animation } = createSheetGesture(
this.el,
this.backdropEl!,
wrapperEl,
Expand All @@ -570,6 +571,7 @@ export class Modal implements ComponentInterface, OverlayInterface {

this.gesture = gesture;
this.moveSheetToBreakpoint = moveSheetToBreakpoint;
this.sheetAnimation = animation;

this.gesture.enable(true);
}
Expand Down Expand Up @@ -719,6 +721,33 @@ export class Modal implements ComponentInterface, OverlayInterface {
return this.currentBreakpoint;
}

private moveToNextBreakpoint() {
const { breakpoints, currentBreakpoint } = this;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tabbing to the handle and pressing Enter multiple times causes the animation to jump.

Screen.Recording.2022-04-04.at.6.24.48.PM.mov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted this behavior slightly, so that advancing is tied to keyup instead of keydown. Setting the current breakpoint does not currently wait for the previous transition to finish. If you perform an action while a transition is inflight, it will disrupt the transition.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's still jumping if you mash Enter; I feel like if there's a transition in progress when you click/activate the handle, it should just do nothing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amandaesmith3 to confirm, if the user performs any action (clicking the handle or pressing enter), while the sheet modal is actively transitioning, the expected behavior would be to perform no operation (skip the interaction)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking back at the code, present and dismiss will both wait for the current transition and then go through with the attempted interaction, so we should probably go with that instead here. Thinking on it, if I hit Enter twice quickly, having it only move to the next breakpoint once would feel off. 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waiting for the transition feels like it would conflict with the design document for the API of moving to a breakpoint:

If the user is in the middle of a gesture when this API is called, the user will be transitioned to the breakpoint value called by the API.

Unless we can cancel the current transition and immediately kick-off the new transition without a glitchy experience. I can do some more discovery on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the behavior, curious to both of your thoughts/feedback/alternatives. It now won't skip/jitter when you hold the enter key, but it will only performs moving to the next breakpoint when the current animation finishes. Hard to demonstrate in a recording.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So when I try it, pressing Enter twice rapidly makes it only move once. It seems like it'll just eat your input if the animation is currently running. My guess is that since the current breakpoint doesn't actually update until after the animation finishes (triggered here), nextBreakpoint is calculated as the same thing for both Enter presses. Which would make the second press not really do anything.

That said, I'm okay with the behavior as-is. Up to you if you want to change it, though if we stick with it as-is, the code should probably be cleaned up to make it more clear.

if (breakpoints && currentBreakpoint !== undefined) {
const allowedBreakpoints = breakpoints.filter((b) => b !== 0);
const currentBreakpointIndex = allowedBreakpoints.indexOf(currentBreakpoint);
const nextBreakpointIndex = (currentBreakpointIndex + 1) % allowedBreakpoints.length;
const nextBreakpoint = allowedBreakpoints[nextBreakpointIndex];

/**
* Sets the current breakpoint to the next available breakpoint.
* If the current breakpoint is the last breakpoint, we set the current
* breakpoint to the first non-zero breakpoint to avoid dismissing the sheet.
*/
if (this.sheetAnimation?.isRunning()) {
this.sheetAnimation.onFinish(() => {
this.setCurrentBreakpoint(nextBreakpoint);
}, { oneTimeCallback: true });
} else {
this.setCurrentBreakpoint(nextBreakpoint);
}
}
}

private onHandleClick = () => {
this.moveToNextBreakpoint();
};

private onBackdropTap = () => {
this.dismiss(undefined, BACKDROP);
};
Expand Down Expand Up @@ -786,7 +815,15 @@ export class Modal implements ComponentInterface, OverlayInterface {
{mode === 'ios' && <div class="modal-shadow"></div>}

<div role="dialog" class="modal-wrapper ion-overlay-wrapper" part="content" ref={(el) => (this.wrapperEl = el)}>
{showHandle && <div class="modal-handle" part="handle"></div>}
{showHandle && (
<button
class="modal-handle"
part="handle"
aria-label="Activate to adjust the size of the card overlaying the screen"
aria-controls={modalId}
onClick={this.onHandleClick}
></button>
)}
<slot></slot>
</div>
</Host>
Expand Down
36 changes: 36 additions & 0 deletions core/src/components/modal/test/sheet/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,39 @@ describe('modal: sheet: setting the breakpoint', () => {
expect(ionBreakpointDidChangeSpy).toHaveReceivedEventTimes(1);
});
});

describe('clicking the handle', () => {
let page: E2EPage;

beforeEach(async () => {
page = await newE2EPage({
url: '/src/components/modal/test/sheet?ionic:_testing=true',
});
});

it('should advance to the next breakpoint', async () => {
const modal = await openModal(page, '#sheet-modal');
const handle = await page.find('ion-modal >>> .modal-handle');

await handle.click();
await modal.waitForEvent('ionBreakpointDidChange');

expect(await modal.callMethod('getCurrentBreakpoint')).toBe(0.5);

await handle.click();
await modal.waitForEvent('ionBreakpointDidChange');

expect(await modal.callMethod('getCurrentBreakpoint')).toBe(0.75);

await handle.click();
await modal.waitForEvent('ionBreakpointDidChange');

expect(await modal.callMethod('getCurrentBreakpoint')).toBe(1);

await handle.click();
await modal.waitForEvent('ionBreakpointDidChange');

// Advancing from the last breakpoint should change the breakpoint to the first non-zero breakpoint
expect(await modal.callMethod('getCurrentBreakpoint')).toBe(0.25);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
const refresher = document.getElementById('refresher');

refresher.addEventListener('ionRefresh', async function () {
console.log('refresh fired')
console.log('refresh fired');
const data = await getAsyncData();
items = items.concat(data);
refresher.complete();
Expand Down