Skip to content

Commit

Permalink
fix(ios): add haptic drag gesture for action sheet and alert componen…
Browse files Browse the repository at this point in the history
…ts (#21060)
  • Loading branch information
liamdebeasi authored May 26, 2020
1 parent e53f024 commit 33be1f0
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 6 deletions.
2 changes: 2 additions & 0 deletions core/src/components/action-sheet/action-sheet.ios.scss
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@
@include margin-horizontal(null, $action-sheet-ios-button-icon-padding-right);

font-size: $action-sheet-ios-button-icon-font-size;

pointer-events: none;
}

.action-sheet-button:last-child {
Expand Down
1 change: 1 addition & 0 deletions core/src/components/action-sheet/action-sheet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
flex-shrink: 0;
align-items: center;
justify-content: center;
pointer-events: none;

width: 100%;
height: 100%;
Expand Down
40 changes: 37 additions & 3 deletions core/src/components/action-sheet/action-sheet.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, h } from '@stencil/core';
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, h, readTask } from '@stencil/core';

import { getIonMode } from '../../global/ionic-global';
import { ActionSheetButton, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { Gesture } from '../../utils/gesture';
import { createButtonActiveGesture } from '../../utils/gesture/button-active';
import { BACKDROP, dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
import { getClassMap } from '../../utils/theme';

Expand All @@ -25,6 +27,9 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {

presented = false;
animation?: any;
private wrapperEl?: HTMLElement;
private groupEl?: HTMLElement;
private gesture?: Gesture;

@Element() el!: HTMLIonActionSheetElement;

Expand Down Expand Up @@ -192,6 +197,35 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
}
}

componentDidUnload() {
if (this.gesture) {
this.gesture.destroy();
this.gesture = undefined;
}
}

componentDidLoad() {
/**
* Do not create gesture if:
* 1. A gesture already exists
* 2. App is running in MD mode
* 3. A wrapper ref does not exist
*/
const { groupEl, wrapperEl } = this;
if (this.gesture || getIonMode(this) === 'md' || !wrapperEl || !groupEl) { return; }

readTask(() => {
const isScrollable = groupEl.scrollHeight > groupEl.clientHeight;
if (!isScrollable) {
this.gesture = createButtonActiveGesture(
wrapperEl,
(refEl: HTMLElement) => refEl.classList.contains('action-sheet-button')
);
this.gesture.enable(true);
}
});
}

render() {
const mode = getIonMode(this);
const allButtons = this.getButtons();
Expand All @@ -216,9 +250,9 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
onIonBackdropTap={this.onBackdropTap}
>
<ion-backdrop tappable={this.backdropDismiss}/>
<div class="action-sheet-wrapper" role="dialog">
<div class="action-sheet-wrapper" role="dialog" ref={el => this.wrapperEl = el}>
<div class="action-sheet-container">
<div class="action-sheet-group">
<div class="action-sheet-group" ref={el => this.groupEl = el}>
{this.header !== undefined &&
<div class="action-sheet-title">
{this.header}
Expand Down
4 changes: 4 additions & 0 deletions core/src/components/alert/alert.ios.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
overflow: hidden;
}

.alert-button .alert-button-inner {
pointer-events: none;
}


// iOS Translucent Alert
// -----------------------------------------
Expand Down
29 changes: 28 additions & 1 deletion core/src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth
import { IonicSafeString } from '../../';
import { getIonMode } from '../../global/ionic-global';
import { AlertButton, AlertInput, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { Gesture } from '../../utils/gesture';
import { createButtonActiveGesture } from '../../utils/gesture/button-active';
import { BACKDROP, dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization';
import { getClassMap } from '../../utils/theme';
Expand All @@ -29,6 +31,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
private inputType?: string;
private processedInputs: AlertInput[] = [];
private processedButtons: AlertButton[] = [];
private wrapperEl?: HTMLElement;
private gesture?: Gesture;

presented = false;

Expand Down Expand Up @@ -171,6 +175,29 @@ export class Alert implements ComponentInterface, OverlayInterface {
this.buttonsChanged();
}

componentDidUnload() {
if (this.gesture) {
this.gesture.destroy();
this.gesture = undefined;
}
}

componentDidLoad() {
/**
* Do not create gesture if:
* 1. A gesture already exists
* 2. App is running in MD mode
* 3. A wrapper ref does not exist
*/
if (this.gesture || getIonMode(this) === 'md' || !this.wrapperEl) { return; }

this.gesture = createButtonActiveGesture(
this.wrapperEl,
(refEl: HTMLElement) => refEl.classList.contains('alert-button')
);
this.gesture.enable(true);
}

/**
* Present the alert overlay after it has been created.
*/
Expand Down Expand Up @@ -482,7 +509,7 @@ export class Alert implements ComponentInterface, OverlayInterface {

<ion-backdrop tappable={this.backdropDismiss}/>

<div class="alert-wrapper">
<div class="alert-wrapper" ref={el => this.wrapperEl = el}>

<div class="alert-head">
{header && <h2 id={hdrId} class="alert-title">{header}</h2>}
Expand Down
58 changes: 58 additions & 0 deletions core/src/utils/gesture/button-active.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { writeTask } from '@stencil/core';

import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from '../native/haptic';

import { Gesture, createGesture } from './index';

export const createButtonActiveGesture = (
el: HTMLElement,
isButton: (refEl: HTMLElement) => boolean
): Gesture => {
let touchedButton: HTMLElement | undefined;

const activateButtonAtPoint = (x: number, y: number, hapticFeedbackFn: () => void) => {
if (typeof (document as any) === 'undefined') { return; }
const target = document.elementFromPoint(x, y) as HTMLElement | null;
if (!target || !isButton(target)) {
clearActiveButton();
return;
}

if (target !== touchedButton) {
clearActiveButton();
setActiveButton(target, hapticFeedbackFn);
}
};

const setActiveButton = (button: HTMLElement, hapticFeedbackFn: () => void) => {
touchedButton = button;
const buttonToModify = touchedButton;
writeTask(() => buttonToModify.classList.add('ion-activated'));
hapticFeedbackFn();
};

const clearActiveButton = (dispatchClick = false) => {
if (!touchedButton) { return; }

const buttonToModify = touchedButton;
writeTask(() => buttonToModify.classList.remove('ion-activated'));

if (dispatchClick) {
touchedButton.click();
}

touchedButton = undefined;
};

return createGesture({
el,
gestureName: 'buttonActiveDrag',
threshold: 0,
onStart: ev => activateButtonAtPoint(ev.currentX, ev.currentY, hapticSelectionStart),
onMove: ev => activateButtonAtPoint(ev.currentX, ev.currentY, hapticSelectionChanged),
onEnd: () => {
clearActiveButton(true);
hapticSelectionEnd();
}
});
};
4 changes: 2 additions & 2 deletions core/src/utils/native/haptic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ const HapticEngine = {
return;
}
if (this.isCapacitor()) {
engine.selectionChanged();
engine.selectionEnd();
} else {
engine.gestureSelectionChanged();
engine.gestureSelectionEnd();
}
}
};
Expand Down

0 comments on commit 33be1f0

Please sign in to comment.