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

fix(modal): status bar color now correct with sheet modal #25424

Merged
merged 10 commits into from
Jun 13, 2022
32 changes: 31 additions & 1 deletion core/src/components/modal/gestures/swipe-to-close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import type { GestureDetail } from '../../../utils/gesture';
import { createGesture } from '../../../utils/gesture';
import { clamp, getElementRoot } from '../../../utils/helpers';
import { setCardStatusBarDark, setCardStatusBarDefault } from '../utils';

import { calculateSpringStep, handleCanDismiss } from './utils';

Expand All @@ -18,13 +19,20 @@ export const SwipeToCloseDefaults = {
};

export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: Animation, onDismiss: () => void) => {
/**
* The step value at which a card modal
* is eligible for dismissing via gesture.
*/
const DISMISS_THRESHOLD = 0.5;

const height = el.offsetHeight;
let isOpen = false;
let canDismissBlocksGesture = false;
let contentEl: HTMLElement | null = null;
let scrollEl: HTMLElement | null = null;
const canDismissMaxStep = 0.2;
let initialScrollY = true;
let lastStep = 0;
const getScrollY = () => {
if (contentEl && isIonContent(contentEl)) {
return (contentEl as HTMLIonContentElement).scrollY;
Expand Down Expand Up @@ -187,6 +195,28 @@ export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: An
const clampedStep = clamp(0.0001, processedStep, maxStep);

animation.progressStep(clampedStep);

/**
* When swiping down half way, the status bar style
* should be reset to its default value.
*
* We track lastStep so that we do not fire these
* functions on every onMove, only when the user has
* crossed a certain threshold.
*/
if (clampedStep >= DISMISS_THRESHOLD && lastStep < DISMISS_THRESHOLD) {
setCardStatusBarDefault();

/**
* However, if we swipe back up, then the
* status bar style should be set to have light
* text on a dark background.
*/
} else if (clampedStep < DISMISS_THRESHOLD && lastStep >= DISMISS_THRESHOLD) {
setCardStatusBarDark();
}

lastStep = clampedStep;
};

const onEnd = (detail: GestureDetail) => {
Expand All @@ -208,7 +238,7 @@ export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: An
* animation can never complete until
* canDismiss is checked.
*/
const shouldComplete = !isAttempingDismissWithCanDismiss && threshold >= 0.5;
const shouldComplete = !isAttempingDismissWithCanDismiss && threshold >= DISMISS_THRESHOLD;
let newStepValue = shouldComplete ? -0.001 : 0.001;

if (!shouldComplete) {
Expand Down
44 changes: 33 additions & 11 deletions core/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { mdLeaveAnimation } from './animations/md.leave';
import type { MoveSheetToBreakpointOptions } from './gestures/sheet';
import { createSheetGesture } from './gestures/sheet';
import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
import { setCardStatusBarDark, setCardStatusBarDefault } from './utils';

/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
Expand Down Expand Up @@ -466,21 +467,31 @@ export class Modal implements ComponentInterface, OverlayInterface {
backdropBreakpoint: this.backdropBreakpoint,
});

/**
* TODO (FW-937) - In the next major release of Ionic, all card modals
* will be swipeable by default. canDismiss will be used to determine if the
* modal can be dismissed. This check should change to check the presence of
* presentingElement instead.
*
* If we did not do this check, then not using swipeToClose would mean you could
* not run canDismiss on swipe as there would be no swipe gesture created.
*/
const hasCardModal = this.swipeToClose || (this.canDismiss !== undefined && this.presentingElement !== undefined);

/**
* We need to change the status bar at the
* start of the animation so that it completes
* by the time the card animation is done.
*/
if (hasCardModal && getIonMode(this) === 'ios') {
setCardStatusBarDark();
}

await this.currentTransition;

if (this.isSheetModal) {
this.initSheetGesture();

/**
* TODO (FW-937) - In the next major release of Ionic, all card modals
* will be swipeable by default. canDismiss will be used to determine if the
* modal can be dismissed. This check should change to check the presence of
* presentingElement instead.
*
* If we did not do this check, then not using swipeToClose would mean you could
* not run canDismiss on swipe as there would be no swipe gesture created.
*/
} else if (this.swipeToClose || (this.canDismiss !== undefined && this.presentingElement !== undefined)) {
} else if (hasCardModal) {
await this.initSwipeToClose();
}

Expand Down Expand Up @@ -631,6 +642,17 @@ export class Modal implements ComponentInterface, OverlayInterface {
return false;
}

/**
* We need to start the status bar change
* before the animation so that the change
* finishes when the dismiss animation does.
* TODO (FW-937)
*/
const hasCardModal = this.swipeToClose || (this.canDismiss !== undefined && this.presentingElement !== undefined);
if (hasCardModal && getIonMode(this) === 'ios') {
setCardStatusBarDefault();
}

/* tslint:disable-next-line */
if (typeof window !== 'undefined' && this.keyboardOpenCallback) {
window.removeEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
Expand Down
31 changes: 31 additions & 0 deletions core/src/components/modal/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { StatusBar, Style } from '../../utils/native/status-bar';
import { win } from '../../utils/window';

/**
* Use y = mx + b to
* figure out the backdrop value
Expand Down Expand Up @@ -57,3 +60,31 @@ export const getBackdropValueForSheet = (x: number, backdropBreakpoint: number)

return x * slope + b;
};

/**
* The tablet/desktop card modal activates
* when the window width is >= 768.
* At that point, the presenting element
* is not transformed, so we do not need to
* adjust the status bar color.
*
* Note: We check supportsDefaultStatusBarStyle so that
* Capacitor <= 2 users do not get their status bar
* stuck in an inconsistent state due to a lack of
* support for Style.Default.
*/
export const setCardStatusBarDark = () => {
if (!win || win.innerWidth >= 768 || !StatusBar.supportsDefaultStatusBarStyle()) {
return;
}

StatusBar.setStyle({ style: Style.Dark });
};

export const setCardStatusBarDefault = () => {
if (!win || win.innerWidth >= 768 || !StatusBar.supportsDefaultStatusBarStyle()) {
return;
}

StatusBar.setStyle({ style: Style.Default });
};
34 changes: 34 additions & 0 deletions core/src/utils/native/status-bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { win } from '../window';

interface StyleOptions {
style: Style;
}

export enum Style {
Dark = 'DARK',
Light = 'LIGHT',
Default = 'DEFAULT',
}

export const StatusBar = {
getEngine() {
return (win as any)?.Capacitor?.isPluginAvailable('StatusBar') && (win as any)?.Capacitor.Plugins.StatusBar;
},
supportsDefaultStatusBarStyle() {
/**
* The 'DEFAULT' status bar style was added
* to the @capacitor/status-bar plugin in Capacitor 3.
* PluginHeaders is only supported in Capacitor 3+,
* so we can use this to detect Capacitor 3.
*/
return !!(win as any)?.Capacitor?.PluginHeaders;
},
setStyle(options: StyleOptions) {
const engine = this.getEngine();
if (!engine) {
return;
}

engine.setStyle(options);
},
};
23 changes: 23 additions & 0 deletions core/src/utils/window/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* When accessing the window, it is important
* to account for SSR applications where the
* window is not available. Code that accesses
* window when it is not available will crash.
* Even checking if `window === undefined` will cause
* apps to crash in SSR.
*
* Use win below to access an SSR-safe version
* of the window.
*
* Example 1:
* Before:
* if (window.innerWidth > 768) { ... }
*
* After:
* import { win } from 'path/to/this/file';
* if (win?.innerWidth > 768) { ... }
*
* Note: Code inside of this if-block will
* not run in an SSR environment.
*/
export const win: Window | undefined = typeof window !== 'undefined' ? window : undefined;