Organizar dados hierarquicamente é uma prática muito comum desde os primórdios da humanidade. Exemplos de tais organizações são os sumários de livros, árvores genealógicas, estruturas de cargos em uma empresa, entre outros. Uma representação frequentemente utilizada em sistemas computacionais para listas hierárquicas é a de árvore. Essa metáfora se refere à estrutura de divisão do raíz em galhos (ou nós) e, por fim, em folhas. Exemplos de tais aplicações são os navegadores de arquivos, como o Windows Explorer e o Nautilus para distribuições Linux. Nelas, a raíz é a unidade de armazenamento, os galhos (ou nós) são as pastas de arquivos e as folhas são os arquivos.
Na linguagem de marcação HTML, estruturas hierárquicas podem ser representadas em listas ordenadas (elemento <ol>
) ou não ordenadas (elemento <ul>
). Porém, tais estruturas são estáticas, ou seja, não é possível controlar a visibilidade dos elementos, muito útil para listas longas e/ou com diversos níveis de profundidade.
Esse tipo de funcionalidade, semelhante aos navegadores de arquivo, pode ser obtida com o uso de JavaScript e CSS. No entanto, semanticamente elas não são entendidas como tal, pois não são marcadas no documento HTML. Para solucionar este problema é possível usar os papeis tree
e treeitem
do WAI-ARIA.
Um lista do tipo tree
é uma lista que pode conter grupos de itens aninhados em sub-níveis que podem ser expandidos (mostra os descendentes do grupo) ou contraídos (esconde os descentes do grupo).
Propriedades para o papel tree
: aria-activedescendant
, aria-atomic
, aria-controls
, aria-describedat
, aria-describedby
, aria-dropeffect
, aria-flowto
, aria-haspopup
, aria-label
, aria-labelledby
, aria-live
, aria-multiselectable
, aria-orientation
, aria-owns
, aria-relevant
, aria-required
.
Atenção: Um elemento com o papel tree deve possuir (owned) obrigatoriamente elementos com papeis: treeitem
e group
.
Estados para o papel tree
: aria-busy
, aria-disabled
, aria-expanded
, aria-grabbed
, aria-hidden
, aria-invalid
.
Propriedades para o papel treeitem
: aria-atomic
, aria-controls
, aria-describedat
, aria-describedby
, aria-dropeffect
, aria-flowto
, aria-haspopup
, aria-label
, aria-labelledby
, aria-level
, aria-live
, aria-owns
, aria-posinset
, aria-relevant
, aria-setsize
.
Estados para o papel treeitem
: aria-busy
, aria-checked
, aria-disabled
, aria-expanded
, aria-grabbed
, aria-hidden
, aria-invalid
.
Propriedades para o papel group
: aria-activedescendant
, aria-atomic
, aria-controls
, aria-describedat
, aria-describedby
, aria-dropeffect
, aria-flowto
, aria-haspopup
, aria-label
, aria-labelledby
, aria-live
, aria-owns
, aria-relevant
.
Estados para o papel group
: aria-busy
, aria-disabled
, aria-expanded
, aria-grabbed
, aria-hidden
, aria-invalid
.
Dica: Veja como testar o exemplo em Como testar o seu código.
Documento: tree.html.
<!DOCTYPE html> <html lang="pt-br"> <head> <meta charset="utf-8"> <title>Exemplo do Papel Árvore</title> <link rel="stylesheet" type="text/css" href="estilo.css"> <script type="text/javascript" src="../libs/jquery.js"></script> <script type="text/javascript" src="script.js"> </script> <script src="../libs/html5.js"></script> </head> <body> <h1>Papel Árvore (<span lang="en">Tree</span>)</h1> <div role="main" class="main"> <div class="treeview"> <ul class="tree"> <li><a href="#flora">Flora</a> <ul class="children"> <li><a href="#ervas">Ervas</a> <ul class="children"> <li><a href="#ervadoce">Erva Doce</a></li> <li><a href="#urtiga">Urtiga</a></li> <li><a href="#boldo">Boldo</a> <ul class="children"> <li><a href="#africano">Africano</a></li> <li><a href="#indigena">Indígena</a></li> <li><a href="#miudo">Miúdo</a></li> </ul> </li> </ul> </li> <li><a href="#flores">Flores</a> <ul class="children"> <li><a href="#margarida">Margarida</a></li> <li><a href="#cravo">Cravo</a></li> <li><a href="#rosa">Rosa</a></li> </ul> </li> </ul> </li> <li><a href="#fauna">Fauna</a> <ul class="children"> <li><a href="#mamiferos">Mamíferos</a> <ul class="children"> <li><a href="#onca">Onça pintada</a></li> <li><a href="#mico">Mico-leão-dourado</a></li> <li><a href="#capivara">Capivara</a></li> </ul> </li> <li><a href="#repteis">Répteis</a> <ul class="children"> <li><a href="#lagarto">Lagarto</a> <ul class="children"> <li><a href="#calango">Calango-verde</a></li> <li><a href="#teiu">Teiú</a></li> <li><a href="#teju">Lagarto-teju</a></li> </ul> </li> <li><a href="#cobra">Cobra</a></li> <li><a href="#tartaruga">Tartaruga</a></li> </ul> </li> </ul> </li> </ul> </div> </div> <footer id="footer" role="contentinfo"> <p>Este exemplo é uma adaptação de <a href="http://accessibleculture.org/research-files/aria-tree-views/treeviewB.html#flowers"> Accessible Culture</a>, distribuída sob a licença <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/3.0/"> Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License</a>.</p> </footer> </body> </html>
Folha de estilo: estilo.css.
header, nav, footer {display: block;} #siteName { border:none; margin: 0; padding:0; } ul {padding-left: 20px;} li { list-style-type: none; outline: none; padding: 5px; } li ul {margin-top: 5px;} .visually-hidden { position: absolute; left: -999em; } .tree { padding: 10px; margin-bottom: 2em; border: 1px solid #999; } .hasChildren {position: relative;} .tree li ul {display: none;} .tree a {padding: 2px 5px 2px 0;} .tree a:focus {outline: 2px dotted #f00;} .tree li li a { background-image: url("img/raquo-blue.png"); background-repeat: no-repeat; background-position: 8px 0.5em; padding-left: 40px; } .tree .hasChildren a { background-image: none; padding-left: 30px; } .tree li .noChildren a { background-image: url("img/raquo-blue.png"); padding-left: 25px; } .toggle { background-position: left top; background-repeat: no-repeat; cursor: pointer; height: 14px; width: 14px; position: absolute; left: 10px; top: 0.4em; } .tree .expanded {background-image: url("img/minus-blue.png");} .tree .collapsed {background-image: url("img/plus-blue.png");} .tree .expanded.hover {background-image: url("img/minus-blue-hover.png");} .tree .collapsed.hover {background-image: url("img/plus-blue-hover.png");} #footer {clear: both;}
Script: script.js.
$(document).ready(function() { if ($('.treeview').length) { //atribui a primeira lista não ordenada que estiver dentro do div //com a classe treeview, pois é a árvore var $tree = $('.treeview ul:first'); $tree.attr({'role': 'tree'}); //variáveis que mantém o controle sobre os nós expandidos ou contraídos da árvore var $allNodes = $('li:visible', $tree);//lista de nós visíveis da árvore var lastNodeIdx = $allNodes.length - 1;//o índice do último nó visível da lista var $lastNode = $allNodes.eq(lastNodeIdx);//último nó visível da lista //expande ou contrai um grupo de nós function toggleGroup($node) { $toggle = $('> div', $node); $childList = $('> ul', $node); //expande ou contraí os nós do grupo com efeito visual slide $childList.slideToggle('fast', function() { //atualiza as variáveis de controle sobre os nós expandidos ou contraídos $allNodes = $('li:visible', $tree); lastNodeIdx = $allNodes.length - 1; $lastNode = $allNodes.eq(lastNodeIdx); } ); //ajuste de estilo e propriedades wai-aria da contração ou expansão do grupo if ($toggle.hasClass('collapsed')) { //ajuste de estilo visual para expandido $toggle.removeClass('collapsed').addClass('expanded'); //indica que um elemento está expandido (semanticamente e não visualmente) $('> a', $node).attr({'aria-expanded': 'true', 'tabindex': '0'}).focus(); } else { //ajuste de estilo visual para contraído $toggle.removeClass('expanded').addClass('collapsed'); //indica que um elemento está contraído (semanticamente e não visualmente) $('> a', $node).attr({'aria-expanded': 'false', 'tabindex': '0'}).focus(); } } //obtém o próximo nó da árvore function nextNodeLink($el, dir) { var thisNodeIdx = $allNodes.index($el.parent()); if (dir == 'up' || dir == 'parent') { var endNodeIdx = 0; var operand = -1; } else { var endNodeIdx = lastNodeIdx; var operand = 1; } if (thisNodeIdx == endNodeIdx) {//se o nós atual for o último return false; //não faz nada } if (dir == 'parent') { var parentNodeIdx = $allNodes.index($el.parent().parent().parent()); var $nextEl = $('> a', $allNodes.eq(parentNodeIdx)); } else { var $nextEl = $('> a', $allNodes.eq(thisNodeIdx + operand)); } $el.attr('tabindex', '-1'); $nextEl.attr('tabindex', '0').focus(); } //para cada link que houver na árvore $('li > a', $tree).each(function() { var $el = $(this); var $node = $el.parent(); $el.attr({'role': 'treeitem', 'aria-selected': 'false', 'tabindex': "-1", 'aria-label': $el.text()}); $node.attr('role', 'presentation'); //se o nó tem nós filhos if ($node.has('ul').length) { $node.addClass('hasChildren'); $childList = $('ul', $node); $childList.attr({'role': 'group'}).hide(); //adiciona o elemento para expandir/contrair e define //aria-expanded no link $('<div aria-hidden="true" class="toggle collapsed">').insertBefore($el); $el.attr('aria-expanded', 'false'); } else {//caso o nó não tenha nós filhos $node.addClass('noChildren'); } //define os eventos de teclado $el.on('keydown', function(e){ if (!(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)) { switch(e.which) { case 38: //cima e.preventDefault(); nextNodeLink($(this), 'up'); break; case 40: //baixo e.preventDefault(); nextNodeLink($(this), 'down'); break; case 37: //esquerda if ($(this).attr('aria-expanded') == 'false' || $node.is('.noChildren')) { nextNodeLink($(this), 'parent'); } else { toggleGroup($node); } break; case 39: //direita if ($(this).attr('aria-expanded') == 'true') { nextNodeLink($(this), 'down'); } else { toggleGroup($node); } break; } } } //atualiza aria-selected quando o estado de foco de um nós muda ).on('focus', function() { $('[aria-selected="true"]', $tree).attr('aria-selected', 'false'); $(this).attr('aria-selected', 'true'); } ); } ); //define tabindex="0" no primeiro link da árvore $('> li:first > a', $tree).attr('tabindex', '0'); //adiciona evento click e estilo hover sobre o elemento com classe toggle $('.toggle').on('click', function() { toggleGroup($(this).parent()); } ).hover( function() { $(this).toggleClass('hover'); } ); } });