Skip to content

Commit 377c4f5

Browse files
authored
fix(modal): status bar color now correct with sheet modal (#25424)
resolves #20501
1 parent c10df52 commit 377c4f5

File tree

5 files changed

+152
-12
lines changed

5 files changed

+152
-12
lines changed

core/src/components/modal/gestures/swipe-to-close.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import type { GestureDetail } from '../../../utils/gesture';
1010
import { createGesture } from '../../../utils/gesture';
1111
import { clamp, getElementRoot } from '../../../utils/helpers';
12+
import { setCardStatusBarDark, setCardStatusBarDefault } from '../utils';
1213

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

@@ -18,13 +19,20 @@ export const SwipeToCloseDefaults = {
1819
};
1920

2021
export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: Animation, onDismiss: () => void) => {
22+
/**
23+
* The step value at which a card modal
24+
* is eligible for dismissing via gesture.
25+
*/
26+
const DISMISS_THRESHOLD = 0.5;
27+
2128
const height = el.offsetHeight;
2229
let isOpen = false;
2330
let canDismissBlocksGesture = false;
2431
let contentEl: HTMLElement | null = null;
2532
let scrollEl: HTMLElement | null = null;
2633
const canDismissMaxStep = 0.2;
2734
let initialScrollY = true;
35+
let lastStep = 0;
2836
const getScrollY = () => {
2937
if (contentEl && isIonContent(contentEl)) {
3038
return (contentEl as HTMLIonContentElement).scrollY;
@@ -187,6 +195,28 @@ export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: An
187195
const clampedStep = clamp(0.0001, processedStep, maxStep);
188196

189197
animation.progressStep(clampedStep);
198+
199+
/**
200+
* When swiping down half way, the status bar style
201+
* should be reset to its default value.
202+
*
203+
* We track lastStep so that we do not fire these
204+
* functions on every onMove, only when the user has
205+
* crossed a certain threshold.
206+
*/
207+
if (clampedStep >= DISMISS_THRESHOLD && lastStep < DISMISS_THRESHOLD) {
208+
setCardStatusBarDefault();
209+
210+
/**
211+
* However, if we swipe back up, then the
212+
* status bar style should be set to have light
213+
* text on a dark background.
214+
*/
215+
} else if (clampedStep < DISMISS_THRESHOLD && lastStep >= DISMISS_THRESHOLD) {
216+
setCardStatusBarDark();
217+
}
218+
219+
lastStep = clampedStep;
190220
};
191221

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

214244
if (!shouldComplete) {

core/src/components/modal/modal.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { mdLeaveAnimation } from './animations/md.leave';
3131
import type { MoveSheetToBreakpointOptions } from './gestures/sheet';
3232
import { createSheetGesture } from './gestures/sheet';
3333
import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
34+
import { setCardStatusBarDark, setCardStatusBarDefault } from './utils';
3435

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

470+
/**
471+
* TODO (FW-937) - In the next major release of Ionic, all card modals
472+
* will be swipeable by default. canDismiss will be used to determine if the
473+
* modal can be dismissed. This check should change to check the presence of
474+
* presentingElement instead.
475+
*
476+
* If we did not do this check, then not using swipeToClose would mean you could
477+
* not run canDismiss on swipe as there would be no swipe gesture created.
478+
*/
479+
const hasCardModal = this.swipeToClose || (this.canDismiss !== undefined && this.presentingElement !== undefined);
480+
481+
/**
482+
* We need to change the status bar at the
483+
* start of the animation so that it completes
484+
* by the time the card animation is done.
485+
*/
486+
if (hasCardModal && getIonMode(this) === 'ios') {
487+
setCardStatusBarDark();
488+
}
489+
469490
await this.currentTransition;
470491

471492
if (this.isSheetModal) {
472493
this.initSheetGesture();
473-
474-
/**
475-
* TODO (FW-937) - In the next major release of Ionic, all card modals
476-
* will be swipeable by default. canDismiss will be used to determine if the
477-
* modal can be dismissed. This check should change to check the presence of
478-
* presentingElement instead.
479-
*
480-
* If we did not do this check, then not using swipeToClose would mean you could
481-
* not run canDismiss on swipe as there would be no swipe gesture created.
482-
*/
483-
} else if (this.swipeToClose || (this.canDismiss !== undefined && this.presentingElement !== undefined)) {
494+
} else if (hasCardModal) {
484495
await this.initSwipeToClose();
485496
}
486497

@@ -631,6 +642,17 @@ export class Modal implements ComponentInterface, OverlayInterface {
631642
return false;
632643
}
633644

645+
/**
646+
* We need to start the status bar change
647+
* before the animation so that the change
648+
* finishes when the dismiss animation does.
649+
* TODO (FW-937)
650+
*/
651+
const hasCardModal = this.swipeToClose || (this.canDismiss !== undefined && this.presentingElement !== undefined);
652+
if (hasCardModal && getIonMode(this) === 'ios') {
653+
setCardStatusBarDefault();
654+
}
655+
634656
/* tslint:disable-next-line */
635657
if (typeof window !== 'undefined' && this.keyboardOpenCallback) {
636658
window.removeEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);

core/src/components/modal/utils.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { StatusBar, Style } from '../../utils/native/status-bar';
2+
import { win } from '../../utils/window';
3+
14
/**
25
* Use y = mx + b to
36
* figure out the backdrop value
@@ -57,3 +60,31 @@ export const getBackdropValueForSheet = (x: number, backdropBreakpoint: number)
5760

5861
return x * slope + b;
5962
};
63+
64+
/**
65+
* The tablet/desktop card modal activates
66+
* when the window width is >= 768.
67+
* At that point, the presenting element
68+
* is not transformed, so we do not need to
69+
* adjust the status bar color.
70+
*
71+
* Note: We check supportsDefaultStatusBarStyle so that
72+
* Capacitor <= 2 users do not get their status bar
73+
* stuck in an inconsistent state due to a lack of
74+
* support for Style.Default.
75+
*/
76+
export const setCardStatusBarDark = () => {
77+
if (!win || win.innerWidth >= 768 || !StatusBar.supportsDefaultStatusBarStyle()) {
78+
return;
79+
}
80+
81+
StatusBar.setStyle({ style: Style.Dark });
82+
};
83+
84+
export const setCardStatusBarDefault = () => {
85+
if (!win || win.innerWidth >= 768 || !StatusBar.supportsDefaultStatusBarStyle()) {
86+
return;
87+
}
88+
89+
StatusBar.setStyle({ style: Style.Default });
90+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { win } from '../window';
2+
3+
interface StyleOptions {
4+
style: Style;
5+
}
6+
7+
export enum Style {
8+
Dark = 'DARK',
9+
Light = 'LIGHT',
10+
Default = 'DEFAULT',
11+
}
12+
13+
export const StatusBar = {
14+
getEngine() {
15+
return (win as any)?.Capacitor?.isPluginAvailable('StatusBar') && (win as any)?.Capacitor.Plugins.StatusBar;
16+
},
17+
supportsDefaultStatusBarStyle() {
18+
/**
19+
* The 'DEFAULT' status bar style was added
20+
* to the @capacitor/status-bar plugin in Capacitor 3.
21+
* PluginHeaders is only supported in Capacitor 3+,
22+
* so we can use this to detect Capacitor 3.
23+
*/
24+
return !!(win as any)?.Capacitor?.PluginHeaders;
25+
},
26+
setStyle(options: StyleOptions) {
27+
const engine = this.getEngine();
28+
if (!engine) {
29+
return;
30+
}
31+
32+
engine.setStyle(options);
33+
},
34+
};

core/src/utils/window/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* When accessing the window, it is important
3+
* to account for SSR applications where the
4+
* window is not available. Code that accesses
5+
* window when it is not available will crash.
6+
* Even checking if `window === undefined` will cause
7+
* apps to crash in SSR.
8+
*
9+
* Use win below to access an SSR-safe version
10+
* of the window.
11+
*
12+
* Example 1:
13+
* Before:
14+
* if (window.innerWidth > 768) { ... }
15+
*
16+
* After:
17+
* import { win } from 'path/to/this/file';
18+
* if (win?.innerWidth > 768) { ... }
19+
*
20+
* Note: Code inside of this if-block will
21+
* not run in an SSR environment.
22+
*/
23+
export const win: Window | undefined = typeof window !== 'undefined' ? window : undefined;

0 commit comments

Comments
 (0)