Skip to content

Instantly share code, notes, and snippets.

@nolanlawson
Last active April 4, 2022 21:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nolanlawson/e63d2261406a15900ec6bd90e150e4b6 to your computer and use it in GitHub Desktop.
Save nolanlawson/e63d2261406a15900ec6bd90e150e4b6 to your computer and use it in GitHub Desktop.
Firefox NVDA display:contents repro
.annotate {
font-style: italic;
color: #366ed4;
}
.hidden {
display: none;
}
.combobox-wrapper {
display: inline-block;
position: relative;
font-size: 16px;
}
.combobox-label {
font-size: 14px;
font-weight: bold;
margin-right: 5px;
}
.listbox,
.grid {
min-width: 230px;
background: white;
border: 1px solid #ccc;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
top: 1.7em;
z-index: 1;
}
.listbox .result {
cursor: default;
margin: 0;
}
.grid .result-row {
padding: 2px;
cursor: default;
margin: 0;
}
.listbox .result:hover,
.grid .result-row:hover {
background: rgb(139, 189, 225);
}
.listbox .focused,
.grid .focused {
background: rgb(139, 189, 225);
}
.grid .focused-cell {
outline-style: dotted;
outline-color: green;
}
.combobox-wrapper input {
font-size: inherit;
border: 1px solid #aaa;
border-radius: 2px;
line-height: 1.5em;
padding-right: 30px;
width: 200px;
}
.combobox-dropdown {
position: absolute;
right: 0;
top: 0;
padding: 0 0 2px;
height: 1.5em;
border-radius: 0 2px 2px 0;
border: 1px solid #aaa;
}
.grid .result-cell {
display: inline-block;
cursor: default;
margin: 0;
padding: 0 5px;
}
.grid .result-cell:last-child {
float: right;
font-size: 12px;
font-weight: 200;
color: #333;
line-height: 24px;
}
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* ARIA Combobox Examples
*/
// via https://www.w3.org/TR/wai-aria-practices-1.1/examples/combobox/aria1.1pattern/listbox-combo.html
var FRUITS_AND_VEGGIES = [
'Apple',
'Artichoke',
'Asparagus',
'Banana',
'Beets',
'Bell pepper',
'Broccoli',
'Brussels sprout',
'Cabbage',
'Carrot',
'Cauliflower',
'Celery',
'Chard',
'Chicory',
'Corn',
'Cucumber',
'Daikon',
'Date',
'Edamame',
'Eggplant',
'Elderberry',
'Fennel',
'Fig',
'Garlic',
'Grape',
'Honeydew melon',
'Iceberg lettuce',
'Jerusalem artichoke',
'Kale',
'Kiwi',
'Leek',
'Lemon',
'Mango',
'Mangosteen',
'Melon',
'Mushroom',
'Nectarine',
'Okra',
'Olive',
'Onion',
'Orange',
'Parship',
'Pea',
'Pear',
'Pineapple',
'Potato',
'Pumpkin',
'Quince',
'Radish',
'Rhubarb',
'Shallot',
'Spinach',
'Squash',
'Strawberry',
'Sweet potato',
'Tomato',
'Turnip',
'Ugli fruit',
'Victoria plum',
'Watercress',
'Watermelon',
'Yam',
'Zucchini'
];
function searchVeggies (searchString) {
var results = [];
for (var i = 0; i < FRUITS_AND_VEGGIES.length; i++) {
var veggie = FRUITS_AND_VEGGIES[i].toLowerCase();
if (veggie.indexOf(searchString.toLowerCase()) === 0) {
results.push(FRUITS_AND_VEGGIES[i]);
}
}
return results;
}
/**
* @function onload
* @desc Initialize the combobox examples once the page has loaded
*/
window.addEventListener('load', function () {
var ex1Combobox = new aria.ListboxCombobox(
document.getElementById('ex1-combobox'),
document.getElementById('ex1-input'),
document.getElementById('ex1-listbox'),
searchVeggies,
false
);
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Repro Firefox display:contents issue</title>
<link rel="stylesheet" href="./combobox.css">
</head>
<body>
<h1>Repro Firefox display:contents issue</h1>
<div id="ex1">
<label for="ex1-input" id="ex1-label" class="combobox-label">
Choice 1 Fruit or Vegetable
</label>
<div class="combobox-wrapper">
<div role="combobox" aria-expanded="false" aria-owns="ex1-listbox" aria-haspopup="listbox" id="ex1-combobox">
<input type="text" aria-autocomplete="list" aria-controls="ex1-listbox" id="ex1-input" aria-activedescendant="">
</div>
<ul aria-labelledby="ex1-label" role="listbox" id="ex1-listbox" class="listbox hidden"></ul>
</div>
</div>
<script src="./utils.js"></script>
<script src="./listbox-combobox.js"></script>
<script src="./combobox.js"></script>
</body>
</html>
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*/
/**
* @constructor
*
* @desc
* Combobox object representing the state and interactions for a combobox
* widget
*
* @param comboboxNode
* The DOM node pointing to the combobox
* @param input
* The input node
* @param listbox
* The listbox node to load results in
* @param searchFn
* The search function. The function accepts a search string and returns an
* array of results.
*/
aria.ListboxCombobox = function (
comboboxNode,
input,
listbox,
searchFn,
shouldAutoSelect,
onShow,
onHide
) {
this.combobox = comboboxNode;
this.input = input;
this.listbox = listbox;
this.searchFn = searchFn;
this.shouldAutoSelect = shouldAutoSelect;
this.onShow = onShow || function () {};
this.onHide = onHide || function () {};
this.activeIndex = -1;
this.resultsCount = 0;
this.shown = false;
this.hasInlineAutocomplete =
input.getAttribute('aria-autocomplete') === 'both';
this.setupEvents();
};
aria.ListboxCombobox.prototype.setupEvents = function () {
document.body.addEventListener('click', this.checkHide.bind(this));
this.input.addEventListener('keyup', this.checkKey.bind(this));
this.input.addEventListener('keydown', this.setActiveItem.bind(this));
this.input.addEventListener('focus', this.checkShow.bind(this));
this.input.addEventListener('blur', this.checkSelection.bind(this));
this.listbox.addEventListener('click', this.clickItem.bind(this));
};
aria.ListboxCombobox.prototype.checkKey = function (evt) {
var key = evt.which || evt.keyCode;
switch (key) {
case aria.KeyCode.UP:
case aria.KeyCode.DOWN:
case aria.KeyCode.ESC:
case aria.KeyCode.RETURN:
evt.preventDefault();
return;
default:
this.updateResults(false);
}
if (this.hasInlineAutocomplete) {
switch (key) {
case aria.KeyCode.BACKSPACE:
return;
default:
this.autocompleteItem();
}
}
};
aria.ListboxCombobox.prototype.updateResults = function (shouldShowAll) {
var searchString = this.input.value;
var results = this.searchFn(searchString);
this.hideListbox();
if (!shouldShowAll && !searchString) {
results = [];
}
if (results.length) {
for (var i = 0; i < results.length; i++) {
var resultWrapper = document.createElement('div')
resultWrapper.style.display = 'contents'
resultWrapper.id = 'id-' + Math.floor(Math.random() * 10000000).toString(16)
var resultItem = document.createElement('li');
resultItem.className = 'result';
resultItem.setAttribute('role', 'option');
resultItem.setAttribute('id', 'result-item-' + i);
resultItem.innerText = results[i];
if (this.shouldAutoSelect && i === 0) {
resultItem.setAttribute('aria-selected', 'true');
aria.Utils.addClass(resultItem, 'focused');
this.activeIndex = 0;
}
resultWrapper.appendChild(resultItem);
this.listbox.appendChild(resultWrapper)
}
aria.Utils.removeClass(this.listbox, 'hidden');
this.combobox.setAttribute('aria-expanded', 'true');
this.resultsCount = results.length;
this.shown = true;
this.onShow();
}
};
aria.ListboxCombobox.prototype.setActiveItem = function (evt) {
var key = evt.which || evt.keyCode;
var activeIndex = this.activeIndex;
if (key === aria.KeyCode.ESC) {
this.hideListbox();
setTimeout((function () {
// On Firefox, input does not get cleared here unless wrapped in
// a setTimeout
this.input.value = '';
}).bind(this), 1);
return;
}
if (this.resultsCount < 1) {
if (this.hasInlineAutocomplete && (key === aria.KeyCode.DOWN || key === aria.KeyCode.UP)) {
this.updateResults(true);
}
else {
return;
}
}
var prevActive = this.getItemAt(activeIndex);
var activeItem;
switch (key) {
case aria.KeyCode.UP:
if (activeIndex <= 0) {
activeIndex = this.resultsCount - 1;
}
else {
activeIndex--;
}
break;
case aria.KeyCode.DOWN:
if (activeIndex === -1 || activeIndex >= this.resultsCount - 1) {
activeIndex = 0;
}
else {
activeIndex++;
}
break;
case aria.KeyCode.RETURN:
activeItem = this.getItemAt(activeIndex);
this.selectItem(activeItem);
return;
case aria.KeyCode.TAB:
this.checkSelection();
this.hideListbox();
return;
default:
return;
}
evt.preventDefault();
activeItem = this.getItemAt(activeIndex);
this.activeIndex = activeIndex;
if (prevActive) {
aria.Utils.removeClass(prevActive, 'focused');
prevActive.setAttribute('aria-selected', 'false');
}
if (activeItem) {
this.input.setAttribute(
'aria-activedescendant',
'result-item-' + activeIndex
);
aria.Utils.addClass(activeItem, 'focused');
activeItem.setAttribute('aria-selected', 'true');
if (this.hasInlineAutocomplete) {
this.input.value = activeItem.innerText;
}
}
else {
this.input.setAttribute(
'aria-activedescendant',
''
);
}
};
aria.ListboxCombobox.prototype.getItemAt = function (index) {
return document.getElementById('result-item-' + index);
};
aria.ListboxCombobox.prototype.clickItem = function (evt) {
if (evt.target && evt.target.nodeName == 'LI') {
this.selectItem(evt.target);
}
};
aria.ListboxCombobox.prototype.selectItem = function (item) {
if (item) {
this.input.value = item.innerText;
this.hideListbox();
}
};
aria.ListboxCombobox.prototype.checkShow = function (evt) {
this.updateResults(false);
};
aria.ListboxCombobox.prototype.checkHide = function (evt) {
if (evt.target === this.input || this.combobox.contains(evt.target)) {
return;
}
this.hideListbox();
};
aria.ListboxCombobox.prototype.hideListbox = function () {
this.shown = false;
this.activeIndex = -1;
this.listbox.innerHTML = '';
aria.Utils.addClass(this.listbox, 'hidden');
this.combobox.setAttribute('aria-expanded', 'false');
this.resultsCount = 0;
this.input.setAttribute(
'aria-activedescendant',
''
);
this.onHide();
};
aria.ListboxCombobox.prototype.checkSelection = function () {
if (this.activeIndex < 0) {
return;
}
var activeItem = this.getItemAt(this.activeIndex);
this.selectItem(activeItem);
};
aria.ListboxCombobox.prototype.autocompleteItem = function () {
var autocompletedItem = this.listbox.querySelector('.focused');
var inputText = this.input.value;
if (!autocompletedItem || !inputText) {
return;
}
var autocomplete = autocompletedItem.innerText;
if (inputText !== autocomplete) {
this.input.value = autocomplete;
this.input.setSelectionRange(inputText.length, autocomplete.length);
}
};
/**
* @namespace aria
*/
var aria = aria || {};
/**
* @desc
* Key code constants
*/
aria.KeyCode = {
BACKSPACE: 8,
TAB: 9,
RETURN: 13,
ESC: 27,
SPACE: 32,
PAGE_UP: 33,
PAGE_DOWN: 34,
END: 35,
HOME: 36,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
DELETE: 46
};
aria.Utils = aria.Utils || {};
// Polyfill src https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
aria.Utils.matches = function (element, selector) {
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function (s) {
var matches = element.parentNode.querySelectorAll(s);
var i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
return element.matches(selector);
};
aria.Utils.remove = function (item) {
if (item.remove && typeof item.remove === 'function') {
return item.remove();
}
if (item.parentNode &&
item.parentNode.removeChild &&
typeof item.parentNode.removeChild === 'function') {
return item.parentNode.removeChild(item);
}
return false;
};
aria.Utils.isFocusable = function (element) {
if (element.tabIndex > 0 || (element.tabIndex === 0 && element.getAttribute('tabIndex') !== null)) {
return true;
}
if (element.disabled) {
return false;
}
switch (element.nodeName) {
case 'A':
return !!element.href && element.rel != 'ignore';
case 'INPUT':
return element.type != 'hidden' && element.type != 'file';
case 'BUTTON':
case 'SELECT':
case 'TEXTAREA':
return true;
default:
return false;
}
};
aria.Utils.getAncestorBySelector = function (element, selector) {
if (!aria.Utils.matches(element, selector + ' ' + element.tagName)) {
// Element is not inside an element that matches selector
return null;
}
// Move up the DOM tree until a parent matching the selector is found
var currentNode = element;
var ancestor = null;
while (ancestor === null) {
if (aria.Utils.matches(currentNode.parentNode, selector)) {
ancestor = currentNode.parentNode;
}
else {
currentNode = currentNode.parentNode;
}
}
return ancestor;
};
aria.Utils.hasClass = function (element, className) {
return (new RegExp('(\\s|^)' + className + '(\\s|$)')).test(element.className);
};
aria.Utils.addClass = function (element, className) {
if (!aria.Utils.hasClass(element, className)) {
element.className += ' ' + className;
}
};
aria.Utils.removeClass = function (element, className) {
var classRegex = new RegExp('(\\s|^)' + className + '(\\s|$)');
element.className = element.className.replace(classRegex, ' ').trim();
};
aria.Utils.bindMethods = function (object /* , ...methodNames */) {
var methodNames = Array.prototype.slice.call(arguments, 1);
methodNames.forEach(function (method) {
object[method] = object[method].bind(object);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment