Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions examples/accessible-toggle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ Toggle.preset = {
onClick: function() {
console.log('Clicked!');
},
onBlur: function() {
console.log('Blurred!');
},
onEscPressed: function() {
console.log('Escape pressed!');
},
},
'#button-2': {
target: '#my-div',
Expand All @@ -77,6 +83,8 @@ Toggle.initFromPreset();
| `hasAnimation` | boolean | `false` | If true, the content has a slideDown / slideUp animation. |
| `isOpened` | boolean | `false` | If true, the content is revealed by default. |
| `mediaQuery` | string | `null` | Apply toggle button to a window match media. |
| `onBlur` | function | `null` | Callback function when the toggle button or its content loses focus. |
| `onClick` | function | `null` | Callback function when you click on the toggle button. |
| `onEscPressed` | function | `null` | Callback function when you press the Escape key while the content is revealed. |
| `prefixId` | string | `toggle` | Define the prefix id of the component. |
| `target` | string | `null` | Specify the content target with a selector. If null, the target is based on the `aria-controls` attribute value from the toggle button. |
29 changes: 27 additions & 2 deletions examples/accessible-toggle/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ <h2>Toggle button with jQuery like slide animation</h2>
aria-labelledby="core-tab-3" hidden>
<h2>Toggle button with an alert box</h2>

<p>A button that triggers an alert box when clicked.</p>
<p>A button that triggers an event on click, blur, and escape key press.</p>

<p id="toggle-3-event" data-event="">Event: none</p>

<button type="button" class="button" aria-controls="toggle-3">Reveal text</button>

Expand Down Expand Up @@ -161,8 +163,31 @@ <h2>Closed on blur</h2>
hasAnimation: true,
},
'button[aria-controls="toggle-3"]': {
closeOnBlur: true,
closeOnEscPress: true,
onBlur: function () {
const status = document.getElementById('toggle-3-event')

if (status) {
status.textContent = 'Event: blur'
status.setAttribute('data-event', 'blur')
}
},
onClick: function () {
window.alert('You have successfully clicked on the button.')
const status = document.getElementById('toggle-3-event')

if (status) {
status.textContent = 'Event: click'
status.setAttribute('data-event', 'click')
}
},
onEscPressed: function () {
const status = document.getElementById('toggle-3-event')

if (status) {
status.textContent = 'Event: esc'
status.setAttribute('data-event', 'esc')
}
},
},
'button[aria-controls="toggle-4"]': {
Expand Down
54 changes: 54 additions & 0 deletions src/classes/Toggle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,58 @@ test.describe('Toggle', () => {

expect(display).toBe('none')
})

test('Click a toggle with onClick, expect the callback is called.', async ({ page }) => {
await page.click('button[aria-controls="core-tab-panel-3"]')
await page.click('button[aria-controls="toggle-3"]')

await expect(page.locator('#toggle-3-event')).toHaveAttribute('data-event', 'click')
})

test('Blur a toggle with closeOnBlur and onBlur, expect the content is hidden and onBlur is called.', async ({
page,
}) => {
await page.click('button[aria-controls="core-tab-panel-3"]')
await page.click('button[aria-controls="toggle-3"]')

let display = await page.$eval('#toggle-3', (content) => window.getComputedStyle(content).display)
expect(display).toBe('block')

await page.locator('button[aria-controls="toggle-3"]').blur()

display = await page.$eval('#toggle-3', (content) => window.getComputedStyle(content).display)
expect(display).toBe('none')

await expect(page.locator('#toggle-3-event')).toHaveAttribute('data-event', 'blur')
})

test('Press Escape on an opened toggle with closeOnEscPress and onEscPressed, expect the content is hidden and onEscPressed is called.', async ({
page,
}) => {
await page.click('button[aria-controls="core-tab-panel-3"]')
await page.click('button[aria-controls="toggle-3"]')

let display = await page.$eval('#toggle-3', (content) => window.getComputedStyle(content).display)
expect(display).toBe('block')

await page.keyboard.press('Escape')

display = await page.$eval('#toggle-3', (content) => window.getComputedStyle(content).display)
expect(display).toBe('none')

await expect(page.locator('#toggle-3-event')).toHaveAttribute('data-event', 'esc')
})

test('Blur a toggle with closeOnBlur, expect the content is hidden.', async ({ page }) => {
await page.click('button[aria-controls="core-tab-panel-6"]')
await page.click('button[aria-controls="toggle-6"]')

let display = await page.$eval('#toggle-6', (content) => window.getComputedStyle(content).display)
expect(display).toBe('block')

await page.locator('button[aria-controls="toggle-6"]').blur()

display = await page.$eval('#toggle-6', (content) => window.getComputedStyle(content).display)
expect(display).toBe('none')
})
})
123 changes: 89 additions & 34 deletions src/classes/Toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import DOMAnimations from './DOMAnimations.ts'
* @property {boolean} hasAnimation - Whether to use animations when opening or closing the toggle.
* @property {boolean} isOpened - Initial state of the toggle, whether it is opened.
* @property {MediaQueryList | null} mediaQuery - Media query condition for initializing the toggle.
* @property {(e: FocusEvent) => any} onBlur - Callback function for blur events.
* @property {(e: MouseEvent) => any} onClick - Callback function for click events.
* @property {(e: KeyboardEvent) => any} onEscPressed - Callback function when the escape key is pressed.
* @property {string} prefixId - Prefix for the toggle ID.
* @property {string | null} target - Selector for the target element.
*/
Expand All @@ -28,7 +30,9 @@ interface ToggleOptions {
hasAnimation: boolean
isOpened: boolean
mediaQuery: MediaQueryList | null
onBlur: (e: FocusEvent) => any // eslint-disable-line no-unused-vars
onClick: (e: MouseEvent) => any // eslint-disable-line no-unused-vars
onEscPressed: (e: KeyboardEvent) => any // eslint-disable-line no-unused-vars
prefixId: string
target: null | string
}
Expand Down Expand Up @@ -88,6 +92,14 @@ export default class Toggle extends AbstractDomElement {
*/
private handleClick: (e: MouseEvent) => void // eslint-disable-line no-unused-vars

/**
* Event handler for escape keydown events.
*
* @private
* @type {(e: KeyboardEvent) => void}
*/
private handleEscPress: (e: KeyboardEvent) => void // eslint-disable-line no-unused-vars

/**
* Event handler for resize events.
*
Expand All @@ -96,6 +108,14 @@ export default class Toggle extends AbstractDomElement {
*/
private handleResize: () => void // eslint-disable-line no-unused-vars

/**
* Bound user callback for click events.
*
* @private
* @type {(e: MouseEvent) => any}
*/
private boundOnClick: (e: MouseEvent) => any // eslint-disable-line no-unused-vars

/**
* Default options for the Toggle component.
*
Expand All @@ -112,7 +132,9 @@ export default class Toggle extends AbstractDomElement {
hasAnimation: false,
isOpened: false,
mediaQuery: null,
onBlur: () => {},
onClick: () => {},
onEscPressed: () => {},
prefixId: 'toggle',
target: null,
}
Expand Down Expand Up @@ -152,7 +174,9 @@ export default class Toggle extends AbstractDomElement {
this.handleResize = this._handleResize.bind(this)
this.handleBlur = this._handleBlur.bind(this)
this.handleClick = this._handleClick.bind(this)
this.handleEscPress = this._handleEscPress.bind(this)
this.handleTargetFocusOut = this._handleTargetFocusOut.bind(this)
this.boundOnClick = this.options.onClick.bind(this)

new ThrottledEvent(window, 'resize').add('resize', this.handleResize)
this.handleResize()
Expand All @@ -167,11 +191,14 @@ export default class Toggle extends AbstractDomElement {
private init(): void {
const el = this.element

const { closeOnBlur, closeOnEscPress, isOpened, onClick, prefixId } = this.options
const { closeOnBlur, closeOnEscPress, isOpened, onBlur, onClick, onEscPressed, prefixId } = this.options
const hasCustomOnBlur = onBlur !== Toggle.defaults.onBlur
const hasCustomOnClick = onClick !== Toggle.defaults.onClick
const hasCustomOnEscPressed = onEscPressed !== Toggle.defaults.onEscPressed

// In case there is an on click callback, add click event listener
if (onClick) {
el.addEventListener('click', onClick.bind(this))
if (hasCustomOnClick) {
el.addEventListener('click', this.boundOnClick)
}

// In case this.target is not defined, stop the initialization
Expand All @@ -184,39 +211,21 @@ export default class Toggle extends AbstractDomElement {
el.setAttribute('aria-expanded', 'false')

// In case this.target is defined, add on click event
if (this.target) {
el.addEventListener('click', this.handleClick)
el.addEventListener('click', this.handleClick)
}
el.addEventListener('click', this.handleClick)

if (!el.hasAttribute('aria-controls')) {
const id = `${prefixId}-${randomId()}`
el.setAttribute('aria-controls', id)
this.target.id = id
}

if (closeOnBlur) {
if (closeOnBlur || hasCustomOnBlur) {
el.addEventListener('blur', this.handleBlur)
this.target.addEventListener('focusout', this.handleTargetFocusOut)
}

if (closeOnEscPress) {
window.addEventListener('keydown', function (e) {
if (e.defaultPrevented) {
return
}

const key = e.key
const id = el.getAttribute('aria-controls')

if (
['Escape', 'Esc'].includes(key) &&
id &&
document.getElementById(id)?.getAttribute('aria-hidden') !== 'true'
) {
el.click()
}
})
if (closeOnEscPress || hasCustomOnEscPressed) {
window.addEventListener('keydown', this.handleEscPress)
}

if (!this.target.hasAttribute('aria-hidden')) {
Expand All @@ -238,25 +247,23 @@ export default class Toggle extends AbstractDomElement {
const instance = AbstractDomElement.getInstance(element) as Toggle | undefined

if (instance) {
const {
element,
target,
options: { onClick },
} = instance
const { element, target, options } = instance
const hasCustomOnClick = options.onClick !== Toggle.defaults.onClick

instance.initialized = false

instance.reset()
element.removeAttribute('aria-expanded')
element.removeEventListener('click', instance.handleClick)
element.removeEventListener('blur', instance.handleBlur)
window.removeEventListener('keydown', instance.handleEscPress)

if (target) {
target.removeEventListener('focusout', instance.handleTargetFocusOut)
}

if (onClick) {
element.removeEventListener('click', onClick)
if (hasCustomOnClick) {
element.removeEventListener('click', instance.boundOnClick)
}
}

Expand Down Expand Up @@ -386,6 +393,7 @@ export default class Toggle extends AbstractDomElement {
*/
_handleBlur(e: FocusEvent) {
const relatedTarget = e.relatedTarget as HTMLElement | null
const { closeOnBlur, onBlur } = this.options

// Check if focus is moving to the button itself
if (relatedTarget === this.element) {
Expand All @@ -397,7 +405,13 @@ export default class Toggle extends AbstractDomElement {
return
}

this.close()
if (closeOnBlur) {
this.close()
}

if (onBlur !== Toggle.defaults.onBlur) {
onBlur.call(this, e)
}
}

/**
Expand All @@ -409,6 +423,7 @@ export default class Toggle extends AbstractDomElement {
*/
_handleTargetFocusOut(e: FocusEvent) {
const relatedTarget = e.relatedTarget as HTMLElement | null
const { closeOnBlur, onBlur } = this.options

// Check if focus is moving to the button
if (relatedTarget === this.element) {
Expand All @@ -420,7 +435,47 @@ export default class Toggle extends AbstractDomElement {
return
}

this.close()
if (closeOnBlur) {
this.close()
}

if (onBlur !== Toggle.defaults.onBlur) {
onBlur.call(this, e)
}
}

/**
* Handles escape keydown events.
*
* @private
* @param {KeyboardEvent} e - The keyboard event.
* @returns {void}
*/
_handleEscPress(e: KeyboardEvent) {
if (e.defaultPrevented) {
return
}

const el = this.element
const { closeOnEscPress, onEscPressed } = this.options
const key = e.key
const id = el.getAttribute('aria-controls')

if (
!['Escape', 'Esc'].includes(key) ||
!id ||
document.getElementById(id)?.getAttribute('aria-hidden') === 'true'
) {
return
}

if (closeOnEscPress) {
el.click()
}
Comment thread
cursor[bot] marked this conversation as resolved.

if (onEscPressed !== Toggle.defaults.onEscPressed) {
onEscPressed.call(this, e)
}
}

/**
Expand Down
Loading