Number spinner horizontal

v0.3a

Implementing this component

The Markup

				
  <form id="number-spinner-horizontal" class="t-neutral">
    <fieldset class="spinner spinner--horizontal l-contain--medium">
       <label for="spinner-input" class="spinner__label">Quantity </label>
       <button class="spinner__button spinner__button--left js-spinner-horizontal-subtract" data-type="subtract" title="Subtract 1" aria-controls="spinner-input">- </button>
       <input type="number" class="spinner__input js-spinner-input-horizontal" id="spinner-input" value="1" min="0" max="20" step="1" pattern="[0-9]*" role="alert" aria-live="assertlive" />
       <button data-type="add" class="spinner__button spinner__button--right js-spinner-horizontal-add" title="Add 1" aria-controls="spinner-input">+ </button>
     </fieldset>
   </form>
				
			

The CSS: Theming

				
/* Theming */

.t-neutral .spinner__button {
    background: #222;
    color: $#fff;
}
.t-neutral .spinner__button:hover,
.t-neutral .spinner__button:focus,
.t-neutral .spinner__button:active {
  background: #444;
}
.t-neutral .spinner__input {
    background: #eee;
}
  
			

The CSS: Resets

        
/* Aggressive reset to remove the initial spinner */
input[type="number"] {
  -moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -moz-appearance: none;
  -webkit-appearance: none;
  appearance:none;
  margin: 0;
}
input[type="number"]:hover::-webkit-inner-spin-button,
input[type="number"]:hover::-webkit-outer-spin-button {
 -moz-appearance: none;
  -webkit-appearance: none;
  appearance:none;
  margin:0;
}
        
      

The CSS: Component

				

/* The component (also includes vertical) */

.spinner {
    margin: 0 0 5em 0;
}

.spinner:after {
    clear: both;
    content: "";
    display: table;
}

.spinner__label {
    display: block;
    text-align: center;
    width: 100%;
}

.spinner__button {
    cursor: pointer;
    display: block;
    font-size: 1em;
    font-weight: 700;
    padding: 1.1em 1em 1.15em;
    text-align: center;
    text-decoration: none;
    transition: background 0.4s ease;
}

.spinner__button--left {
    border-bottom-left-radius: 5px;
    border-top-left-radius: 5px;
}

.spinner__button--right {
    border-bottom-right-radius: 5px;
    border-top-right-radius: 5px;
}

.spinner__button--top {
    border-top-left-radius: 5px;
    border-top-right-radius: 5px;
}

.spinner__button--bottom {
    border-bottom-left-radius: 5px;
    border-bottom-right-radius: 5px;
}

.spinner--vertical .spinner__button {
    padding:2em 1em;
    width:100%;
}

.spinner__input {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background: none;
    border: none;
    -webkit-border-radius: 0;
    border-radius: 0;
    border-radius: none;
    box-sizing: border-box;
    display: block;
    font-size: 1em;
    line-height: 1.5;
    text-align: center;
}

.spinner--horizontal .spinner__input {
    padding: 1em 1em 1.05em 1em;
     @media all and (min-width:40em) {
        padding: 1.1em 1em 1.05em 1em;
    }
}

.spinner--vertical .spinner__input {
    padding:1.5em 1em 1.5em 1em;
    width:100%;
}

.spinner--horizontal .spinner__button {
    float: left;
    width: 25%;
}

.spinner--horizontal .spinner__input {
    float: left;
    width: 50%;
}
				
			

The Javascript (1kb minified)

				
    //gets the input by element Id, gets min, max, and step from the markup. Gets the subtract and add buttons either by optional classnames, or by the next or last element sibling.
  var NumberSpinner = function(elemId, subtractClassName, addClassName) {
    'use strict';
    var spinnerInput = document.getElementById(elemId);
    var btnSubtract = document.querySelector(addClassName) || spinnerInput.previousElementSibling;
    var btnAdd = document.querySelector(subtractClassName) || spinnerInput.nextElementSibling;
    var minLimit, maxLimit, step;

    function init(){
      minLimit = makeNumber(getAttribute(spinnerInput, 'min')) || 0,
      maxLimit = makeNumber(getAttribute(spinnerInput, 'max')) || false,
      step = makeNumber(getAttribute(spinnerInput, 'step') || '1');

      btnSubtract.addEventListener('click', changeSpinner, false);
      btnAdd.addEventListener('click', changeSpinner, false);
      btnSubtract.addEventListener('keyup', keySpinner, false);
      btnAdd.addEventListener('keyup', keySpinner, false);
      if(supportsTouch()) {
        btnSubtract.addEventListener('touchend', removeClickDelay, false);
        btnAdd.addEventListener('touchend', removeClickDelay, false);
      }
      if(supportsPointer()) {
        btnSubtract.addEventListener('pointerup', removeClickDelay, false);
        btnAdd.addEventListener('pointerup', removeClickDelay, false);
      }
    }
    function removeClickDelay(e) {
      e.preventDefault();
      e.target.click();
    }
    function makeNumber(inputString){
      return parseInt(inputString, 10);
    }
    function update(direction){
      var num = makeNumber(spinnerInput.value);
      if(direction === 'add'){
        spinnerInput.value = ((num + step) <= maxLimit) ? (num + step) : spinnerInput.value;
      } else if(direction === 'subtract') {
        spinnerInput.value = ((num - step) >= minLimit) ? (num - step) : spinnerInput.value;
      }
    }
    function getAttribute(el, attr){
      var hasGetAttr = (el.getAttribute && el.getAttribute(attr)) || null;
      if(!hasGetAttr) {
        var attrs = el.attributes;
        for(var i = 0, len = attrs.length; i < len; i++){
          if(attrs[i].nodeName === attr) {
            hasGetAttr = attrs[i].nodeValue;
          }
        }
      }
      return hasGetAttr;
    }
    /* Touch and Pointer support */
    function supportsTouch(){
      return ('ontouchstart' in window);
    }
    function supportsPointer(){
      return ('pointerdown' in window);
    }
    /* Keyboard support */
    function keySpinner(e){
      switch(e.keyCode){
        case 40:
        case 37: // Down, Left
          update('subtract');
          btnSubtract.focus();
          break;
        case 38:
        case 39: // Top, Right
          update('add');
          btnAdd.focus();
          break;
      }
    }
    function changeSpinner(e) {
      e.preventDefault();
      var increment = getAttribute(e.target, 'data-type');
      update(increment);
    }
    init();
  };

				
			

About the component

TBD.

Screen Readers

Apple Voiceover

TBD.

JAWS

TBD.

NVDA

TBD.

Keyboard control

TBD.

High Contrast

TBD.

Inputs

TBD.

Cross browser notes

TBD.

Cross device notes

TBD.