diff --git a/App.js b/App.js
index 515ad839..b5089187 100644
--- a/App.js
+++ b/App.js
@@ -12,6 +12,7 @@ import SnippetEditor from "./app/SnippetEditorExample";
// Required to make Material UI work with touch screens.
import injectTapEventPlugin from "react-tap-event-plugin";
import Checkbox from "./composites/Plugin/Shared/components/Checkbox";
+import KeywordInput from "./composites/Plugin/Shared/components/KeywordInput";
const components = [
{
@@ -62,6 +63,14 @@ const components = [
onChange={ event => console.log( event ) }
/>,
},
+ {
+ id: "focus-keyword",
+ name: "Keyword",
+ component: ,
+ },
{
id: "sidebar-collapsible",
name: "Sidebar Collapsible",
diff --git a/composites/Plugin/Shared/components/KeywordInput.js b/composites/Plugin/Shared/components/KeywordInput.js
new file mode 100644
index 00000000..dddc530f
--- /dev/null
+++ b/composites/Plugin/Shared/components/KeywordInput.js
@@ -0,0 +1,121 @@
+import React from "react";
+import styled from "styled-components";
+import PropTypes from "prop-types";
+
+import colors from "../../../../style-guide/colors.json";
+
+let KeywordField = styled.input`
+ margin-right: 0.5em;
+ border-color: ${ props => props.borderColor };
+`;
+
+const ErrorText = styled.div`
+ font-size: 1em;
+ color: ${ colors.$color_red };
+ margin: 1em 0;
+ min-height: 1.8em;
+`;
+
+const ErrorMessage = "Are you trying to use multiple keywords? You should add them separately below.";
+
+class KeywordInput extends React.Component {
+ /**
+ * Constructs a KeywordInput component
+ *
+ * @param {Object} props The props for this input field component.
+ * @param {String} props.id The id of the KeywordInput.
+ * @param {IconsButton} props.label The label of the KeywordInput.
+ * @param {boolean} props.keyword The initial keyword passed to the state.
+ *
+ * @returns {void}
+ */
+ constructor( props ) {
+ super( props );
+
+ this.onChange = this.handleChange.bind( this );
+
+ this.state = {
+ showErrorMessage: false,
+ keyword: props.keyword,
+ };
+ }
+
+ /**
+ * Checks the keyword input for comma-separated words
+ *
+ * @param {String} keywordText The text of the input
+ *
+ * @returns {void}
+ */
+ checkKeywordInput( keywordText ) {
+ let separatedWords = keywordText.split( "," );
+ if ( separatedWords.length > 1 ) {
+ this.setState( { showErrorMessage: true } );
+ } else {
+ this.setState( { showErrorMessage: false } );
+ }
+ }
+
+ /**
+ * Displays the error message
+ *
+ * @param {String} input The text of the input
+ *
+ * @returns {Element} ErrorText The error message element
+ */
+ displayErrorMessage( input = "" ) {
+ if ( this.state.showErrorMessage && input !== "" ) {
+ return (
+
+ { ErrorMessage }
+
+ );
+ }
+ }
+
+ /**
+ * Handles changes in the KeywordInput.
+ *
+ * @param {Event} event The onChange event.
+ *
+ * @returns {void} Calls the checkKeywordInput-function.
+ */
+ handleChange( event ) {
+ this.setState( { keyword: event.target.value } );
+ this.checkKeywordInput( event.target.value );
+ }
+
+ /**
+ * Renders an input field, a label, and if the condition is met, an error message.
+ *
+ * @returns {ReactElement} The KeywordField react component including its label and eventual error message.
+ */
+ render() {
+ const color = this.state.showErrorMessage ? "red" : "white";
+ return(
+
+
+
+ { this.displayErrorMessage( this.state.keyword ) }
+
+ );
+ }
+}
+
+KeywordInput.propTypes = {
+ id: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ keyword: PropTypes.string,
+};
+
+KeywordInput.defaultProps = {
+ keyword: "",
+};
+
+export default KeywordInput;
+
diff --git a/composites/Plugin/Shared/tests/KeywordInputTest.js b/composites/Plugin/Shared/tests/KeywordInputTest.js
new file mode 100644
index 00000000..1611dd6f
--- /dev/null
+++ b/composites/Plugin/Shared/tests/KeywordInputTest.js
@@ -0,0 +1,57 @@
+import React from "react";
+import renderer from "react-test-renderer";
+import EnzymeAdapter from "enzyme-adapter-react-16";
+import Enzyme from "enzyme/build/index";
+
+import KeywordInput from "../components/KeywordInput";
+
+Enzyme.configure( { adapter: new EnzymeAdapter() } );
+
+describe( KeywordInput, () => {
+ it( "matches the snapshot by default", () => {
+ const component = renderer.create(
+ {
+ } } label="test label"/>
+ );
+
+ let tree = component.toJSON();
+ expect( tree ).toMatchSnapshot();
+ } );
+
+ it( "does not display the error message for a single keyword", () => {
+ const wrapper = Enzyme.mount(
+
+ );
+ wrapper.find( "input" ).simulate( "change", {
+ target: {
+ value: "Keyword1",
+ },
+ } );
+ expect( wrapper.state().showErrorMessage ).toEqual( false );
+ } );
+
+ it( "does not display the error message for two words separated by whitespace", () => {
+ const wrapper = Enzyme.mount(
+
+ );
+ wrapper.find( "input" ).simulate( "change", {
+ target: {
+ value: "Keyword1 Keyword2",
+ },
+ } );
+ expect( wrapper.state().showErrorMessage ).toEqual( false );
+ } );
+
+ it( "displays the error message for comma-separated words", () => {
+ const wrapper = Enzyme.mount(
+
+ );
+ wrapper.find( "input" ).simulate( "change", {
+ target: {
+ value: "Keyword1, Keyword2",
+ },
+ } );
+ expect( wrapper.state().showErrorMessage ).toEqual( true );
+ expect( wrapper.find( "div" ).text() ).toBeTruthy();
+ } );
+} );
diff --git a/composites/Plugin/Shared/tests/__snapshots__/KeywordInputTest.js.snap b/composites/Plugin/Shared/tests/__snapshots__/KeywordInputTest.js.snap
new file mode 100644
index 00000000..fdc6e459
--- /dev/null
+++ b/composites/Plugin/Shared/tests/__snapshots__/KeywordInputTest.js.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`KeywordInput matches the snapshot by default 1`] = `
+Array [
+ ,
+ .c0 {
+ margin-right: 0.5em;
+ border-color: white;
+}
+
+,
+]
+`;
diff --git a/yarn.lock b/yarn.lock
index 0a13ab12..f6802b2f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2195,6 +2195,12 @@ enzyme-adapter-utils@^1.3.0:
object.assign "^4.0.4"
prop-types "^15.6.0"
+enzyme-react-intl@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/enzyme-react-intl/-/enzyme-react-intl-2.0.0.tgz#1000400c0ab2ec6e7393f3f09cf8435cd3c884bf"
+ dependencies:
+ jsonfile "^4.0.0"
+
enzyme-to-json@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.3.tgz#ede45938fb309cd87ebd4386f60c754525515a07"
@@ -3063,7 +3069,7 @@ globule@^1.0.0:
lodash "~4.17.4"
minimatch "~3.0.2"
-graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
@@ -4365,6 +4371,12 @@ json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+jsonfile@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"