From 52ffa741e3566f648921bc165a490785900844fb Mon Sep 17 00:00:00 2001 From: Koen Vendrik Date: Sun, 4 Nov 2018 17:55:18 -0500 Subject: [PATCH 01/16] adds support for borderless frame --- .eslintrc | 5 +- playground/Playground.tsx | 32 +++++++- src/Intercom.tsx | 72 +++++++++++++---- .../BorderlessFrameListener.ts | 78 +++++++++++++++++++ .../BorderlessFrameListener/index.ts | 1 + src/components/index.ts | 6 +- 6 files changed, 174 insertions(+), 20 deletions(-) create mode 100644 src/components/BorderlessFrameListener/BorderlessFrameListener.ts create mode 100644 src/components/BorderlessFrameListener/index.ts diff --git a/.eslintrc b/.eslintrc index 563eaf1..77d58b2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,6 +9,7 @@ "react/jsx-filename-extension": "off", "import/no-unresolved": "off", "import/extensions": "off", + "import/prefer-default-export": "off", "jsx-a11y/anchor-is-valid": "off", "react/no-unescaped-entities": "off", "react/sort-comp": "off", @@ -20,7 +21,9 @@ "devDependencies": ["**/*.test.tsx"] } ], - "no-use-before-define": "off" + "no-use-before-define": "off", + "no-restricted-syntax": "off", + "no-continue": "off" }, "env": { "browser": true, diff --git a/playground/Playground.tsx b/playground/Playground.tsx index 3572a62..37c8a02 100644 --- a/playground/Playground.tsx +++ b/playground/Playground.tsx @@ -1,5 +1,33 @@ import * as React from 'react'; - +import {bind} from 'lodash-decorators'; import Intercom from '../src'; -export default () => ; +interface State { + open: boolean; +} + +export default class Playground extends React.PureComponent<{}, State> { + state = { + open: false, + }; + + render() { + const {open} = this.state; + return ( + <> + + + + ); + } + + @bind() + private openIntercom() { + this.setState({open: true}); + } + + @bind() + private closeIntercom() { + this.setState({open: false}); + } +} diff --git a/src/Intercom.tsx b/src/Intercom.tsx index daa3227..e1c0546 100644 --- a/src/Intercom.tsx +++ b/src/Intercom.tsx @@ -6,8 +6,11 @@ import { classNames, IntercomType, } from './utilities'; -import {ImportIsolatedRemote} from './components'; - +import { + ImportIsolatedRemote, + BorderlessFrameListener, + BorderlessFrameSizes, +} from './components'; import * as styles from './Intercom.scss'; /* eslint-disable camelcase */ @@ -29,14 +32,21 @@ export interface Props { } interface FakeState { - open: boolean; - animating: boolean; + open?: boolean; + animating?: boolean; + borderlessFrameSizes?: BorderlessFrameSizes | null; +} + +interface State { + frame: HTMLIFrameElement | null; } const ANIMATION_DURATION = 300; -export default class Intercom extends React.PureComponent { - private frame: HTMLIFrameElement | null = null; +export default class Intercom extends React.PureComponent { + state: State = { + frame: null, + }; componentWillReceiveProps({open: nextOpen, user: nextUser}: Props) { const {user} = this.props; @@ -56,23 +66,46 @@ export default class Intercom extends React.PureComponent { render() { const {appId} = this.props; + const {frame} = this.state; const importUrl = `https://widget.intercom.io/widget/${appId}`; - return ( - ); + + return ( + <> + + {borderlessFrameListener} + + ); } - private updateState({open, animating}: FakeState) { - const {frame} = this; + private updateState({ + open = false, + animating = false, + borderlessFrameSizes = null, + }: FakeState) { + const {frame} = this.state; if (!frame) { return; } + if (borderlessFrameSizes) { + const {width, height} = borderlessFrameSizes; + frame.setAttribute('style', `width: ${width}; height: ${height};`); + } else { + frame.removeAttribute('style'); + } + const className = classNames( styles.Intercom, open && styles.IntercomOpen, @@ -95,6 +128,7 @@ export default class Intercom extends React.PureComponent { } = this.props; const intercom = getIntercomFromFrame(frame); + this.setState({frame}); intercom('boot', { app_id: appId, @@ -107,6 +141,7 @@ export default class Intercom extends React.PureComponent { onOpen(); } }); + intercom('onHide', () => { this.updateState({open: true, animating: true}); setTimeout( @@ -122,8 +157,6 @@ export default class Intercom extends React.PureComponent { intercom('onUnreadCountChange', onUnreadCountChange); } - this.frame = frame; - if (open) { intercom('show'); } else { @@ -136,10 +169,17 @@ export default class Intercom extends React.PureComponent { } private getIntercom() { - const {frame} = this; + const {frame} = this.state; if (!frame) { return () => {}; } return getIntercomFromFrame(frame); } + + @bind() + private handleBorderlessFrameSizesUpdate( + borderlessFrameSizes: BorderlessFrameSizes, + ) { + this.updateState({borderlessFrameSizes}); + } } diff --git a/src/components/BorderlessFrameListener/BorderlessFrameListener.ts b/src/components/BorderlessFrameListener/BorderlessFrameListener.ts new file mode 100644 index 0000000..85c3d4c --- /dev/null +++ b/src/components/BorderlessFrameListener/BorderlessFrameListener.ts @@ -0,0 +1,78 @@ +import * as React from 'react'; + +export interface BorderlessFrameSizes { + width: string; + height: string; +} + +export interface Props { + frame: HTMLIFrameElement; + onSizesUpdate(newSizes: BorderlessFrameSizes): void; +} + +const BORDERLESS_FRAME_CLASS = 'intercom-borderless-frame'; +const BORDERLESS_FRAME_GRADIENT_CLASS = 'intercom-gradient'; + +class BorderlessFrameListener extends React.Component { + private observer: MutationObserver = getObserver( + this.props.frame, + this.props.onSizesUpdate, + ); + + componentWillMount() { + const { + frame: {contentWindow}, + } = this.props; + const node = document.createElement('style'); + node.innerHTML = ` + .${BORDERLESS_FRAME_GRADIENT_CLASS} { + width: 100% !important; + height: 100% !important; + } + `; + contentWindow!.document.head!.appendChild(node); + } + + componentWillUnmount() { + this.observer.disconnect(); + } + + render() { + return null; + } +} + +function getObserver( + frame: HTMLIFrameElement, + onSizesUpdate: Props['onSizesUpdate'], +) { + const frameBody = frame.contentWindow!.document.body; + + const observer = new MutationObserver(mutations => { + for (const {target} of mutations) { + const node = target as HTMLIFrameElement; + if (!node.classList.contains(BORDERLESS_FRAME_CLASS)) { + continue; + } + + const { + style: {height}, + offsetWidth, + } = node; + + onSizesUpdate({ + width: `${offsetWidth}px`, + height: height || '0px', + }); + } + }); + + observer.observe(frameBody, { + attributes: true, + subtree: true, + }); + + return observer; +} + +export default BorderlessFrameListener; diff --git a/src/components/BorderlessFrameListener/index.ts b/src/components/BorderlessFrameListener/index.ts new file mode 100644 index 0000000..cc331b8 --- /dev/null +++ b/src/components/BorderlessFrameListener/index.ts @@ -0,0 +1 @@ +export {default, Props, BorderlessFrameSizes} from './BorderlessFrameListener'; diff --git a/src/components/index.ts b/src/components/index.ts index 3725eb3..5e43c2c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,5 +2,9 @@ export { default as ImportIsolatedRemote, Props as ImportIsolatedRemoteProps, } from './ImportIsolatedRemote'; - +export { + default as BorderlessFrameListener, + Props as BorderlessFrameListenerProps, + BorderlessFrameSizes, +} from './BorderlessFrameListener'; export {default as Portal, Props as PortalProps} from './Portal'; From 3287074aeb0700c4786adf7bdc84192050053e0e Mon Sep 17 00:00:00 2001 From: Koen Vendrik Date: Sun, 4 Nov 2018 18:03:00 -0500 Subject: [PATCH 02/16] updates playground ts config --- playground/tsconfig.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 playground/tsconfig.json diff --git a/playground/tsconfig.json b/playground/tsconfig.json new file mode 100644 index 0000000..c22ff7c --- /dev/null +++ b/playground/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.json", + "include": ["./**/*"], + "exclude": [] +} From afddd4383ab67242adc8f75b5f1eddd3bab3c341 Mon Sep 17 00:00:00 2001 From: Koen Vendrik Date: Thu, 6 Dec 2018 12:41:14 -0500 Subject: [PATCH 03/16] adds notification frame and hidden launcher support --- src/Intercom.scss | 9 +- src/Intercom.tsx | 20 +++- .../BorderlessFrameListener.ts | 106 +++++++++++------- .../ImportIsolatedRemote.tsx | 18 ++- src/utilities/index.ts | 1 + src/utilities/injectCustomStyles.ts | 9 ++ 6 files changed, 116 insertions(+), 47 deletions(-) create mode 100644 src/utilities/injectCustomStyles.ts diff --git a/src/Intercom.scss b/src/Intercom.scss index 43b76a6..749dc4d 100644 --- a/src/Intercom.scss +++ b/src/Intercom.scss @@ -3,12 +3,17 @@ bottom: 0; right: 0; z-index: 999; - width: 90px; - height: 90px; + width: 0; + height: 0; background: transparent; border: none; } +.Intercom-HasLauncher { + width: 90px; + height: 90px; +} + .IntercomAnimating { pointer-events: none; } diff --git a/src/Intercom.tsx b/src/Intercom.tsx index e1c0546..f4c9173 100644 --- a/src/Intercom.tsx +++ b/src/Intercom.tsx @@ -5,6 +5,7 @@ import { objectEqual, classNames, IntercomType, + injectCustomStyles, } from './utilities'; import { ImportIsolatedRemote, @@ -25,6 +26,7 @@ export interface Props { appId: string; user?: User; open?: boolean; + launcher?: boolean; onOpen?(): void; onClose?(): void; onUnreadCountChange?(unreadCount: number): void; @@ -44,6 +46,10 @@ interface State { const ANIMATION_DURATION = 300; export default class Intercom extends React.PureComponent { + static defaultProps: Partial = { + launcher: true, + }; + state: State = { frame: null, }; @@ -65,7 +71,7 @@ export default class Intercom extends React.PureComponent { } render() { - const {appId} = this.props; + const {appId, launcher} = this.props; const {frame} = this.state; const importUrl = `https://widget.intercom.io/widget/${appId}`; @@ -73,6 +79,7 @@ export default class Intercom extends React.PureComponent { ); @@ -93,6 +100,7 @@ export default class Intercom extends React.PureComponent { animating = false, borderlessFrameSizes = null, }: FakeState) { + const {launcher} = this.props; const {frame} = this.state; if (!frame) { @@ -108,6 +116,7 @@ export default class Intercom extends React.PureComponent { const className = classNames( styles.Intercom, + launcher && styles['Intercom-HasLauncher'], open && styles.IntercomOpen, animating && styles.IntercomAnimating, ); @@ -163,6 +172,15 @@ export default class Intercom extends React.PureComponent { this.updateState({open: false, animating: false}); } + injectCustomStyles( + frame, + ` + .intercom-launcher-frame-shadow { + box-shadow: none !important; + } + `, + ); + if (onInitialization) { onInitialization(intercom); } diff --git a/src/components/BorderlessFrameListener/BorderlessFrameListener.ts b/src/components/BorderlessFrameListener/BorderlessFrameListener.ts index 85c3d4c..1a6c05c 100644 --- a/src/components/BorderlessFrameListener/BorderlessFrameListener.ts +++ b/src/components/BorderlessFrameListener/BorderlessFrameListener.ts @@ -7,72 +7,102 @@ export interface BorderlessFrameSizes { export interface Props { frame: HTMLIFrameElement; + launcher: boolean; onSizesUpdate(newSizes: BorderlessFrameSizes): void; } -const BORDERLESS_FRAME_CLASS = 'intercom-borderless-frame'; -const BORDERLESS_FRAME_GRADIENT_CLASS = 'intercom-gradient'; +const LAUNCHER_SIZE_PIXELS = 60; +const LAUNCHER_MARGIN_PIXELS = 20; class BorderlessFrameListener extends React.Component { - private observer: MutationObserver = getObserver( + private observer: MutationObserver = this.getObserver( this.props.frame, this.props.onSizesUpdate, ); componentWillMount() { + this.injectCustomStyles(); + } + + componentWillUnmount() { + this.observer.disconnect(); + } + + render() { + return null; + } + + private injectCustomStyles() { const { frame: {contentWindow}, } = this.props; + const node = document.createElement('style'); + node.innerHTML = ` - .${BORDERLESS_FRAME_GRADIENT_CLASS} { + .intercom-gradient { width: 100% !important; height: 100% !important; } `; + contentWindow!.document.head!.appendChild(node); } - componentWillUnmount() { - this.observer.disconnect(); - } + private getObserver( + frame: HTMLIFrameElement, + onSizesUpdate: Props['onSizesUpdate'], + ) { + const {launcher} = this.props; + const frameBody = frame.contentWindow!.document.body; - render() { - return null; - } -} + const observer = new MutationObserver(mutations => { + for (const {target} of mutations) { + const node = target as HTMLIFrameElement; -function getObserver( - frame: HTMLIFrameElement, - onSizesUpdate: Props['onSizesUpdate'], -) { - const frameBody = frame.contentWindow!.document.body; - - const observer = new MutationObserver(mutations => { - for (const {target} of mutations) { - const node = target as HTMLIFrameElement; - if (!node.classList.contains(BORDERLESS_FRAME_CLASS)) { - continue; - } + if ( + !node.classList.contains('intercom-borderless-frame') && + !node.classList.contains('intercom-notifications-frame') + ) { + continue; + } - const { - style: {height}, - offsetWidth, - } = node; + const { + style: {height}, + offsetWidth, + } = node; - onSizesUpdate({ - width: `${offsetWidth}px`, - height: height || '0px', - }); - } - }); + const finalWidth = launcher + ? offsetWidth + LAUNCHER_MARGIN_PIXELS + : offsetWidth; + let finalHeight = height || '0px'; - observer.observe(frameBody, { - attributes: true, - subtree: true, - }); + if (height && launcher) { + finalHeight = addMarginToPixels( + height, + LAUNCHER_SIZE_PIXELS + LAUNCHER_MARGIN_PIXELS * 2, + ); + } + + onSizesUpdate({ + width: `${finalWidth}px`, + height: finalHeight, + }); + } + }); + + observer.observe(frameBody, { + attributes: true, + subtree: true, + }); + + return observer; + } +} - return observer; +function addMarginToPixels(pixelsString: string, margin: number) { + const pixels = Number(pixelsString.replace('px', '')); + return `${pixels + margin}px`; } export default BorderlessFrameListener; diff --git a/src/components/ImportIsolatedRemote/ImportIsolatedRemote.tsx b/src/components/ImportIsolatedRemote/ImportIsolatedRemote.tsx index d95c3af..5a7e34a 100644 --- a/src/components/ImportIsolatedRemote/ImportIsolatedRemote.tsx +++ b/src/components/ImportIsolatedRemote/ImportIsolatedRemote.tsx @@ -29,14 +29,20 @@ export default class ImportIsolatedRemote extends React.PureComponent< script.src = source; this.scriptNode = script; - frame.onload = () => { + function loadScript() { contentWindow.document.body.appendChild(script); - script.onload = () => onImported(frame); - }; + script.onload = () => onImported(frame!); + } + + // fix for FF which refreshes the content of the iframe + // when done loading and therefor doesn't support + // immediately loading the script + if ((frame.contentDocument as any).readyState === 'uninitialized') { + frame.onload = loadScript; + return; + } - // some browsers don't trigger iframe.onload (like Safari 11.1.1) - // this immediate page refresh fixes that - contentWindow.location.reload(); + loadScript(); } componentWillUnmount() { diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 09f6029..bf30b85 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -2,3 +2,4 @@ export {default as classNames} from './classNames'; export {default as getIntercomFromFrame} from './getIntercomFromFrame'; export {default as objectEqual} from './objectEqual'; export {IntercomType, IntercomWindow} from './types'; +export {default as injectCustomStyles} from './injectCustomStyles'; diff --git a/src/utilities/injectCustomStyles.ts b/src/utilities/injectCustomStyles.ts new file mode 100644 index 0000000..d45b74f --- /dev/null +++ b/src/utilities/injectCustomStyles.ts @@ -0,0 +1,9 @@ +export default function injectCustomStyles( + frame: HTMLIFrameElement, + styles: string, +) { + const {contentWindow} = frame; + const node = document.createElement('style'); + node.innerHTML = styles; + contentWindow!.document.head!.appendChild(node); +} From e18fc182ee81ddbe15dd3edcd776c81d06702a87 Mon Sep 17 00:00:00 2001 From: Koen Vendrik Date: Thu, 6 Dec 2018 12:49:34 -0500 Subject: [PATCH 04/16] adds note to readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 6787146..9e80cb1 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ yarn add intercom-react * `onClose` (optional): called when intercom closes. * `onUnreadCountChange` (optional): called when the unread count changes. * `onInitialization` (optional): called when intercom has initialized. The component passes the `intercom` method to this callback in case you require advanced usage like [emitting events](https://developers.intercom.com/docs/intercom-javascript#section-intercomtrackevent) or [pre-populating content](https://developers.intercom.com/docs/intercom-javascript#section-intercomshownewmessage). +* `launcher` (optional): whether a launcher button should be shown. Defaults to `true`. ## How is this different? @@ -47,6 +48,17 @@ What this means is that if you would get into a situation where you would have t I wrote this component to create an isolated Intercom component that cleans up after itself when unmounted for a "true" React experience. +## A few notes on reliability + +The main purpose of this component is to provide a way for you to intigrate Intercom into your project without having it live in the global scope and it therefor being unmountable. + +Getting that to work took quite a bit of [reverse engineering](https://github.com/kvendrik/intercom-react/pull/15) which means that **things can stop working in future versions of the Intercom library**. I would therefor recommend that: + +1. You only use this library if you have a solid reason for needing Intercom to be unmountable. If not I recommend you use a solution like [`react-intercom`](https://github.com/nhagen/react-intercom) which simply mounts Intercom to the global scope. +2. If you do decide to use this library make sure you have a solid bug tracking library like [Bugsnag](https://www.bugsnag.com/) or [Sentry](https://sentry.io) in place so you get alerted if Intercom does stop working. + +Having that said I appriate your intrest in the library and look forward to hearing your experience with it 🙌 . + ## 🏗 Contributing 1. Make your changes. From ea33686b884c63c1eadab9df6322ce3607cca10b Mon Sep 17 00:00:00 2001 From: Koen Vendrik Date: Thu, 6 Dec 2018 12:53:10 -0500 Subject: [PATCH 05/16] =?UTF-8?q?adds=20=E2=9A=A0=EF=B8=8F=20=20emoji?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e80cb1..c563db9 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ What this means is that if you would get into a situation where you would have t I wrote this component to create an isolated Intercom component that cleans up after itself when unmounted for a "true" React experience. -## A few notes on reliability +## ⚠️ A few notes on reliability The main purpose of this component is to provide a way for you to intigrate Intercom into your project without having it live in the global scope and it therefor being unmountable. From c8406f32ff669ab21c4c19a41890a011e925c6a9 Mon Sep 17 00:00:00 2001 From: Koen Vendrik Date: Thu, 6 Dec 2018 12:57:57 -0500 Subject: [PATCH 06/16] adds link to installation instructions --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c563db9..5a99823 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ An [Intercom](http://intercom.com/) component for React that truly encapsulates yarn add intercom-react ``` +_Before you install the library make sure to read the [notes on reliability](#️-a-few-notes-on-reliability)._ + ## Setup ```tsx @@ -52,7 +54,7 @@ I wrote this component to create an isolated Intercom component that cleans up a The main purpose of this component is to provide a way for you to intigrate Intercom into your project without having it live in the global scope and it therefor being unmountable. -Getting that to work took quite a bit of [reverse engineering](https://github.com/kvendrik/intercom-react/pull/15) which means that **things can stop working in future versions of the Intercom library**. I would therefor recommend that: +Getting that to work took quite a bit of [reverse engineering](https://github.com/kvendrik/intercom-react/pull/15) which means that **things might stop working in future versions of the Intercom library**. I would therefor recommend that: 1. You only use this library if you have a solid reason for needing Intercom to be unmountable. If not I recommend you use a solution like [`react-intercom`](https://github.com/nhagen/react-intercom) which simply mounts Intercom to the global scope. 2. If you do decide to use this library make sure you have a solid bug tracking library like [Bugsnag](https://www.bugsnag.com/) or [Sentry](https://sentry.io) in place so you get alerted if Intercom does stop working. From 49ecb88dc54d97201bf61e3a2a5b5db6dcc6b6ec Mon Sep 17 00:00:00 2001 From: Koen Vendrik Date: Thu, 6 Dec 2018 13:10:13 -0500 Subject: [PATCH 07/16] fixes typos --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5a99823..6491403 100644 --- a/README.md +++ b/README.md @@ -52,14 +52,11 @@ I wrote this component to create an isolated Intercom component that cleans up a ## ⚠️ A few notes on reliability -The main purpose of this component is to provide a way for you to intigrate Intercom into your project without having it live in the global scope and it therefor being unmountable. +The main purpose of this component is to provide a way for you to integrate Intercom into your project without having it live in the global scope and it therefor being unmountable. -Getting that to work took quite a bit of [reverse engineering](https://github.com/kvendrik/intercom-react/pull/15) which means that **things might stop working in future versions of the Intercom library**. I would therefor recommend that: +Getting that to work took quite a bit of [reverse engineering](https://github.com/kvendrik/intercom-react/pull/15) and I haven't been able to find a way to include a specific version of the library yet which means that **things might stop working in future versions of the Intercom library**. I would therefor recommend that you only use this library if you have a solid reason for needing Intercom to be unmountable. If not I recommend you use a solution like [`react-intercom`](https://github.com/nhagen/react-intercom) which simply mounts Intercom to the global scope. -1. You only use this library if you have a solid reason for needing Intercom to be unmountable. If not I recommend you use a solution like [`react-intercom`](https://github.com/nhagen/react-intercom) which simply mounts Intercom to the global scope. -2. If you do decide to use this library make sure you have a solid bug tracking library like [Bugsnag](https://www.bugsnag.com/) or [Sentry](https://sentry.io) in place so you get alerted if Intercom does stop working. - -Having that said I appriate your intrest in the library and look forward to hearing your experience with it 🙌 . +Having that said I appreciate your interest in the library and look forward to hearing your experience with it 🙌 . ## 🏗 Contributing From 0db75af6ff6721deec640d979516a1065e1bcb10 Mon Sep 17 00:00:00 2001 From: Koen Vendrik Date: Thu, 6 Dec 2018 13:37:05 -0500 Subject: [PATCH 08/16] fixes build errors --- package.json | 25 ++++++++++++------------- src/Intercom.scss | 2 +- src/Intercom.tsx | 8 +++++--- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 38d75db..912b9a1 100644 --- a/package.json +++ b/package.json @@ -6,31 +6,30 @@ "type": "git", "url": "https://github.com/kvendrik/intercom-react.git" }, - "keywords": [ - "intercom", - "react", - "live", - "chat", - "support" - ], + "keywords": ["intercom", "react", "live", "chat", "support"], "main": "build/index.js", "module": "build/index.es.js", "types": "build/types/index.d.ts", "author": "Koen Vendrik ", "license": "MIT", "scripts": { - "playground": "webpack-dev-server --hot --inline --config ./playground/webpack.config.js", + "playground": + "webpack-dev-server --hot --inline --config ./playground/webpack.config.js", "lint": "yarn lint:ts && yarn lint:tslint && yarn lint:eslint", "lint:ts": "tsc --noEmit", - "lint:eslint": "eslint src --ext .tsx --ext .ts && eslint playground --ext .tsx --ext .ts", - "lint:tslint": "tslint -c tslint.json --project tsconfig.json 'src/**/*.ts' 'src/**/*.tsx' --exclude 'src/**/*.d.ts' && tslint -c tslint.json --project tsconfig.json 'playground/**/*.ts' 'playground/**/*.tsx' --exclude 'playground/**/*.d.ts'", + "lint:eslint": + "eslint src --ext .tsx --ext .ts && eslint playground --ext .tsx --ext .ts", + "lint:tslint": + "tslint -c tslint.json --project tsconfig.json 'src/**/*.ts' 'src/**/*.tsx' --exclude 'src/**/*.d.ts' && tslint -c tslint.json --project tsconfig.json 'playground/**/*.ts' 'playground/**/*.tsx' --exclude 'playground/**/*.d.ts'", "test": "jest --silent", "test:bundle": "yarn build && bundlesize", "test:debug": "jest", "test:coverage": "yarn test --coverage", - "test:coveralls": "yarn test --coverage --coverageReporters=text-lcov | coveralls", - "test:ci": "yarn lint && yarn test && yarn test:coveralls && yarn test:bundle", - "build": "rollup -c && mv build/src build/types", + "test:coveralls": + "yarn test --coverage --coverageReporters=text-lcov | coveralls", + "test:ci": + "yarn lint && yarn test && yarn test:coveralls && yarn test:bundle", + "build": "yarn clean && rollup -c && mv build/src build/types", "clean": "rm -rf build" }, "bundlesize": [ diff --git a/src/Intercom.scss b/src/Intercom.scss index 749dc4d..c7e1eaa 100644 --- a/src/Intercom.scss +++ b/src/Intercom.scss @@ -9,7 +9,7 @@ border: none; } -.Intercom-HasLauncher { +.IntercomHasLauncher { width: 90px; height: 90px; } diff --git a/src/Intercom.tsx b/src/Intercom.tsx index f4c9173..2849ea4 100644 --- a/src/Intercom.tsx +++ b/src/Intercom.tsx @@ -39,13 +39,13 @@ interface FakeState { borderlessFrameSizes?: BorderlessFrameSizes | null; } -interface State { +export interface State { frame: HTMLIFrameElement | null; } const ANIMATION_DURATION = 300; -export default class Intercom extends React.PureComponent { +class Intercom extends React.PureComponent { static defaultProps: Partial = { launcher: true, }; @@ -116,7 +116,7 @@ export default class Intercom extends React.PureComponent { const className = classNames( styles.Intercom, - launcher && styles['Intercom-HasLauncher'], + launcher && styles.IntercomHasLauncher, open && styles.IntercomOpen, animating && styles.IntercomAnimating, ); @@ -201,3 +201,5 @@ export default class Intercom extends React.PureComponent { this.updateState({borderlessFrameSizes}); } } + +export default Intercom; From 767c2b44d4c6bbfdb8ecce93cd9db7f20278771d Mon Sep 17 00:00:00 2001 From: Koen Vendrik Date: Thu, 6 Dec 2018 14:32:50 -0500 Subject: [PATCH 09/16] fixes tests --- package.json | 26 +-- src/Intercom.tsx | 21 +- .../tests/ImportIsolatedRemote.test.tsx | 109 +++++----- src/components/Portal/tests/Portal.test.tsx | 39 +++- src/tests/Intercom.test.tsx | 204 +++++++++++------- yarn.lock | 45 +++- 6 files changed, 282 insertions(+), 162 deletions(-) diff --git a/package.json b/package.json index 912b9a1..4bc00ef 100644 --- a/package.json +++ b/package.json @@ -6,29 +6,30 @@ "type": "git", "url": "https://github.com/kvendrik/intercom-react.git" }, - "keywords": ["intercom", "react", "live", "chat", "support"], + "keywords": [ + "intercom", + "react", + "live", + "chat", + "support" + ], "main": "build/index.js", "module": "build/index.es.js", "types": "build/types/index.d.ts", "author": "Koen Vendrik ", "license": "MIT", "scripts": { - "playground": - "webpack-dev-server --hot --inline --config ./playground/webpack.config.js", + "playground": "webpack-dev-server --hot --inline --config ./playground/webpack.config.js", "lint": "yarn lint:ts && yarn lint:tslint && yarn lint:eslint", "lint:ts": "tsc --noEmit", - "lint:eslint": - "eslint src --ext .tsx --ext .ts && eslint playground --ext .tsx --ext .ts", - "lint:tslint": - "tslint -c tslint.json --project tsconfig.json 'src/**/*.ts' 'src/**/*.tsx' --exclude 'src/**/*.d.ts' && tslint -c tslint.json --project tsconfig.json 'playground/**/*.ts' 'playground/**/*.tsx' --exclude 'playground/**/*.d.ts'", - "test": "jest --silent", + "lint:eslint": "eslint src --ext .tsx --ext .ts && eslint playground --ext .tsx --ext .ts", + "lint:tslint": "tslint -c tslint.json --project tsconfig.json 'src/**/*.ts' 'src/**/*.tsx' --exclude 'src/**/*.d.ts' && tslint -c tslint.json --project tsconfig.json 'playground/**/*.ts' 'playground/**/*.tsx' --exclude 'playground/**/*.d.ts'", + "test": "jest", "test:bundle": "yarn build && bundlesize", "test:debug": "jest", "test:coverage": "yarn test --coverage", - "test:coveralls": - "yarn test --coverage --coverageReporters=text-lcov | coveralls", - "test:ci": - "yarn lint && yarn test && yarn test:coveralls && yarn test:bundle", + "test:coveralls": "yarn test --coverage --coverageReporters=text-lcov | coveralls", + "test:ci": "yarn lint && yarn test && yarn test:coveralls && yarn test:bundle", "build": "yarn clean && rollup -c && mv build/src build/types", "clean": "rm -rf build" }, @@ -39,6 +40,7 @@ } ], "devDependencies": { + "@shopify/enzyme-utilities": "^1.1.4", "@types/enzyme": "^3.1.9", "@types/jest": "^22.2.3", "@types/react": "^16.3.11", diff --git a/src/Intercom.tsx b/src/Intercom.tsx index 2849ea4..15000e3 100644 --- a/src/Intercom.tsx +++ b/src/Intercom.tsx @@ -172,14 +172,7 @@ class Intercom extends React.PureComponent { this.updateState({open: false, animating: false}); } - injectCustomStyles( - frame, - ` - .intercom-launcher-frame-shadow { - box-shadow: none !important; - } - `, - ); + this.injectCustomLauncherStyles(); if (onInitialization) { onInitialization(intercom); @@ -194,6 +187,18 @@ class Intercom extends React.PureComponent { return getIntercomFromFrame(frame); } + private injectCustomLauncherStyles() { + const {frame} = this.state; + injectCustomStyles( + frame!, + ` + .intercom-launcher-frame-shadow { + box-shadow: none !important; + } + `, + ); + } + @bind() private handleBorderlessFrameSizesUpdate( borderlessFrameSizes: BorderlessFrameSizes, diff --git a/src/components/ImportIsolatedRemote/tests/ImportIsolatedRemote.test.tsx b/src/components/ImportIsolatedRemote/tests/ImportIsolatedRemote.test.tsx index 40f7039..2b9b028 100644 --- a/src/components/ImportIsolatedRemote/tests/ImportIsolatedRemote.test.tsx +++ b/src/components/ImportIsolatedRemote/tests/ImportIsolatedRemote.test.tsx @@ -1,77 +1,78 @@ import * as React from 'react'; -import {mount} from 'enzyme'; +import {mount, shallow} from 'enzyme'; import ImportIsolatedRemote from '..'; describe('', () => { const mockProps = { title: 'some-title', - source: 'https://js.intercomcdn.com/shim.d862c967.js', + source: 'some-url', onImported: () => {}, }; - it('renders an iframe', async () => { - const node = await mount(); - expect(node.find('iframe').exists()).toBeTruthy(); + describe('title', () => { + it('gets passed into the iframe', () => { + const node = shallow(); + expect(node.find('iframe').prop('title')).toBe(mockProps.title); + }); }); - it('imports the given source', async () => { - const onImportedSpy = jest.fn(); - const node = await mount( - , - ); - - const iframeNode = node.find('iframe').getDOMNode() as HTMLIFrameElement; - const frameOnLoad = iframeNode!.onload!; - frameOnLoad.call({}); - - const script = iframeNode.contentWindow!.document.querySelector('script'); - expect(script!.src).toBe(mockProps.source); + describe('source', () => { + it('imports the given source', () => { + const onImportedSpy = jest.fn(); + const node = mount( + , + ); + const iframeNode = node.find('iframe').getDOMNode() as HTMLIFrameElement; + const script = iframeNode.contentWindow!.document.querySelector('script'); + expect(script!.src).toBe(mockProps.source); + }); + + it('doesnt load script when the iframe has loaded after the component unmounted', () => { + const node = mount(); + const iframeNode = node.find('iframe').getDOMNode() as HTMLIFrameElement; + const script = iframeNode.contentWindow!.document.querySelector('script'); + + node.unmount(); + + expect(script!.onload!).toBeNull(); + }); }); - it('triggers onImported when the script has loaded', async () => { - const onImportedSpy = jest.fn(); - const node = await mount( - , - ); - - const iframeNode = node.find('iframe').getDOMNode() as HTMLIFrameElement; - const frameOnLoad = iframeNode!.onload!; - frameOnLoad.call({}); - - const script = iframeNode.contentWindow!.document.querySelector('script'); - const callback = script!.onload!; + describe('onImported()', () => { + it('triggers onImported when the script has loaded', async () => { + const onImportedSpy = jest.fn(); + const node = mount( + , + ); - callback.call(script, {} as Event); - expect(onImportedSpy).toBeCalled(); - }); + const iframeNode = node.find('iframe').getDOMNode() as HTMLIFrameElement; + const script = iframeNode.contentWindow!.document.querySelector('script'); + const callback = script!.onload!; - it('doesnt trigger onImported when the script loads after the component has already been unmounted', async () => { - const onImportedSpy = jest.fn(); - const node = await mount( - , - ); + callback.call(script, {} as Event); + expect(onImportedSpy).toBeCalled(); + }); - const iframeNode = node.find('iframe').getDOMNode() as HTMLIFrameElement; - const frameOnLoad = iframeNode!.onload!; - frameOnLoad.call({}); + it('doesnt trigger onImported when the script loads after the component has already been unmounted', () => { + const onImportedSpy = jest.fn(); + const node = mount( + , + ); - const script = iframeNode.contentWindow!.document.querySelector('script'); + const iframeNode = node.find('iframe').getDOMNode() as HTMLIFrameElement; + const script = iframeNode.contentWindow!.document.querySelector('script'); - node.unmount(); + node.unmount(); - expect(script!.onload!).toBeNull(); - expect(onImportedSpy).not.toBeCalled(); + expect(script!.onload!).toBeNull(); + expect(onImportedSpy).not.toBeCalled(); + }); }); - it('doesnt load script when the iframe has loaded after the component unmounted', async () => { - const node = await mount(); - const iframeNode = node.find('iframe').getDOMNode() as HTMLIFrameElement; - - const script = iframeNode.contentWindow!.document.querySelector('script'); - - node.unmount(); - - expect(iframeNode!.onload!).toBeNull(); - expect(script).toBeFalsy(); + describe('