mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-10 00:27:41 +08:00
fix(ios): add haptic drag gesture for action sheet and alert components (#21060)
This commit is contained in:
@ -134,6 +134,8 @@
|
|||||||
@include margin-horizontal(null, $action-sheet-ios-button-icon-padding-right);
|
@include margin-horizontal(null, $action-sheet-ios-button-icon-padding-right);
|
||||||
|
|
||||||
font-size: $action-sheet-ios-button-icon-font-size;
|
font-size: $action-sheet-ios-button-icon-font-size;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-sheet-button:last-child {
|
.action-sheet-button:last-child {
|
||||||
|
|||||||
@ -112,6 +112,7 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@ -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 { getIonMode } from '../../global/ionic-global';
|
||||||
import { ActionSheetButton, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
|
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 { BACKDROP, dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
|
||||||
import { getClassMap } from '../../utils/theme';
|
import { getClassMap } from '../../utils/theme';
|
||||||
|
|
||||||
@ -25,6 +27,9 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
|||||||
|
|
||||||
presented = false;
|
presented = false;
|
||||||
animation?: any;
|
animation?: any;
|
||||||
|
private wrapperEl?: HTMLElement;
|
||||||
|
private groupEl?: HTMLElement;
|
||||||
|
private gesture?: Gesture;
|
||||||
|
|
||||||
@Element() el!: HTMLIonActionSheetElement;
|
@Element() el!: HTMLIonActionSheetElement;
|
||||||
|
|
||||||
@ -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() {
|
render() {
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const allButtons = this.getButtons();
|
const allButtons = this.getButtons();
|
||||||
@ -216,9 +250,9 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
|||||||
onIonBackdropTap={this.onBackdropTap}
|
onIonBackdropTap={this.onBackdropTap}
|
||||||
>
|
>
|
||||||
<ion-backdrop tappable={this.backdropDismiss}/>
|
<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-container">
|
||||||
<div class="action-sheet-group">
|
<div class="action-sheet-group" ref={el => this.groupEl = el}>
|
||||||
{this.header !== undefined &&
|
{this.header !== undefined &&
|
||||||
<div class="action-sheet-title">
|
<div class="action-sheet-title">
|
||||||
{this.header}
|
{this.header}
|
||||||
|
|||||||
@ -19,6 +19,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-button .alert-button-inner {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// iOS Translucent Alert
|
// iOS Translucent Alert
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth
|
|||||||
import { IonicSafeString } from '../../';
|
import { IonicSafeString } from '../../';
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import { AlertButton, AlertInput, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
|
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 { BACKDROP, dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
|
||||||
import { sanitizeDOMString } from '../../utils/sanitization';
|
import { sanitizeDOMString } from '../../utils/sanitization';
|
||||||
import { getClassMap } from '../../utils/theme';
|
import { getClassMap } from '../../utils/theme';
|
||||||
@ -29,6 +31,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
|||||||
private inputType?: string;
|
private inputType?: string;
|
||||||
private processedInputs: AlertInput[] = [];
|
private processedInputs: AlertInput[] = [];
|
||||||
private processedButtons: AlertButton[] = [];
|
private processedButtons: AlertButton[] = [];
|
||||||
|
private wrapperEl?: HTMLElement;
|
||||||
|
private gesture?: Gesture;
|
||||||
|
|
||||||
presented = false;
|
presented = false;
|
||||||
|
|
||||||
@ -171,6 +175,29 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
|||||||
this.buttonsChanged();
|
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.
|
* Present the alert overlay after it has been created.
|
||||||
*/
|
*/
|
||||||
@ -482,7 +509,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
|||||||
|
|
||||||
<ion-backdrop tappable={this.backdropDismiss}/>
|
<ion-backdrop tappable={this.backdropDismiss}/>
|
||||||
|
|
||||||
<div class="alert-wrapper">
|
<div class="alert-wrapper" ref={el => this.wrapperEl = el}>
|
||||||
|
|
||||||
<div class="alert-head">
|
<div class="alert-head">
|
||||||
{header && <h2 id={hdrId} class="alert-title">{header}</h2>}
|
{header && <h2 id={hdrId} class="alert-title">{header}</h2>}
|
||||||
|
|||||||
58
core/src/utils/gesture/button-active.ts
Normal file
58
core/src/utils/gesture/button-active.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -69,9 +69,9 @@ const HapticEngine = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.isCapacitor()) {
|
if (this.isCapacitor()) {
|
||||||
engine.selectionChanged();
|
engine.selectionEnd();
|
||||||
} else {
|
} else {
|
||||||
engine.gestureSelectionChanged();
|
engine.gestureSelectionEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user