Last active
April 4, 2022 21:02
-
-
Save nolanlawson/e63d2261406a15900ec6bd90e150e4b6 to your computer and use it in GitHub Desktop.
Firefox NVDA display:contents repro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 | |
); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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