diff --git a/biostar/forum/forms.py b/biostar/forum/forms.py index 1bddad0a9..b50327b76 100644 --- a/biostar/forum/forms.py +++ b/biostar/forum/forms.py @@ -168,7 +168,12 @@ class PostLongForm(forms.Form): choices = informative_choices(choices=choices) post_type = forms.IntegerField(label="Post Type", - widget=forms.Select(choices=choices, attrs={'class': "ui dropdown"}), + widget=forms.Select(choices=choices, attrs={ + 'class': "ui dropdown", + 'aria-label': "Select post type", + 'aria-required': "true", + 'aria-describedby': "post_type_help" + }), help_text="Select a post type.") title = forms.CharField(label="Post Title", max_length=200, min_length=2, validators=[valid_title, validate_ascii], diff --git a/biostar/forum/templates/forms/field_tags.html b/biostar/forum/templates/forms/field_tags.html index 571df2652..7268b5722 100644 --- a/biostar/forum/templates/forms/field_tags.html +++ b/biostar/forum/templates/forms/field_tags.html @@ -1,20 +1,131 @@ - {% if initial %} - - - {% else %} - - - {% endif %} - - + diff --git a/biostar/forum/templates/new_post.html b/biostar/forum/templates/new_post.html index b3a411b77..abf8c0f3b 100644 --- a/biostar/forum/templates/new_post.html +++ b/biostar/forum/templates/new_post.html @@ -41,7 +41,7 @@
{{ form.post_type.help_text }} Click here for +
{{ form.post_type.help_text }} Click here for more
{% include 'forms/help_text.html' %}{{ form.tag_val.help_text }}
+{{ form.tag_val.help_text }}
diff --git a/biostar/forum/templatetags/forum_tags.py b/biostar/forum/templatetags/forum_tags.py index 899cc0e75..e9a6069c4 100644 --- a/biostar/forum/templatetags/forum_tags.py +++ b/biostar/forum/templatetags/forum_tags.py @@ -320,8 +320,12 @@ def inplace_type_field(post=None, field_id='type'): (opt[0] in Post.TOP_LEVEL), choices) post_type = forms.IntegerField(label="Post Type", - widget=forms.Select(choices=choices, attrs={'class': "ui fluid dropdown", - 'id': field_id}), + widget=forms.Select(choices=choices, attrs={ + 'class': "ui fluid dropdown", + 'id': field_id, + 'aria-label': "Select post type", + 'aria-required': "true" + }), help_text="Select a post type.") value = post.type if post else Post.QUESTION diff --git a/themes/bioconductor/static/theme.css b/themes/bioconductor/static/theme.css index 57b662019..7812d71cf 100644 --- a/themes/bioconductor/static/theme.css +++ b/themes/bioconductor/static/theme.css @@ -118,3 +118,107 @@ body { background-color: #87B13F !important; color: white; } + +/* ======================================== + Accessibility improvements for dropdowns + ======================================== */ + +/* Keep native select accessible for screen readers while Semantic UI enhances it */ +.ui.dropdown.accessible-dropdown { + /* Ensure the select element is not completely hidden */ + position: relative !important; +} + +/* Ensure native select elements are accessible when JavaScript is disabled */ +.ui.dropdown select { + opacity: 1 !important; + position: static !important; + height: auto !important; + width: 100% !important; + visibility: visible !important; +} + +/* When Semantic UI is active, keep select minimally visible for screen readers */ +.ui.dropdown.accessible-dropdown.active select, +.ui.dropdown.accessible-dropdown.visible select { + /* Keep it accessible but visually hidden when dropdown is enhanced */ + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +/* Style native selects to look consistent */ +.ui.dropdown.disabled select, +noscript .ui.dropdown select, +.no-js .ui.dropdown select { + display: block !important; + opacity: 1 !important; + position: static !important; + height: auto !important; + width: 100% !important; + visibility: visible !important; + background: white; + border: 1px solid rgba(34,36,38,.15); + border-radius: .28571429rem; + padding: .67857143em 1em; + font-size: 1em; + line-height: 1.21428571em; + min-height: 80px; +} + +/* Hide the custom dropdown UI when JavaScript is disabled */ +noscript .ui.dropdown .text, +noscript .ui.dropdown .dropdown.icon, +.no-js .ui.dropdown .text, +.no-js .ui.dropdown .dropdown.icon { + display: none !important; +} + +/* Ensure focus is visible on all interactive elements */ +.ui.dropdown:focus, +.ui.dropdown:focus-within, +.ui.dropdown select:focus, +.ui.dropdown.accessible-dropdown:focus-within { + outline: 2px solid #0066cc !important; + outline-offset: 2px !important; +} + +/* Improve screen reader announcements */ +.sr-only, +[role="status"][aria-live="polite"] { + position: absolute; + left: -10000px; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; +} + +/* Ensure form validation messages are accessible */ +.field .error.message { + color: #9f3a38; + font-weight: 600; +} + +.field .error.message:before { + content: "Error: "; + font-weight: bold; +} + +/* Ensure dropdown menu items are keyboard accessible */ +.ui.dropdown .menu > .item { + cursor: pointer; +} + +.ui.dropdown .menu > .item:focus, +.ui.dropdown .menu > .item.selected { + background: rgba(0, 0, 0, 0.05) !important; + outline: 2px solid #0066cc !important; + outline-offset: -2px !important; +} diff --git a/themes/bioconductor/static/theme.js b/themes/bioconductor/static/theme.js index d2309a715..06e170211 100644 --- a/themes/bioconductor/static/theme.js +++ b/themes/bioconductor/static/theme.js @@ -1,7 +1,24 @@ +// Enhanced tags dropdown with accessibility support function tags_dropdown() { - $('.tags').dropdown({ allowAdditions: true, + forceSelection: false, + // Make keyboard navigation work better + keys: { + delimiter: false, + deleteKey: 8, + leftArrow: 37, + upArrow: 38, + rightArrow: 39, + downArrow: 40, + enter: 13, + escape: 27, + tab: 9 + }, + // Callback after dropdown is shown + onShow: function() { + enhance_tags_dropdown_aria($(this)); + }, // Get form field to add to onChange: function (value, text, $selectedItem) { // Get form field to add to @@ -11,10 +28,35 @@ function tags_dropdown() { // Set text instead of value value = $('').text(value).html(); tag_field.val(value); + + // Update ARIA attributes + enhance_tags_dropdown_aria($(this)); + + // Announce to screen readers (just the tag text, no prefix) + if (!$('#tags-announcement').length) { + $('body').append(''); + } + $('#tags-announcement').text(text); + } + }); + + // Ensure dropdown has proper ARIA attributes + $('.tags').each(function() { + var $dropdown = $(this); + $dropdown.attr('role', 'combobox'); + $dropdown.attr('aria-haspopup', 'listbox'); + if ($dropdown.attr('multiple')) { + $dropdown.attr('aria-multiselectable', 'true'); } }); + $('.tags .dropdown.icon').attr('aria-hidden', 'true'); + + // Initial ARIA enhancement + $('.tags').each(function() { + enhance_tags_dropdown_aria($(this)); + }); + $('.tags > input.search').keydown(function (event) { - // Prevent submitting form when adding tag by pressing ENTER. var ek = event.keyCode || event.which; var value = $(this).val().trim(); @@ -31,11 +73,170 @@ function tags_dropdown() { return value } }) +} + +// Enhance tags dropdown with proper ARIA attributes +function enhance_tags_dropdown_aria($dropdown) { + // Find the menu element + var $menu = $dropdown.children('.menu'); + if ($menu.length > 0) { + // Check if menu is visible + var isVisible = $menu.hasClass('visible') || $menu.hasClass('active'); + $dropdown.attr('aria-expanded', isVisible ? 'true' : 'false'); + + // Ensure menu has proper ID + if (!$menu.attr('id')) { + var dropdownId = $dropdown.attr('id') || 'tags-dropdown-' + Math.random().toString(36).substr(2, 9); + $dropdown.attr('id', dropdownId); + $menu.attr('id', dropdownId + '-menu'); + } + + // Set role on menu + $menu.attr('role', 'listbox'); + + // Link dropdown to its menu + $dropdown.attr('aria-controls', $menu.attr('id')); + + // Enhance menu items + $menu.find('.item').each(function(index) { + var $item = $(this); + $item.attr('role', 'option'); + $item.attr('tabindex', '-1'); + + // Mark selected items + if ($item.hasClass('selected') || $item.hasClass('active')) { + $item.attr('aria-selected', 'true'); + } else { + $item.attr('aria-selected', 'false'); + } + }); + } +} +// Initialize all dropdowns with accessibility features +function init_accessible_dropdowns() { + // Initialize all Semantic UI dropdowns with accessibility enhancements + $('.ui.dropdown:not(.tags):not(.accessible-dropdown)').each(function() { + var $dropdown = $(this); + + $dropdown.dropdown({ + // Preserve accessibility + forceSelection: false, + keys: { + delimiter: false, + deleteKey: 8, + leftArrow: 37, + upArrow: 38, + rightArrow: 39, + downArrow: 40, + enter: 13, + escape: 27, + tab: 9 + }, + // Callback after dropdown is initialized and shown + onShow: function() { + enhance_dropdown_aria($dropdown); + }, + onChange: function(value, text, $selectedItem) { + // Update ARIA attributes when selection changes + enhance_dropdown_aria($dropdown); + + // Announce selection to screen readers (just the option text, no prefix) + if (!$('#dropdown-announcement').length) { + $('body').append(''); + } + $('#dropdown-announcement').text(text); + } + }); + + // Ensure proper ARIA attributes + if (!$dropdown.attr('aria-label')) { + var label = $dropdown.closest('.field').find('label').text(); + if (label) { + $dropdown.attr('aria-label', label); + } + } + + // Make dropdown icons decorative + $dropdown.find('.dropdown.icon').attr('aria-hidden', 'true'); + + // Initial ARIA enhancement + enhance_dropdown_aria($dropdown); + }); +} + +// Enhance Semantic UI dropdown with proper ARIA attributes for screen readers +function enhance_dropdown_aria($dropdown) { + // Ensure the dropdown element has proper ID + if (!$dropdown.attr('id')) { + var randomId = 'dropdown-' + Math.random().toString(36).substr(2, 9); + $dropdown.attr('id', randomId); + } + + // Set role and ARIA attributes on the main dropdown container + $dropdown.attr('role', 'combobox'); + $dropdown.attr('aria-haspopup', 'listbox'); + + // Find the menu element + var $menu = $dropdown.children('.menu'); + if ($menu.length > 0) { + // Check if menu is visible + var isVisible = $menu.hasClass('visible') || $menu.hasClass('active'); + $dropdown.attr('aria-expanded', isVisible ? 'true' : 'false'); + + // Ensure menu has proper ID + if (!$menu.attr('id')) { + $menu.attr('id', $dropdown.attr('id') + '-menu'); + } + + // Set role on menu + $menu.attr('role', 'listbox'); + + // Link dropdown to its menu + $dropdown.attr('aria-controls', $menu.attr('id')); + + // Enhance menu items + $menu.find('.item').each(function(index) { + var $item = $(this); + $item.attr('role', 'option'); + $item.attr('tabindex', '-1'); + + // Mark selected items + if ($item.hasClass('selected') || $item.hasClass('active')) { + $item.attr('aria-selected', 'true'); + } else { + $item.attr('aria-selected', 'false'); + } + }); + } + + // Update aria-label to reflect current selection without repetition + var currentText = $dropdown.find('.text').text().trim(); + var placeholder = $dropdown.data('placeholder') || ''; + + // Only update if there's a valid selection (not placeholder text) + if (currentText && currentText !== placeholder && currentText !== 'Select post type' && currentText !== '') { + // Use a clean aria-label with just the current selection + $dropdown.attr('aria-label', currentText); + } else { + // Reset to base label when no selection + var baseLabel = $dropdown.attr('data-base-label'); + if (!baseLabel) { + // Store the original aria-label on first run + baseLabel = $dropdown.attr('aria-label') || 'Select option'; + $dropdown.attr('data-base-label', baseLabel); + } + $dropdown.attr('aria-label', baseLabel); + } } $(document).ready(function () { + // Initialize tags dropdown tags_dropdown(); + + // Initialize all other dropdowns with accessibility + init_accessible_dropdowns(); + function cancel_answers() { $('.answer-text').hide(); $('.answer-text textarea').val('')