diff --git a/packages/clay-autocomplete/LICENSE.md b/packages/clay-autocomplete/LICENSE.md
new file mode 100644
index 0000000000..70f93e9462
--- /dev/null
+++ b/packages/clay-autocomplete/LICENSE.md
@@ -0,0 +1,29 @@
+# Software License Agreement (BSD License)
+
+Copyright (c) 2014, Liferay Inc.
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms, with or without modification, are
+permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above
+ copyright notice, this list of conditions and the
+ following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the
+ following disclaimer in the documentation and/or other
+ materials provided with the distribution.
+
+* The name of Liferay Inc. may not be used to endorse or promote products
+ derived from this software without specific prior
+ written permission of Liferay Inc.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/clay-autocomplete/README.md b/packages/clay-autocomplete/README.md
new file mode 100644
index 0000000000..053ec0c0bc
--- /dev/null
+++ b/packages/clay-autocomplete/README.md
@@ -0,0 +1,26 @@
+# clay-autocomplete
+
+Autocomplete textarea
+
+## Setup
+
+1. Install NodeJS >= v0.12.0 and NPM >= v3.0.0, if you don't have it yet. You
+can find it [here](https://nodejs.org).
+
+2. Install local dependencies:
+
+ ```
+ npm install
+ ```
+
+3. Build the code:
+
+ ```
+ npm run build
+ ```
+
+4. Open the demo at demos/index.html on your browser.
+
+## Contribute
+
+We'd love to get contributions from you! Please, check our [Contributing Guidelines](CONTRIBUTING.md) to see how you can help us improve.
diff --git a/packages/clay-autocomplete/demos/a11y.html b/packages/clay-autocomplete/demos/a11y.html
new file mode 100644
index 0000000000..f9f17b0e7b
--- /dev/null
+++ b/packages/clay-autocomplete/demos/a11y.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Demo: ClayAutocomplete
+
+
+
+
+
+
+
+
+
+ ClayAutocomplete
+
+
+
+
+
+
+
diff --git a/packages/clay-autocomplete/demos/data.json b/packages/clay-autocomplete/demos/data.json
new file mode 100644
index 0000000000..77fe659347
--- /dev/null
+++ b/packages/clay-autocomplete/demos/data.json
@@ -0,0 +1,52 @@
+[
+ "json el0",
+ "json el1",
+ "json el2",
+ "json el3",
+ "json el4",
+ "json el5",
+ "json el6",
+ "json el7",
+ "json el8",
+ "json el9",
+ "json el10",
+ "json el11",
+ "json el12",
+ "json el13",
+ "json el14",
+ "json el15",
+ "json el16",
+ "json el17",
+ "json el18",
+ "json el19",
+ "json el20",
+ "json el21",
+ "json el22",
+ "json el23",
+ "json el24",
+ "json el25",
+ "json el26",
+ "json el27",
+ "json el28",
+ "json el29",
+ "json el30",
+ "json el31",
+ "json el32",
+ "json el33",
+ "json el34",
+ "json el35",
+ "json el36",
+ "json el37",
+ "json el38",
+ "json el39",
+ "json el40",
+ "json el41",
+ "json el42",
+ "json el43",
+ "json el44",
+ "json el45",
+ "json el46",
+ "json el47",
+ "json el48",
+ "json el49"
+]
diff --git a/packages/clay-autocomplete/demos/index.html b/packages/clay-autocomplete/demos/index.html
new file mode 100644
index 0000000000..a0a84428db
--- /dev/null
+++ b/packages/clay-autocomplete/demos/index.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+ Demo: ClayAutocomplete
+
+
+
+
+
+
+
+
+
+
+ ClayAutocomplete
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/clay-autocomplete/package.json b/packages/clay-autocomplete/package.json
new file mode 100644
index 0000000000..d2eeb687a7
--- /dev/null
+++ b/packages/clay-autocomplete/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "clay-autocomplete",
+ "version": "1.0.0-alpha.8",
+ "description": "Metal ClayAutocomplete component",
+ "license": "BSD",
+ "repository": "https://github.com/liferay/clay/tree/master/packages/clay-autocomplete",
+ "engines": {
+ "node": ">=0.12.0",
+ "npm": ">=3.0.0"
+ },
+ "main": "lib/ClayAutocomplete.js",
+ "esnext:main": "src/ClayAutocomplete.js",
+ "jsnext:main": "src/ClayAutocomplete.js",
+ "files": [
+ "lib",
+ "src",
+ "test"
+ ],
+ "scripts": {
+ "build": "npm run soy && webpack",
+ "dev": "webpack-dev-server --inline --hot",
+ "compile": "babel -d lib/ src/ -s --ignore src/__tests__",
+ "prepublish": "npm run soy && npm run compile",
+ "soy": "metalsoy"
+ },
+ "keywords": [
+ "clay",
+ "metal"
+ ],
+ "dependencies": {
+ "fuzzy": "^0.1.3",
+ "metal": "^2.16.0",
+ "metal-component": "^2.16.0",
+ "metal-soy": "^2.16.0",
+ "metal-state": "^2.16.0",
+ "metal-uri": "^3.1.1",
+ "metal-web-component": "^2.16.0"
+ },
+ "devDependencies": {
+ "babel-cli": "^6.24.1",
+ "babel-core": "^6.25.0",
+ "babel-loader": "^7.0.0",
+ "babel-plugin-transform-node-env-inline": "^0.1.1",
+ "babel-preset-env": "^1.6.0",
+ "browserslist-config-clay-components": "^1.0.0-alpha.2",
+ "clay": "^2.0.0-beta.4",
+ "metal-dom": "^2.13.2",
+ "metal-soy-loader": "^1.0.0-alpha.4",
+ "metal-tools-soy": "^6.0.0",
+ "webpack": "^3.0.0",
+ "webpack-dev-server": "^2.11.2"
+ },
+ "browserslist": [
+ "extends browserslist-config-clay-components"
+ ]
+}
diff --git a/packages/clay-autocomplete/src/ClayAutocomplete.js b/packages/clay-autocomplete/src/ClayAutocomplete.js
new file mode 100644
index 0000000000..8d6327c925
--- /dev/null
+++ b/packages/clay-autocomplete/src/ClayAutocomplete.js
@@ -0,0 +1,135 @@
+import Component from 'metal-component';
+import defineWebComponent from 'metal-web-component';
+import Soy from 'metal-soy';
+import {Config} from 'metal-state';
+
+import DataProvider from './DataProvider';
+import templates from './ClayAutocomplete.soy';
+
+/* eslint-disable require-jsdoc */
+
+/**
+ * Metal ClayAutocomplete component.
+ */
+class ClayAutocomplete extends Component {
+ created() {
+ this._query = this.value;
+ this.provider = new DataProvider(this.data, {
+ elements: this.maxSuggestions,
+ paramName: this.paramName,
+ });
+ this.provider.on(
+ 'suggestionsUpdated',
+ this._updateSuggestions.bind(this)
+ );
+ }
+
+ _clearSuggestions() {
+ if (this._suggestions.length > 0) {
+ this._updateSuggestions([]);
+ }
+ }
+
+ _updateSuggestions(list) {
+ this._suggestions = list;
+ }
+
+ _handleClick(event) {
+ this.submit(event);
+ }
+
+ _handleSearch(event) {
+ this.value = event.delegateTarget.value.toLowerCase();
+ if (this.value.length >= 3) {
+ this._query = this.value;
+ this.provider.updateSuggestions(this._query);
+ } else {
+ this._clearSuggestions();
+ }
+ this._selectedSuggestionIndex = 0;
+ }
+
+ _handleFocus() {
+ this._hasFocus = true;
+ }
+
+ _handleBlur() {
+ this._hasFocus = false;
+ }
+
+ _handleHover(e) {
+ this.selectSuggestion(parseInt(e.target.dataset.index, 10) + 1);
+ }
+
+ _handleKeydown(event) {
+ this._hasFocus = true;
+ switch (event.key) {
+ case 'ArrowDown':
+ event.preventDefault();
+ this.selectSuggestion(this._selectedSuggestionIndex + 1);
+ break;
+
+ case 'ArrowUp':
+ event.preventDefault();
+ this.selectSuggestion(this._selectedSuggestionIndex - 1);
+ break;
+
+ case 'Enter':
+ event.target.blur();
+ this.submit(event);
+ break;
+
+ case 'Escape':
+ this._hasFocus = false;
+ this.selectSuggestion(0);
+ break;
+ }
+ }
+
+ selectSuggestion(pos) {
+ const total = this._suggestions.length + 1;
+
+ this._selectedSuggestionIndex = (pos + total) % total;
+
+ this.value =
+ this._selectedSuggestionIndex > 0
+ ? this._suggestions[this._selectedSuggestionIndex - 1]
+ : this._query;
+ }
+
+ submit(event) {
+ this.emit('submitQuery', {
+ query: this.value,
+ selectedItem: this._suggestions[this._selectedSuggestionIndex - 1],
+ originalEvent: event || null,
+ });
+ }
+}
+
+ClayAutocomplete.STATE = {
+ _hasFocus: Config.bool()
+ .value(false)
+ .internal(),
+ _query: Config.string()
+ .value('')
+ .internal(),
+ _selectedSuggestionIndex: Config.number()
+ .value(0)
+ .internal(),
+ _suggestions: Config.array()
+ .value([])
+ .internal(),
+ data: Config.oneOfType([Config.array(), Config.string()]),
+ elementClasses: Config.string(),
+ id: Config.string(),
+ maxSuggestions: Config.number().value(5),
+ paramName: Config.string().value('query'),
+ value: Config.string().value(''),
+};
+
+defineWebComponent('clay-autocomplete', ClayAutocomplete);
+
+Soy.register(ClayAutocomplete, templates);
+
+export {ClayAutocomplete};
+export default ClayAutocomplete;
diff --git a/packages/clay-autocomplete/src/ClayAutocomplete.soy b/packages/clay-autocomplete/src/ClayAutocomplete.soy
new file mode 100644
index 0000000000..79d75f25a3
--- /dev/null
+++ b/packages/clay-autocomplete/src/ClayAutocomplete.soy
@@ -0,0 +1,86 @@
+{namespace ClayAutocomplete}
+
+/**
+ * This renders the component's whole content.
+ */
+{template .render}
+ {@param _suggestions: list>}
+ {@param? _handleBlur: any}
+ {@param? _handleClick: any}
+ {@param? _handleFocus: any}
+ {@param? _handleHover: any}
+ {@param? _handleKeydown: any}
+ {@param? _handleSearch: any}
+ {@param? _hasFocus: bool}
+ {@param? _selectedSuggestionIndex: int}
+ {@param? elementClasses: string}
+ {@param? id: string}
+ {@param? value: string}
+
+ {let $expanded: $_hasFocus and $value and length($_suggestions) /}
+
+ {let $attributes kind="attributes"}
+ class="clay-autocomplete dropdown-full form-group
+ {if $elementClasses}
+ {sp}{$elementClasses}
+ {/if}
+ "
+
+ {if $id}
+ id="{$id}"
+ {/if}
+ {/let}
+
+ {let $listAttributes kind="attributes"}
+ class="dropdown-menu
+ {if $expanded}
+ {sp}show
+ {/if}
+ "
+ {/let}
+
+
+
+
+ {foreach $item in $_suggestions}
+
+ {call .item}
+ {param item: $item /}
+ {param itemIndex: index($item) /}
+ {param _selectedSuggestionIndex: $_selectedSuggestionIndex /}
+ {param _handleClick: $_handleClick /}
+ {param _handleHover: $_handleHover /}
+ {/call}
+
+ {/foreach}
+
+
+{/template}
+
+{template .item}
+ {@param item: string}
+ {@param itemIndex: int}
+ {@param? _selectedSuggestionIndex: int}
+ {@param _handleClick: any}
+ {@param _handleHover: any}
+
+ {let $elementAttributes kind="attributes"}
+ class="dropdown-item
+ {if $_selectedSuggestionIndex - 1 == $itemIndex}
+ {sp}active
+ {/if}
+ "
+ data-onmousedown="{$_handleClick}"
+ data-onmouseover="{$_handleHover}"
+ data-index="{$itemIndex}"
+ {/let}
+ {$item}
+{/template}
\ No newline at end of file
diff --git a/packages/clay-autocomplete/src/DataProvider.js b/packages/clay-autocomplete/src/DataProvider.js
new file mode 100644
index 0000000000..86361561d5
--- /dev/null
+++ b/packages/clay-autocomplete/src/DataProvider.js
@@ -0,0 +1,57 @@
+import {isString} from 'metal';
+import Uri from 'metal-uri';
+import fuzzy from 'fuzzy';
+import EventEmitter from 'metal-events';
+
+/* eslint-disable require-jsdoc */
+
+function getDataFromArray(query, data, cb) {
+ cb(
+ fuzzy
+ .filter(query, data, {
+ extract: el => el,
+ })
+ .map(e => e.string)
+ );
+}
+
+function getDataFromURL(query, url, paramName = 'q', cb) {
+ fetch(new Uri(url).setParameterValue(paramName, query))
+ .then(response => response.json())
+ .then(json => cb(json));
+}
+
+export default class DataProvider extends EventEmitter {
+ constructor(dataSource, options) {
+ super();
+ this.options = Object.assign({}, this.defaults, options);
+ this.dataSource = dataSource;
+ this.setSuggestions = this.setSuggestions.bind(this);
+ }
+
+ get defaults() {
+ return {elements: 5, paramName: 'q'};
+ }
+
+ updateSuggestions(query) {
+ const {dataSource, setSuggestions, options} = this;
+
+ if (this.lock) return false;
+ this.lock = true;
+ if (Array.isArray(dataSource)) {
+ getDataFromArray(query, dataSource, setSuggestions);
+ } else if (isString(dataSource)) {
+ getDataFromURL(
+ query,
+ dataSource,
+ options.paramName,
+ setSuggestions
+ );
+ }
+ }
+
+ setSuggestions(data) {
+ this.emit('suggestionsUpdated', data.slice(0, this.options.elements));
+ this.lock = false;
+ }
+}
diff --git a/packages/clay-autocomplete/src/__tests__/ClayAutocomplete.js b/packages/clay-autocomplete/src/__tests__/ClayAutocomplete.js
new file mode 100644
index 0000000000..a9c435c14f
--- /dev/null
+++ b/packages/clay-autocomplete/src/__tests__/ClayAutocomplete.js
@@ -0,0 +1,34 @@
+import ClayAutocomplete from '../ClayAutocomplete';
+
+let component;
+
+describe('ClayAutocomplete', function() {
+ afterEach(() => {
+ if (component) {
+ component.dispose();
+ }
+ });
+
+ it('should render the default markup', () => {
+ component = new ClayAutocomplete();
+
+ expect(component).toMatchSnapshot();
+ });
+
+ it('should render a ClayAutocomplete with classes', () => {
+ component = new ClayAutocomplete({
+ elementClasses: 'my-custom-class',
+ });
+
+ expect(component).toMatchSnapshot();
+ });
+
+ it('should render a ClayAutocomplete with id', () => {
+ component = new ClayAutocomplete({
+ id: 'myId',
+ });
+
+ expect(component).toMatchSnapshot();
+ });
+
+});
diff --git a/packages/clay-autocomplete/src/__tests__/__snapshots__/ClayAutocomplete.js.snap b/packages/clay-autocomplete/src/__tests__/__snapshots__/ClayAutocomplete.js.snap
new file mode 100644
index 0000000000..d1e5c85a7a
--- /dev/null
+++ b/packages/clay-autocomplete/src/__tests__/__snapshots__/ClayAutocomplete.js.snap
@@ -0,0 +1 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
diff --git a/packages/clay-autocomplete/webpack.config.js b/packages/clay-autocomplete/webpack.config.js
new file mode 100644
index 0000000000..447ee336af
--- /dev/null
+++ b/packages/clay-autocomplete/webpack.config.js
@@ -0,0 +1,40 @@
+const path = require('path');
+const webpack = require('webpack');
+
+module.exports = {
+ entry: './src/ClayAutocomplete.js',
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /(node_modules|bower_components)/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ compact: false,
+ presets: ['babel-preset-env'],
+ plugins: ['babel-plugin-transform-node-env-inline'],
+ },
+ },
+ },
+ {
+ test: /\.soy$/,
+ loader: 'metal-soy-loader',
+ },
+ ],
+ },
+ devtool: 'cheap-module-source-map',
+ output: {
+ library: 'metal',
+ libraryTarget: 'this',
+ filename: './build/globals/clay-autocomplete.js',
+ publicPath: '/packages/clay-autocomplete/',
+ },
+ plugins: [new webpack.optimize.ModuleConcatenationPlugin()],
+ resolve: {
+ mainFields: ['esnext:main', 'main'],
+ },
+ devServer: {
+ contentBase: path.join(__dirname, '..', '..'),
+ },
+};