ArtigoAcessibilidade em OutSystems: o que corrigir antes de publicar

Diana Marques

/

OutSystems resolve um problema real: entregar aplicações complexas em menos tempo. Mas isso não garante que essas aplicações possam ser usadas por todas as pessoas. Na prática, a acessibilidade continua a ser tratada como uma fase, algo que entra depois dos fluxos fechados, da lógica de negócio validada, e esse momento raramente chega com tempo ou espaço para ser feito bem. A plataforma não é o obstáculo: o OutSystems tem o que é preciso para suportar interfaces verdadeiramente acessíveis. O que falta é processo, saber onde intervir, como e quando.

Em auditorias recentes a aplicações desenvolvidas em OutSystems, identificámos vários problemas de acessibilidade, quase metade críticos, ou seja, impossibilitando por completo o uso por teclado ou leitor de ecrã. Este artigo foca-se nos três padrões mais recorrentes: formulários sem associação semântica, componentes interativos com ARIA incompleto e falhas na navegação por teclado. Problemas concretos, com soluções concretas, sem reconstruir o que já existe.




Antes de tudo: ativar o EnableAccessibilityFeatures

O OutSystems UI tem um parâmetro de entrada nos layouts (EnableAccessibilityFeatures) que, quando ativado, liga um conjunto de funcionalidades essenciais de acessibilidade: indicadores de foco visíveis, skip-to-content, links acessíveis e contraste melhorado. Os exemplos deste artigo foram testados contra WCAG 2.2 AA com OutSystems UI v2.28.1.

Um cenário que se repete com frequência: durante uma demo, o cliente repara num contorno a aparecer nos botões e menus ao clicar. "Conseguem remover isto?" A resposta mais rápida (e mais comum) é colocar o parâmetro a False. O problema desaparece do ecrã. A acessibilidade também, aumentando significativamente o risco de incumprimento de critérios WCAG relacionados com foco visível, navegação e acesso ao conteúdo (independentemente do trabalho feito ao nível dos componentes).

Ativar o EnableAccessibilityFeatures estabelece uma base importante (indicadores de foco, skip-to-content, contraste), mas não corrige automaticamente a semântica dos componentes, a gestão de foco em modais ou a associação entre campos e mensagens de erro. As secções seguintes assumem este parâmetro ativo.




1. Formulários: o label está visível, mas não está associado

Este é provavelmente o problema mais comum em projetos OutSystems, e quase sempre pela mesma razão: o widget Label foi colocado visualmente junto ao Input, ficou bem no ecrã, passou na revisão, e assumiu-se que isso era suficiente. Não é.




Labels sem associação programática

A proximidade visual não cria associação programática. Um utilizador com visão normal lê o campo e a etiqueta juntos e percebe a relação. Um leitor de ecrã navega pelo DOM, e se a ligação não existir no código, o campo é anunciado sem nome. Em formulários longos, o utilizador encontra uma sequência de campos anónimos sem conseguir perceber o que preencher em cada um.

Para que a associação exista, o <label> precisa do atributo for a apontar para o id do input:

HTML

<label for="Input_Name">Nome</label>

<input type="text" id="Input_Name" />



Em OS11, usa-se a propriedade Input Widget do widget Label para criar esta ligação.


Um detalhe que apanha muita gente: o id gerado pelo OutSystems é dinâmico e pode mudar após publish. A associação deve ser validada no output final e não apenas no Service Studio.




Mensagens de erro sem contexto de campo

O segundo problema desta secção é menos visível mas igualmente frequente: as mensagens de erro de validação inline geradas pelo OutSystems não incluem, por defeito, a associação ao campo. O requisito de acessibilidade é simples, a mensagem deve estar ligada ao campo via aria-describedby, mas em OutSystems essa ligação tem de ser adicionada manualmente. O utilizador de leitor de ecrã ouve o erro, mas não sabe a que campo pertence. Em formulários com validação ativa, isto torna a correção de erros praticamente impossível sem visão.




Como testar

Abrir o NVDA ou VoiceOver (iOS e macOS), TalkBack (Android) ou JAWS em contexto empresarial. De seguida, navegar para cada campo com Tab e verificar o que é anunciado. Se se ouvir apenas o tipo de campo, "edit", sem nome, a associação está em falta. Introduzir dados inválidos e submeter o formulário: as mensagens de erro devem ser anunciadas junto ao campo correspondente. Se se ouvir os erros algures na página mas sem contexto de campo, o aria-describedby está ausente.

Nota: Para obter melhores resultados são recomendadas as seguintes combinações de teste: NVDA + Firefox e JAWS + Chrome.




2. Componentes interativos: ARIA incompleto ou ausente

Há uma armadilha específica de OutSystems aqui: os componentes nativos, como Popup, Bottom Sheet, Tabs, têm uma aparência cuidada e um comportamento visual correto. Isso cria uma confiança que nem sempre é justificada. O que está no ecrã não reflete necessariamente o que está no DOM, e é o DOM que o leitor de ecrã lê.

Modais e painéis: foco sem gestão

O caso mais frequente que encontramos é o Popup ou Bottom Sheet aberto sem gestão de foco: o modal aparece, o conteúdo está visível, mas o foco do teclado ficou na página por baixo. O utilizador de leitor de ecrã continua a navegar no conteúdo anterior sem saber que algo mudou. Este comportamento tem de ser implementado explicitamente, incluindo a focus trap, que garante que o foco não sai do modal enquanto este está aberto.

Um modal precisa de incluir, no mínimo:

HTML

<div class="modal" role="dialog"

aria-modal="true"

aria-labelledby="modal-title">

<div id="modal-title" class="modal-header">

Confirmar ação

</div>

<div class="modal-body">...</div>

</div>




Em OutSystems, isto não é automático: a focus trap deve ser implementada via JavaScript.

No Service Studio, criar um bloco PopupLayout com a estrutura do conteúdo do Popup, que será colocado dentro do Popup Built-in Widget de OutSystems UI. No bloco PopupLayout deverá ser adicionado um OnInitialize com um JavaScript node com o seguinte código:


JavaScript

const wrapper = document.getElementById($parameters.WrapperId);

if (!wrapper) {

console.warn("[FocusTrap] Wrapper not found:", $parameters.WrapperId);

} else {

const FOCUSABLE = [

'a[href]',

'area[href]',

'input:not([disabled]):not([type="hidden"])',

'select:not([disabled])',

'textarea:not([disabled])',

'button:not([disabled])',

'iframe',

'object',

'embed',

'audio[controls]',

'video[controls]',

'summary',

'[tabindex]:not([tabindex="-1"])',

'[contenteditable]:not([contenteditable="false"])'

].join(',');

const isReachable = (el) =>

el.getClientRects().length > 0 &&

getComputedStyle(el).visibility !== 'hidden' &&

!el.hasAttribute('inert') &&

!el.closest('[aria-hidden="true"]');

const getFocusable = () =>

Array.from(wrapper.querySelectorAll(FOCUSABLE)).filter(isReachable);

const controller = new AbortController();

wrapper.addEventListener('keydown', (e) => {

if (e.key !== 'Tab') return;

const items = getFocusable();

if (items.length === 0) {

e.preventDefault();

return;

}


const first = items[0];

const last = items[items.length - 1];

if (e.shiftKey && document.activeElement === first) {

e.preventDefault();

last.focus();

} else if (!e.shiftKey && document.activeElement === last) {

e.preventDefault();

first.focus();

}

}, { signal: controller.signal });

wrapper._focusTrapAbort = controller;

}



É necessário ainda, adicionar ao Popup a classe "has-accessible-features” e assim, ao abrir o modal, move o foco para o primeiro elemento interativo dentro dele, ao fechar, devolve o foco ao elemento que desencadeou a abertura. O foco não deve conseguir sair do modal com Tab enquanto este estiver aberto.




Tabs: estrutura visual correta, ARIA incompleto

O problema não se limita a modais. Nos Tabs, a estrutura visual está correta, mas a estrutura ARIA nem sempre está completa por defeito. Para ficar em conformidade com o W3C ARIA Authoring Practices Guide, cada botão de tab precisa de role="tab", aria-selected e aria-controls, e esses botões devem estar num container com role="tablist". O painel de conteúdo precisa de role="tabpanel", e o painel deve ter aria-labelledby a apontar para o tab correspondente. Um tab selecionado visualmente pode ser indistinguível programaticamente sem estes atributos.




Componentes Forge e custom: listas sem opções anunciadas

Nos componentes Forge e custom, o padrão é diferente mas igualmente comum: role="listbox" no contentor sem role="option" nos itens. O leitor de ecrã anuncia que existe uma lista mas não consegue ler o que está dentro dela.

As extended properties do OutSystems tornam a adição de atributos ARIA direta quando se sabe o que adicionar. O mecanismo existe, é uma vantagem real da plataforma. O que falta é saber exatamente o que cada padrão requer.




Como testar

Inspecionar o DOM com o modal aberto: verificar se role="dialog", aria-modal e aria-labelledby estão presentes e corretos. Abrir o modal com teclado e confirmar que o foco se move para dentro dele imediatamente e que não é possível sair com Tab sem o fechar. Ao fechar, o foco deve regressar ao elemento que desencadeou a abertura.

Para os Tabs, navegar entre eles com as teclas de seta e verificar o que o leitor de ecrã anuncia. Deve indicar qual o tab ativo e quantos existem, por exemplo, "Detalhes, separador selecionado, 1 de 3". Para dropdowns, abrir a lista e percorrer os itens: se o leitor de ecrã anunciar apenas "lista" sem ler as opções, o role="option" está ausente.




3. Navegação por teclado: foco invisível ou preso

Estes são dois problemas distintos que frequentemente se confundem e importa separar porque têm causas e soluções diferentes.

Foco invisível

É o mais comum e o mais rápido de resolver. Em muitos projetos OutSystems que revemos, o EnableAccessibilityFeatures está a False, e isso sozinho suprime os indicadores de foco em toda a aplicação. O que fica por fazer depois é garantir que overrides de CSS no tema personalizado ou componentes Forge não voltam a suprimir o outline. Para esse ajuste fino, existe uma técnica CSS padrão (não específica de OutSystems) que resolve de forma sistemática: :focus-visible, aplicado ao nível do tema.


CSS

:focus-visible {

outline: var(--border-size-m) solid #005FCC;

outline-offset: 2px;

}




A vantagem do :focus-visible sobre o :focus simples é que o indicador aparece para utilizadores de teclado mas não para interações de rato, o que elimina a objeção estética sem comprometer a acessibilidade.

Nota: Deve ter-se em conta os critérios de contraste mínimo: indicador de foco só precisa de 3:1 contra o fundo, 1.4.11 Non-Text Contrast.




Foco preso

É mais insidioso. Em OutSystems, isto acontece tipicamente no Side Panel, no Popup e menus de navegação mobile com animações de entrada. O foco entra no componente mas não fica contido, ou fica contido mas sem forma de sair. O utilizador de teclado fica bloqueado sem qualquer indicação visual do que aconteceu. Este problema é completamente invisível em revisão visual e em testes automáticos. Só aparece ao realizar testes manuais com teclado.

Uma nota sobre componentes Forge que se aplica a ambos os problemas: ser bem avaliado ou amplamente utilizado não é garantia de conformidade. Vemos regularmente componentes Forge populares com foco invisível ou preso. Qualquer componente Forge deve ser testado explicitamente antes de entrar em produção e não ser assumido como acessível.




Como testar

Percorrer toda a aplicação com Tab, Shift+Tab, Enter, Space e teclas de seta. O foco deve ser sempre visível e o percurso deve ser lógico. Para foco preso: abrir um Side Panel ou Popup e tentar fechar com Escape. Se Escape não funcionar, ou se o foco sair do componente sem o fechar, há um problema. Se em algum ponto não for claro onde está o foco, mesmo com os olhos no ecrã, o componente falha.




Checklist rápida antes de publicar

Antes de dar uma aplicação como pronta, confirme:

  • EnableAccessibilityFeatures está ativo nos layouts aplicáveis;
  • Labels e campos estão programaticamente associados (for / id);
  • Mensagens de erro estão ligadas aos campos via aria-describedby;
  • Modais e painéis gerem o foco na abertura e no fecho (focus trap);
  • Tabs, dropdowns e componentes custom têm a estrutura ARIA completa (role, aria-selected, aria-controls, etc.);
  • O foco é sempre visível e a ordem de navegação por teclado é lógica;
  • Componentes Forge foram testados manualmente, não assumidos como acessíveis.



O que muda quando isto é tratado desde o início

Acessibilidade não atrasa projetos (e muito menos em OutSystems). A falta de processo é que os atrasa, quando os problemas aparecem tarde, em auditoria, em produção ou em reclamação.

O que este artigo descreve não são exceções nem casos limite. São padrões que se repetem. Os dados de auditorias a várias aplicações OutSystems mostram um padrão consistente: formulários sem associação semântica, foco não gerido e componentes com ARIA incompleto estão sistematicamente entre os problemas mais frequentes.

Construir rápido em OutSystems é o primeiro passo. Construir para todos acontece ao mesmo tempo, se soubermos para onde olhar.






Referências técnicas

WCAG 2.2 - critérios de conformidade

WAI-ARIA Authoring Practices Guide - Dialog (Modal) Pattern

WAI-ARIA Authoring Practices Guide - Tabs Pattern

Documentação OutSystems - Accessibility (EnableAccessibilityFeatures)


FAQ

É um parâmetro de entrada no bloco de layout. Quando definido como True, ativa um conjunto base de funcionalidades de acessibilidade nos layouts do OutSystems UI: indicadores de foco visíveis, skip-to-content, links acessíveis e melhor contraste. Não corrige, por si só, a semântica dos componentes, o foco dos modais ou as associações entre campos e mensagens de erro.

A proximidade visual entre um Label e um Input não cria uma associação programática. No OS11, essa ligação é feita através da propriedade Input Widget do widget Label. Como o ID gerado pode mudar após a publicação, valide a associação no resultado final, e não apenas no Service Studio. As mensagens de erro também devem ser associadas ao respetivo campo através de aria-describedby, algo que o OutSystems não adiciona por defeito.

Parcialmente. Se o Popup tiver a classe has-accessible-features, a plataforma move o foco para dentro do popup ao abrir e devolve-o ao elemento que acionou a abertura ao fechar. O que não faz é manter o foco dentro do modal. O focus trap tem de ser adicionado manualmente através de JavaScript. A classe "has-accessibility-features" não é herdada do parâmetro do layout, tem de ser adicionada às Style Classes do widget Popup.

O padrão Tabs não é totalmente compatível com ARIA por defeito: role="tabpanel" não está alinhado com o elemento semântico <article> utilizado para os painéis. A OutSystems documenta uma correção: manter a estrutura <article> e mover os atributos de acessibilidade para um <div> interno, aplicado através de um nó JavaScript no OnReady. Valide de acordo com a versão de OutSystems UI utilizada, uma vez que o comportamento sofreu alterações entre versões.

EnableAccessibilityFeatures ativado, labels associados através de for/id, erros associados através de aria-describedby, modais com gestão de foco, Tabs/componentes com ARIA completo, foco sempre visível, componentes Forge testados manualmente.