From 0fdde392680340caa39b5cfc5ba681b451d7a066 Mon Sep 17 00:00:00 2001 From: Matej Lednicky Date: Tue, 13 Sep 2022 22:41:16 +0200 Subject: [PATCH] feat(DIST-1865): Add option to enable auto focus New option `autoFocus` / `data-tf-auto-focus` to enable auto focus. Each embed also returns `focus()` method to dispatch the focus event manually. --- docs/configuration.md | 4 +- packages/demo-html/public/widget-focus.html | 54 +++++++++++++++++++ packages/embed/README.md | 4 +- packages/embed/src/base/embed-types.ts | 12 +++++ packages/embed/src/base/url-options.ts | 6 --- .../create-popover/create-popover.ts | 12 ++--- .../factories/create-popup/create-popup.ts | 13 ++--- .../create-sidetab/create-sidetab.ts | 12 ++--- .../factories/create-slider/create-slider.ts | 13 ++--- .../factories/create-widget/create-widget.ts | 22 ++++++-- .../factories/create-widget/widget-options.ts | 6 +++ .../build-options-from-attributes.ts | 2 +- packages/embed/src/utils/build-iframe-src.ts | 2 - .../embed/src/utils/build-iframe.src.spec.ts | 2 - .../src/utils/create-iframe/create-iframe.ts | 6 ++- 15 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 packages/demo-html/public/widget-focus.html diff --git a/docs/configuration.md b/docs/configuration.md index 5d27ca3c..25636e00 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -49,7 +49,7 @@ If you embed via HTML, you need to pass optinos as attributes with `data-tf-` pr | hideFooter | boolean | hide form progress bar and navigation buttons (does not apply to Chat UI) | `false` | | hideHeaders | boolean | hide header that appears when you have a question group, or a long question (does not apply to Chat UI) | `false` | | opacity | number | form background opacity, number from 0 (fully transparent) 100 (fully opaque) | `100` | -| disableAutoFocus | boolean | disable form auto focus when loaded | `false` | +| autoFocus | boolean | enable form auto focus when loaded | `false` | | open | string | open embed based on user action (see below) | `undefined` | | openValue | number | based on `open` (see below) | `undefined` | | enableSandbox | boolean | enable [sandbox mode](https://help.typeform.com/hc/en-us/articles/360029295952) (disables submissions and tracking) | `false` | @@ -74,7 +74,7 @@ If you embed via HTML, you need to pass optinos as attributes with `data-tf-` pr ## Options in plain HTML embed -To embed via HTML without writing JavaScript code, use `data-tf-widget=""` for widget embed. You can define options as data attributes with `data-tf-` prefix and dashes in name (eg. `disableAutoFocus` becomes `data-tf-disable-auto-focus`). For example: +To embed via HTML without writing JavaScript code, use `data-tf-widget=""` for widget embed. You can define options as data attributes with `data-tf-` prefix and dashes in name (eg. `autoFocus` becomes `data-tf-auto-focus`). For example: ```html
diff --git a/packages/demo-html/public/widget-focus.html b/packages/demo-html/public/widget-focus.html new file mode 100644 index 00000000..a9b06108 --- /dev/null +++ b/packages/demo-html/public/widget-focus.html @@ -0,0 +1,54 @@ + + + + + + Static HTML Demo + + + + + +

Auto focus and manual focus()

+

You can and then use the button in top left to focus the typeform (once it looses focus).

+ + +
+

You can (once it looses focus).

+ + + + + diff --git a/packages/embed/README.md b/packages/embed/README.md index 9c2b7320..7abd132b 100644 --- a/packages/embed/README.md +++ b/packages/embed/README.md @@ -129,7 +129,7 @@ Closing and opening a typeform in modal window will restart the progress from th | hideFooter | boolean | hide form progress bar and navigation buttons (does not apply to Chat UI) | `false` | | hideHeaders | boolean | hide header that appears when you have a question group, or a long question (does not apply to Chat UI) | `false` | | opacity | number | form background opacity, number from 0 (fully transparent) 100 (fully opaque) | `100` | -| disableAutoFocus | boolean | disable form auto focus when loaded | `false` | +| autoFocus | boolean | enable form auto focus when loaded | `false` | | open | string | open embed based on user action (see below) | `undefined` | | openValue | number | based on `open` (see below) | `undefined` | | enableSandbox | boolean | enable [sandbox mode](https://help.typeform.com/hc/en-us/articles/360029295952) (disables submissions and tracking) | `false` | @@ -155,7 +155,7 @@ Closing and opening a typeform in modal window will restart the progress from th ### Options in plain HTML embed - to embed via HTML without writing JavaScript code, use `data-tf-widget=""` for widget embed (see example above) -- define options as data attributes with `data-tf-` prefix and dashes in name (eg. `disableAutoFocus` becomes `data-tf-disable-auto-focus`) +- define options as data attributes with `data-tf-` prefix and dashes in name (eg. `autoFocus` becomes `data-tf-auto-focus`) - set a boolean property to `true` by omitting attribute value, (eg. `
` - pass function name for callbacks, eg. `data-tf-on-ready="myReadyFunction"` if this function is available on global scope (eg. `window`) - to pass `string[]` use comma-separated string, eg. `transitiveSearchParams: ['foo', 'bar']` becomes `data-tf-transitive-search-params="foo,bar"` diff --git a/packages/embed/src/base/embed-types.ts b/packages/embed/src/base/embed-types.ts index d9792005..d2026536 100644 --- a/packages/embed/src/base/embed-types.ts +++ b/packages/embed/src/base/embed-types.ts @@ -1 +1,13 @@ export type EmbedType = 'widget' | 'popup' | 'slider' | 'popover' | 'side-tab' + +export type EmbedWidget = { + unmount: () => void + refresh: () => void + focus: () => void +} + +export type EmbedPopup = EmbedWidget & { + open: () => void + close: () => void + toggle: () => void +} diff --git a/packages/embed/src/base/url-options.ts b/packages/embed/src/base/url-options.ts index dc829c4b..65acffbc 100644 --- a/packages/embed/src/base/url-options.ts +++ b/packages/embed/src/base/url-options.ts @@ -54,12 +54,6 @@ export type UrlOptions = { * @type {string | boolean} */ shareGaInstance?: string | boolean - /** - * Disables form auto focusing. - * - * @type {boolean} - */ - disableAutoFocus?: boolean /** * Enable sandbox mode for the form. * Allow testing without adding an entry to results or affecting metrics. diff --git a/packages/embed/src/factories/create-popover/create-popover.ts b/packages/embed/src/factories/create-popover/create-popover.ts index df8b0c63..29eb2e2b 100644 --- a/packages/embed/src/factories/create-popover/create-popover.ts +++ b/packages/embed/src/factories/create-popover/create-popover.ts @@ -11,17 +11,12 @@ import { makeAutoResize, } from '../../utils' import type { RemoveHandler } from '../../utils' +import { EmbedPopup } from '../../base' import { PopoverOptions } from './popover-options' import { buildNotificationDot, canBuildNotificationDot, saveNotificationDotHideUntilTime } from './notification-days' -export type Popover = { - open: () => void - close: () => void - toggle: () => void - refresh: () => void - unmount: () => void -} +export type Popover = EmbedPopup const replaceIcon = (iconToReplace: HTMLElement, newIcon: HTMLElement) => { const element = iconToReplace.parentNode @@ -120,7 +115,7 @@ const defaultOptions = { export const createPopover = (formId: string, userOptions: PopoverOptions = {}): Popover => { const options = { ...defaultOptions, ...userOptions } - const { iframe, embedId, refresh } = createIframe(formId, 'popover', options) + const { iframe, embedId, refresh, focus } = createIframe(formId, 'popover', options) let openHandler: RemoveHandler @@ -254,6 +249,7 @@ export const createPopover = (formId: string, userOptions: PopoverOptions = {}): close, toggle, refresh, + focus, unmount, } } diff --git a/packages/embed/src/factories/create-popup/create-popup.ts b/packages/embed/src/factories/create-popup/create-popup.ts index 4216a8d9..7d1481ae 100644 --- a/packages/embed/src/factories/create-popup/create-popup.ts +++ b/packages/embed/src/factories/create-popup/create-popup.ts @@ -11,16 +11,11 @@ import { import type { RemoveHandler } from '../../utils' import { POPUP_SIZE } from '../../constants' import { isInPage, isOpen, makeAutoResize } from '../../utils' +import { EmbedPopup } from '../../base' import { PopupOptions } from './popup-options' -export type Popup = { - open: () => void - close: () => void - toggle: () => void - refresh: () => void - unmount: () => void -} +export type Popup = EmbedPopup const buildPopup = () => { const popup = document.createElement('div') @@ -66,13 +61,14 @@ export const createPopup = (formId: string, userOptions: PopupOptions = {}): Pop close: () => {}, toggle: () => {}, refresh: () => {}, + focus: () => {}, unmount: () => {}, } } const { width, height, size = POPUP_SIZE, onClose, ...options } = userOptions - const { iframe, embedId, refresh } = createIframe(formId, 'popup', options) + const { iframe, embedId, refresh, focus } = createIframe(formId, 'popup', options) const scrollInitialState = document.body.style.overflow let openHandler: RemoveHandler @@ -153,6 +149,7 @@ export const createPopup = (formId: string, userOptions: PopupOptions = {}): Pop close, toggle, refresh, + focus, unmount, } } diff --git a/packages/embed/src/factories/create-sidetab/create-sidetab.ts b/packages/embed/src/factories/create-sidetab/create-sidetab.ts index 3f3f4838..84374757 100644 --- a/packages/embed/src/factories/create-sidetab/create-sidetab.ts +++ b/packages/embed/src/factories/create-sidetab/create-sidetab.ts @@ -11,16 +11,11 @@ import { makeAutoResize, } from '../../utils' import type { RemoveHandler } from '../../utils' +import { EmbedPopup } from '../../base' import { SidetabOptions } from './sidetab-options' -export type Sidetab = { - open: () => void - close: () => void - toggle: () => void - refresh: () => void - unmount: () => void -} +export type Sidetab = EmbedPopup const defaultOptions = { buttonColor: '#3a7685', @@ -103,7 +98,7 @@ const replaceElementChild = (childToReplace: HTMLElement, newChild: HTMLElement) export const createSidetab = (formId: string, userOptions: SidetabOptions = {}): Sidetab => { const options = { ...defaultOptions, ...userOptions } - const { iframe, embedId, refresh } = createIframe(formId, 'side-tab', options) + const { iframe, embedId, refresh, focus } = createIframe(formId, 'side-tab', options) const sidetab = buildSidetab(options.width, options.height) const wrapper = buildWrapper() const spinner = buildSpinner() @@ -191,6 +186,7 @@ export const createSidetab = (formId: string, userOptions: SidetabOptions = {}): close, toggle, refresh, + focus, unmount, } } diff --git a/packages/embed/src/factories/create-slider/create-slider.ts b/packages/embed/src/factories/create-slider/create-slider.ts index 85c6a67a..4b90d086 100644 --- a/packages/embed/src/factories/create-slider/create-slider.ts +++ b/packages/embed/src/factories/create-slider/create-slider.ts @@ -12,16 +12,11 @@ import { } from '../../utils' import type { RemoveHandler } from '../../utils' import { SLIDER_POSITION, SLIDER_WIDTH } from '../../constants' +import { EmbedPopup } from '../../base' import { SliderOptions } from './slider-options' -export type Slider = { - open: () => void - close: () => void - toggle: () => void - refresh: () => void - unmount: () => void -} +export type Slider = EmbedPopup const buildSlider = (position: 'right' | 'left') => { const slider = document.createElement('div') @@ -59,12 +54,13 @@ export const createSlider = (formId: string, userOptions: SliderOptions = {}): S close: () => {}, toggle: () => {}, refresh: () => {}, + focus: () => {}, unmount: () => {}, } } const { position = SLIDER_POSITION, width = SLIDER_WIDTH, onClose, ...options } = userOptions - const { iframe, embedId, refresh } = createIframe(formId, 'slider', options) + const { iframe, embedId, refresh, focus } = createIframe(formId, 'slider', options) const scrollInitialState = document.body.style.overflow let openHandler: RemoveHandler @@ -150,6 +146,7 @@ export const createSlider = (formId: string, userOptions: SliderOptions = {}): S close, toggle, refresh, + focus, unmount, } } diff --git a/packages/embed/src/factories/create-widget/create-widget.ts b/packages/embed/src/factories/create-widget/create-widget.ts index 4e0ad794..ab04606c 100644 --- a/packages/embed/src/factories/create-widget/create-widget.ts +++ b/packages/embed/src/factories/create-widget/create-widget.ts @@ -9,17 +9,16 @@ import { } from '../../utils' import { getFormHeightChangedHandler, + getFormReadyHandler, getFormThemeHandler, getWelcomeScreenHiddenHandler, } from '../../utils/create-iframe/get-form-event-handler' +import { EmbedWidget } from '../../base' import { WidgetOptions } from './widget-options' import { buildWidget } from './elements' -export type Widget = { - refresh: () => void - unmount: () => void -} +export type Widget = EmbedWidget const buildCloseButton = () => { const closeButton = document.createElement('a') @@ -32,6 +31,7 @@ export const createWidget = (formId: string, options: WidgetOptions): Widget => if (!hasDom()) { return { refresh: () => {}, + focus: () => {}, unmount: () => {}, } } @@ -43,7 +43,7 @@ export const createWidget = (formId: string, options: WidgetOptions): Widget => widgetOptions.forceTouch = true } - const { embedId, iframe, refresh } = createIframe(formId, 'widget', widgetOptions) + const { embedId, iframe, refresh, focus } = createIframe(formId, 'widget', widgetOptions) const widget = buildWidget(iframe, options.width, options.height) if (widgetOptions.autoResize) { @@ -64,6 +64,17 @@ export const createWidget = (formId: string, options: WidgetOptions): Widget => ) } + if (widgetOptions.autoFocus) { + window.addEventListener( + 'message', + getFormReadyHandler(embedId, () => { + setTimeout(() => { + focus() + }, 1000) + }) + ) + } + const appendWidget = () => options.container.append(widget) options.container.innerHTML = '' @@ -122,6 +133,7 @@ export const createWidget = (formId: string, options: WidgetOptions): Widget => return { refresh, + focus, unmount: () => unmountElement(widget), } } diff --git a/packages/embed/src/factories/create-widget/widget-options.ts b/packages/embed/src/factories/create-widget/widget-options.ts index ed6de6fc..436b6620 100644 --- a/packages/embed/src/factories/create-widget/widget-options.ts +++ b/packages/embed/src/factories/create-widget/widget-options.ts @@ -23,4 +23,10 @@ export type WidgetOptions = BaseOptions & * @type {boolean} */ lazy?: boolean + /** + * Enabled form auto focus. + * + * @type {boolean} + */ + autoFocus?: boolean } diff --git a/packages/embed/src/initializers/build-options-from-attributes.ts b/packages/embed/src/initializers/build-options-from-attributes.ts index 791e807b..4aa12184 100644 --- a/packages/embed/src/initializers/build-options-from-attributes.ts +++ b/packages/embed/src/initializers/build-options-from-attributes.ts @@ -36,7 +36,7 @@ export const buildOptionsFromAttributes = (element: HTMLElement) => { forceTouch: 'boolean', enableFullscreen: 'boolean', inlineOnMobile: 'boolean', - disableAutoFocus: 'boolean', + autoFocus: 'boolean', tracking: 'record', redirectTarget: 'string', iframeProps: 'record', diff --git a/packages/embed/src/utils/build-iframe-src.ts b/packages/embed/src/utils/build-iframe-src.ts index 60f6f48c..6897fe62 100644 --- a/packages/embed/src/utils/build-iframe-src.ts +++ b/packages/embed/src/utils/build-iframe-src.ts @@ -39,7 +39,6 @@ const mapOptionsToQueryParams = ( opacity, disableTracking, enableSandbox, - disableAutoFocus, shareGaInstance, forceTouch, enableFullscreen, @@ -58,7 +57,6 @@ const mapOptionsToQueryParams = ( 'embed-hide-headers': hideHeaders ? 'true' : undefined, 'embed-opacity': opacity, 'disable-tracking': disableTracking || enableSandbox ? 'true' : undefined, - 'disable-auto-focus': disableAutoFocus ? 'true' : undefined, '__dangerous-disable-submissions': enableSandbox ? 'true' : undefined, 'share-ga-instance': shareGaInstance ? 'true' : undefined, 'force-touch': forceTouch ? 'true' : undefined, diff --git a/packages/embed/src/utils/build-iframe.src.spec.ts b/packages/embed/src/utils/build-iframe.src.spec.ts index 6961cd4d..5cb05380 100644 --- a/packages/embed/src/utils/build-iframe.src.spec.ts +++ b/packages/embed/src/utils/build-iframe.src.spec.ts @@ -67,7 +67,6 @@ describe('build-iframe-src', () => { hideHeaders: true, opacity: 50, disableTracking: true, - disableAutoFocus: true, hidden: { foo: 'foo value', bar: '@bar&value?', @@ -91,7 +90,6 @@ describe('build-iframe-src', () => { '&embed-hide-headers=true' + '&embed-opacity=50' + '&disable-tracking=true' + - '&disable-auto-focus=true' + '&__dangerous-disable-submissions=true' + '&typeform-embed-auto-resize=true' + '&utm_foo=utm+foo+value&foobar=foobar%26value' + diff --git a/packages/embed/src/utils/create-iframe/create-iframe.ts b/packages/embed/src/utils/create-iframe/create-iframe.ts index 4e8496fa..38fa7d99 100644 --- a/packages/embed/src/utils/create-iframe/create-iframe.ts +++ b/packages/embed/src/utils/create-iframe/create-iframe.ts @@ -71,7 +71,11 @@ export const createIframe = (formId: string, type: EmbedType, options: CreateIfr const refresh = () => refreshIframe(iframe) - return { iframe, embedId, refresh } + const focus = () => { + iframe.contentWindow?.postMessage('embed-focus', '*') + } + + return { iframe, embedId, refresh, focus } } type CreateIframeOptions = UrlOptions & ActionableOptions & IframeOptions