From c4afcf5dabf97cee957c39b4270edf04924f23d7 Mon Sep 17 00:00:00 2001 From: Nicolas JUEN Date: Wed, 5 Jul 2023 15:57:54 +0200 Subject: [PATCH 1/9] fix(ci) Force using --frozen-lockfile for the yarn to let pass tests --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9952fc2..ca43547 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ 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 From db26a52814f6d198e1d54b2342b74b3853d3cf79 Mon Sep 17 00:00:00 2001 From: Nicolas JUEN Date: Wed, 5 Jul 2023 16:21:34 +0200 Subject: [PATCH 2/9] fix(ci) Add env variable CI to the script --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ca43547..607aa1c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,4 +16,4 @@ jobs: - name: Install playwright browsers run: npx playwright install --with-deps - name: Run tests - run: npx playwright test \ No newline at end of file + run: ENV=CI npx playwright test \ No newline at end of file From 8e51988ec4728af60eb2291deb0fb34531fd4616 Mon Sep 17 00:00:00 2001 From: mricoul Date: Fri, 7 Nov 2025 14:36:07 +0100 Subject: [PATCH 3/9] feat (Accordion): removes keyboard navigation from Accordion Removes the keyboard navigation functionality from the Accordion component. This feature caused conflicts with expected browser behavior and accessibility expectations. --- src/classes/Accordion.js | 122 +-------------------------------------- 1 file changed, 1 insertion(+), 121 deletions(-) diff --git a/src/classes/Accordion.js b/src/classes/Accordion.js index 2ec8dcd..cbc3143 100644 --- a/src/classes/Accordion.js +++ b/src/classes/Accordion.js @@ -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() @@ -119,8 +114,6 @@ class Accordion extends AbstractDomElement { trigger.addEventListener('focus', this._handleButtonFocus) trigger.addEventListener('blur', this._handleButtonBlur) }) - - document.addEventListener('keydown', this._handleKeydown) } /** @@ -161,8 +154,6 @@ class Accordion extends AbstractDomElement { }) super.destroy() - - document.addEventListener('keydown', this._handleKeydown) } /** @@ -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 @@ -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 * From 30d0bdab6a367b4851b785b7b079ea885f332a00 Mon Sep 17 00:00:00 2001 From: mricoul Date: Fri, 7 Nov 2025 14:36:39 +0100 Subject: [PATCH 4/9] chore (package): bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bb5ed1..e8a85c0 100644 --- a/package.json +++ b/package.json @@ -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": { From 0d48876d019a3f2b82d778af187d0553d64e7dd9 Mon Sep 17 00:00:00 2001 From: mricoul Date: Fri, 7 Nov 2025 15:04:56 +0100 Subject: [PATCH 5/9] feat (Dropdown): adds close on blur functionality to dropdown Adds a "closeOnBlur" option to the dropdown component. This allows the dropdown to close when the button loses focus, improving accessibility and user experience. Fixes #45 --- examples/accessible-dropdown/index.html | 42 +++++++++++++++++-------- src/classes/Dropdown.js | 23 ++++++++++++-- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/examples/accessible-dropdown/index.html b/examples/accessible-dropdown/index.html index 54aa8bc..d6b298f 100644 --- a/examples/accessible-dropdown/index.html +++ b/examples/accessible-dropdown/index.html @@ -10,15 +10,15 @@

Accessible Dropdown

- +

Back to components list

- +

Demo

Non automatic selection

- +

Action buttons

- + +

Action buttons

+ + + - +

Code

- +

See the Pen Accessible Collapsible Dropdown Listbox by Be API (@beapi) @@ -114,12 +128,12 @@

Code

- + - \ No newline at end of file + diff --git a/src/classes/Dropdown.js b/src/classes/Dropdown.js index c37a67f..d7813f9 100644 --- a/src/classes/Dropdown.js +++ b/src/classes/Dropdown.js @@ -24,6 +24,7 @@ class Dropdown extends AbstractDomElement { this._onResize = onResize.bind(this) this._handleKeydown = handleKeydown.bind(this) this._handleButtonClick = handleButtonClick.bind(this) + this._handleButtonBlur = handleButtonBlur.bind(this) this._handleListItemClick = handleListItemClick.bind(this) this._handleOutsideElementClick = handleOutsideElementClick.bind(this) this._focusPreviousElement = focusPreviousElement.bind(this) @@ -52,7 +53,7 @@ class Dropdown extends AbstractDomElement { this.active = true const el = this._element - const { automaticSelection, buttonSelector, labelSelector, listClassName, listSelector } = this._settings + const { automaticSelection, buttonSelector, closeOnBlur, labelSelector, listClassName, listSelector } = this._settings const buttonId = `${this.id}-button` const labelId = `${this.id}-label` @@ -111,6 +112,10 @@ class Dropdown extends AbstractDomElement { } } + if (closeOnBlur) { + this.button.addEventListener('blur', this._handleButtonBlur) + } + this.button.addEventListener('click', this._handleButtonClick) document.addEventListener('click', this._handleOutsideElementClick) document.addEventListener('keydown', this._handleKeydown) @@ -322,6 +327,19 @@ function handleButtonClick() { this.opened ? this.close() : this.open() } +/** + * Handle button blur on dropdown button + * + * @author Milan Ricoul + */ +function handleButtonBlur() { + if (!this.opened) { + return + } + + this.close() +} + /** * Handle list items click * @@ -475,7 +493,7 @@ function focusLastElement() { * @returns {void} */ function handleOutsideElementClick(e) { - if (this.opened && !this._element.contains(e.target)) { + if (this.opened && !this._element.contains(e.target) && !this._settings.closeOnBlur) { this.close(this.id) } } @@ -492,6 +510,7 @@ function onResize() { Dropdown.defaults = { automaticSelection: false, buttonSelector: 'button', + closeOnBlur: false, labelSelector: '.dropdown__label', listClassName: 'dropdown__list', listSelector: 'ul', From b605b987b8c6d01a91644fc012470516627439e7 Mon Sep 17 00:00:00 2001 From: mricoul Date: Fri, 7 Nov 2025 15:05:05 +0100 Subject: [PATCH 6/9] fix (Dropdown.test): fixes dropdown visibility and item handling Addresses issues with dropdown visibility after losing focus and corrects list item selection after item manipulation. Fixes #45 --- src/classes/Dropdown.test.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/classes/Dropdown.test.js b/src/classes/Dropdown.test.js index 6830d08..e046727 100644 --- a/src/classes/Dropdown.test.js +++ b/src/classes/Dropdown.test.js @@ -65,17 +65,26 @@ test.describe('Dropdown', () => { expect(display).toBe('none') }) + test('Focus the dropdown button, click on the body, expect the listbox is not visible.', async ({ page }) => { + await page.focus('#dropdown-6 button') + await page.keyboard.down('Enter') + await page.click('body') + const display = await page.$eval('#dropdown-6 ul', (listbox) => window.getComputedStyle(listbox).display) + + expect(display).toBe('none') + }) + test('Click on "Add item" button, expect there is a Dummy list item.', async ({ page }) => { await page.click('#add') - const lastItemText = await page.locator('#dropdown-6 li').last().textContent() + const lastItemText = await page.locator('#dropdown-7 li').last().textContent() expect(lastItemText).toBe('Dummy') }) test('Click on "Remove first item" button, expect the new first item is "Movies".', async ({ page }) => { await page.click('#remove') - const firstItemText = await page.locator('#dropdown-6 li').first().textContent() + const firstItemText = await page.locator('#dropdown-7 li').first().textContent() expect(firstItemText).toBe('Movies') }) @@ -83,7 +92,7 @@ test.describe('Dropdown', () => { await page.click('#remove-all') const isListItemsEmpty = await page - .locator('#dropdown-6 ul') + .locator('#dropdown-7 ul') .evaluate((element) => element.textContent.trim() === '') expect(isListItemsEmpty).toBe(true) }) From a6075118243997adbf1e6ebddae2eaa27f36a765 Mon Sep 17 00:00:00 2001 From: mricoul Date: Fri, 7 Nov 2025 15:08:19 +0100 Subject: [PATCH 7/9] fix (Accordion.test): updates accordion tests for focus handling Updates accordion tests to verify correct focus handling after clicking an accordion button. The focus should remain on the clicked button, not move to the panel content. Removes keyboard navigation tests, as they will be implemented in a separate commit. --- src/classes/Accordion.test.js | 61 ++--------------------------------- 1 file changed, 2 insertions(+), 59 deletions(-) diff --git a/src/classes/Accordion.test.js b/src/classes/Accordion.test.js index 4b586a7..04138f1 100644 --- a/src/classes/Accordion.test.js +++ b/src/classes/Accordion.test.js @@ -5,10 +5,10 @@ test.describe('Accordion', () => { await page.goto('http://localhost:5173/examples/accessible-accordion/index.html') }) - test('Click the first tab accordion, expect the first focusable element in the panel is focused.', async ({page}) => { + test('Click the first accordion button, expect this accordion button is still focused.', async ({page}) => { const id = await page.locator('#accordion-demo-1').getAttribute('data-id') await page.click(`#accordion-${id}-1`) - await expect(page.locator('#cufc1-1')).toBeFocused() + await expect(page.locator(`#accordion-${id}-1`)).toBeFocused() }) test('Click the second tab accordion, expect the second panel is visible.', async ({page}) => { @@ -21,61 +21,4 @@ test.describe('Accordion', () => { expect(display).toBe('block') }) - - test('Press "ArrowDown" key when the first tab accordion is focused, expect the second tab accordion is focused.', async ({page}) => { - const id = await page.locator('#accordion-demo-1').getAttribute('data-id') - await page.focus(`#accordion-${id}-1`) - await page.keyboard.press('ArrowDown') - const secondAccordionTab = await page.$(`#accordion-${id}-2`) - - expect(await page.evaluate(elem => window.document.activeElement === elem, secondAccordionTab)).toEqual(true) - }) - - test('Press "ArrowUp" key when the first tab accordion is focused, expect the last tab accordion is focused.', async ({page}) => { - const id = await page.locator('#accordion-demo-1').getAttribute('data-id') - await page.focus(`#accordion-${id}-1`) - await page.keyboard.press('ArrowUp') - const lastAccordionTab = await page.$(`#accordion-${id}-3`) - - expect(await page.evaluate(elem => window.document.activeElement === elem, lastAccordionTab)).toEqual(true) - }) - - test('Press "ArrowUp" key when the last tab accordion is focused, expect the second tab accordion is focused.', async ({page}) => { - const id = await page.locator('#accordion-demo-1').getAttribute('data-id') - await page.focus(`#accordion-${id}-3`) - await page.keyboard.press('ArrowUp') - const secondAccordionTab = await page.$(`#accordion-${id}-2`) - - expect(await page.evaluate(elem => window.document.activeElement === elem, secondAccordionTab)).toEqual(true) - }) - - test('Press "ArrowDown" key when the last tab accordion is focused, expect the first tab accordion is focused.', async ({page}) => { - const id = await page.locator('#accordion-demo-1').getAttribute('data-id') - - await page.focus(`#accordion-${id}-3`) - await page.keyboard.press('ArrowDown') - const firstAccordionTab = await page.$(`#accordion-${id}-1`) - - expect(await page.evaluate(elem => window.document.activeElement === elem, firstAccordionTab)).toEqual(true) - }) - - test('Press "Home" key when the last tab accordion is focused, expect the first tab accordion is focused.', async ({page}) => { - const id = await page.locator('#accordion-demo-1').getAttribute('data-id') - - await page.focus(`#accordion-${id}-3`) - await page.keyboard.press('Home') - const firstAccordionTab = await page.$(`#accordion-${id}-1`) - - expect(await page.evaluate(elem => window.document.activeElement === elem, firstAccordionTab)).toEqual(true) - }) - - test('Press "End" key when the first tab accordion is focused, expect the last tab accordion is focused.', async ({page}) => { - const id = await page.locator('#accordion-demo-1').getAttribute('data-id') - - await page.focus(`#accordion-${id}-1`) - await page.keyboard.press('End') - const lastAccordionTab = await page.$(`#accordion-${id}-3`) - - expect(await page.evaluate(elem => window.document.activeElement === elem, lastAccordionTab)).toEqual(true) - }) }) From 109b04ff0e7914a5faf258a50c9c9ea5a565739a Mon Sep 17 00:00:00 2001 From: mricoul Date: Fri, 7 Nov 2025 14:38:19 +0100 Subject: [PATCH 8/9] docs (CHANGELOG): add change log for 1.7.0 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 040179b..97b775e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. From 2386b61ddf0c69db9b57118d2beecbb736473e7a Mon Sep 17 00:00:00 2001 From: mricoul Date: Fri, 7 Nov 2025 15:13:59 +0100 Subject: [PATCH 9/9] docs (accordion): adds closeOnBlur option to dropdown component Adds the `closeOnBlur` option to the dropdown component, allowing it to close automatically when the button loses focus, enhancing usability. --- examples/accessible-dropdown/README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/accessible-dropdown/README.md b/examples/accessible-dropdown/README.md index 8482196..1e4b6be 100644 --- a/examples/accessible-dropdown/README.md +++ b/examples/accessible-dropdown/README.md @@ -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