Skip to content
Open
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 packages/demo-html/public/popover-html.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
id="button"
data-tf-notification-days="3"
data-tf-enable-sandbox
data-tf-button-props="ariaLabel=Typeform Button"
></div>
<script src="./lib/embed.js"></script>
</body>
Expand Down
2 changes: 2 additions & 0 deletions packages/demo-html/public/popover-js.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
foo: 'foo value',
bar: 'bar value',
},
buttonProps: { ariaLabel: 'Typeform Button' },
})

document.getElementById('button').onclick = toggle
Expand All @@ -47,6 +48,7 @@
foo: 'foo value',
bar: 'bar value',
},
buttonProps: { ariaLabel: 'Typeform Button' },
customIcon: 'https://images.typeform.com/images/dUMgPyHJHM4B',
})
}
Expand Down
1 change: 1 addition & 0 deletions packages/embed/e2e/functional/popover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ for (const [title, path] of pages) {
const openIcon = button.getByTestId('tf-v1-popover-button-icon')

await expect(button, 'button displayed on page load').toBeVisible()
await expect(button).toHaveAttribute('aria-label', 'Typeform Button')
await expect(defaultIcon).toBeVisible()
await expect(wrapper, 'popover not displayed on page load').toHaveCount(0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ describe('#createPopover', () => {
iframe?.onload?.({ isTrusted: true } as Event)
expect(screen.getByTestId('tf-v1-popover-button-icon')).toBeInTheDocument()
})

it('should mark the trigger button as expanded', () => {
const button = screen.getByTestId('tf-v1-popover-button')
expect(button).toHaveAttribute('aria-expanded', 'false')
popover.open()
jest.runAllTimers()
expect(button).toHaveAttribute('aria-expanded', 'true')
})
})

describe('#close', () => {
Expand All @@ -71,6 +79,16 @@ describe('#createPopover', () => {
expect(screen.getByTestId('default-icon')).toBeInTheDocument()
})

it('should mark the trigger button as collapsed', async () => {
const button = screen.getByTestId('tf-v1-popover-button')
popover.open()
jest.runAllTimers()
screen.getByTestId('iframe')?.onload?.({ isTrusted: true } as Event)
popover.close()
expect(button).toHaveAttribute('aria-expanded', 'false')
await waitForElementToBeRemoved(() => screen.queryByTestId('tf-v1-popover-wrapper'))
})

it('should run onClose callback if provided', () => {
const onClose = jest.fn()
const popover = createPopover('formId', { onClose })
Expand Down Expand Up @@ -185,5 +203,31 @@ describe('#createPopover', () => {
expect(screen.getByTitle('pop')).toHaveAttribute('aria-label', 'foo')
})
})

describe('#default aria-label', () => {
it('should have a default aria-label for accessibility', () => {
popover = createPopover('formId')
const button = screen.getByTestId('tf-v1-popover-button')
expect(button).toHaveAttribute('aria-label', 'Open form')
})

it('should allow overriding the default aria-label', () => {
popover = createPopover('formId', { buttonProps: { ariaLabel: 'Custom label' } })
const button = screen.getByTestId('tf-v1-popover-button')
expect(button).toHaveAttribute('aria-label', 'Custom label')
})

it('should allow overriding the default aria-label with a hyphenated key (data-tf-button-props)', () => {
popover = createPopover('formId', { buttonProps: { 'aria-label': 'Custom label' } as never })
const button = screen.getByTestId('tf-v1-popover-button')
expect(button).toHaveAttribute('aria-label', 'Custom label')
})

it('should use the default aria-label when ariaLabel is undefined', () => {
popover = createPopover('formId', { buttonProps: { ariaLabel: undefined } })
const button = screen.getByTestId('tf-v1-popover-button')
expect(button).toHaveAttribute('aria-label', 'Open form')
})
})
})
})
10 changes: 10 additions & 0 deletions packages/embed/src/factories/create-popover/create-popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ const buildIcon = (customIcon?: string, color?: string) => {
return triggerIcon
}

const DEFAULT_TRIGGER_ARIA_LABEL = 'Open form'

const buildTriggerButton = (color: string, buttonProps: ButtonProps = {}) => {
const textColor = getTextColor(color)
const button = document.createElement('button')
Expand All @@ -86,6 +88,12 @@ const buildTriggerButton = (color: string, buttonProps: ButtonProps = {}) => {
button.style.backgroundColor = color
button.style.color = textColor
addAttributesToElement(button, buttonProps)
// Icon-only button needs an accessible name. Check the DOM so any key form the user passed counts.
if (!button.hasAttribute('aria-label')) {
button.setAttribute('aria-label', DEFAULT_TRIGGER_ARIA_LABEL)
}
// The label stays constant, open state is conveyed through aria-expanded
button.setAttribute('aria-expanded', 'false')
return button
}

Expand Down Expand Up @@ -182,6 +190,7 @@ export const createPopover = (formId: string, userOptions: PopoverOptions = {}):

const open = () => {
if (!isOpen(wrapper)) {
button.setAttribute('aria-expanded', 'true')
hideTooltip()
hideNotificationDot()
setTimeout(() => {
Expand All @@ -207,6 +216,7 @@ export const createPopover = (formId: string, userOptions: PopoverOptions = {}):

const close = () => {
if (isOpen(popover)) {
button.setAttribute('aria-expanded', 'false')
handlePreventReopenOnClose(options, formId)
userOptions.onClose?.()
setTimeout(() => {
Expand Down
18 changes: 18 additions & 0 deletions packages/embed/src/utils/add-attributes-to-element.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ describe('#addAttributesToElement', () => {
expect(element.getAttribute('data-custom-value')).toBe('bar')
})

it('should not double-hyphenate attributes that already contain hyphens', () => {
const element = document.createElement('button')
addAttributesToElement(element, {
'aria-label': 'test',
'data-custom-id': '123',
})
expect(element.getAttribute('aria-label')).toBe('test')
expect(element.getAttribute('data-custom-id')).toBe('123')
})

it('should skip null and undefined values', () => {
const element = document.createElement('button')
addAttributesToElement(element, { title: undefined, 'data-foo': null, 'aria-label': 'ok' })
expect(element.hasAttribute('title')).toBe(false)
expect(element.hasAttribute('data-foo')).toBe(false)
expect(element.getAttribute('aria-label')).toBe('ok')
})

it('should set element style from a string', () => {
const element = document.createElement('button')
addAttributesToElement(element, {
Expand Down
3 changes: 3 additions & 0 deletions packages/embed/src/utils/add-attributes-to-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { camelCaseToKebabCase } from './load-options-from-attributes'

export const addAttributesToElement = (element: HTMLElement, props = {}) => {
Object.keys(props).forEach((key) => {
if (props[key] === undefined || props[key] === null) {
return
}
element.setAttribute(camelCaseToKebabCase(key), props[key])
})
}
3 changes: 2 additions & 1 deletion packages/embed/src/utils/load-options-from-attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ export const camelCaseToKebabCase = (value: string) => {
return value
.split('')
.map((letter, i) => {
if (letter.toUpperCase() === letter) {
// Only add hyphen before uppercase letters (A-Z), not other characters like hyphens or numbers
if (letter >= 'A' && letter <= 'Z') {
return `${i !== 0 ? '-' : ''}${letter.toLowerCase()}`
}
return letter
Expand Down
Loading