Custom Select Box
by JeremiePat
HTML
<select name="test" tabindex="-1">
<option>Cherry</option>
<option>Lemon</option>
<option>Banana</option>
<option>Strawberry</option>
<option>Apple</option>
</select>
<!-- This is our main container for our widget.
The tabindex attribute is what allow the user to focus
out widget using the tabulation key. -->
<div class="select" tabindex="0">
<!-- This container will be used to display the current value of the widget -->
<span class="value">Cherry</span>
<!-- This container will contain all the option available for our widget.
Because it's a list, it make sens to use the ul element. -->
<ul class="optList hidden">
<!-- Each option only contain the value to be displayed, we'll see later
how to handle the real value that will be send with the form data -->
<li class="option">Cherry</li>
<li class="option">Lemon</li>
<li class="option">Banana</li>
<li class="option">Strawberry</li>
<li class="option">Apple</li>
</ul>
</div>
CSS
body { margin : 2em; }
select {
position: absolute;
left: -5000em;
}
.select {
/* This will create a positioning context for the list of option */
position: relative;
/* This will make our widget become part of the text flow and sizable at the same time */
display : inline-block;
}
.select.active,
.select:focus {
outline: none;
/* This box-shadow property is not exactly required, however it's so important to be sure
the active state is visibled that we use it as a default value, feel free to overide it. */
box-shadow: 0 0 3px 1px #227755;
}
/* The .select selector here is a syntaxe sugar to be sure the classes we define are
the ones inside our widget. */
.select .optList {
/* This will make sure our list of option will be display below the value
and out of the HTML flow */
position: absolute;
top : 100%;
left : 0;
}
.select .optList.hidden {
/* This is a simple way to hide the list in an accessible way, we will talk more about accessibility in the end */
max-height: 0;
visibility: hidden;
}
.select {
/* All size will be express with the em value for accessibility resons
(to make sur the widget remain resizable if the user use its
browser's zoom in a text only mode). The computation are made
assuming 1em == 16px which is the default value in almost all browsers.
If you are lost with px to em conversion, try http://riddle.pl/emcalc/ */
font-size : 0.625em; /* this (10px) is the new font size context for em value in this context */
font-family : Verdana, Arial, sans-serif;
-moz-box-sizing : border-box;
box-sizing : border-box;
/* We need to handle extra room for the down arrow we will add */
padding : .1em 2.5em .2em .5em; /* 1px 25px 2px 5px */
width : 10em; /* 100px */
border : .2em solid #000; /* 2px */
border-radius : .4em; /* 4px */
box-shadow : 0 .1em .2em rgba(0,0,0,.45); /* 0 1px 2px */
/* The first declaration is for browsers that do not...
JavaScript
HTMLElement.prototype.hasClass = function (className) {
var tester = new RegExp(' *\\b' + className + '\\b *', 'g');
return this.className.search(tester) > -1;
}
HTMLElement.prototype.addClass = function (className) {
this.className += ' ' + className;
}
HTMLElement.prototype.removeClass = function (className) {
var tester = new RegExp(' *\\b' + className + '\\b *', 'g');
this.className = this.className.replace(tester, ' ');
}
HTMLElement.prototype.toggleClass = function (className) {
var func = this.hasClass(className) ? 'removeClass' : 'addClass';
this[func](className);
}
NodeList.prototype.forEach = function (callback) {
Array.prototype.forEach.call(this, callback);
}
window.addEventListener('load', function () {
var selectList = document.querySelectorAll('.select');
selectList.forEach(function (select) {
var widget = select.previousElementSibling,
value = select.querySelector('.value'),
optionList = select.querySelectorAll('.option');
value.innerHTML = widget.value;
optionList.forEach(function (option, index) {
option.addEventListener('click', function (event) {
widget.selectedIndex = index;
}, false);
option.addEventListener('mouseover', function (event) {
optionList.forEach(function (option) {
option.removeClass('highlight');
});
option.addClass('highlight');
}, false);
});
select.addEventListener('keyup', function (event) {
var length = widget.length,
index = widget.selectedIndex;
if (event.keyCode === 40 && index < length - 1) { index++; }
if (event.keyCode === 38 && index > 0) { index--; }
widget.selectedIndex = index;
value.innerHTML = widget.value;
});
select.addEventListener('click', function (event) {
event.stopPropagation();
this.querySelector('.optList').toggleClass('hidden');
selectList.forEach(function (select) {
...