Skip to content

Commit

Permalink
feat(TU-8855): Do not auto-open modal if there already is a modal open
Browse files Browse the repository at this point in the history
Option `respectOpenModals` will prevent modal from opening when the
auto-open condition is triggered if there already is a modal open:
- `all` - with any typeform
- `same` - with the same typeform
  • Loading branch information
mathio committed Feb 23, 2024
1 parent 257c260 commit 780f627
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 8 deletions.
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ If you embed via HTML, you need to pass optinos as attributes with `data-tf-` pr
| redirectTarget | string | target for [typeforms with redirect](https://www.typeform.com/help/a/redirect-on-completion-or-redirect-through-endings-360060589532/), valid values are `_self`, `_top`, `_blank` or `_parent` ([see docs on anchor target](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target)) | `_parent` |
| disableScroll | boolean | disable navigation between questions via scrolling and swiping | `false` |
| preselect | object | preselect answer to the first question ([more info in help center](https://www.typeform.com/help/a/preselect-answers-through-typeform-links-for-advanced-users-4410202791060/)) | `undefined` |
| respectOpenModals | `all` / `same` | do not open if there already is a modal with typeform open (`same` - same form, `all` - any form) | `undefined` |

## Options in plain HTML embed

Expand Down
46 changes: 46 additions & 0 deletions packages/demo-html/public/behavioral-js/respect-js.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Open and respect</title>
<link rel="stylesheet" href="../lib/css/popover.css" />
<link rel="stylesheet" href="../lib/css/slider.css" />
<link rel="stylesheet" href="../lib/css/sidetab.css" />
</head>
<body>
<h1>Respect</h1>
<form action="?" method="get">
<label><input type="radio" name="respect" value="all" /> all</label><br/>
<label><input type="radio" name="respect" value="same" /> same forms</label><br/>
<label><input type="radio" name="respect" value="none" /> none</label>
</form>
<p>Form in popover opens in 1 second.</p>
<p><strong>If respect = none</strong> the same form in slider opens in 2 second.</p>
<p><strong>If respect = none or same</strong> different form in side tab might open in 3 seconds.</p>

<script src="../lib/embed.js"></script>
<script>
document.querySelectorAll('input').forEach(radio => {
radio.onclick = () => document.querySelector('form').submit()
})
const [, respectOpenModals = 'none' ] = window.location.search.match(/\?respect=([a-z]+)/) || []
document.querySelector(`input[name=respect][value=${respectOpenModals}]`).checked = true

window.tf.createPopover('HLjqXS5W', {
open: 'time',
openValue: 1000,
respectOpenModals
})
window.tf.createSlider('HLjqXS5W', {
position: 'left',
open: 'time',
openValue: 2000,
respectOpenModals
})
window.tf.createSidetab('Cqrg7cgL', {
open: 'time',
openValue: 3000,
respectOpenModals
})
</script>
</body>
</html>
3 changes: 2 additions & 1 deletion packages/embed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ Closing and opening a typeform in modal window will restart the progress from th
| disableScroll | boolean | disable navigation between questions via scrolling and swiping | `false` |
| hubspot | boolean | enable HubSpot source tracking - for details see article [Set up source tracking for HubSpot](https://www.typeform.com/help/a/set-up-source-tracking-for-hub-spot-4413167079316/) | `false` |
| [fullScreen](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/widget-full-screen-html) | boolean | enable full screen view, set `<body>` size, resize on screen resize - also when browser navbars are displayed on mobile | `false` |
| [preselect] (https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/preselect-html) | object | preselect answer to the first question ([more info in help center](https://www.typeform.com/help/a/preselect-answers-through-typeform-links-for-advanced-users-4410202791060/)) | `undefined` |
| [preselect](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/preselect-html) | object | preselect answer to the first question ([more info in help center](https://www.typeform.com/help/a/preselect-answers-through-typeform-links-for-advanced-users-4410202791060/)) | `undefined` |
| [respectOpenModals](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/respect-js.html) | `all` / `same` | do not open if there already is a modal with typeform open (`same` - same form, `all` - any form) | `undefined` |

### Options in plain HTML embed

Expand Down
13 changes: 13 additions & 0 deletions packages/embed/src/base/behavioral-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,22 @@ export type BehavioralOptions = {
* @type {BehavioralType}
*/
open?: BehavioralType
/**
* Value to complement the "open" option:
* - load: not used
* - exit: pixels for the height of trigger area
* - scroll: % of page to be scrolled
* - time: milliseconds to wait before launching
*/
openValue?: number
/**
* When the user closes the modal, it will not automatically reopen on next page visit.
*/
preventReopenOnClose?: boolean
/**
* Do not open if there already is a modal with typeform open.
* - all: any other typeform
* - same: only if the form is the same as this one (same form ID)
*/
respectOpenModals?: 'all' | 'same'
}
39 changes: 34 additions & 5 deletions packages/embed/src/utils/create-custom-launch-options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@ describe('handleCustomOpen', () => {
const mockOpen = jest.fn()
const formIdMock = 'aFormId'

afterEach(() => {
beforeEach(() => {
jest.clearAllMocks()
})

describe('on load', () => {
beforeAll(() => {
handleCustomOpen(mockOpen, { open: 'load' }, formIdMock)
})

it('should open the popup', () => {
handleCustomOpen(mockOpen, { open: 'load' }, formIdMock)
expect(mockOpen).toHaveBeenCalledTimes(1)
})
})
Expand Down Expand Up @@ -149,4 +146,36 @@ describe('handleCustomOpen', () => {
expect(mockOpen).not.toHaveBeenCalled()
})
})

describe('when respectOpenModals is "all"', () => {
it('should open the modal when another modal is not open', () => {
handleCustomOpen(mockOpen, { open: 'load', respectOpenModals: 'all' }, formIdMock)
expect(mockOpen).toHaveBeenCalledTimes(1)
})

it('should NOT open the modal when another modal is already open', () => {
jest.spyOn(document, 'querySelector').mockReturnValue({ offsetHeight: 100 } as HTMLDivElement)
handleCustomOpen(mockOpen, { open: 'load', respectOpenModals: 'all' }, formIdMock)
expect(mockOpen).not.toHaveBeenCalled()
})
})

describe('when respectOpenModals is "same"', () => {
it('should open the modal when another modal is NOT open', () => {
handleCustomOpen(mockOpen, { open: 'load', respectOpenModals: 'same' }, formIdMock)
expect(mockOpen).toHaveBeenCalledTimes(1)
})

it('should open the modal when another modal is already open with DIFFERENT form', () => {
jest.spyOn(document, 'querySelector').mockReturnValue({ offsetHeight: 100 } as HTMLDivElement)
handleCustomOpen(mockOpen, { open: 'load', respectOpenModals: 'same' }, formIdMock)
expect(mockOpen).toHaveBeenCalledTimes(1)
})

it('should NOT open the modal when another modal is already open with THE SAME form', () => {
document.body.innerHTML += `<div class="tf-v1-popup"><iframe src="typeform.com/to/${formIdMock}" /></div>`
handleCustomOpen(mockOpen, { open: 'load', respectOpenModals: 'same' }, formIdMock)
expect(mockOpen).not.toHaveBeenCalled()
})
})
})
30 changes: 28 additions & 2 deletions packages/embed/src/utils/create-custom-launch-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,34 @@ export const handlePreventReopenOnClose = (options: BehavioralOptions, formId: s
options.preventReopenOnClose && setPreventReopenOnCloseCookieValue(formId)
}

export const handleCustomOpen = (open: () => void, options: BehavioralOptions, formId: string) => {
const { open: openType, openValue: value, preventReopenOnClose } = options
const hasOpenModalEmbedInPage = () => {
const elm = document.querySelector(
'.tf-v1-popup, .tf-v1-slider, .tf-v1-popover-wrapper, .tf-v1-sidetab-wrapper'
) as HTMLElement
return !!(elm?.offsetHeight || elm?.offsetWidth || elm?.getClientRects()?.length)
}

const hasOpenModalEmbedInPageWithFormId = (formId: string) => {
const elms = document.querySelectorAll('.tf-v1-popup, .tf-v1-slider, .tf-v1-popover-wrapper, .tf-v1-sidetab-wrapper')
return Array.from(elms).some((elm) => {
const iframeSrc = elm.querySelector('iframe')?.src
return iframeSrc?.includes(`typeform.com/to/${formId}`) || iframeSrc?.startsWith(formId)
})
}

const openWithRespect = (open: () => void, formId: string, respectOpenModals?: 'all' | 'same') => () => {
if (respectOpenModals === 'all' && hasOpenModalEmbedInPage()) {
return
}
if (respectOpenModals === 'same' && hasOpenModalEmbedInPageWithFormId(formId)) {
return
}
return open()
}

export const handleCustomOpen = (openFn: () => void, options: BehavioralOptions, formId: string) => {
const { open: openType, openValue: value, preventReopenOnClose, respectOpenModals } = options
const open = respectOpenModals ? openWithRespect(openFn, formId, respectOpenModals) : openFn

if (preventReopenOnClose && getPreventReopenOnCloseCookieValue(formId)) {
return emptyHandler
Expand Down

0 comments on commit 780f627

Please sign in to comment.