/** * Accessibility Controller * Enhances custom JavaScript input components with proper ARIA attributes and keyboard support * * @author Leantime Team * @copyright 2024 Leantime */ leantime.accessibilityController = (function () { /** * Enhance Chosen dropdowns with ARIA attributes */ var enhanceChosenAccessibility = function() { jQuery('.chosen-container').each(function() { var $container = jQuery(this); // Skip if already enhanced if ($container.data('a11y-enhanced')) { return; } var $originalSelect = null; // Try to find the original select element var containerId = $container.attr('id'); if (containerId && containerId.indexOf('_chosen') > -1) { // Standard case: container has ID like "status-select_chosen" var selectId = containerId.replace('_chosen', ''); $originalSelect = jQuery('#' + selectId); } // Fallback: look for a hidden select element that precedes this container if (!$originalSelect || !$originalSelect.length) { $originalSelect = $container.prev('select[data-placeholder]'); } // Second fallback: look for any hidden select near this container if (!$originalSelect || !$originalSelect.length) { $originalSelect = $container.siblings('select').first(); } // If we still can't find the select, skip this container if (!$originalSelect || !$originalSelect.length) { return; } // Get label text var labelText = ''; var selectId = $originalSelect.attr('id'); if (selectId) { var $label = jQuery('label[for="' + selectId + '"]'); if ($label.length) { labelText = $label.text().trim(); } } // Set ARIA attributes on Chosen container var $chosenSingle = $container.find('.chosen-single'); var $chosenChoices = $container.find('.chosen-choices'); if ($chosenSingle.length) { // Single select $chosenSingle.attr({ 'role': 'combobox', 'aria-haspopup': 'listbox', 'aria-expanded': 'false', 'aria-label': labelText || $originalSelect.attr('data-placeholder') || 'Select option', 'tabindex': '0' }); } if ($chosenChoices.length) { // Multi-select $chosenChoices.attr({ 'role': 'combobox', 'aria-haspopup': 'listbox', 'aria-expanded': 'false', 'aria-label': labelText || $originalSelect.attr('data-placeholder') || 'Select options', 'aria-multiselectable': 'true', 'tabindex': '0' }); } // Update aria-expanded on open/close $container.on('chosen:showing_dropdown', function() { $chosenSingle.add($chosenChoices).attr('aria-expanded', 'true'); }); $container.on('chosen:hiding_dropdown', function() { $chosenSingle.add($chosenChoices).attr('aria-expanded', 'false'); }); // Set role on dropdown $container.find('.chosen-drop').attr('role', 'listbox'); $container.find('.chosen-results li').attr('role', 'option'); // Mark as enhanced $container.data('a11y-enhanced', true); }); }; /** * Enhance SlimSelect with ARIA attributes */ var enhanceSlimSelectAccessibility = function() { jQuery('.ss-main').each(function() { var $ssMain = jQuery(this); var $originalSelect = $ssMain.prev('select'); if (!$originalSelect.length) { return; } var $label = jQuery('label[for="' + $originalSelect.attr('id') + '"]'); var labelText = $label.length ? $label.text().trim() : ''; $ssMain.attr({ 'role': 'combobox', 'aria-haspopup': 'listbox', 'aria-label': labelText || $originalSelect.attr('data-placeholder') || 'Select option', 'aria-multiselectable': $originalSelect.attr('multiple') ? 'true' : 'false' }); }); }; /** * Enhance TagsInput with ARIA attributes */ var enhanceTagsInputAccessibility = function() { jQuery('div.tagsinput').each(function() { var $tagsInput = jQuery(this); var $originalInput = $tagsInput.next('input[type="text"]'); if (!$originalInput.length) { return; } var inputId = $originalInput.attr('id'); var $label = jQuery('label[for="' + inputId + '"]'); var labelText = $label.length ? $label.text().trim() : 'Enter tags'; $tagsInput.attr({ 'role': 'list', 'aria-label': labelText }); // Set role on individual tags $tagsInput.find('span.tag').each(function() { jQuery(this).attr('role', 'listitem'); }); // Make tag input accessible var $input = $tagsInput.find('input'); $input.attr({ 'aria-label': 'Add new tag', 'aria-describedby': inputId + '-help' }); // Add help text if doesn't exist if (inputId && !jQuery('#' + inputId + '-help').length) { $tagsInput.after( '' + 'Type and press enter to add tags. Press backspace to remove the last tag.' + '' ); } }); }; /** * Enhance Datepickers with ARIA attributes */ var enhanceDatepickerAccessibility = function() { jQuery('input.hasDatepicker').each(function() { var $input = jQuery(this); var inputId = $input.attr('id'); var $label = jQuery('label[for="' + inputId + '"]'); var labelText = $label.length ? $label.text().trim() : ''; $input.attr({ 'role': 'textbox', 'aria-label': labelText || 'Select date', 'aria-describedby': inputId + '-help' }); // Add help text if doesn't exist if (inputId && !jQuery('#' + inputId + '-help').length) { $input.after( '' + 'Date input. Use arrow keys to navigate calendar. Press enter to select date.' + '' ); } }); // Enhance datepicker widget when it opens jQuery(document).on('focus', 'input.hasDatepicker', function() { setTimeout(function() { var $widget = jQuery('#ui-datepicker-div'); if ($widget.is(':visible')) { $widget.attr({ 'role': 'dialog', 'aria-label': 'Choose date', 'aria-modal': 'true' }); } }, 100); }); }; /** * Fix time picker label associations */ var fixTimepickerLabels = function() { jQuery('input[type="time"]').each(function() { var $timeInput = jQuery(this); var id = $timeInput.attr('id'); if (!id) { return; } // If no label exists, create ARIA label from context if (!jQuery('label[for="' + id + '"]').length) { var labelText = 'Time'; // Try to infer from nearby elements var $prevLabel = $timeInput.closest('.form-group').find('label').first(); if ($prevLabel.length) { labelText = $prevLabel.text().trim() + ' time'; } $timeInput.attr('aria-label', labelText); } }); }; /** * Make kanban cards keyboard accessible */ var enhanceKanbanCardAccessibility = function() { jQuery('.ticketBox').each(function() { var $card = jQuery(this); if (!$card.attr('tabindex')) { $card.attr('tabindex', '0'); } var headline = $card.find('.ticketHeadline').text().trim(); if (headline) { $card.attr({ 'role': 'article', 'aria-label': 'Task: ' + headline }); } // Make card clickable with keyboard (only if not already bound) if (!$card.data('keyboard-bound')) { $card.on('keydown', function(e) { // Only handle Enter/Space if the card itself has focus // Don't intercept events from interactive children (dropdowns, buttons, links, inputs) var $target = jQuery(e.target); // Check if the target is an interactive element var isInteractive = $target.is('a, button, input, select, textarea, [role="button"], [tabindex]') || $target.closest('.dropdown-toggle, .ticketDropdown, .inlineDropDownContainer').length > 0; // Only handle the event if the card itself was focused and not an interactive child if ((e.key === 'Enter' || e.key === ' ') && !isInteractive && e.target === this) { e.preventDefault(); var $link = $card.find('a').first(); if ($link.length) { $link[0].click(); } } }); $card.data('keyboard-bound', true); } }); }; /** * Initialize all accessibility enhancements */ var init = function() { // Run immediately on page load enhanceChosenAccessibility(); enhanceSlimSelectAccessibility(); enhanceTagsInputAccessibility(); enhanceDatepickerAccessibility(); fixTimepickerLabels(); enhanceKanbanCardAccessibility(); // Re-run when new content is loaded (HTMX, modals, etc.) jQuery(document).on('htmx:afterSwap shown.bs.modal', function() { setTimeout(function() { enhanceChosenAccessibility(); enhanceSlimSelectAccessibility(); enhanceTagsInputAccessibility(); enhanceDatepickerAccessibility(); fixTimepickerLabels(); enhanceKanbanCardAccessibility(); }, 100); }); // Re-run when Chosen is re-initialized jQuery(document).on('chosen:ready', function() { // Wait a bit longer to ensure Chosen is fully ready setTimeout(enhanceChosenAccessibility, 100); }); // Single retry after initial page load to catch late-initializing dropdowns setTimeout(enhanceChosenAccessibility, 1000); }; // Public API return { init: init, enhanceChosenAccessibility: enhanceChosenAccessibility, enhanceSlimSelectAccessibility: enhanceSlimSelectAccessibility, enhanceTagsInputAccessibility: enhanceTagsInputAccessibility, enhanceDatepickerAccessibility: enhanceDatepickerAccessibility, fixTimepickerLabels: fixTimepickerLabels, enhanceKanbanCardAccessibility: enhanceKanbanCardAccessibility }; })(); // Initialize on page load jQuery(document).ready(function() { leantime.accessibilityController.init(); });