1

I have a small carousel that plays automatically on page load, using HTML, CSS and JavaScript and definitely no jQuery.

To add a pause/play option there is a span with role="checkbox" followed by a label.

The label itself is hidden and has no content. The span has two pseudo elements. On first showing, the pseudo element shows the ⏸ character, controlled by a CSS ::after class. When clicked, the span has the class "is-clicked" added, at which point the ▶ character is displayed, controlled by another ::after class

It is focusable and can be activated with the keyboard by hitting the Enter key, but when I check with Lighthouse, I keep getting the "Focusable elements should have interactive semantics".

Why is this?

Here is the code:

/* detect keyboard users */
function handleFirstTab(e) {
    if (e.key === 'Tab') { // the 'I am a keyboard user' key
        document.body.classList.add('user-is-tabbing');
        window.removeEventListener('keydown', handleFirstTab);
    }
}
let checkboxEl = document.getElementById('checkbox');
let labelEl = document.getElementById('checkboxLabel');

labelEl.onclick = function handleLabelClick() {
  checkboxEl.focus();
  toggleCheckbox();
}

function toggleCheckbox() {
  let isChecked = checkboxEl.classList.contains('is-checked');

  checkboxEl.classList.toggle('is-checked', !isChecked);
  checkboxEl.setAttribute('aria-checked', !isChecked);
}

checkboxEl.onclick = function handleClick() { 
  toggleCheckbox();
}

checkboxEl.onkeypress = function handleKeyPress(event) { 
  let isEnterOrSpace = event.keyCode === 32 || event.keyCode === 13;

  if(isEnterOrSpace) {
    toggleCheckbox();
  }
}
.link {
  height: auto;
  border: 1px solid #000;
  margin-bottom: 1rem;
  width: 80%;
  display: block;
}
#carousel-checkbox {
  margin-bottom: 1rem;
  height: 50px;
  width: 100px;
  display: inline-block;
}
#carousel-checkbox input {
  display: none;
}

#carousel-checkbox label {
  display: inline-block;
  vertical-align: middle;
}
#carousel-checkbox #checkbox {
  position: relative;
  top: 0;
  left: 30px;
  padding: 0.5rem 1rem;
  background: rgba(255,255,255, 0.5);
  }
#carousel-checkbox #checkbox:hover {
  cursor: pointer;
}
#carousel-checkbox #checkbox:focus {
  border: 1px dotted var(--medium-grey);
}
#carousel-checkbox #checkbox::after {
  content: "⏸";
  font-size: 1.5rem;
  color: var(--theme-dark);
}
#carousel-checkbox #checkbox.is-checked::after {
  content: "▶";
}
<div class="link">A bit of text with <a href="/">a dummy link</a> to demonstrate the keyboard tabbing navigation. </div>
<div id="carousel-checkbox"><span id="checkbox" tabindex="0" role="checkbox" aria-checked="false" aria-labelledby="checkboxLabel"></span><label id="checkboxLabel"></label></div>
<div class="link">Another link to <a href="/">another dummy link</a></div>

Why is this? Is it because the pseudo elements don't have a name attribute or something like that?

I have tried a different way, by dropping the pseudo elements and trying to change the span innerHTML depending on whether the class 'is-clicked' exists or not, but although I can get the pause character to display initially, it won't change the innerHTML to the play character when the span is clicked again.

Gillian
  • 287
  • 2
  • 11

2 Answers2

0

Short Answer

This is a warning rather than an error, it is telling you to check that the item actually is interactive.

Now you have got the interactivity on the element so you can ignore that issue.

Long answer

Why not just use a <input type="checkbox"> and save yourself an awful lot of extra work?

You can hide a checkbox with a visually hidden class.

This then allows you to do the same trick with a pseudo element as the visual representation of the state.

I have made several changes to your example that mean you don't have to worry about capturing keypresses etc. and can just use a click handler so your JS is far simpler.

Notice the trick with the label where I add some visually hidden text within it so the label is still visible (so we can still use psuedo elements!).

I then use #checkbox1 ~ label to access the label with CSS so we can change the state.

The final thing to notice is how I changed the content property slightly. This is because some screen readers will try and read out pseudo elements so I added alt text that was blank. Support isn't great at just over 70%, but it is worth adding for browsers that do support it.

Example

The below hopefully illustrates a way of achieving what you want with a checkbox.

There may be a few errors as I just adapted your code so please do not just copy and paste!

note: a checkbox should not work with Enter, only with Space. If you want it to work with both it should instead be a toggle switch etc. so that would be a completely different pattern.

let checkboxEl = document.getElementById('checkbox1');
let labelEl = document.querySelector('#checkboxLabel');

function toggleCheckbox() {
  let isChecked = checkboxEl.classList.contains('is-checked');
  checkboxEl.classList.toggle('is-checked', !isChecked);
  checkboxEl.setAttribute('aria-checked', !isChecked);
}

checkboxEl.onclick = function handleClick() { 
  toggleCheckbox();
}
.link {
  height: auto;
  border: 1px solid #000;
  margin-bottom: 1rem;
  width: 80%;
  display: block;
}

#carousel-checkbox {
  margin-bottom: 1rem;
  height: 50px;
  width: 100px;
  display: inline-block;
}

.visually-hidden { 
    border: 0;
    padding: 0;
    margin: 0;
    position: absolute !important;
    height: 1px; 
    width: 1px;
    overflow: hidden;
    clip: rect(1px 1px 1px 1px); /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
    clip: rect(1px, 1px, 1px, 1px); /*maybe deprecated but we need to support legacy browsers */
    clip-path: inset(50%); /*modern browsers, clip-path works inwards from each corner*/
    white-space: nowrap; /* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */
}

#carousel-checkbox label {
  display: inline-block;
  vertical-align: middle;
}

#carousel-checkbox #checkbox1 {
  position: relative;
  top: 0;
  left: 30px;
  padding: 0.5rem 1rem;
  background: rgba(255,255,255, 0.5);
  }
#carousel-checkbox #checkbox1 ~label:hover {
  cursor: pointer;
}

#carousel-checkbox #checkbox1:focus ~ label {
  border: 1px dotted #333;
}

#carousel-checkbox #checkbox1 ~label::after {
  content: "⏸" / "";
  font-size: 1.5rem;
  color: #000;
}

#carousel-checkbox #checkbox1.is-checked ~label::after {
  content: "▶" / "";
}
<div class="link">A bit of text with <a href="/">a dummy link</a> to demonstrate the keyboard tabbing navigation. </div>
<div id="carousel-checkbox">
  <input type="checkbox" id="checkbox1" class="visually-hidden">
  <label for="checkbox1" id="checkboxLabel">
    <span class="visually-hidden">Pause animations</span>
  </label>
  </div>
<div class="link">Another link to <a href="/">another dummy link</a></div>
Gillian
  • 287
  • 2
  • 11
GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64
  • I couldn't see the ```::after``` elements, but removing ``` / ""``` sorted that. My other changes are: adding ```z-index: 98;``` to ```#carousel-checkbox #checkbox1``` and ```background: #fff; padding: 0.75rem 1rem; position: relative; z-index: 99;``` added to ```#carousel-checkbox #checkbox1 ~label::after``` – Gillian Dec 10 '21 at 16:38
  • As I said it was just adapted from your code and it was purely to show the principle so it was bound to have a few bits needed fiddling! . The question is does it help you with solving the issue (fingers crossed) or do you need any more help! – GrahamTheDev Dec 11 '21 at 10:51
  • Thanks, Graham. I thought I had it all sorted, and it was running perfectly on my Windows 10 desktop and on my Android phone. But it won't work on my iPad. I've set it up on codepen here: https://codepen.io/Scrabble96/pen/dyVpENO and it doesn't work there, either. – Gillian Dec 11 '21 at 15:16
  • where do you get `.user-is-tabbing` from as that is interfering with the pen, but it does work for me (just the focus indicator is never visible that is all). – GrahamTheDev Dec 11 '21 at 15:24
  • Sorry, that's from my main template.css (some code you gave me before, for a site menu, I think ;-)). I've removed that code. But I still can't get it to work on my iPad (iOS v.15.1) on either Firefox or Safari. – Gillian Dec 11 '21 at 17:33
  • It refuses to change from the pause symbol to the play symbol when tapped. And doesn't actually pause anything, either. – Gillian Dec 11 '21 at 17:34
  • Sounds like it isn't responding to the click on the label to update the underlying checkbox. Try giving the ` – GrahamTheDev Dec 11 '21 at 17:48
  • 1
    Hm. I've moved the label away from the checkbox and given both the size 2 x 2rem. I can see the checkbox with and without a checkmark. So that works. It's just not doing anything else. I shall leave this and come back to it tomorrow with a fresh mind and perhaps try your SVG suggestion. – Gillian Dec 11 '21 at 18:13
  • 1
    Solved the iPad/iOS issue: I found a very old hack from 2013 which involves simply adding a blank `````` between the ```input``` and the ```label```. – Gillian Dec 12 '21 at 12:07
0

In the end, I gave up on using a checkbox, due to the difficulties with iPad/iOS not responding to checkbox events. Whilst it worked in codepen on iOS it wouldn't work on the actual site. So I switched to a button.

Here is the code, which is fully accessible with no 'interactive semantics' warnings, shown with some dummy slides. The animation is based on having only three slides. If you wanted more or less, then the timings would have to be adjusted accordingly. All I need now is to style the pause button.

let element = document.getElementById("pause");
function toggleButton() {
    element.classList.toggle("paused");
    if (element.innerHTML === "⏸") {
          element.innerHTML = "▶";
          } 
        else {
        element.innerHTML = "⏸";
          }  
}
element.onclick = function handleClick() {
            toggleButton();
        }
#carousel {
  height: auto;
  max-width: 1040px;
  position: relative;
  margin: 4rem auto 0;
}
#carousel > * {
  animation: 12s autoplay6 infinite linear;
  position: absolute; 
  top: 0;
  left: 0;
  opacity: 0.0;
}
#carousel .one {
  position: relative;
}
.homeSlides {
  height: 150px;
  width: 400px;
  background-color: #ff0000;
}
.homeSlides.two {
  background-color: #0fff00;
}
.homeSlides.three {
  background-color: #e7e7e7;
}
@keyframes autoplay6 {
  0% {opacity: 0.0}
  4% {opacity: 1.0}
  33.33% {opacity: 1.0}
  37.33% {opacity: 0.0}
  100% {opacity: 0.0}
}
#carousel > *:nth-child(1) {
  animation-delay: 0s;
}
#carousel > *:nth-child(2) {
  animation-delay: 4s;
}
#carousel > *:nth-child(3) {
  animation-delay: 8s;
}
#carousel-button {
  position: relative;
  height: 100%;
  width: auto;
}
#carousel-button button {
  position: absolute;
  top: -3.5rem;
  right: 5rem;
  padding: 0 0.5rem 0.25rem;;
  background: #fff;
  z-index: 98;
  font-size: 2rem;
  cursor: pointer;
 }
body.user-is-tabbing #carousel-button button:focus {
  outline: 1px dotted #333;
}
body:not(.user-is-tabbing) #carousel-button button:focus {
  outline: none;
}
#carousel-button button:hover {
  cursor: pointer;
}
#carousel-button ~ #carousel * {
  animation-play-state: running;
}
#carousel-button button.paused ~ #carousel * {
  animation-play-state: paused;
}
<div id="carousel-button"><button id="pause" class="">⏸</button>
<div id="carousel">
  <div class="homeSlides one">This is div one</div>
  <div class="homeSlides two">This is div two</div>
  <div class="homeSlides three">This is div three</div>
    </div>
</div>
Gillian
  • 287
  • 2
  • 11