Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion biostar/forum/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
139 changes: 125 additions & 14 deletions biostar/forum/templates/forms/field_tags.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,131 @@

{% if initial %}

<input type="hidden" name="{{ form_field.name }}" id="{{ form_field.id_for_label }}" value="{{ initial }}">
{% else %}
<input type="hidden" name="{{ form_field.name }}" id="{{ form_field.id_for_label }}">

{% endif %}

<select multiple="multiple" data-value="{{ form_field.id_for_label }}" class="ui search selection dropdown tags">
<option value="">Search tags</option>
<!-- Use native select for better screen reader accessibility -->
<select multiple="multiple"
name="{{ form_field.name }}"
id="{{ form_field.id_for_label }}"
class="ui search selection dropdown tags accessible-dropdown"
aria-label="Select post tags. Use arrow keys to navigate, space or enter to select. Hold Ctrl or Cmd to select multiple tags."
aria-required="true"
aria-describedby="tag_val_help"
data-placeholder="Select tags"
tabindex="0">
{% for value, is_selected in dropdown_options %}

<option value="{{ value }}" {% if is_selected %}selected="selected"{% endif %}>
{{ value }}
{{ value }}
</option>

{% endfor %}
</select>

<!-- Screen reader announcement region -->
<div id="tags-announcement" role="status" aria-live="polite" aria-atomic="true" style="position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;"></div>

</select>
<script>
(function() {
// Initialize Semantic UI dropdown with accessibility enhancements
$(document).ready(function() {
var $dropdown = $('.ui.dropdown.tags.accessible-dropdown');

// Don't initialize if dropdown is not found
if ($dropdown.length === 0) return;

// Initialize with accessibility-friendly settings
$dropdown.dropdown({
allowAdditions: true,
hideAdditions: false,
forceSelection: false,
// Preserve the select element for screen readers
preserveHTML: true,
// Make sure keyboard navigation works
keys: {
delimiter: false, // Don't use delimiter
deleteKey: 8, // backspace
leftArrow: 37,
upArrow: 38,
rightArrow: 39,
downArrow: 40,
enter: 13,
escape: 27,
tab: 9
},
// Callback after dropdown is shown
onShow: function() {
enhanceAccessibleDropdownAria();
},
// Ensure the original select stays synchronized
onChange: function(value, text, $choice) {
// Ensure the hidden select is properly updated
$dropdown.val(value).trigger('change');

// Update ARIA attributes
enhanceAccessibleDropdownAria();

// Announce changes to screen readers (just the tag values, no prefix)
var announcement = Array.isArray(value) ? value.join(', ') : (value || '');
if (announcement) {
$('#tags-announcement').text(announcement);
}
}
});

// Function to enhance ARIA attributes on the Semantic UI generated elements
function enhanceAccessibleDropdownAria() {
// Ensure the dropdown element has proper ID
if (!$dropdown.attr('id')) {
$dropdown.attr('id', 'tag_val');
}

// Set role and ARIA attributes on the main dropdown container
$dropdown.attr('role', 'combobox');
$dropdown.attr('aria-haspopup', 'listbox');
$dropdown.attr('aria-multiselectable', 'true');

// 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');
}
});
}
}

// Make the dropdown icon decorative only
$dropdown.siblings('.dropdown.icon').attr('aria-hidden', 'true');

// Initial ARIA enhancement
enhanceAccessibleDropdownAria();

// Set initial selection if provided
{% if initial %}
var initialTags = '{{ initial }}'.split(',').map(function(tag) { return tag.trim(); }).filter(function(tag) { return tag; });
if (initialTags.length > 0) {
$dropdown.dropdown('set selected', initialTags);
}
{% endif %}
});
})();
</script>
4 changes: 2 additions & 2 deletions biostar/forum/templates/new_post.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<div class="required field">
<label>{{ form.post_type.label }}</label>
{{ form.post_type }}
<p class="muted" style="display: contents; ">{{ form.post_type.help_text }} Click here for
<p class="muted" id="post_type_help" style="display: contents; ">{{ form.post_type.help_text }} Click here for
more</p> {% include 'forms/help_text.html' %}
</div>

Expand All @@ -51,7 +51,7 @@
{% block new_tag_val %}
{{ form.tag_val }}
{% endblock %}
<p class="muted">{{ form.tag_val.help_text }}</p>
<p class="muted" id="tag_val_help">{{ form.tag_val.help_text }}</p>
</div>


Expand Down
8 changes: 6 additions & 2 deletions biostar/forum/templatetags/forum_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 104 additions & 0 deletions themes/bioconductor/static/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading