From a4150010a80dbfff6bde684c39da2ea3878adef7 Mon Sep 17 00:00:00 2001 From: Josh Thomas Date: Wed, 30 Jan 2019 12:55:38 -0600 Subject: [PATCH] fix(react): duplicate events being fired in ionic/react (#17321) --- react/src/components/IonModal.tsx | 2 +- react/src/components/IonPopover.tsx | 2 +- .../components/createControllerComponent.tsx | 2 +- .../src/components/createOverlayComponent.tsx | 2 +- react/src/components/navigation/IonTabBar.tsx | 6 +++- react/src/components/utils.ts | 30 +++++++++++++++---- react/src/index.ts | 25 ++-------------- react/src/register.ts | 14 +++++++++ react/src/{components => }/types.ts | 12 ++++++++ 9 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 react/src/register.ts rename react/src/{components => }/types.ts (56%) diff --git a/react/src/components/IonModal.tsx b/react/src/components/IonModal.tsx index b2291cbb37..50e452c0f4 100644 --- a/react/src/components/IonModal.tsx +++ b/react/src/components/IonModal.tsx @@ -1,6 +1,6 @@ import { Components } from '@ionic/core'; import { createOverlayComponent } from './createOverlayComponent'; -import { Omit } from './types'; +import { Omit } from '../types'; export type ModalOptions = Omit & { children: React.ReactNode; diff --git a/react/src/components/IonPopover.tsx b/react/src/components/IonPopover.tsx index f43f253235..65dd5d623f 100644 --- a/react/src/components/IonPopover.tsx +++ b/react/src/components/IonPopover.tsx @@ -1,6 +1,6 @@ import { Components } from '@ionic/core'; import { createOverlayComponent } from './createOverlayComponent'; -import { Omit } from './types'; +import { Omit } from '../types'; export type PopoverOptions = Omit & { children: React.ReactNode; diff --git a/react/src/components/createControllerComponent.tsx b/react/src/components/createControllerComponent.tsx index 4b12ac1072..e5bf03c37e 100644 --- a/react/src/components/createControllerComponent.tsx +++ b/react/src/components/createControllerComponent.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { attachEventProps } from './utils' import { ensureElementInBody, dashToPascalCase } from './utils'; -import { OverlayComponentElement, OverlayControllerComponentElement } from './types'; +import { OverlayComponentElement, OverlayControllerComponentElement } from '../types'; export function createControllerComponent>(tagName: string, controllerTagName: string) { const displayName = dashToPascalCase(tagName); diff --git a/react/src/components/createOverlayComponent.tsx b/react/src/components/createOverlayComponent.tsx index dafd901b84..b25e154c07 100644 --- a/react/src/components/createOverlayComponent.tsx +++ b/react/src/components/createOverlayComponent.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { attachEventProps } from './utils' import { ensureElementInBody, dashToPascalCase } from './utils'; -import { OverlayComponentElement, OverlayControllerComponentElement } from './types'; +import { OverlayComponentElement, OverlayControllerComponentElement } from '../types'; export function createOverlayComponent>(tagName: string, controllerTagName: string) { const displayName = dashToPascalCase(tagName); diff --git a/react/src/components/navigation/IonTabBar.tsx b/react/src/components/navigation/IonTabBar.tsx index 9f68e64fbe..cff103305c 100644 --- a/react/src/components/navigation/IonTabBar.tsx +++ b/react/src/components/navigation/IonTabBar.tsx @@ -64,7 +64,11 @@ class IonTabBar extends Component { onTabButtonClick = (e: CustomEvent<{ href: string, selected: boolean, tab: string }>) => { - this.props.history.push(e.detail.href); + const targetUrl = (this.state.activeTab === e.detail.tab) ? + this.state.tabs[e.detail.tab].originalHref : + this.state.tabs[e.detail.tab].currentHref; + + this.props.history.push(targetUrl); } renderChild = (activeTab: string) => (child: React.ReactElement void }>) => { diff --git a/react/src/components/utils.ts b/react/src/components/utils.ts index a9149c4a81..066428948c 100644 --- a/react/src/components/utils.ts +++ b/react/src/components/utils.ts @@ -1,17 +1,32 @@ +/** + * Checks if an event is supported in the current execution environment. + * @license Modernizr 3.0.0pre (Custom Build) | MIT + */ +function isCoveredByReact(eventNameSuffix: string) { + const eventName = 'on' + eventNameSuffix; + let isSupported = eventName in document; + + if (!isSupported) { + const element = document.createElement('div'); + element.setAttribute(eventName, 'return;'); + isSupported = typeof (element)[eventName] === 'function'; + } + + return isSupported; +} function syncEvent(node: Element, eventName: string, newEventHandler: (e: Event) => any) { - const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); const eventStore = (node as any).__events || ((node as any).__events = {}); - const oldEventHandler = eventStore[eventNameLc]; + const oldEventHandler = eventStore[eventName]; // Remove old listener so they don't double up. if (oldEventHandler) { - node.removeEventListener(eventNameLc, oldEventHandler); + node.removeEventListener(eventName, oldEventHandler); } // Bind new listener. if (newEventHandler) { - node.addEventListener(eventNameLc, eventStore[eventNameLc] = function handler(e: Event) { + node.addEventListener(eventName, eventStore[eventName] = function handler(e: Event) { newEventHandler.call(this, e); }); } @@ -35,7 +50,12 @@ export function attachEventProps(node: E, props: any) { } if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { - syncEvent(node, name.substring(2), props[name]); + const eventName = name.substring(2); + const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); + + if (!isCoveredByReact(eventNameLc)) { + syncEvent(node, eventNameLc, props[name]); + } } else { (node as any)[name] = props[name]; } diff --git a/react/src/index.ts b/react/src/index.ts index c25b833f67..0b01f9489d 100644 --- a/react/src/index.ts +++ b/react/src/index.ts @@ -1,26 +1,5 @@ -import { addIcons } from 'ionicons'; -import { ICON_PATHS } from 'ionicons/icons'; -import { IonicConfig } from '@ionic/core'; -import { defineCustomElements } from '@ionic/core/loader'; - export * from './components'; -export interface IonicGlobal { - config?: any; - ael?: (elm: any, eventName: string, cb: (ev: Event) => void, opts: any) => void; - raf?: (ts: number) => void; - rel?: (elm: any, eventName: string, cb: (ev: Event) => void, opts: any) => void; -} +export * from './types'; -export interface IonicWindow extends Window { - Ionic: IonicGlobal; -} - -export function registerIonic(config: IonicConfig = {}) { - const win: IonicWindow = window as any; - const Ionic = (win.Ionic = win.Ionic || {}); - addIcons(ICON_PATHS); - - Ionic.config = config; - defineCustomElements(window); -} +export * from './register'; diff --git a/react/src/register.ts b/react/src/register.ts new file mode 100644 index 0000000000..ac5aaec443 --- /dev/null +++ b/react/src/register.ts @@ -0,0 +1,14 @@ +import { addIcons } from 'ionicons'; +import { ICON_PATHS } from 'ionicons/icons'; +import { IonicConfig } from '@ionic/core'; +import { defineCustomElements } from '@ionic/core/loader'; +import { IonicWindow } from './types'; + +export function registerIonic(config: IonicConfig = {}) { + const win: IonicWindow = window as any; + const Ionic = (win.Ionic = win.Ionic || {}); + addIcons(ICON_PATHS); + + Ionic.config = config; + defineCustomElements(window); +} diff --git a/react/src/components/types.ts b/react/src/types.ts similarity index 56% rename from react/src/components/types.ts rename to react/src/types.ts index 57231ede9c..b5a9ee5b90 100644 --- a/react/src/components/types.ts +++ b/react/src/types.ts @@ -5,6 +5,18 @@ export interface OverlayComponentElement extends HTMLStencilElement { 'present': () => Promise; 'dismiss': (data?: any, role?: string | undefined) => Promise; } + export interface OverlayControllerComponentElement extends HTMLStencilElement { 'create': (opts: any) => Promise; } + +export interface IonicGlobal { + config?: any; + ael?: (elm: any, eventName: string, cb: (ev: Event) => void, opts: any) => void; + raf?: (ts: number) => void; + rel?: (elm: any, eventName: string, cb: (ev: Event) => void, opts: any) => void; +} + +export interface IonicWindow extends Window { + Ionic: IonicGlobal; +}