Skip to content

Commit

Permalink
MWPW-141392: Tooltip On Focus & Aria-checked (adobecom#1782)
Browse files Browse the repository at this point in the history
MWPW-141392
MWPW-141394
MWPW-141395
---------

Co-authored-by: Blaine Gunn <[email protected]>
  • Loading branch information
joaquinrivero and Blainegunn authored Feb 6, 2024
1 parent c971747 commit 5a11bab
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 34 deletions.
8 changes: 7 additions & 1 deletion libs/blocks/review/components/review/RatingInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const RatingInput = ({
starString,
starStringPlural,
tooltip,
isChecked,
onBlur,
onFocus,
}) => {
const handleClick = (ev, isKeyboardSelection = false) => {
if (onClick) onClick(index, ev, { isKeyboardSelection });
Expand Down Expand Up @@ -51,12 +54,15 @@ const RatingInput = ({
<input
data-tooltip=${tooltip}
name="rating"
aria-label=${label}
aria-label="${tooltip} ${label}"
type="radio"
className=${ratingsInputClassNames}
onClick=${handleClick}
onKeyPress=${handleKeyPress}
value=${index}
aria-checked=${isChecked ? 'true' : 'false'}
onBlur=${onBlur}
onFocus=${onFocus}
/>
`;
};
Expand Down
13 changes: 8 additions & 5 deletions libs/blocks/review/components/review/Ratings.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const Ratings = ({
if (fieldSetMouseOut.hovering) {
// only the inputs have value
if (fieldSetMouseOut.event.target.value) {
setKeyboardFocusIndex(null);
const hoveredRating = parseInt(fieldSetMouseOut.event.target.value, 10);
setCurrentRating(hoveredRating);
if (onRatingHover) onRatingHover({ rating: hoveredRating });
Expand All @@ -54,11 +55,13 @@ const Ratings = ({

if (!fieldSetMouseLeave.hovering) {
setHoverIndex(null);
setKeyboardFocusIndex(null);
}

if (!fieldSetMouseLeave.hovering && rating !== currentRating) {
setCurrentRating(rating);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fieldSetMouseOut, fieldSetMouseLeave, hoverIndex]);

useEffect(() => {
Expand Down Expand Up @@ -87,7 +90,7 @@ const Ratings = ({
};

const onBlur = (ev) => {
if (ev.relatedTarget === null || ev.relatedTarget.nodeName !== 'INPUT') {
if (ev?.relatedTarget?.nodeName !== 'INPUT') {
// Focus has left the rating fields
setCurrentRating(rating);
setKeyboardFocusIndex(null);
Expand All @@ -103,8 +106,7 @@ const Ratings = ({
const tooltip = tooltips && tooltips[i - 1];
ratings.push(
html`<${RatingInput}
key="rating"
-${i}
key=${`rating-${i}`}
isActive=${i <= currentRating}
isHovering=${hoverIndex === i}
isInteractive=${isInteractive}
Expand All @@ -114,6 +116,9 @@ const Ratings = ({
starString=${starString}
starStringPlural=${starStringPlural}
tooltip=${tooltip}
onBlur=${onBlur}
onFocus=${onFocus}
isChecked=${i === currentRating}
/>`,
);
}
Expand All @@ -124,9 +129,7 @@ const Ratings = ({
<fieldset
ref=${fieldSetRef}
className="hlx-Review-ratingFields"
onFocus=${onFocus}
onMouseDown=${onMouseDown}
onBlur=${onBlur}
disabled=${!isInteractive}
>
${starsLegend && legentElement} ${ratings.map((rate) => html`${rate}`)}
Expand Down
1 change: 1 addition & 0 deletions test/blocks/form/form.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('Form Block', async () => {
reqCheck.checked = true;
reqCheck.closest('.field-wrapper').dispatchEvent(new Event('input'));
submit.dispatchEvent(new Event('click'));
await new Promise((resolve) => { setTimeout(() => resolve(), 150); });
const thanks = document.querySelector('.thank-you');
expect(thanks).to.exist;
});
Expand Down
161 changes: 133 additions & 28 deletions test/blocks/review/components/review/Ratings.test.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,169 @@
import { expect } from '@esm-bundle/chai';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import { html, render } from '../../../../../libs/deps/htm-preact.js';
import { waitForElement } from '../../../../helpers/waitfor.js';
import { loadStyle } from '../../../../../libs/utils/utils.js';

import Ratings from '../../../../../libs/blocks/review/components/review/Ratings.js';

const loadStyles = (path) => new Promise((resolve) => {
loadStyle(`../../../../libs/${path}`, resolve);
});

describe('Ratings keypress', () => {
let mockFn;
before(async () => {
loadStyles('../../../../libs/blocks/review/review.css');
});
beforeEach(() => {
mockFn = sinon.spy();
const ratings = html`<${Ratings}
count="5"
isInteractive="true"
onClick=${mockFn}
onRatingHover=${mockFn}
rating="0"
starsLegend="Choose a star rating"
starString="star"
starStringPlural="stars"
tooltips=${['Poor', 'Bad', 'Good', 'Very good', 'Excellent']}
tooltipDelay="5"
/>`;
render(ratings, document.body);
});

it('should test keypress on ratings', async () => {
const ratingElement = await waitForElement('.hlx-Review-ratingFields');
await new Promise((resolve) => { setTimeout(() => resolve(), 20); });
const stars = ratingElement.querySelectorAll('input');
stars[0].focus();
await sendKeys({ press: 'ArrowRight' });
expect(document.activeElement).to.equal(stars[1]);
await sendKeys({ press: 'ArrowRight' });
expect(document.activeElement).to.equal(stars[2]);
await sendKeys({ press: 'Enter' });
await new Promise((resolve) => { setTimeout(() => resolve(), 100); });
expect(stars[2].getAttribute('aria-checked')).to.equal('true');
});
});

describe('Ratings', () => {
let mockFn;
before(async () => {
loadStyles('../../../../libs/blocks/review/review.css');
});
beforeEach(() => {
const mockFn = () => {};
mockFn = sinon.spy();
const ratings = html`<${Ratings}
count="5"
isInteractive="true"
onClick=${mockFn}
onRatingHover=${mockFn}
rating="4"
rating="2"
starsLegend="Choose a star rating"
starString="star"
starStringPlural="stars"
/>
tooltips=['This sucks', 'Meh', "It's OK", 'I like it', 'Best thing ever']
tooltipDelay=5000 />`;
tooltips=${['Poor', 'Bad', 'Good', 'Very good', 'Excellent']}
tooltipDelay="5"
/>`;
render(ratings, document.body);
});
afterEach(() => {
document.body.innerHTML = '';
});

it('should display ratings', async () => {
const ratingElement = await waitForElement('.hlx-Review-ratingFields');
expect(ratingElement).to.exist;
});

it('should test keypress on ratings', async () => {
it('should call onClick when a rating star is clicked', async () => {
const ratingElement = await waitForElement('.hlx-Review-ratingFields');
const inputElements = ratingElement.querySelectorAll('input');
const keyPressEvent = new KeyboardEvent('keypress', {
bubbles: true,
cancelable: true,
keyCode: 13,
});
inputElements[3].dispatchEvent(keyPressEvent);
expect(inputElements[3].classList.contains('is-Active')).to.be.true;
const stars = ratingElement.querySelectorAll('input');
const first = stars[0];
const second = stars[1];
first.click();
await new Promise((resolve) => { setTimeout(() => resolve(), 100); });
expect(first.getAttribute('aria-checked')).to.equal('true');
expect(second.getAttribute('aria-checked')).to.equal('false');
second.click();
await new Promise((resolve) => { setTimeout(() => resolve(), 100); });
expect(first.getAttribute('aria-checked')).to.equal('false');
expect(second.getAttribute('aria-checked')).to.equal('true');
});

it('should test mouse events on fieldset', async () => {
it('should call focus and blur', async () => {
const ratingElement = await waitForElement('.hlx-Review-ratingFields');
const mouseDownEvent = document.createEvent('MouseEvents');
mouseDownEvent.initEvent('mousedown', true, true);
ratingElement.dispatchEvent(mouseDownEvent);
const inputElement = ratingElement.querySelectorAll('input')[0];
expect(inputElement.classList.contains('is-Active')).to.be.true;
const stars = ratingElement.querySelectorAll('input');
const third = stars[2];
third.focus();
await new Promise((resolve) => { setTimeout(() => resolve(), 10); });
expect(document.activeElement).to.equal(third);
third.blur();
expect(document.activeElement).not.to.equal(third);
ratingElement.blur();
});

it('should test focus on ratings', async () => {
it('should have correct tooltip data on a rating star', async () => {
const ratingElement = await waitForElement('.hlx-Review-ratingFields');
await new Promise((resolve) => { setTimeout(() => resolve(), 100); });
const star = ratingElement.querySelectorAll('input')[2]; // For example, the third star
const tooltipText = star.getAttribute('data-tooltip');
await new Promise((resolve) => { setTimeout(() => resolve(), 10); });
expect(tooltipText).to.equal('Good');
});
it('should handle mouseover, mouseout, and onMouseDown', async () => {
const ratingElement = await waitForElement('.hlx-Review-ratingFields');
const inputElements = ratingElement.querySelectorAll('input');
ratingElement.dispatchEvent(new Event('focus'));
expect(inputElements[3].classList.contains('is-Active')).to.be.true;
const stars = ratingElement.querySelectorAll('input');
const forth = stars[3];
const fifth = stars[4];
forth.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
await new Promise((resolve) => { setTimeout(() => resolve(), 150); });
expect(forth.classList.contains('is-hovering')).to.be.true;
forth.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
forth.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
await new Promise((resolve) => { setTimeout(() => resolve(), 150); });
expect(forth.classList.contains('is-hovering')).to.be.false;
fifth.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
fifth.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
await new Promise((resolve) => { setTimeout(() => resolve(), 150); });
expect(fifth.classList.contains('is-Active')).to.be.true;
forth.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
forth.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
});
});

describe('Ratings when not interactive', () => {
before(async () => {
await loadStyles('../../../../libs/blocks/review/review.css');
});

beforeEach(() => {
const ratings = html`<${Ratings}
count="5"
isInteractive=${false}
onClick=${() => {}}
onRatingHover=${() => {}}
rating="3"
starsLegend="Choose a star rating"
starString="star"
starStringPlural="stars"
tooltips=${['Poor', 'Bad', 'Good', 'Very good', 'Excellent']}
tooltipDelay="5"
/>`;
render(ratings, document.body);
});

it('should test click on ratings input', async () => {
it('should not respond to mouse events', async () => {
const ratingElement = await waitForElement('.hlx-Review-ratingFields');
const inputElements = ratingElement.querySelectorAll('input');
inputElements[3].dispatchEvent(new Event('click'));
expect(inputElements[3].classList.contains('is-Active')).to.be.true;
if (!ratingElement) throw new Error('Rating element not found');
const stars = ratingElement.querySelectorAll('input');
const testStar = stars[2];
testStar.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
testStar.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
testStar.click();
await new Promise((resolve) => { setTimeout(() => resolve(), 200); });
expect(testStar.classList.contains('interaction-class')).to.be.false;
});
});

0 comments on commit 5a11bab

Please sign in to comment.