Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install dependencies
run: yarn
run: yarn install --frozen-lockfile
- name: Install playwright browsers
run: npx playwright install --with-deps
- name: Run tests
run: npx playwright test
run: ENV=CI npx playwright test
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 1.7.0 - 2025-11-07

- Remove non accessible keyboard shortcuts for Accordion component
- Add new boolean setting `closeOnBlur` for Dropdown component. If `true`, the expanded list will be closed on button blur.

## 1.6.2 - 2024-12-17

- Add new event `onInit` option for Accordion component.
Expand Down
25 changes: 13 additions & 12 deletions examples/accessible-dropdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,20 @@ Dropdown.initFromPreset();

### Options

| name | type | default | description |
|------------------------|---------------------------|--------------------|-------------------------------------------------|
| name | type | default | description |
|------------------------|---------------------------|--------------------|----------------------------------------------------------------------------------------------------------------|
| `automaticSelection` | boolean \|\| string | `false` | if `true`, first item is automatically selected, if `string`, check if element exists and it will be selected. |
| `buttonSelector` | string | `button` | Button selector. |
| `labelSelector` | string | `.dropdown__label` | Label selector. |
| `listSelector` | string | `ul` | Listbox selector. |
| `mediaQuery` | null or matchMedia object | `null` | Set dropdown for a specific media query. |
| `nonSelectedItemLabel` | string | `No item selected` | Default button text if no items are selected. |
| `onChange` | null or function | `null` | Event on dropdown change. |
| `onClose` | null or function | `null` | Event on dropdown close. |
| `onListItemClick` | null or function | `null` | Event on dropdown list item click. |
| `onOpen` | null or function | `null` | Event on dropdown open. |
| `prefixId` | string | `dropdown` | Define the prefix id of the component. |
| `buttonSelector` | string | `button` | Button selector. |
| `closeOnBlur` | boolean | `false` | if `true`, the dropdown closes automatically when the button loses focus. |
| `labelSelector` | string | `.dropdown__label` | Label selector. |
| `listSelector` | string | `ul` | Listbox selector. |
| `mediaQuery` | null or matchMedia object | `null` | Set dropdown for a specific media query. |
| `nonSelectedItemLabel` | string | `No item selected` | Default button text if no items are selected. |
| `onChange` | null or function | `null` | Event on dropdown change. |
| `onClose` | null or function | `null` | Event on dropdown close. |
| `onListItemClick` | null or function | `null` | Event on dropdown list item click. |
| `onOpen` | null or function | `null` | Event on dropdown open. |
| `prefixId` | string | `dropdown` | Define the prefix id of the component. |

### Methods

Expand Down
42 changes: 29 additions & 13 deletions examples/accessible-dropdown/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
<body>
<div class="wrapper">
<h1>Accessible Dropdown</h1>

<p class="back">
<a href="../../index.html">Back to components list</a>
</p>

<h2>Demo</h2>

<h3>Non automatic selection</h3>

<div id="dropdown-1" class="dropdown">
<span class="dropdown__label">Choose an element</span>
<button aria-haspopup="listbox">Choose an element</button>
Expand Down Expand Up @@ -88,7 +88,7 @@ <h3>Dropdown only for &lt; 1024px devices</h3>
</div>

<h3>Action buttons</h3>

<div id="dropdown-6" class="dropdown">
<span class="dropdown__label">Choose an element</span>
<button aria-haspopup="listbox">Choose an element</button>
Expand All @@ -101,25 +101,39 @@ <h3>Action buttons</h3>
</ul>
</div>

<h3>Action buttons</h3>

<div id="dropdown-7" class="dropdown">
<span class="dropdown__label">Choose an element</span>
<button aria-haspopup="listbox">Choose an element</button>
<ul tabindex="-1" role="listbox">
<li role="option">Book</li>
<li role="option">Movies</li>
<li role="option">Music</li>
<li role="option">Video games</li>
<li role="option">Paint</li>
</ul>
</div>

<button id="add">Add a dummy item</button>
<button id="remove">Remove the first item</button>
<button id="remove-all">Remove all items</button>

<h2>Code</h2>

<p class="codepen" data-height="600" data-theme-id="light" data-default-tab="html,result" data-slug-hash="VwQbYqN" data-user="beapi" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/beapi/pen/VwQbYqN">
Accessible Collapsible Dropdown Listbox</a> by Be API (<a href="https://codepen.io/beapi">@beapi</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
</div>

<script type="module">
import { Dropdown } from '../../be-a11y.js'
import '../../src/css/index.css'
import './style.css'

Dropdown.preset = {
'#dropdown-1': {},
'#dropdown-2': {
Expand All @@ -145,15 +159,18 @@ <h2>Code</h2>
'#dropdown-5': {
mediaQuery: window.matchMedia('(max-width: 1024px)')
},
'#dropdown-6': {},
'#dropdown-6': {
closeOnBlur: true,
},
'#dropdown-7': {}
}

Dropdown.initFromPreset()

const addBtn = document.getElementById('add')
const removeBtn = document.getElementById('remove')
const removeAllBtn = document.getElementById('remove-all')
const dropdownInstance = Dropdown.getInstance('#dropdown-6')
const dropdownInstance = Dropdown.getInstance('#dropdown-7')

addBtn.addEventListener('click', function() {
const listItem = document.createElement('li')
Expand All @@ -162,13 +179,12 @@ <h2>Code</h2>
})

removeBtn.addEventListener('click', function() {
dropdownInstance.removeItem(document.getElementById('dropdown-6').querySelectorAll('li')[0])
dropdownInstance.removeItem(document.getElementById('dropdown-7').querySelectorAll('li')[0])
})

removeAllBtn.addEventListener('click', function() {
dropdownInstance.removeAllItems()
})

</script>
</body>
</html>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@beapi/be-a11y",
"version": "1.6.2",
"version": "1.7.0",
"type": "module",
"description": "Collection of usefull accessible components",
"repository": {
Expand Down
122 changes: 1 addition & 121 deletions src/classes/Accordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ class Accordion extends AbstractDomElement {
this._handleButtonBlur = handleButtonBlur.bind(this)
this._handleButtonFocus = handleButtonFocus.bind(this)
this._handleButtonClick = handleButtonClick.bind(this)
this._handleKeydown = handleKeydown.bind(this)
this._focusPreviousTab = focusPreviousTab.bind(this)
this._focusNextTab = focusNextTab.bind(this)
this._focusFirstTab = focusFirstTab.bind(this)
this._focusLastTab = focusLastTab.bind(this)

new ThrottledEvent(window, 'resize').add('resize', this._onResizeHandler)
this._onResizeHandler()
Expand Down Expand Up @@ -119,8 +114,6 @@ class Accordion extends AbstractDomElement {
trigger.addEventListener('focus', this._handleButtonFocus)
trigger.addEventListener('blur', this._handleButtonBlur)
})

document.addEventListener('keydown', this._handleKeydown)
}

/**
Expand Down Expand Up @@ -161,8 +154,6 @@ class Accordion extends AbstractDomElement {
})

super.destroy()

document.addEventListener('keydown', this._handleKeydown)
}

/**
Expand All @@ -181,21 +172,10 @@ class Accordion extends AbstractDomElement {

this.activePanel = panel

const firstFocusableElement = panel.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)[0]

if (this._settings.hasAnimation && window.getComputedStyle(panel).display === 'none') {
DOMAnimations.slideDown(panel, 500, () => {
if (firstFocusableElement) {
firstFocusableElement.focus()
}
})
DOMAnimations.slideDown(panel, 500)
} else {
panel.style.display = 'block'
if (firstFocusableElement) {
firstFocusableElement.focus()
}
}

return
Expand Down Expand Up @@ -303,106 +283,6 @@ function handleButtonClick(e) {
}
}

/**
* Handle keyboard keydown
*
* @param {KeyboardEvent} e Keyboard keydown event
*
* @returns {void}
*
* @author Milan Ricoul
*/
function handleKeydown(e) {
if (!this.focus) {
return
}

switch (e.code) {
case 'ArrowUp':
e.preventDefault()
this._focusPreviousTab()
break
case 'ArrowDown':
e.preventDefault()
this._focusNextTab()
break
case 'Home':
e.preventDefault()
this._focusFirstTab()
break
case 'End':
e.preventDefault()
this._focusLastTab()
break
}
}

/**
* Focus the previous trigger. If not previous trigger, focus the last trigger.
*
* @returns {void}
*
* @author Milan Ricoul
*/
function focusPreviousTab() {
const s = this._settings
const activeElement = document.activeElement
const triggers = this._element.querySelectorAll(s.triggerSelector)
const triggersCount = triggers.length

if (activeElement.classList.contains(s.triggerSelector.substring(1))) {
const currentIndexOfActiveElement = Array.prototype.indexOf.call(triggers, activeElement)

triggers[currentIndexOfActiveElement === 0 ? triggersCount - 1 : currentIndexOfActiveElement - 1].focus()
}
}

/**
* Focus the next trigger. If not next trigger, focus the first trigger.
*
* @returns {void}
*
* @author Milan Ricoul
*/
function focusNextTab() {
const s = this._settings
const activeElement = document.activeElement
const triggers = this._element.querySelectorAll(s.triggerSelector)
const triggersCount = triggers.length

if (activeElement.classList.contains(s.triggerSelector.substring(1))) {
const currentIndexOfActiveElement = Array.prototype.indexOf.call(triggers, activeElement)

triggers[currentIndexOfActiveElement === triggersCount - 1 ? 0 : currentIndexOfActiveElement + 1].focus()
}
}

/**
* Focus the first trigger.
*
* @returns {void}
*
* @author Milan Ricoul
*/
function focusFirstTab() {
this._element.querySelectorAll(this._settings.triggerSelector)[0].focus()
}

/**
* Focus the last trigger.
*
* @returns {void}
*
* @author Milan Ricoul
*/
function focusLastTab() {
const s = this._settings
const triggers = this._element.querySelectorAll(s.triggerSelector)
const triggersCount = triggers.length

triggers[triggersCount - 1].focus()
}

/**
* Events
*
Expand Down
Loading
Loading