Skip to content

Commit

Permalink
feat(button-bar): improve screen reader support (#176)
Browse files Browse the repository at this point in the history
* feat(button-bar): add accessibility behavior

* test(button-bar): add test for keyboard event

* test(button-bar): enter keypress should not effect to checkbox and radio

* fix(button-bar): disabled button should not be focusable

* fix(button-bar): support adding item after rendered

* refactor(button-bar): clean code to more readable

* refactor(button-bar): add comment and remove unwanted

* docs(button-bar): fix comments

* refactor(button-bar): remove mutation observer and use capture tab instead

* docs(button-bar): fix wording

* docs(button-bar): fix wording and demo

* style(button-bar): linting

Co-authored-by: Wasuwat Limsuparhat <[email protected]>
Co-authored-by: Sarin Udompanish
  • Loading branch information
Theeraphat-Sorasetsakul and wsuwt authored Feb 4, 2022
1 parent 3746e6e commit f121ea6
Show file tree
Hide file tree
Showing 6 changed files with 401 additions and 74 deletions.
95 changes: 47 additions & 48 deletions packages/elements/src/button-bar/__demo__/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,82 +45,81 @@
</demo-block>

<demo-block header="Managed" layout="normal">
<ef-button-bar managed>
<ef-button toggles>One</ef-button>
<ef-button toggles>Two</ef-button>
<ef-button toggles>Three</ef-button>
<ef-button toggles>Four</ef-button>
<ef-button toggles>Five</ef-button>
<ef-button-bar managed role="radiogroup">
<ef-button toggles role="radio">One</ef-button>
<ef-button toggles role="radio">Two</ef-button>
<ef-button toggles role="radio">Three</ef-button>
<ef-button toggles role="radio">Four</ef-button>
<ef-button toggles role="radio">Five</ef-button>
</ef-button-bar>
<ef-button-bar managed>
<ef-button toggles active>One</ef-button>
<ef-button toggles>Two</ef-button>
<ef-button toggles>Three</ef-button>
<ef-button toggles>Four</ef-button>
<ef-button toggles>Five</ef-button>
<ef-button-bar managed role="radiogroup">
<ef-button toggles active role="radio">One</ef-button>
<ef-button toggles role="radio">Two</ef-button>
<ef-button toggles role="radio">Three</ef-button>
<ef-button toggles role="radio">Four</ef-button>
<ef-button toggles role="radio">Five</ef-button>
</ef-button-bar>
<ef-button-bar managed>
<ef-button toggles active>One</ef-button>
<ef-button toggles active>Two</ef-button>
<ef-button toggles>Three</ef-button>
<ef-button toggles>Four</ef-button>
<ef-button toggles>Five</ef-button>
<ef-button-bar managed role="radiogroup">
<ef-button toggles active role="radio">One</ef-button>
<ef-button toggles active role="radio">Two</ef-button>
<ef-button toggles role="radio">Three</ef-button>
<ef-button toggles role="radio">Four</ef-button>
<ef-button toggles role="radio">Five</ef-button>
</ef-button-bar>
</demo-block>

<demo-block header="Managed + Static" layout="normal">
<ef-button-bar managed>
<ef-button toggles active>One</ef-button>
<ef-button toggles>Two</ef-button>
<ef-button toggles>Three</ef-button>
<ef-button toggles>Four</ef-button>
<ef-button toggles>Five</ef-button>
<ef-button icon="tick"></ef-button>
<ef-button-bar managed role="radiogroup">
<ef-button toggles active role="radio">One</ef-button>
<ef-button toggles role="radio">Two</ef-button>
<ef-button toggles role="radio">Three</ef-button>
<ef-button toggles role="radio">Four</ef-button>
<ef-button toggles role="radio">Five</ef-button>
<ef-button icon="tick" aria-label="tick icon"></ef-button>
</ef-button-bar>
</demo-block>

<demo-block header="Managed + Individual" layout="normal">
<ef-button-bar managed>
<ef-button-bar managed role="radiogroup">
<ef-button toggles active>One</ef-button>
<ef-button toggles>Two</ef-button>
<ef-button toggles>Three</ef-button>
<ef-button toggles>Four</ef-button>
<ef-button toggles>Five</ef-button>
<ef-button toggles role="radio">Two</ef-button>
<ef-button toggles role="radio">Three</ef-button>
<ef-button toggles role="radio">Four</ef-button>
<ef-button toggles role="radio">Five</ef-button>
</ef-button-bar>
<ef-button icon="tick"></ef-button>
<ef-button icon="tick" aria-label="tick icon"></ef-button>
</demo-block>

<demo-block header="Complex nested layout" layout="normal">
<ef-button-bar fill>
<ef-button-bar fill aria-label="Text formatting">
<ef-button disabled>Roboto (14pt)</ef-button>
<ef-button toggles active icon="bold"></ef-button>
<ef-button toggles icon="italic"></ef-button>
<ef-button toggles icon="underline"></ef-button>
<ef-button-bar managed>
<ef-button toggles active icon="text-left"></ef-button>
<ef-button toggles icon="text-center"></ef-button>
<ef-button toggles icon="text-right"></ef-button>
<ef-button toggles icon="align-justify"></ef-button>
<ef-button toggles active icon="bold" aria-label="bold"></ef-button>
<ef-button toggles icon="italic" aria-label="italic"></ef-button>
<ef-button toggles icon="underline" aria-label="underline"></ef-button>
<ef-button-bar managed role="radiogroup">
<ef-button toggles role="radio" active role="radio" icon="text-left" aria-label="left Alignment"></ef-button>
<ef-button toggles role="radio" icon="text-center" aria-label="center Alignment"></ef-button>
<ef-button toggles role="radio" icon="text-right" aria-label="right Alignment"></ef-button>
<ef-button toggles role="radio" icon="align-justify" aria-label="justify Alignment"></ef-button>
</ef-button-bar>
<ef-button icon="increase-indent"></ef-button>
<ef-button icon="decrease-indent"></ef-button>
<ef-button icon="image"></ef-button>
<ef-button icon="print"></ef-button>
<ef-button icon="increase-indent" aria-label="increase indent"></ef-button>
<ef-button icon="decrease-indent" aria-label="decrease indent"></ef-button>
<ef-button icon="image" aria-label="image"></ef-button>
<ef-button icon="print" aria-label="print"></ef-button>
</ef-button-bar>

<ef-button-bar>
<ef-button>One</ef-button>
<ef-button>Two</ef-button>
<ef-button-bar managed>
<ef-button toggles>Three</ef-button>
<ef-button toggles>Four</ef-button>
<ef-button-bar managed role="radiogroup">
<ef-button toggles role="radio">Three</ef-button>
<ef-button toggles role="radio">Four</ef-button>
<ef-button-bar>
<ef-button toggles>Five</ef-button>
<ef-button toggles>Six</ef-button>
</ef-button-bar>
</ef-button-bar>
</ef-button-bar>
</demo-block>

</body>
</html>
157 changes: 146 additions & 11 deletions packages/elements/src/button-bar/__test__/button-bar.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { fixture, expect, html, oneEvent } from '@refinitiv-ui/test-helpers';
import { fixture, expect, html, oneEvent, keyboardEvent, isIE, elementUpdated } from '@refinitiv-ui/test-helpers';

import { Button } from '@refinitiv-ui/elements/button';
import { ButtonBar } from '@refinitiv-ui/elements/button-bar';
import '@refinitiv-ui/elemental-theme/light/ef-button-bar';

const keyArrowLeft = keyboardEvent('keydown', { key: isIE() ? 'Left' : 'ArrowLeft'});
const keyArrowRight = keyboardEvent('keydown', { key: isIE() ? 'Right' : 'ArrowRight' });
const keyArrowDown = keyboardEvent('keydown', { key: isIE() ? 'Down' : 'ArrowDown' });
const keyArrowUp = keyboardEvent('keydown', { key: isIE() ? 'Up' : 'ArrowUp'});
const keyHome = keyboardEvent('keydown', { key: 'Home'});
const keyEnd = keyboardEvent('keydown', { key: 'End'});
const keyTab = keyboardEvent('keydown', { key: 'Tab'});

describe('button-bar/ButtonBar', () => {
it('should be created', async () => {
const el = await fixture(html`<ef-button-bar></ef-button-bar>`);
Expand Down Expand Up @@ -35,7 +43,7 @@ describe('button-bar/ButtonBar', () => {
const el = await fixture(html`
<ef-button-bar></ef-button-bar>
`);
const nodes = el.defaultSlot.assignedNodes();
const nodes = el.defaultSlot.value.assignedNodes();
const buttons = nodes.filter(node => node instanceof Element);
expect(buttons.length).to.equal(0);
});
Expand All @@ -50,7 +58,7 @@ describe('button-bar/ButtonBar', () => {
<ef-button>Five</ef-button>
</ef-button-bar>
`);
const nodes = el.defaultSlot.assignedNodes();
const nodes = el.defaultSlot.value.assignedNodes();
const buttons = nodes.filter(node => node instanceof Button);
expect(buttons.length).to.equal(5);
});
Expand All @@ -61,7 +69,7 @@ describe('button-bar/ButtonBar', () => {
const el = await fixture(html`
<ef-button-bar></ef-button-bar>
`);
const nodes = el.defaultSlot.assignedNodes();
const nodes = el.defaultSlot.value.assignedNodes();
const buttons = nodes.filter(node => node instanceof Element);
expect(buttons.length).to.equal(0);
});
Expand All @@ -76,7 +84,7 @@ describe('button-bar/ButtonBar', () => {
<ef-button>Five</ef-button>
</ef-button-bar>
`);
const nodes = el.defaultSlot.assignedNodes();
const nodes = el.defaultSlot.value.assignedNodes();
const buttons = nodes.filter(node => node instanceof Button);
expect(buttons.length).to.equal(5);
});
Expand All @@ -91,7 +99,7 @@ describe('button-bar/ButtonBar', () => {
<ef-button toggles>Five</ef-button>
</ef-button-bar>
`);
const nodes = el.defaultSlot.assignedNodes();
const nodes = el.defaultSlot.value.assignedNodes();
const buttons = nodes.filter(node => node instanceof Button);
const [firstButton] = buttons;
expect(firstButton).to.be.exist;
Expand Down Expand Up @@ -141,7 +149,7 @@ describe('button-bar/ButtonBar', () => {
<ef-button toggles>Five</ef-button>
</ef-button-bar>
`);
const buttons = el.defaultSlot.assignedNodes()
const buttons = el.defaultSlot.value.assignedNodes()
.filter(node => node instanceof Button);
const inactiveButton = buttons.find(button => !button.active);
setTimeout(() =>
Expand All @@ -166,13 +174,13 @@ describe('button-bar/ButtonBar', () => {
</ef-button-bar>
</ef-button-bar>
`);
const secondSplitButton = el.defaultSlot.assignedNodes()
const secondSplitButton = el.defaultSlot.value.assignedNodes()
.filter(node => node instanceof ButtonBar)
.find(node => node);
const thirdSplitButton = secondSplitButton.defaultSlot.assignedNodes()
const thirdSplitButton = secondSplitButton.defaultSlot.value.assignedNodes()
.filter(node => node instanceof ButtonBar)
.find(node => node);
const button = thirdSplitButton.defaultSlot.assignedNodes()
const button = thirdSplitButton.defaultSlot.value.assignedNodes()
.filter(node => node instanceof Button)
.find(node => node);
setTimeout(() =>
Expand All @@ -188,7 +196,7 @@ describe('button-bar/ButtonBar', () => {
<ef-button>Without toggles</ef-button>
</ef-button-bar>
`);
const button = el.defaultSlot.assignedNodes()
const button = el.defaultSlot.value.assignedNodes()
.filter(node => node instanceof Button)
.find((_, index) => index === 1);
setTimeout(() =>
Expand All @@ -198,5 +206,132 @@ describe('button-bar/ButtonBar', () => {
expect(event.target.toggles).to.equal(false);
});
});

describe('Group Tabindex', () => {
let el;
let btn1;
let btn2;
let btn3;
let btn4;
let bar;
beforeEach(async () => {
el = await fixture(`<ef-button-bar>
<ef-button-bar managed role="radiogroup" id="bar">
<ef-button id="btn1" toggles role="radio" active>1</ef-button>
<ef-button id="btn2" toggles role="radio">2</ef-button>
</ef-button-bar>
<ef-button id="btn3" toggles active>3</ef-button>
<ef-button id="btn4" disabled>4</ef-button>
</ef-button-bar>`);
btn1 = el.querySelector('#btn1');
btn2 = el.querySelector('#btn2');
btn3 = el.querySelector('#btn3');
btn4 = el.querySelector('#btn4');
bar = el.querySelector('#bar');
btn1.focus();
})
it('Should initial tabIndex=0 at first child', async () => {
const group = el.getFocusableButtons();
group.forEach((button, index) => {
expect(button.getAttribute('tabIndex')).to.equal(index === 0 ? '0' : '-1');
});
});
it('Should set tabIndex=0 to previous button when navigate left', async () => {
setTimeout(() => el.dispatchEvent(keyArrowLeft)); // will navigate to last focusable button
const event1 = await oneEvent(el, 'keydown');
expect(event1.key).to.equal('ArrowLeft');
expect(document.activeElement).to.equal(btn3);
el.getFocusableButtons().forEach((button, index) => {
expect(button.getAttribute('tabIndex')).to.equal(index === 2 ? '0' : '-1');
});
setTimeout(() => el.dispatchEvent(keyArrowLeft));
const event2 = await oneEvent(el, 'keydown');
expect(event2.key).to.equal('ArrowLeft');
expect(document.activeElement).to.equal(btn2);
el.getFocusableButtons().forEach((button, index) => {
expect(button.getAttribute('tabIndex')).to.equal(index === 1 ? '0' : '-1');
});
});
it('Should set tabIndex=0 to next button when navigate right', async () => {
setTimeout(() => el.dispatchEvent(keyArrowRight));
const event1 = await oneEvent(el, 'keydown');
expect(event1.key).to.equal('ArrowRight');
expect(document.activeElement).to.equal(btn2);
el.getFocusableButtons().forEach((button, index) => {
expect(button.getAttribute('tabIndex')).to.equal(index === 1 ? '0' : '-1');
});
setTimeout(() => el.dispatchEvent(keyArrowRight));
const event2 = await oneEvent(el, 'keydown');
expect(event2.key).to.equal('ArrowRight');
expect(document.activeElement).to.equal(btn3);
el.getFocusableButtons().forEach((button, index) => {
expect(button.getAttribute('tabIndex')).to.equal(index === 2 ? '0' : '-1');
});
});
it('Should set tabIndex=0 to next button and loop inside managed button-bar when navigate down', async () => {
setTimeout(() => bar.dispatchEvent(keyArrowDown));
const event1 = await oneEvent(bar, 'keydown');
expect(event1.key).to.equal('ArrowDown');
expect(document.activeElement).to.equal(btn2);
expect(btn1.getAttribute('tabIndex')).to.equal('-1');
expect(btn2.getAttribute('tabIndex')).to.equal('0');
setTimeout(() => bar.dispatchEvent(keyArrowDown));
const event2 = await oneEvent(bar, 'keydown');
expect(event2.key).to.equal('ArrowDown');
expect(document.activeElement).to.equal(btn1);
expect(btn2.getAttribute('tabIndex')).to.equal('-1');
expect(btn1.getAttribute('tabIndex')).to.equal('0');
});
it('Should set tabIndex=0 to previous button and loop inside managed button-bar when navigate up', async () => {
setTimeout(() => bar.dispatchEvent(keyArrowUp));
const event1 = await oneEvent(bar, 'keydown');
expect(event1.key).to.equal('ArrowUp');
expect(document.activeElement).to.equal(btn2);
expect(btn1.getAttribute('tabIndex')).to.equal('-1');
expect(btn2.getAttribute('tabIndex')).to.equal('0');
setTimeout(() => bar.dispatchEvent(keyArrowUp));
const event2 = await oneEvent(bar, 'keydown');
expect(event2.key).to.equal('ArrowUp');
expect(document.activeElement).to.equal(btn1);
expect(btn2.getAttribute('tabIndex')).to.equal('-1');
expect(btn1.getAttribute('tabIndex')).to.equal('0');
});
it('Should set tabIndex=0 to last button when keydown End', async () => {
setTimeout(() => el.dispatchEvent(keyEnd));
const event1 = await oneEvent(el, 'keydown');
expect(event1.key).to.equal('End');
expect(document.activeElement).to.equal(btn3);
el.getFocusableButtons().forEach((button, index) => {
expect(button.getAttribute('tabIndex')).to.equal(index === 2 ? '0' : '-1');
});
});
it('Should set tabIndex=0 to first button when keydown Home', async () => {
btn3.focus();
setTimeout(() => el.dispatchEvent(keyHome));
const event1 = await oneEvent(el, 'keydown');
expect(event1.key).to.equal('Home');
expect(document.activeElement).to.equal(btn1);
el.getFocusableButtons().forEach((button, index) => {
expect(button.getAttribute('tabIndex')).to.equal(index === 0 ? '0' : '-1');
});
});
it('Should out of focus when press Tab in case inject a new button', async () => {
const newButton = document.createElement('ef-button');
newButton.id = 'btn5';
newButton.innerText = 'newButton';
el.appendChild(newButton);
await elementUpdated(el);

btn1.focus();
setTimeout(() => el.dispatchEvent(keyTab));
const event1 = await oneEvent(el, 'keydown');
expect(event1.key).to.equal('Tab');

const group = el.getFocusableButtons();
const addedButton = group.find((button) => button.id === 'btn5');
expect(addedButton.getAttribute('tabIndex')).to.equal('-1');
expect(btn1.getAttribute('tabIndex')).to.equal('0', 'Current focusing button should still be focusable');
});
});
});

Loading

0 comments on commit f121ea6

Please sign in to comment.