From f9948c643f4256d20157ee5571f4abb279fed575 Mon Sep 17 00:00:00 2001
From: Robert Messerle <rmesserle@google.com>
Date: Wed, 28 Jan 2015 10:19:02 -0800
Subject: [PATCH] feat(autocomplete): added initial files for autocomplete

feat(autocomplete): adds accessibility support

TODO: wire aria-activedescendant as a watched property with a value of the active listItem id

refactor(autocomplete): re-organizes aria changes to live in controller

chore(autocomplete): removes temporary comments

refactor(autocomplete): renames ambiguous directive

refactor(styles): renames `visuallyhidden` to `visually-hidden` for consistency.

refactor(autocomplete): removes unused template file

refactor(autocomplete): uses `$mdConstant` rather than hard-coded values

refactor(autocomplete): various cleanup based on feedback

refactor(autocomplete): cleans up scope confusion

refactor(autocomplete): includes theming, updates directive name to prevent potential conflicts
---
 config/karma.conf.js                          |   2 +-
 docs/app/css/style.css                        |  11 --
 docs/app/partials/menu-link.tmpl.html         |   2 +-
 docs/app/partials/menu-toggle.tmpl.html       |   2 +-
 .../autocomplete/autocomplete-theme.scss      |  20 +++
 src/components/autocomplete/autocomplete.js   |  11 ++
 src/components/autocomplete/autocomplete.scss | 145 +++++++++++++++++
 .../autocomplete/autocomplete.spec.js         |  64 ++++++++
 .../autocomplete/demoBasicUsage/index.html    |  18 +++
 .../autocomplete/demoBasicUsage/script.js     |  26 ++++
 .../autocomplete/demoBasicUsage/style.css     |   3 +
 .../autocomplete/js/autocompleteController.js | 147 ++++++++++++++++++
 .../autocomplete/js/autocompleteDirective.js  |  83 ++++++++++
 .../autocomplete/js/highlightController.js    |  22 +++
 .../autocomplete/js/highlightDirective.js     |  37 +++++
 .../autocomplete/js/listItemDirective.js      |  22 +++
 src/core/style/structure.scss                 |  12 ++
 17 files changed, 613 insertions(+), 14 deletions(-)
 create mode 100644 src/components/autocomplete/autocomplete-theme.scss
 create mode 100644 src/components/autocomplete/autocomplete.js
 create mode 100644 src/components/autocomplete/autocomplete.scss
 create mode 100644 src/components/autocomplete/autocomplete.spec.js
 create mode 100644 src/components/autocomplete/demoBasicUsage/index.html
 create mode 100644 src/components/autocomplete/demoBasicUsage/script.js
 create mode 100644 src/components/autocomplete/demoBasicUsage/style.css
 create mode 100644 src/components/autocomplete/js/autocompleteController.js
 create mode 100644 src/components/autocomplete/js/autocompleteDirective.js
 create mode 100644 src/components/autocomplete/js/highlightController.js
 create mode 100644 src/components/autocomplete/js/highlightDirective.js
 create mode 100644 src/components/autocomplete/js/listItemDirective.js

diff --git a/config/karma.conf.js b/config/karma.conf.js
index be110ec5a89..4cf5cf8f021 100644
--- a/config/karma.conf.js
+++ b/config/karma.conf.js
@@ -10,7 +10,7 @@ module.exports = function(config) {
       // demos in the tests, and Karma doesn't support advanced
       // globbing.
       'src/components/*/*.js',
-      'src/components/tabs/js/*.js'
+      'src/components/*/js/*.js'
   ];
 
   var COMPILED_SRC = [
diff --git a/docs/app/css/style.css b/docs/app/css/style.css
index 0339e6f8454..00ee92b4fef 100644
--- a/docs/app/css/style.css
+++ b/docs/app/css/style.css
@@ -83,17 +83,6 @@ code:not(.highlight) {
   -webkit-font-smoothing: auto;
 }
 
-.visuallyhidden {
-  border: 0;
-  clip: rect(0 0 0 0);
-  height: 1px;
-  margin: -1px;
-  overflow: hidden;
-  padding: 0;
-  position: absolute;
-  text-transform: none;
-  width: 1px;
-}
 .md-sidenav-inner {
   background: #fff;
 }
diff --git a/docs/app/partials/menu-link.tmpl.html b/docs/app/partials/menu-link.tmpl.html
index 93bac37ffad..5604f1bed72 100644
--- a/docs/app/partials/menu-link.tmpl.html
+++ b/docs/app/partials/menu-link.tmpl.html
@@ -1,7 +1,7 @@
 <md-button ng-class="{'active' : isSelected()}"
   ng-href="#{{section.url}}">
   {{section | humanizeDoc}}
-  <span class="visuallyhidden"
+  <span class="visually-hidden"
     ng-if="isSelected()">
     current page
   </span>
diff --git a/docs/app/partials/menu-toggle.tmpl.html b/docs/app/partials/menu-toggle.tmpl.html
index 0a399b94eb0..ce7649aa289 100644
--- a/docs/app/partials/menu-toggle.tmpl.html
+++ b/docs/app/partials/menu-toggle.tmpl.html
@@ -6,7 +6,7 @@
   {{section.name}}
   <span aria-hidden="true" class="md-toggle-icon"
   ng-class="{'toggled' : isOpen()}"></span>
-  <span class="visuallyhidden">
+  <span class="visually-hidden">
     Toggle {{isOpen()? 'expanded' : 'collapsed'}}
   </span>
 </md-button>
diff --git a/src/components/autocomplete/autocomplete-theme.scss b/src/components/autocomplete/autocomplete-theme.scss
new file mode 100644
index 00000000000..41e698cfd8b
--- /dev/null
+++ b/src/components/autocomplete/autocomplete-theme.scss
@@ -0,0 +1,20 @@
+md-autocomplete {
+  background: '{{background-50}}';
+  button {
+    background: '{{background-200}}';
+  }
+  ul {
+    background: '{{background-50}}';
+    li {
+      border-top: 1px solid '{{background-400}}';
+      color: '{{background-900}}';
+      .highlight {
+        color: '{{background-600}}';
+      }
+      &:hover,
+      &.selected {
+        background: '{{background-200}}';
+      }
+    }
+  }
+}
diff --git a/src/components/autocomplete/autocomplete.js b/src/components/autocomplete/autocomplete.js
new file mode 100644
index 00000000000..1402e26b744
--- /dev/null
+++ b/src/components/autocomplete/autocomplete.js
@@ -0,0 +1,11 @@
+(function () {
+  'use strict';
+  /**
+   * @ngdoc module
+   * @name material.components.autocomplete
+   */
+  /*
+   * @see js folder for autocomplete implementation
+   */
+  angular.module('material.components.autocomplete', [ 'material.core' ]);
+})();
diff --git a/src/components/autocomplete/autocomplete.scss b/src/components/autocomplete/autocomplete.scss
new file mode 100644
index 00000000000..d044311bc15
--- /dev/null
+++ b/src/components/autocomplete/autocomplete.scss
@@ -0,0 +1,145 @@
+@keyframes md-autocomplete-list-out {
+  0% {
+    animation-timing-function: linear;
+  }
+  50% {
+    opacity: 0;
+    height: 40px;
+    animation-timing-function: ease-in;
+  }
+  100% {
+    height: 0;
+    opacity: 0;
+  }
+}
+@keyframes md-autocomplete-list-in {
+  0% {
+    opacity: 0;
+    height: 0;
+    animation-timing-function: ease-out;
+  }
+  50% {
+    opacity: 0;
+    height: 40px;
+  }
+  100% {
+    opacity: 1;
+    height: 40px;
+  }
+}
+md-content {
+  overflow: visible;
+}
+md-autocomplete {
+  box-shadow: 0 2px 5px rgba(black, 0.25);
+  border-radius: 2px;
+  display: block;
+  height: 40px;
+  position: relative;
+  overflow: visible;
+
+  md-autocomplete-wrap {
+    display: block;
+    position: relative;
+    overflow: visible;
+    height: 40px;
+
+    md-progress-linear {
+      position: absolute;
+      bottom: 0; left: 0; width: 100%;
+      height: 3px;
+      transition: none;
+
+      .md-container {
+        transition: none;
+        top: auto;
+        height: 3px;
+      }
+      &.ng-enter {
+        transition: opacity 0.15s linear;
+        &.ng-enter-active {
+          opacity: 1;
+        }
+      }
+      &.ng-leave {
+        transition: opacity 0.15s linear;
+        &.ng-leave-active {
+          opacity: 0;
+        }
+      }
+    }
+  }
+  input {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    box-sizing: border-box;
+    border: none;
+    box-shadow: none;
+    padding: 0 15px;
+    font-size: 14px;
+    line-height: 40px;
+    height: 40px;
+    outline: none;
+    z-index: 2;
+    background: transparent;
+  }
+  button {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    line-height: 20px;
+    z-index: 2;
+    text-align: center;
+    width: 20px;
+    height: 20px;
+    cursor: pointer;
+    border: none;
+    border-radius: 50%;
+    padding: 0;
+    font-size: 12px;
+    &.ng-enter {
+      transform: scale(0);
+      transition: transform 0.15s ease-out;
+      &.ng-enter-active {
+        transform: scale(1);
+      }
+    }
+    &.ng-leave {
+      transition: transform 0.15s ease-out;
+      &.ng-leave-active {
+        transform: scale(0);
+      }
+    }
+  }
+  ul {
+    position: absolute;
+    top: 100%;
+    left: 0;
+    right: 0;
+    box-shadow: 0 2px 5px rgba(black, 0.25);
+    margin: 0;
+    list-style: none;
+    padding: 0;
+    overflow: auto;
+    max-height: 41px * 5.5;
+    li {
+      border-top: 1px solid #ddd;
+      padding: 0 15px;
+      line-height: 40px;
+      font-size: 14px;
+      overflow: hidden;
+      height: 40px;
+      transition: background 0.15s linear;
+      cursor: pointer;
+      margin: 0;
+      &.ng-enter {
+        animation: md-autocomplete-list-in 0.2s;
+      }
+      &.ng-leave {
+        animation: md-autocomplete-list-out 0.2s;
+      }
+    }
+  }
+}
diff --git a/src/components/autocomplete/autocomplete.spec.js b/src/components/autocomplete/autocomplete.spec.js
new file mode 100644
index 00000000000..9cc42a91104
--- /dev/null
+++ b/src/components/autocomplete/autocomplete.spec.js
@@ -0,0 +1,64 @@
+describe('<md-autocomplete>', function() {
+
+  beforeEach(module('material.components.autocomplete'));
+
+  function compile (str, scope) {
+    var container;
+    inject(function ($compile) {
+      container = $compile(str)(scope);
+      scope.$apply();
+    });
+    return container;
+  }
+
+  function createScope () {
+    var scope;
+    var items = ['foo', 'bar', 'baz'].map(function (item) { return { display: item }; });
+    inject(function ($rootScope) {
+      scope = $rootScope.$new();
+      scope.match = function (term) {
+        return items.filter(function (item) {
+          return item.display.indexOf(term) === 0;
+        });
+      };
+      scope.searchText = '';
+      scope.selectedItem = null;
+    });
+    return scope;
+  }
+
+  describe('basic functionality', function () {
+    it('should fail', inject(function($timeout, $mdConstant) {
+      var scope = createScope();
+      var template = '\
+          <md-autocomplete\
+              md-selected-item="selectedItem"\
+              md-search-text="searchText"\
+              md-items="item in match(searchText)"\
+              md-item-text="display"\
+              placeholder="placeholder">\
+            <span md-highlight-text="searchText">{{item.display}}</span>\
+          </md-autocomplete>';
+      var element = compile(template, scope);
+      var ctrl    = element.controller('mdAutocomplete');
+
+      expect(scope.searchText).toBe('');
+      expect(scope.selectedItem).toBe(null);
+
+      element.scope().searchText = 'fo';
+      ctrl.keydown({});
+      element.scope().$apply();
+      $timeout.flush();
+
+      expect(scope.searchText).toBe('fo');
+      expect(scope.match(scope.searchText).length).toBe(1);
+      expect(element.find('li').length).toBe(1);
+
+      ctrl.keydown({ keyCode: $mdConstant.KEY_CODE.DOWN_ARROW, preventDefault: angular.noop });
+      ctrl.keydown({ keyCode: $mdConstant.KEY_CODE.ENTER, preventDefault: angular.noop });
+      scope.$apply();
+      expect(scope.searchText).toBe('foo');
+    }));
+  });
+
+});
\ No newline at end of file
diff --git a/src/components/autocomplete/demoBasicUsage/index.html b/src/components/autocomplete/demoBasicUsage/index.html
new file mode 100644
index 00000000000..c37f9b04688
--- /dev/null
+++ b/src/components/autocomplete/demoBasicUsage/index.html
@@ -0,0 +1,18 @@
+<div ng-app="inputBasicDemo" ng-controller="DemoCtrl as ctrl" layout="column">
+
+  <md-content class="md-padding" layout="column">
+
+    <md-autocomplete
+        md-selected-item="ctrl.selectedItem"
+        md-search-text="ctrl.searchText"
+        md-items="item in ctrl.getItems(ctrl.searchText)"
+        md-item-text="display"
+        placeholder="select a state...">
+      <span md-highlight-text="ctrl.searchText">{{item.display}}</span>
+    </md-autocomplete>
+
+    <p>Current search term: "{{ctrl.searchText}}"</p>
+
+  </md-content>
+
+</div>
diff --git a/src/components/autocomplete/demoBasicUsage/script.js b/src/components/autocomplete/demoBasicUsage/script.js
new file mode 100644
index 00000000000..4e77f38db44
--- /dev/null
+++ b/src/components/autocomplete/demoBasicUsage/script.js
@@ -0,0 +1,26 @@
+angular
+    .module('autocompleteDemo', ['ngMaterial'])
+    .controller('DemoCtrl', DemoCtrl);
+
+function DemoCtrl ($timeout, $q) {
+  var self = this;
+  this.selectedItem = null;
+  this.searchText = null;
+  this.states = 'Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Deleware,\
+        Florida, Georgia, Hawaii, Idaho, Illanois, Indiana, Iowa, Kansas, Kentucky, Louisiana,\
+        Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana,\
+        Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina,\
+        North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina,\
+        South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia,\
+        Wisconsin, Wyoming'.split(/, +/g).map(function (state) { return { value: state.toLowerCase(), display: state }; });
+  this.getItems = getItems;
+
+  function getItems (query) {
+    if (!query) return [];
+    var deferred = $q.defer();
+    var lowercaseQuery = angular.lowercase(query);
+    var results = self.states.filter(function (state) { return state.value.indexOf(lowercaseQuery) === 0; });
+    $timeout(function () { deferred.resolve(results); }, Math.random() * 1000, false);
+    return deferred.promise;
+  }
+}
diff --git a/src/components/autocomplete/demoBasicUsage/style.css b/src/components/autocomplete/demoBasicUsage/style.css
new file mode 100644
index 00000000000..33bcba73697
--- /dev/null
+++ b/src/components/autocomplete/demoBasicUsage/style.css
@@ -0,0 +1,3 @@
+md-content {
+  min-height: 500px;
+}
\ No newline at end of file
diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js
new file mode 100644
index 00000000000..13ad812e48c
--- /dev/null
+++ b/src/components/autocomplete/js/autocompleteController.js
@@ -0,0 +1,147 @@
+(function () {
+  'use strict';
+  angular
+      .module('material.components.autocomplete')
+      .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
+
+  function MdAutocompleteCtrl ($scope, $element, $timeout, $q, $mdUtil, $mdConstant) {
+
+    //-- private variables
+    var self = this,
+        itemParts = $scope.itemsExpr.split(/\ in\ /i),
+        itemExpr = itemParts[1],
+        elements = {
+          main:  $element[0],
+          ul:    $element[0].getElementsByTagName('ul')[0],
+          input: $element[0].getElementsByTagName('input')[0]
+        },
+        promise  = null,
+        cache    = {};
+
+    //-- public variables
+    self.scope    = $scope;
+    self.parent   = $scope.$parent;
+    self.itemName = itemParts[0];
+    self.matches  = [];
+    self.loading  = false;
+    self.hidden   = true;
+    self.index    = 0;
+    self.keydown  = keydown;
+    self.clear    = clearValue;
+    self.select   = select;
+    self.fetch    = $mdUtil.debounce(fetchResults);
+
+    //-- return init
+    return init();
+
+    //-- start method definitions
+    function init () {
+      configureWatchers();
+      configureAria();
+    }
+
+    function configureAria () {
+      var ul = angular.element(elements.ul),
+          input = angular.element(elements.input),
+          id = ul.attr('id') || 'ul_' + $mdUtil.nextUid();
+      ul.attr('id', id);
+      input.attr('aria-owns', id);
+    }
+
+    function configureWatchers () {
+      $scope.$watch('searchText', function (searchText) {
+        if (!searchText) {
+          self.loading = false;
+          return self.matches = [];
+        }
+        var term = searchText.toLowerCase();
+        if (promise && promise.cancel) {
+          promise.cancel();
+          promise = null;
+        }
+        if (cache[term]) {
+          self.matches = cache[term];
+        } else if (!self.hidden) {
+          self.loading = true;
+          self.fetch(searchText);
+        }
+      });
+    }
+
+    function fetchResults (searchText) {
+      var items = $scope.$parent.$eval(itemExpr),
+          term = searchText.toLowerCase();
+      promise = $q.when(items).then(function (matches) {
+        cache[term] = matches;
+        if (searchText !== $scope.searchText) return; //-- just cache the results if old request
+        promise = null;
+        self.loading = false;
+        self.matches = matches;
+      });
+    }
+
+    function keydown (event) {
+      switch (event.keyCode) {
+        case $mdConstant.KEY_CODE.DOWN_ARROW:
+          if (self.loading) return;
+          event.preventDefault();
+          self.index = Math.min(self.index + 1, self.matches.length - 1);
+          updateScroll();
+          break;
+        case $mdConstant.KEY_CODE.UP_ARROW:
+          if (self.loading) return;
+          event.preventDefault();
+          self.index = Math.max(0, self.index - 1);
+          updateScroll();
+          break;
+        case $mdConstant.KEY_CODE.ENTER:
+          if (self.loading) return;
+          event.preventDefault();
+          select(self.index);
+          break;
+        case $mdConstant.KEY_CODE.ESCAPE:
+          self.matches = [];
+          self.hidden = true;
+          self.index = -1;
+          break;
+        default:
+          self.index = -1;
+          self.hidden = false;
+          //-- after value updates, check if list should be hidden
+          $timeout(function () { self.hidden = isHidden(); });
+      }
+    }
+
+    function clearValue () {
+      $scope.searchText = '';
+      select(-1);
+    }
+
+    function isHidden () {
+      return self.matches.length === 1 && $scope.searchText === getDisplayValue(self.matches[0]);
+    }
+
+    function getDisplayValue (item) {
+      return (item && $scope.itemText) ? item[$scope.itemText] : item;
+    }
+
+    function select (index) {
+      $scope.searchText = getDisplayValue(self.matches[index]) || $scope.searchText;
+      self.hidden  = true;
+      self.index   = -1;
+      self.matches = [];
+    }
+
+    function updateScroll () {
+      var top = 41 * self.index,
+          bot = top + 41,
+          hgt = 41 * 5.5;
+      if (top < elements.ul.scrollTop) {
+        elements.ul.scrollTop = top;
+      } else if (bot > elements.ul.scrollTop + hgt) {
+        elements.ul.scrollTop = bot - hgt;
+      }
+    }
+
+  }
+})();
diff --git a/src/components/autocomplete/js/autocompleteDirective.js b/src/components/autocomplete/js/autocompleteDirective.js
new file mode 100644
index 00000000000..7c861381f44
--- /dev/null
+++ b/src/components/autocomplete/js/autocompleteDirective.js
@@ -0,0 +1,83 @@
+(function () {
+  'use strict';
+  angular
+      .module('material.components.autocomplete')
+      .directive('mdAutocomplete', MdAutocomplete);
+
+  /**
+   * @ngdoc directive
+   * @name mdAutocomplete
+   * @module material.components.autocomplete
+   *
+   * @description
+   * `<md-autocomplete>` allows you to provide real-time suggestions as the user types.
+   *
+   * @param {string=} md-search-text A model to bind the search text to
+   * @param {object=} md-selected-item A model to bind the selected item to
+   * @param {expression=} md-items An expression in the format of `item in items` to iterate over matches for your search.
+   * @param {string=} md-item-text A property on your object used to convert your object to a string
+   * @param {placeholder=} Placeholder text that will be forwarded to the input.
+   *
+   * @usage
+   * <hljs lang="html">
+   *   <md-autocomplete
+   *       md-selected-item="selectedItem"
+   *       md-search-text="searchText"
+   *       md-items="item in getMatches(searchText)"
+   *       md-item-text="display">
+   *     <span md-highlight-text="searchText">{{item.display}}</span>
+   *   </md-autocomplete>
+   * </hlhs>
+   */
+
+  function MdAutocomplete () {
+    return {
+      template: '\
+        <md-autocomplete-wrap role="listbox">\
+          <input type="text"\
+              ng-model="searchText"\
+              ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
+              placeholder="{{placeholder}}"\
+              aria-label="{{placeholder}}"\
+              aria-autocomplete="list"\
+              aria-haspopup="true"\
+              aria-activedescendant=""\
+              aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
+          <button\
+              type="button"\
+              ng-if="searchText"\
+              ng-click="$mdAutocompleteCtrl.clear()">\
+              <span aria-hidden="true">X</span>\
+              <span class="visually-hidden">Clear</span>\
+              </button>\
+          <md-progress-linear ng-if="$mdAutocompleteCtrl.loading" md-mode="indeterminate"></md-progress-linear>\
+        </md-autocomplete-wrap>\
+        <ul role="presentation">\
+          <li ng-repeat="(index, item) in $mdAutocompleteCtrl.matches"\
+              ng-class="{ selected: index === $mdAutocompleteCtrl.index }"\
+              ng-if="searchText && !$mdAutocompleteCtrl.hidden"\
+              ng-click="$mdAutocompleteCtrl.select(index)"\
+              ng-transclude\
+              md-autocomplete-list-item="$mdAutocompleteCtrl.itemName">\
+          </li>\
+        </ul>\
+        <aria-status\
+            class="visually-hidden"\
+            aria-atomic="true"\
+            role="status"\
+            aria-live="polite">\
+          <p ng-repeat="item in $mdAutocompleteCtrl.matches">{{item.display}}</p>\
+        </aria-status>',
+      transclude: true,
+      controller: 'MdAutocompleteCtrl',
+      controllerAs: '$mdAutocompleteCtrl',
+      scope: {
+        searchText: '=mdSearchText',
+        selectedItem: '=mdSelectedItem',
+        itemsExpr: '@mdItems',
+        itemText: '@mdItemText',
+        placeholder: '@placeholder'
+      }
+    };
+  }
+})();
diff --git a/src/components/autocomplete/js/highlightController.js b/src/components/autocomplete/js/highlightController.js
new file mode 100644
index 00000000000..f7815009558
--- /dev/null
+++ b/src/components/autocomplete/js/highlightController.js
@@ -0,0 +1,22 @@
+(function () {
+  'use strict';
+  angular
+      .module('material.components.autocomplete')
+      .controller('MdHighlightCtrl', MdHighlightCtrl);
+
+  function MdHighlightCtrl ($scope, $element, $interpolate) {
+    var term = $element.attr('md-highlight-text'),
+        text = $interpolate($element.text())($scope);
+    $scope.$watch(term, function (term) {
+      var regex = new RegExp('^' + sanitize(term), 'i'),
+          html = text.replace(regex, '<span class="highlight">$&</span>');
+      $element.html(html);
+    });
+
+    function sanitize (term) {
+      if (!term) return term;
+      return term.replace(/[\*\[\]\(\)\{\}\\\^\$]/g, '\\$&');
+    }
+  }
+
+})();
diff --git a/src/components/autocomplete/js/highlightDirective.js b/src/components/autocomplete/js/highlightDirective.js
new file mode 100644
index 00000000000..8a0a795fd97
--- /dev/null
+++ b/src/components/autocomplete/js/highlightDirective.js
@@ -0,0 +1,37 @@
+(function () {
+  'use strict';
+  angular
+      .module('material.components.autocomplete')
+      .directive('mdHighlightText', MdHighlight);
+
+  /**
+   * @ngdoc directive
+   * @name mdHighlightText
+   * @module material.components.autocomplete
+   *
+   * @description
+   * The `md-highlight-text` directive allows you to specify text that should be highlighted within
+   * an element.  Highlighted text will be wrapped in `<span class="highlight"></span>` which can
+   * be styled through CSS.  Please note that child elements may not be used with this directive.
+   *
+   * @param {string=} md-highlight-text A model to be searched for
+   *
+   * @usage
+   * <hljs lang="html">
+   * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
+   * <ul>
+   *   <li ng-repeat="result in results" md-highlight-text="searchTerm">
+   *     {{result.text}}
+   *   </li>
+   * </ul>
+   * </hljs>
+   */
+
+  function MdHighlight () {
+    return {
+      terminal: true,
+      scope: false,
+      controller: 'MdHighlightCtrl'
+    };
+  }
+})();
diff --git a/src/components/autocomplete/js/listItemDirective.js b/src/components/autocomplete/js/listItemDirective.js
new file mode 100644
index 00000000000..a63da8e48f6
--- /dev/null
+++ b/src/components/autocomplete/js/listItemDirective.js
@@ -0,0 +1,22 @@
+(function () {
+  'use strict';
+  angular
+      .module('material.components.autocomplete')
+      .directive('mdAutocompleteListItem', MdAutocompleteListItem);
+
+  function MdAutocompleteListItem ($compile, $mdUtil) {
+    return {
+      require: '^?mdAutocomplete',
+      terminal: true,
+      link: link,
+      scope: false
+    };
+    function link (scope, element, attr, ctrl) {
+      var newScope = ctrl.parent.$new(false, ctrl.parent);
+      var itemName = ctrl.scope.$eval(attr.mdAutocompleteListItem);
+      newScope[itemName] = scope.item;
+      $compile(element.contents())(newScope);
+      element.attr({ 'role': 'option', 'id': 'item_' + $mdUtil.nextUid() });
+    }
+  }
+})();
diff --git a/src/core/style/structure.scss b/src/core/style/structure.scss
index 635bcb0ac4a..deb10738ea7 100644
--- a/src/core/style/structure.scss
+++ b/src/core/style/structure.scss
@@ -148,6 +148,18 @@ input {
   }
 }
 
+.visually-hidden {
+  border: 0;
+  clip: rect(0 0 0 0);
+  height: 1px;
+  margin: -1px;
+  overflow: hidden;
+  padding: 0;
+  position: absolute;
+  text-transform: none;
+  width: 1px;
+}
+
 .md-shadow {
   position: absolute;
   top: 0;