Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(REACH-574): Allow passing any attributes to the button for popover and sidetab #598

Merged
merged 1 commit into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ If you embed via HTML, you need to pass optinos as attributes with `data-tf-` pr
| shareGaInstance | string / boolean | shares Google Analytics instance of the host page with embedded typeform, you can provide your Google Analytics ID to specify which instance to share (if you have more than one in your page) | `false` |
| inlineOnMobile | boolean | removes placeholder welcome screen in mobile and makes form show inline instead of fullscreen | `false` |
| iframeProps | object | HTML attributes to be passed directly to the iframe with typeform | `undefined` |
| buttonProps | object | HTML attributes to be passed directly to the button created by embed SDK (only for popover and sidetab) | `undefined` |
| lazy | boolean | enable lazy loading (for widget only), typeform starts loading when user scrolls to it, [see demo](https://github.com/Typeform/embed-demo/blob/main/demo-html/widget-lazy-html/index.html) | `false` |
| keepSession | boolean | preserve form state when modal window is closed (and re-opened) | `false` |
| 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` |
Expand Down
1 change: 1 addition & 0 deletions packages/demo-html/public/sidetab-html.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
data-tf-medium="demo-test"
data-tf-button-text="open sidetab"
data-tf-hidden="foo=foo value,bar=bar value"
data-tf-button-props="ariaLabel=Sidetab Button,dataTestid=my-form-button"
></div>
<script src="./lib/embed.js"></script>
</body>
Expand Down
1 change: 1 addition & 0 deletions packages/demo-nextjs/pages/popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function PopoverPage({ id }) {
onReady={handleOnReady}
medium="demo-test"
hidden={{ foo: 'foo value', bar: 'bar value' }}
buttonProps={{ ariaLabel: 'Typeform Button', dataTestid: 'demo-button' }}
/>
</main>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/embed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Closing and opening a typeform in modal window will restart the progress from th
| [shareGaInstance](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/widget-inline) | string / boolean | shares Google Analytics instance of the host page with embedded typeform, you can provide your Google Analytics ID to specify which instance to share (if you have more than one in your page) | `false` |
| [inlineOnMobile](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/widget-inline) | boolean | removes placeholder welcome screen in mobile and makes form show inline instead of fullscreen | `false` |
| [iframeProps](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/widget-js) | object | HTML attributes to be passed directly to the iframe with typeform | `undefined` |
| [buttonProps](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/sidetab-html) | object | HTML attributes to be passed directly to the button created by embed SDK (only for popover and sidetab) | `undefined` |
| [lazy](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/widget-lazy-html) | boolean | enable lazy loading (for widget only), typeform starts loading when user scrolls to it, [see demo](https://github.com/Typeform/embed-demo/blob/main/demo-html/widget-lazy-html/index.html) | `false` |
| [keepSession](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/keep-session-html) | boolean | preserve form state when modal window is closed (and re-opened) | `false` |
| 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` |
Expand Down
7 changes: 7 additions & 0 deletions packages/embed/src/base/button-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PartialElementWithAdditionalAttributes } from './partial-element-with-additional-attributes'

export type ButtonProps = PartialElementWithAdditionalAttributes<HTMLButtonElement>

export type ButtonOptions = {
buttonProps?: ButtonProps
}
11 changes: 4 additions & 7 deletions packages/embed/src/base/iframe-options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// per docs: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style
// setting style as string is not advised, but perfectly valid approach
interface StyleString {
style: string
}
type HTMLIFrameElementWithStyleString = Omit<HTMLIFrameElement, 'style'> & StyleString
import { PartialElementWithAdditionalAttributes } from './partial-element-with-additional-attributes'

export type IframeProps = PartialElementWithAdditionalAttributes<HTMLIFrameElement>

export type IframeOptions = {
iframeProps?: Partial<HTMLIFrameElementWithStyleString>
iframeProps?: IframeProps
}
1 change: 1 addition & 0 deletions packages/embed/src/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './behavioral-options'
export * from './sizeable-options'
export * from './iframe-options'
export * from './modal-window-options'
export * from './button-options'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
interface AdditionalAttributes {
// per docs: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style
// setting style as string is not advised, but perfectly valid approach
style: string

// data attributes
[key: `data${string}`]: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😮

}

export type PartialElementWithAdditionalAttributes<Element> = Partial<Omit<Element, 'style'> & AdditionalAttributes>
Original file line number Diff line number Diff line change
Expand Up @@ -176,5 +176,14 @@ describe('#createSidetab', () => {
)
})
})

describe('#buttonProps', () => {
it('should render popover with custom button attributes', async () => {
popover = createPopover('formId', { buttonProps: { ariaLabel: 'foo', title: 'pop' } })
popover.open()
jest.runAllTimers()
expect(screen.getByTitle('pop')).toHaveAttribute('aria-label', 'foo')
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
isInPage,
makeAutoResize,
invokeWithoutDefault,
addAttributesToElement,
} from '../../utils'
import type { RemoveHandler } from '../../utils'
import { EmbedPopup } from '../../base'
import { ButtonProps, EmbedPopup } from '../../base'

import { PopoverOptions } from './popover-options'
import { buildNotificationDot, canBuildNotificationDot, saveNotificationDotHideUntilTime } from './notification-days'
Expand Down Expand Up @@ -81,13 +82,14 @@ const buildIcon = (customIcon?: string, color?: string) => {
return triggerIcon
}

const buildTriggerButton = (color: string) => {
const buildTriggerButton = (color: string, buttonProps: ButtonProps = {}) => {
const textColor = getTextColor(color)
const button = document.createElement('button')
button.className = 'tf-v1-popover-button'
button.dataset.testid = 'tf-v1-popover-button'
button.style.backgroundColor = color
button.style.color = textColor
addAttributesToElement(button, buttonProps)
return button
}

Expand Down Expand Up @@ -126,7 +128,7 @@ export const createPopover = (formId: string, userOptions: PopoverOptions = {}):
const spinner = buildSpinner()
const closeIcon = buildCloseIcon()
const closeModal = buildCloseIcon('button', 'tf-v1-popover-close')
const button = buildTriggerButton(options.buttonColor || defaultOptions.buttonColor)
const button = buildTriggerButton(options.buttonColor || defaultOptions.buttonColor, options.buttonProps)

const container = options.container || document.body

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SizeableOptions,
IframeOptions,
ModalWindowOptions,
ButtonOptions,
} from '../../base'

export type PopoverOptions = BaseOptions &
Expand All @@ -14,7 +15,8 @@ export type PopoverOptions = BaseOptions &
ActionableOptions &
BehavioralOptions &
SizeableOptions &
IframeOptions & {
IframeOptions &
ButtonOptions & {
/**
* Specify the size of the popover (only applies if using mode "popover").
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,13 @@ describe('#createSidetab', () => {
expect(screen.getByTestId('tf-v1-sidetab')).toHaveStyle({ width: '400px', height: '600px' })
})
})

describe('#buttonProps', () => {
it('should render sidetab with custom button attributes', async () => {
sidetab = createSidetab('formId', { buttonProps: { ariaLabel: 'foo', title: 'button' } })
sidetab.open()
jest.runAllTimers()
expect(screen.getByTitle('button')).toHaveAttribute('aria-label', 'foo')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
isInPage,
makeAutoResize,
invokeWithoutDefault,
addAttributesToElement,
} from '../../utils'
import type { RemoveHandler } from '../../utils'
import { EmbedPopup } from '../../base'
import { ButtonProps, EmbedPopup } from '../../base'

import { SidetabOptions } from './sidetab-options'

Expand Down Expand Up @@ -47,12 +48,13 @@ const buildSpinner = () => {
return icon
}

const buildTriggerButton = (color: string) => {
const buildTriggerButton = (color: string, buttonProps: ButtonProps = {}) => {
const textColor = getTextColor(color)
const button = document.createElement('button')
button.className = 'tf-v1-sidetab-button'
button.style.backgroundColor = color
button.style.color = textColor
addAttributesToElement(button, buttonProps)
return button
}

Expand Down Expand Up @@ -103,7 +105,7 @@ export const createSidetab = (formId: string, userOptions: SidetabOptions = {}):
const sidetab = buildSidetab(options.width, options.height)
const wrapper = buildWrapper()
const spinner = buildSpinner()
const button = buildTriggerButton(options.buttonColor || defaultOptions.buttonColor)
const button = buildTriggerButton(options.buttonColor || defaultOptions.buttonColor, options.buttonProps)
const buttonText = buildTriggerButtonText(options.buttonText || defaultOptions.buttonText)
const icon = buildIcon(options.customIcon, options.buttonColor || defaultOptions.buttonColor)
const closeIcon = buildCloseIcon()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SizeableOptions,
IframeOptions,
ModalWindowOptions,
ButtonOptions,
} from '../../base'

export type SidetabOptions = BaseOptions &
Expand All @@ -14,7 +15,8 @@ export type SidetabOptions = BaseOptions &
ActionableOptions &
BehavioralOptions &
SizeableOptions &
IframeOptions & {
IframeOptions &
ButtonOptions & {
/**
* Specify the button text
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ describe('build-options-from-attributes', () => {
data-tf-disable-scroll
data-tf-full-screen
data-tf-no-heading
data-tf-iframe-props="title=foo"
data-tf-button-props="aria-label=bar"
></div>`

it('should load correct options', () => {
Expand Down Expand Up @@ -75,6 +77,12 @@ describe('build-options-from-attributes', () => {
disableScroll: true,
fullScreen: true,
noHeading: true,
iframeProps: {
title: 'foo',
},
buttonProps: {
'aria-label': 'bar',
},
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const buildOptionsFromAttributes = (element: HTMLElement) => {
tracking: 'record',
redirectTarget: 'string',
iframeProps: 'record',
buttonProps: 'record',
lazy: 'boolean',
keepSession: 'boolean',
hubspot: 'boolean',
Expand Down
35 changes: 35 additions & 0 deletions packages/embed/src/utils/add-attributes-to-element.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { addAttributesToElement } from './add-attributes-to-element'

describe('#addAttributesToElement', () => {
it('should add attributes to an element', () => {
const element = document.createElement('button')
addAttributesToElement(element, {
title: 'test',
})
expect(element.getAttribute('title')).toBe('test')
})

it('should add attributes with dashes to an element', () => {
const element = document.createElement('button')
addAttributesToElement(element, {
ariaLabel: 'foo',
dataCustomValue: 'bar',
})
expect(element.getAttribute('aria-label')).toBe('foo')
expect(element.getAttribute('data-custom-value')).toBe('bar')
})

it('should set element style from a string', () => {
const element = document.createElement('button')
addAttributesToElement(element, {
style: 'color:blue; background:none; text-decoration:underline; padding:20px; margin:10px;',
})
expect(element).toHaveStyle({
color: 'blue',
background: 'none',
textDecoration: 'underline',
padding: '20px',
margin: '10px',
})
})
})
7 changes: 7 additions & 0 deletions packages/embed/src/utils/add-attributes-to-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { camelCaseToKebabCase } from './load-options-from-attributes'

export const addAttributesToElement = (element: HTMLElement, props = {}) => {
Object.keys(props).forEach((key) => {
element.setAttribute(camelCaseToKebabCase(key), props[key])
})
}
14 changes: 13 additions & 1 deletion packages/embed/src/utils/create-iframe/create-iframe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ describe('create-iframe', () => {
onQuestionChanged: jest.fn(),
onHeightChanged: jest.fn(),
domain: 'custom.example.com',
iframeProps: { title: 'hello' },
iframeProps: {
title: 'hello',
ariaLabel: 'foo bar',
style: 'border:1px red solid;margin:10px;', // not recommended, but valid
},
}

beforeEach(() => {
Expand Down Expand Up @@ -53,6 +57,14 @@ describe('create-iframe', () => {
expect(iframe.getAttribute('title')).toBe('hello')
})

it('should set correct iframe aria-label', () => {
expect(iframe.getAttribute('aria-label')).toBe('foo bar')
})

it('should set correct iframe style', () => {
expect(iframe).toHaveStyle({ border: '1px red solid', margin: '10px' })
})

it('should set correct iframe permissions', () => {
expect(iframe.allow).toBe('microphone; camera')
})
Expand Down
7 changes: 2 additions & 5 deletions packages/embed/src/utils/create-iframe/create-iframe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EmbedType, UrlOptions, ActionableOptions, IframeOptions } from '../../base'
import { buildIframeSrc } from '../build-iframe-src'
import { setupGaInstance } from '../'
import { addAttributesToElement, setupGaInstance } from '../'

import { generateEmbedId } from './generate-embed-id'
import {
Expand Down Expand Up @@ -42,10 +42,7 @@ export const createIframe = (type: EmbedType, { formId, domain, options }: Creat
iframe.dataset.testid = 'iframe'
iframe.style.border = '0px'
iframe.allow = 'microphone; camera'

Object.keys(iframeProps).forEach((key) => {
iframe.setAttribute(key, iframeProps[key])
})
addAttributesToElement(iframe, iframeProps)

iframe.addEventListener('load', triggerIframeRedraw, { once: true })

Expand Down
1 change: 1 addition & 0 deletions packages/embed/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from './make-auto-resize'
export * from './change-color-opacity'
export * from './hubspot'
export * from './invoke-without-default'
export * from './add-attributes-to-element'