Skip to content

Commit

Permalink
Remove customized (unmaintained) dropdown, improve aria a11y for drop…
Browse files Browse the repository at this point in the history
…down
  • Loading branch information
wxiaoguang committed Jun 1, 2022
1 parent 0e51694 commit 0da4f05
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 4,448 deletions.
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,6 @@ fomantic:
cd $(FOMANTIC_WORK_DIR) && npm install --no-save
cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config
cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/
cp -f web_src/js/vendor/dropdown.js $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/definitions/modules
cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build
rm -f $(FOMANTIC_WORK_DIR)/build/*.min.*

Expand Down
104 changes: 2 additions & 102 deletions web_src/fomantic/build/semantic.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 84 additions & 0 deletions web_src/js/features/aria.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import $ from 'jquery';

let ariaIdCounter = 0;

function generateAriaId() {
return `_aria_auto_id_${ariaIdCounter++}`;
}

// make the item has role=option, and add an id if there wasn't one yet.
function prepareMenuItem($item) {
$item.attr({'role': 'option'});
if (!$item.attr('id')) $item.attr('id', generateAriaId());
}

// when the menu items are loaded from AJAX requests, the items are created dynamically
const defaultCreateDynamicMenu = $.fn.dropdown.settings.templates.menu;
$.fn.dropdown.settings.templates.menu = function(response, fields, preserveHTML, className) {
const ret = defaultCreateDynamicMenu(response, fields, preserveHTML, className);
const $wrapper = $('<div>').append(ret);
const $items = $wrapper.find('> .item');
$items.each((_, item) => {
prepareMenuItem($(item));
});
return $wrapper.html();
};

function attachOneDropdownAria($dropdown) {
const $textSearch = $dropdown.find('input.search').eq(0);
const $focusable = $textSearch.length ? $textSearch : $dropdown; // see comment below
if (!$focusable.length) return;

// prepare menu list
const $menu = $dropdown.find('> .menu');
if (!$menu.attr('id')) $menu.attr('id', generateAriaId());
$menu.attr('role', 'listbox');

// dropdown has 2 different focusing behaviors
// * with search input: the input is focused, and it works perfectly with aria-activedescendant pointing another sibling element.
// * without search input (but the readonly text), the dropdown itself is focused. then the aria-activedescendant points to the element inside dropdown,
// which make the UI flicking when navigating between list options, that's the best effect at the moment.

$focusable.attr({
'role': 'combobox',
'aria-controls': $menu.attr('id'),
'aria-expanded': 'false',
});

$menu.find('> .item').each((_, item) => {
prepareMenuItem($(item));
});

// update aria attributes according current active/selected item
const refreshAria = () => {
const isMenuVisible = !$menu.is('.hidden') && !$menu.is('.animating.out');
$focusable.attr('aria-expanded', isMenuVisible ? 'true' : 'false');

let $active = $menu.find('> .item.active');
if (!$active.length) $active = $menu.find('> .item.selected'); // it's strange that we need this fallback at the moment

// if there is an active item, use its id. if no active item, then the empty string is set
$focusable.attr('aria-activedescendant', $active.attr('id'));
};

// use setTimeout to run the refreshAria in next tick
$focusable.on('focus', () => {
setTimeout(refreshAria, 0);
});
$focusable.on('mouseup', () => {
setTimeout(refreshAria, 0);
});
$focusable.on('blur', () => {
setTimeout(refreshAria, 0);
});
$dropdown.on('keyup', (e) => {
const key = e.key;
if (key === 'Tab' || key === 'Space' || key === 'Enter' || key.startsWith('Arrow')) {
setTimeout(refreshAria, 0);
}
});
}

export function attachDropdownAria($dropdowns) {
$dropdowns.each((_, e) => attachOneDropdownAria($(e)));
}
40 changes: 40 additions & 0 deletions web_src/js/features/aria.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
**This document is used as aria/a11y reference for future developers**

ARIA Dropdown:

```html
<div>
<input role="combobox" aria-haspopup="listbox" aria-expanded="false" aria-controls="the-menu-listbox" aria-activedescendant="item-id-123456">
<ul id="the-menu-listbox" role="listbox">
<li role="option" id="item-id-123456" aria-selected="true">
<a tabindex="-1" href="....">....</a>
</li>
</ul>
</div>
```


Fomantic UI Dropdown:

```html
<!-- read-only dropdown -->
<div class="ui dropdown"> <!-- focused here, then it's not perfect to use aria-activedescendant to point to the menu item -->
<input type="hidden" ...>
<div class="text">Default</div>
<div class="menu transition hidden" tabindex="-1">
<div class="item active selected">Default</div>
<div class="item">...</div>
</div>
</div>

<!-- search input dropdown -->
<div class="ui dropdown">
<input type="hidden" ...>
<input class="search" autocomplete="off" tabindex="0"> <!-- focused here -->
<div class="text"></div>
<div class="menu transition visible" tabindex="-1">
<div class="item selected">...</div>
<div class="item">...</div>
</div>
</div>
```
12 changes: 8 additions & 4 deletions web_src/js/features/common-global.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {mqBinarySearch} from '../utils.js';
import createDropzone from './dropzone.js';
import {initCompColorPicker} from './comp/ColorPicker.js';
import {showGlobalErrorMessage} from '../bootstrap.js';
import {attachDropdownAria} from './aria.js';

const {appUrl, csrfToken} = window.config;

Expand Down Expand Up @@ -97,24 +98,27 @@ export function initGlobalCommon() {
}

// Semantic UI modules.
$('.dropdown:not(.custom)').dropdown({
const $uiDropdowns = $('.ui.dropdown');
$uiDropdowns.filter(':not(.custom)').dropdown({
fullTextSearch: 'exact'
});
$('.jump.dropdown').dropdown({
$uiDropdowns.filter('.jump').dropdown({
action: 'hide',
onShow() {
$('.tooltip').popup('hide');
},
fullTextSearch: 'exact'
});
$('.slide.up.dropdown').dropdown({
$uiDropdowns.filter('.slide.up').dropdown({
transition: 'slide up',
fullTextSearch: 'exact'
});
$('.upward.dropdown').dropdown({
$uiDropdowns.filter('.upward').dropdown({
direction: 'upward',
fullTextSearch: 'exact'
});
attachDropdownAria($uiDropdowns);

$('.ui.checkbox').checkbox();

// init popups
Expand Down
Loading

0 comments on commit 0da4f05

Please sign in to comment.