Best approach to conditionally adding an HTML attribute

I’m building out a dynamic accordion with W3’s accessible tabs design pattern. I’m trying to find the best approach to adding a hidden attribute to every tabpanel but the first, so that the first accordion item is open by default.

Here’s the cleanest approach I’ve found so far:

...
   <Set tabContent>
     Set up tab content here
    </Set>    
    <If check="{Get loop=count}" is value=1>
    <div tabindex="0"
       role="tabpanel"
       id="Section{Get loop=count}"
       aria-labelledby="Tab{Get loop=count}"
       >
      <Get tabContent />
      </div>
      <Else />
    <div tabindex="0"
       role="tabpanel"
       id="Section{Get loop=count}"
       aria-labelledby="Tab{Get loop=count}"
       hidden
       > <Get tabContent />
      </div>
      </If>
...

I tried setting up the hidden attribute as a conditional variable and loading it inline, and I tried wrapping just the opening tag in my if statement, but neither rendered the way I needed them to. I wonder if it would be worth thinking about a simpler way to handle conditional html attributes?

1 Like

I see what you mean… The attribute hidden is currently impossible to add conditionally, because it doesn’t take a value - the key itself needs to be there (or not).

I can think of similar attributes without value, like autoplay and download.

A shortcut could be an attribute named attribute, which just adds whatever is passed as tag attribute(s). I checked that there’s no existing attribute in HTML or SVG with the same name - but, to ensure there’s no naming conflict, maybe it should be called tag-attribute.

That should support something like:

<div tag-attribute="{If loop=count more_than value=1}hidden{/If}">

Would that work for you?

That would do the trick. We can probably leave it till after launch since there’s a pretty easy workaround.

@julia Now that tag-attribute has been released, do you have an example of how to do a simple accordion?

Cheers, Richard

Hi @RichardC

Sure! I usually use W3C’s accessible tab JS for my tabs and accordions. It supports a “vertical” mode that works nicely for accordions, supporting keyboard navigation with arrows and “tab” activation with spacebar :slight_smile:

With the new tag-attribute functionality, I would write my L&L like so assuming it’s getting content from a simple CPT “support” with the title informing the accordion item label and the content informing the accordion panel content:

<div class="tabs" role="tablist" aria-label="Frequently Asked Questions" aria-orientation="vertical">
  <Loop type=support order="desc">
    <Set current_count><Get loop=count /></Set>
    <div class="tt-item">
      <button class="button-faq p-2 h6" 
              role="tab"
              aria-selected="{If variable=current_count more_than value=1}false{Else /}true{/If}"
              aria-controls="faqSection{Get loop=count}"
              id="faqTab{Get loop=count}"
              tag-attributes="{If variable=current_count more_than value=1}tabindex='-1'{/If}"><Field title /></button>
      <div class="faq-tab pb-3 px-2"
           tabindex="0"
           role="tabpanel"
           id="faqSection{Get loop=count}"
           aria-labelledby="faqTab{Get loop=count}"
           tag-attributes="{If variable=current_count more_than value=1}hidden{/If}"
           >      
        <Field content />
      </div>
    </div>
  </Loop>
</div>

In the context of an L&L template, I just paste the provided javascript into the Script tab and then style as needed to make it look like an accordion. The only important note for styling is that you can target active tab buttons this way: .tabs[aria-orientation="vertical"] button[role="tab"][aria-selected="true"]

Let me know if it works for you!

2 Likes

@julia, Thanks! Unfortunately, it’s not working well so far. My first item is expanded, but none of the others are. No icons show and clicking on the headings does nothing.

The script shows two errors:
'use strict'; says to use the function form

class TabsManual { says: 'class' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

Not sure how to act on those.

Also, I don’t know what I’m missing without the CSS for:
tt-item button-faq p-2 h6 faq-tab pb-3 px-2

Cheers, Richard

It’s possible they’ve updated the JS since I last copied it, or I made some mods and forgot about it. Here’s what I use:

/*
*   This content is licensed according to the W3C Software License at
*   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*/



(function () {
  var tablist = document.querySelectorAll('[role="tablist"]')[0];
  var tabs;
  var panels;

  generateArrays();

  function generateArrays () {
    tabs = document.querySelectorAll('[role="tab"]');
    panels = document.querySelectorAll('[role="tabpanel"]');
  }

  // For easy reference
  var keys = {
    end: 35,
    home: 36,
    left: 37,
    up: 38,
    right: 39,
    down: 40,
    remove: 46,
    enter: 13,
    space: 32
  };

  // Add or subtract depending on key pressed
  var direction = {
    37: -1,
    38: -1,
    39: 1,
    40: 1
  }

  // Bind listeners
  for (i = 0; i < tabs.length; ++i) {
    addListeners(i);
  }

  function addListeners (index) {
    tabs[index].addEventListener('click', clickEventListener);
    tabs[index].addEventListener('keydown', keydownEventListener);
    tabs[index].addEventListener('keyup', keyupEventListener);

    // Build an array with all tabs (<button>s) in it
    tabs[index].index = index;
  }

  // When a tab is clicked, activateTab is fired to activate it
  function clickEventListener (event) {
    var tab = event.target;
    activateTab(tab, false);
  }

  // Handle keydown on tabs
  function keydownEventListener (event) {
    var key = event.keyCode;

    switch (key) {
      case keys.end:
        event.preventDefault();
        // Activate last tab
        focusLastTab();
        break;
      case keys.home:
        event.preventDefault();
        // Activate first tab
        focusFirstTab();
        break;

      // Up and down are in keydown
      // because we need to prevent page scroll >:)
      case keys.up:
      case keys.down:
        determineOrientation(event);
        break;
    }
  }

  // Handle keyup on tabs
  function keyupEventListener (event) {
    var key = event.keyCode;

    switch (key) {
      case keys.left:
      case keys.right:
        determineOrientation(event);
        break;
      case keys.remove:
        determineDeletable(event);
        break;
      case keys.enter:
      case keys.space:
        activateTab(event.target);
        break;
    }
  }

  // When a tablist’s aria-orientation is set to vertical,
  // only up and down arrow should function.
  // In all other cases only left and right arrow function.
  function determineOrientation (event) {
    var key = event.keyCode;
    var vertical = tablist.getAttribute('aria-orientation') == 'vertical';
    var proceed = false;

    if (vertical) {
      if (key === keys.up || key === keys.down) {
        event.preventDefault();
        proceed = true;
      };
    }
    else {
      if (key === keys.left || key === keys.right) {
        proceed = true;
      }
    }

    if (proceed) {
      switchTabOnArrowPress(event);
    }
  }

  // Either focus the next, previous, first, or last tab
  // depending on key pressed
  function switchTabOnArrowPress (event) {
    var pressed = event.keyCode;

    if (direction[pressed]) {
      var target = event.target;
      if (target.index !== undefined) {
        if (tabs[target.index + direction[pressed]]) {
          tabs[target.index + direction[pressed]].focus();
        }
        else if (pressed === keys.left || pressed === keys.up) {
          focusLastTab();
        }
        else if (pressed === keys.right || pressed == keys.down) {
          focusFirstTab();
        }
      }
    }
  }

  // Activates any given tab panel
  function activateTab (tab, setFocus) {
    setFocus = setFocus || true;
    // Deactivate all other tabs
    deactivateTabs();

    // Remove tabindex attribute
    tab.removeAttribute('tabindex');

    // Set the tab as selected
    tab.setAttribute('aria-selected', 'true');

    // Get the value of aria-controls (which is an ID)
    var controls = tab.getAttribute('aria-controls');

    // Remove hidden attribute from tab panel to make it visible
    document.getElementById(controls).removeAttribute('hidden');

    // Set focus when required
    if (setFocus) {
      tab.focus();
    }
  }

  // Deactivate all tabs and tab panels
  function deactivateTabs () {
    for (t = 0; t < tabs.length; t++) {
      tabs[t].setAttribute('tabindex', '-1');
      tabs[t].setAttribute('aria-selected', 'false');
    }

    for (p = 0; p < panels.length; p++) {
      panels[p].setAttribute('hidden', 'hidden');
    }
  }

  // Make a guess
  function focusFirstTab () {
    tabs[0].focus();
  }

  // Make a guess
  function focusLastTab () {
    tabs[tabs.length - 1].focus();
  }

  // Detect if a tab is deletable
  function determineDeletable (event) {
    target = event.target;

    if (target.getAttribute('data-deletable') !== null) {
      // Delete target tab
      deleteTab(event, target);

      // Update arrays related to tabs widget
      generateArrays();

      // Activate the closest tab to the one that was just deleted
      if (target.index - 1 < 0) {
        activateTab(tabs[0]);
      }
      else {
        activateTab(tabs[target.index - 1]);
      }
    }
  }

  // Deletes a tab and its panel
  function deleteTab (event) {
    var target = event.target;
    var panel = document.getElementById(target.getAttribute('aria-controls'));

    target.parentElement.removeChild(target);
    panel.parentElement.removeChild(panel);
  }

  // Determine whether there should be a delay
  // when user navigates with the arrow keys
  function determineDelay () {
    var hasDelay = tablist.hasAttribute('data-delay');
    var delay = 0;

    if (hasDelay) {
      var delayValue = tablist.getAttribute('data-delay');
      if (delayValue) {
        delay = delayValue;
      }
      else {
        // If no value is specified, default to 300ms
        delay = 300;
      }
    }

    return delay;
  }
}());

The JS doesn’t add icons, I generally do that with a pseudoelement on the button element like so:

.tabs[aria-orientation="vertical"] button::after {
  content: "⬇";
  font-family: "my-icomoon-family";
  font-weight: normal;
  pointer-events: none;
}
.tabs[aria-orientation="vertical"] button[aria-selected="true"]::after {
  content: "⬆";
}

You could also use images or SVGs encoded in base64 as background images if you don’t want to install a whole icon family, or even place icons inline in the html and conditionally display them or use transforms to indicate open vs closed :slight_smile: