Skip to content

Commit

Permalink
feat(rating): add accessibility support
Browse files Browse the repository at this point in the history
* feat(rating): add accessibility support

* refactor(rating): refactor legacy code

* test(rating): add test case

* docs(rating): update documentation

* refactor(rating): change variable names

* refactor(rating): change variable name

* refactor(rating): move set aria value method to setter

* refactor(rating): add max value variable

* feat(rating): add focused outline

* fix(rating): only remove role slider attr
  • Loading branch information
Sarin-Udompanish authored Jun 6, 2022
1 parent 4d3febe commit 2a1b7e4
Show file tree
Hide file tree
Showing 8 changed files with 3,208 additions and 7,980 deletions.
9 changes: 9 additions & 0 deletions documents/src/pages/elements/rating.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,12 @@ customRating.addEventListener('value-changed', (event) => {
document.getElementById('result').textContent = 'You have selected: ' + event.detail.value;
});
```

## Accessibility
::a11y-intro::

`ef-rating` does not assign any roles as it's not an actionable component.

However, in the interactive mode, rating will be assigned `role="slider"`. Users can use arrow keys to update value. Current value of the rating will be updated to `aria-valuenow`. You can assign `aria-label` and `aria-valuetext` to `ef-rating` to give the component an assistive name for screen reader.

::a11y-end::
10,540 changes: 2,658 additions & 7,882 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/elemental-theme/src/custom-elements/ef-rating.less
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@
}
}

&[focused=visible] {
outline: @input-border-width @input-border-style @input-focused-border-color;
outline-offset: 2px; //not support IE11
}
}
57 changes: 48 additions & 9 deletions packages/elements/src/rating/__demo__/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
#custom {
font-size: 30px;
}
.valueText {
font-size: 16px;
display: inline-block;
margin-right: 15px;
width: 150px;
text-transform: capitalize;
}
</style>

<demo-block layout="normal" header="Default" tags="default">
Expand All @@ -40,30 +47,62 @@
</div>
</demo-block>

<demo-block layout="normal" header="Interactive" tags="interactive,value">
<demo-block layout="normal" header="Max" tags="max,value">
<div>
<ef-rating interactive value="5"></ef-rating>
<ef-rating max="1" value="3"></ef-rating>
</div>
<div>
<ef-rating interactive value="2.5"></ef-rating>
<ef-rating max="7" value="4.75"></ef-rating>
</div>
<div>
<ef-rating interactive value="3"></ef-rating>
<ef-rating max="10" value="3"></ef-rating>
</div>
<div>
<ef-rating max="3.5" value="3"></ef-rating>
</div>
</demo-block>

<demo-block layout="normal" header="Max" tags="max,value">
<demo-block layout="normal" header="Interactive" tags="interactive,max,value">
<div>
<ef-rating max="1" value="3"></ef-rating>
<p class="valueText">default: -5</p>
<ef-rating interactive value="-5"></ef-rating>
</div>
<div>
<ef-rating max="7" value="4.75"></ef-rating>
<p class="valueText">default: 0.2</p>
<ef-rating interactive value="0.2"></ef-rating>
</div>
<div>
<ef-rating max="10" value="3"></ef-rating>
<p class="valueText">default: 0.5</p>
<ef-rating interactive value="0.5"></ef-rating>
</div>
<div>
<ef-rating max="3.5" value="3"></ef-rating>
<p class="valueText">default: 1.2</p>
<ef-rating interactive value="1.2"></ef-rating>
</div>
<div>
<p class="valueText">default: 3</p>
<ef-rating interactive value="3"></ef-rating>
</div>
<div>
<p class="valueText">default: 6</p>
<ef-rating interactive value="6"></ef-rating>
</div>
<br />
<div>
<p class="valueText">default: 1, max: 0</p>
<ef-rating interactive value="1" max="0"></ef-rating>
</div>
<div>
<p class="valueText">default: 1, max: 1.2</p>
<ef-rating interactive value="1" max="1.2"></ef-rating>
</div>
<div>
<p class="valueText">default: 1, max: 1.5</p>
<ef-rating interactive value="1" max="1.5"></ef-rating>
</div>
<div>
<p class="valueText">default: 6, max: 3</p>
<ef-rating interactive value="6" max="3"></ef-rating>
</div>
</demo-block>

Expand Down
237 changes: 230 additions & 7 deletions packages/elements/src/rating/__test__/rating.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fixture, expect, elementUpdated } from '@refinitiv-ui/test-helpers';
import { fixture, expect, elementUpdated, oneEvent, nextFrame } from '@refinitiv-ui/test-helpers';
import { valueUpdated, keyArrowLeft, keyArrowRight, keyArrowDown, keyArrowUp, keyHome, keyEnd } from './utils'

// import element and theme
import '@refinitiv-ui/elements/rating';
Expand All @@ -17,9 +18,10 @@ describe('rating/Rating', () => {

it('Contains the correct structure', async () => {
expect(el.getAttribute('max')).to.be.null;
expect(el.getAttribute('value')).to.equal('0');
expect(el.getAttribute('interactive')).to.be.null;
expect(el.stars.length).to.equal(5);
expect(el.value).to.equal('0');
const stars = el.shadowRoot.querySelectorAll('[part~="icon"]');
expect(stars.length).to.equal(5);
});

it('Max is changed', async () => {
Expand Down Expand Up @@ -109,21 +111,242 @@ describe('rating/Rating', () => {
el.value = null;
await elementUpdated(el);
expect(el.value).to.equal('0');

el.value = NaN;
await elementUpdated(el);
expect(el.value).to.equal('0');
});

it('When via max invalid type', async () => {

el.max = 'abcd';
await elementUpdated(el);
expect(el.max).to.equal('5');
expect(el.max).to.equal(el.MAX_VALUE);

el.max = undefined;
await elementUpdated(el);
expect(el.max).to.equal('5');
expect(el.max).to.equal(el.MAX_VALUE);

el.max = null;
await elementUpdated(el);
expect(el.max).to.equal('5');
expect(el.max).to.equal(el.MAX_VALUE);

el.value = NaN;
await elementUpdated(el);
expect(el.max).to.equal(el.MAX_VALUE);
});
});

describe('Keyboard Events', () => {
beforeEach(async () => {
el = await fixture('<ef-rating interactive></ef-rating>');
});
it('Arrow up/down should do nothing when interactive is not activated', async () => {
el.interactive = false;
await elementUpdated(el);
el.dispatchEvent(keyArrowUp);
expect(el.value).to.equal('0');

valueUpdated('2', el);
el.dispatchEvent(keyArrowDown);
expect(el.value).to.equal('2');
});
it('Arrow Up/Right should increase value correctly', async () => {
el.dispatchEvent(keyArrowUp);
expect(el.value).to.equal('1');

el.dispatchEvent(keyArrowRight);
expect(el.value).to.equal('2');

valueUpdated('-10', el);
el.dispatchEvent(keyArrowRight);
expect(el.value).to.equal('1');

valueUpdated('0.5', el);
el.dispatchEvent(keyArrowRight);
expect(el.value).to.equal('1');

valueUpdated('5', el);
el.dispatchEvent(keyArrowRight);
expect(el.value).to.equal('5', 'not exceed default max value.');

valueUpdated('6', el);
el.dispatchEvent(keyArrowRight);
expect(el.value).to.equal('6', 'do nothing when value is exceed max value.');
});
it('Arrow Up/Right should increase value correctly when max value has been changed', async () => {
el.value = '1';
el.max = '1.5';
await elementUpdated(el);
el.dispatchEvent(keyArrowUp);
expect(el.value).to.equal('2');

el.max = '1';
await elementUpdated(el);
el.dispatchEvent(keyArrowRight);
expect(el.value).to.equal('2');
});
it('Arrow Down/Left should decrease value correctly', async () => {
valueUpdated('3', el);
el.dispatchEvent(keyArrowDown);
expect(el.value).to.equal('2');

el.dispatchEvent(keyArrowLeft);
expect(el.value).to.equal('1');

el.dispatchEvent(keyArrowLeft);
expect(el.value).to.equal('1');

valueUpdated('-10', el);
el.dispatchEvent(keyArrowLeft);
expect(el.value).to.equal('-10');

valueUpdated('0.25', el);
el.dispatchEvent(keyArrowLeft);
expect(el.value).to.equal('0.25');

valueUpdated('0.75', el);
el.dispatchEvent(keyArrowLeft);
expect(el.value).to.equal('0.75');

valueUpdated('1.25', el);
el.dispatchEvent(keyArrowLeft);
expect(el.value).to.equal('1');
});
it('Arrow Down/Left should decrease value correctly when max value has been changed', async () => {
el.value = '2.5';
el.max = '2';
await elementUpdated(el);
el.dispatchEvent(keyArrowLeft);
expect(el.value).to.equal('1');

el.value = '3.75';
el.max = '3.5';
await elementUpdated(el);
el.dispatchEvent(keyArrowLeft);
expect(el.value).to.equal('3');

el.value = '0.5';
el.max = '0.5';
await elementUpdated(el);
el.dispatchEvent(keyArrowLeft);
expect(el.value).to.equal('0.5');
});
it('End key should increase value correctly', async () => {
el.dispatchEvent(keyEnd);
expect(el.value).to.equal('5');

el.value = '5';
el.max = '3';
await elementUpdated(el);
el.dispatchEvent(keyEnd);
expect(el.value).to.equal('5');
});
it('Home key should decrease value correctly', async () => {
valueUpdated('10', el);
el.dispatchEvent(keyHome);
expect(el.value).to.equal('1');

el.value = '3';
el.max = '2';
await elementUpdated(el);
el.dispatchEvent(keyHome);
expect(el.value).to.equal('1');
});
it('Fired event correctly', async () => {
let event;

valueUpdated('2', el);

setTimeout(() => el.dispatchEvent(keyArrowUp));
event = await oneEvent(el, 'value-changed');
expect(event.detail.value).to.equal('3');

setTimeout(() => el.dispatchEvent(keyArrowRight));
event = await oneEvent(el, 'value-changed');
expect(event.detail.value).to.equal('4');

setTimeout(() => el.dispatchEvent(keyArrowDown));
event = await oneEvent(el, 'value-changed');
expect(event.detail.value).to.equal('3');

setTimeout(() => el.dispatchEvent(keyArrowLeft));
event = await oneEvent(el, 'value-changed');
expect(event.detail.value).to.equal('2');;

setTimeout(() => el.dispatchEvent(keyEnd));
event = await oneEvent(el, 'value-changed');
expect(event.detail.value).to.equal('5');;

setTimeout(() => el.dispatchEvent(keyHome));
event = await oneEvent(el, 'value-changed');
expect(event.detail.value).to.equal('1');;
});
it('Should not fired value-changed event', async () => {
let isFired = false;
el.value = '3';
el.max = '2';
await elementUpdated(el);

el.addEventListener('value-changed', () => {
isFired = true;
});

el.dispatchEvent(keyArrowUp);
expect(isFired).to.equal(false);

el.dispatchEvent(keyArrowRight);
expect(isFired).to.equal(false);

el.dispatchEvent(keyEnd);
expect(isFired).to.equal(false);

el.value = '0.25';
el.max = '4';
await elementUpdated(el);

el.dispatchEvent(keyArrowDown);
expect(isFired).to.equal(false);

el.dispatchEvent(keyArrowLeft);
expect(isFired).to.equal(false);

el.dispatchEvent(keyHome);
expect(isFired).to.equal(false);
});
});

describe('Accessibility', () => {
beforeEach(async () => {
el = await fixture('<ef-rating interactive></ef-rating>');
});
it('Should have correct attribute', async () => {
expect(el.getAttribute('role')).to.equal('slider');
expect(el.getAttribute('tabindex')).to.equal('0');
expect(el.getAttribute('aria-valuemin')).to.equal('1');
expect(el.getAttribute('aria-valuemax')).to.equal(el.MAX_VALUE);
expect(el.getAttribute('aria-valuenow')).to.equal('0');
});
it('Should remove attributes when interactive attribute has been changed', async () => {
el.interactive = false;
await elementUpdated(el);
expect(el.getAttribute('role')).to.equal(null);
expect(el.getAttribute('aria-valuemin')).to.equal(null);
expect(el.getAttribute('aria-valuemax')).to.equal(null);
expect(el.getAttribute('aria-valuenow')).to.equal(null);
});
it('Should update aria-valuenow when value updated', async () => {
expect(el.getAttribute('aria-valuenow')).to.equal('0');

valueUpdated('2', el);
await nextFrame();
expect(el.getAttribute('aria-valuenow')).to.equal('2');
});
it('Should update aria-valuemax when max value updated', async () => {
expect(el.getAttribute('aria-valuemax')).to.equal(el.MAX_VALUE);

valueUpdated('10', el, 'max');
await nextFrame();
expect(el.getAttribute('aria-valuemax')).to.equal('10');
});
});
});
Loading

0 comments on commit 2a1b7e4

Please sign in to comment.