diff --git a/apps/components/App.js b/apps/components/App.js
index 7301d1166b..b02c24d533 100644
--- a/apps/components/App.js
+++ b/apps/components/App.js
@@ -17,6 +17,7 @@ import Wizard from "./WizardWrapper";
import { Loader } from "@yoast/components";
import FacebookPreviewExample from "./FacebookPreviewExample";
import LinkSuggestionsWrapper from "./LinkSuggestionsExample";
+import WordOccurrencesWrapper from "./WordOccurrencesWrapper";
// Setup empty translations to prevent Jed error.
setLocaleData( { "": {} }, "yoast-components" );
@@ -97,6 +98,11 @@ const components = [
name: "FacebookPreview",
component: ,
},
+ {
+ id: "wordoccurrences-example",
+ name: "WordOccurrences",
+ component: ,
+ },
];
const LanguageDirectionContainer = styled.div`
diff --git a/apps/components/WordOccurrencesWrapper.js b/apps/components/WordOccurrencesWrapper.js
new file mode 100644
index 0000000000..0e10122d97
--- /dev/null
+++ b/apps/components/WordOccurrencesWrapper.js
@@ -0,0 +1,236 @@
+import React from "react";
+
+import ExamplesContainer from "./ExamplesContainer";
+import { WordOccurrences } from "@yoast/components";
+
+const initialRelevantWords = [
+ {
+ _word: "davids",
+ _stem: "david",
+ _occurrences: 2,
+ },
+ {
+ _word: "goliaths",
+ _stem: "goliath",
+ _occurrences: 6,
+ },
+ {
+ _word: "word",
+ _stem: "word",
+ _occurrences: 3,
+ },
+ {
+ _word: "yoast",
+ _stem: "yoast",
+ _occurrences: 8,
+ },
+ {
+ _word: "test",
+ _stem: "test",
+ _occurrences: 10,
+ },
+ {
+ _word: "apps",
+ _stem: "app",
+ _occurrences: 6,
+ },
+ {
+ _word: "teletubbies",
+ _stem: "teletubby",
+ _occurrences: 11,
+ },
+ {
+ _word: "strange",
+ _stem: "stange",
+ _occurrences: 4,
+ },
+ {
+ _word: "improvisation",
+ _stem: "improvisation",
+ _occurrences: 4,
+ },
+ {
+ _word: "ranking",
+ _stem: "rank",
+ _occurrences: 5,
+ },
+ {
+ _word: "google",
+ _stem: "google",
+ _occurrences: 5,
+ },
+ {
+ _word: "terms",
+ _stem: "term",
+ _occurrences: 8,
+ },
+ {
+ _word: "wordpress",
+ _stem: "wordpress",
+ _occurrences: 9,
+ },
+ {
+ _word: "inspiration",
+ _stem: "inspiration",
+ _occurrences: 2,
+ },
+ {
+ _word: "internal",
+ _stem: "internal",
+ _occurrences: 2,
+ },
+ {
+ _word: "linking",
+ _stem: "link",
+ _occurrences: 2,
+ },
+ {
+ _word: "suggestions",
+ _stem: "suggestion",
+ _occurrences: 3,
+ },
+ {
+ _word: "keyword",
+ _stem: "keyword",
+ _occurrences: 3,
+ },
+ {
+ _word: "analysis",
+ _stem: "analysis",
+ _occurrences: 4,
+ },
+ {
+ _word: "linguïns",
+ _stem: "linguïn",
+ _occurrences: 5,
+ },
+];
+
+const RelevantWordInputList = ( props ) => {
+ return props.relevantWords.map(
+ ( relevantWord, index ) => {
+ return ;
+ }
+ );
+};
+
+const RelevantWordInputRow = ( props ) => {
+ function changeWord( event ) {
+ props.onChange( event.target.value, "_word", props.index );
+ }
+ function changeStem( event ) {
+ props.onChange( event.target.value, "_stem", props.index );
+ }
+ function changeOccurrences( event ) {
+ props.onChange( event.target.value, "_occurrences", props.index );
+ }
+ function onDeleteClick(){
+ props.onDeleteClick( props.index );
+ }
+ return (
+
+
+
+
+
+
+ );
+};
+
+class WordOccurrencesWrapper extends React.Component {
+ constructor( props ) {
+ super( props );
+
+ this.state = {
+ relevantWords: initialRelevantWords.map( initialWord => Object.assign( {}, initialWord ) ),
+ };
+
+ this.changeRelevantWord = this.changeRelevantWord.bind( this );
+ this.removeRelevantWord = this.removeRelevantWord.bind( this );
+ this.addRelevantWordRow = this.addRelevantWordRow.bind( this );
+ this.resetRelevantWords = this.resetRelevantWords.bind( this );
+ }
+
+ changeRelevantWord( input, type, index ) {
+ this.setState( ( prevState ) => {
+ const nextState = Object.assign( {}, prevState );
+ nextState.relevantWords = [ ...prevState.relevantWords ];
+ nextState.relevantWords[ index ][ type ] = input;
+ return nextState;
+ } );
+ }
+
+ removeRelevantWord( index ) {
+ this.setState( ( prevState ) => {
+ const nextState = Object.assign( {}, prevState );
+ nextState.relevantWords.splice( index, 1 );
+ return {
+ relevantWords: nextState.relevantWords,
+ };
+ } );
+ }
+
+ addRelevantWordRow() {
+ this.setState( prevState => {
+ prevState.relevantWords.push(
+ {
+ _word: "",
+ _stem: "",
+ _occurrence: "",
+ }
+ );
+ return (
+ {
+ relevantWords: prevState.relevantWords,
+ }
+ );
+ } );
+ }
+
+ resetRelevantWords() {
+ this.setState( {
+ relevantWords: initialRelevantWords.map( initialWord => Object.assign( {}, initialWord ) ),
+ } );
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default WordOccurrencesWrapper;
diff --git a/packages/components/src/KeywordSuggestions.js b/packages/components/src/KeywordSuggestions.js
index c0c14e1364..97e9d83101 100644
--- a/packages/components/src/KeywordSuggestions.js
+++ b/packages/components/src/KeywordSuggestions.js
@@ -7,6 +7,7 @@ import { isFeatureEnabled } from "@yoast/feature-flag";
// Yoast dependencies.
import WordList from "./WordList";
+import WordOccurrences from "./WordOccurrences";
/**
@@ -89,23 +90,25 @@ const getKeywordSuggestionExplanation = keywords => {
* @returns {JSX.Element} Rendered WordList component.
*/
const KeywordSuggestions = ( { relevantWords, keywordLimit } ) => {
- const keywords = relevantWords.slice( 0, keywordLimit ).map( word => {
- return isFeatureEnabled( "improvedInternalLinking" ) ? word.getWord() : word.getCombination();
- } );
+ if ( isFeatureEnabled( "improvedInternalLinking" ) ) {
+ return ;
+ }
- return ( {
- return ( { getKeywordSuggestionExplanation( keywords ) }
);
- } }
- showAfterList={
- () => {
- return getKeywordResearchArticleLink();
+ const prominentWords = relevantWords.slice( 0, keywordLimit ).map( word => word.getWord() );
+ return (
+ {
+ return ( { getKeywordSuggestionExplanation( prominentWords ) }
);
+ } }
+ showAfterList={
+ () => {
+ return getKeywordResearchArticleLink();
+ }
}
- }
- />
+ />
);
};
diff --git a/packages/components/src/WordOccurrences.js b/packages/components/src/WordOccurrences.js
new file mode 100644
index 0000000000..8822314697
--- /dev/null
+++ b/packages/components/src/WordOccurrences.js
@@ -0,0 +1,123 @@
+import React from "react";
+import PropTypes from "prop-types";
+import styled from "styled-components";
+
+const ProminentWordOccurrence = styled.p`
+ display: inline-block;
+ margin: 2px 8px;
+`;
+
+const ProminentWord = styled( ProminentWordOccurrence )`
+ font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+
+const WordOccurrencesList = styled.ol`
+ list-style: none;
+ padding: 0;
+ width: 100%;
+`;
+
+const WordBarContainer = styled.li`
+ margin: 8px 0;
+ height: calc( 1.375em + 4px );
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ background:
+ linear-gradient(
+ to right,
+ rgba(164, 40, 106, 0.2) ${ props => props.width },
+ ${ props => props.width },
+ transparent
+ );
+`;
+
+/**
+ * A list item containing a word and its occurrence, with a gradient background reflecting the relative occurrence.
+ *
+ * @constructor
+ *
+ * @param {Object} word The word.
+ * @param {number} occurrences The word's occurrence.
+ * @param {string} width A string indicating the percentage of the bar that should be coloured purple.
+ * @returns {JSX} The list item.
+ */
+const WordBar = ( { word, occurrence, width } ) => {
+ return (
+
+ { word }
+ { occurrence }
+
+ );
+};
+
+WordBar.propTypes = {
+ word: PropTypes.string.isRequired,
+ occurrence: PropTypes.number.isRequired,
+ width: PropTypes.string.isRequired,
+};
+
+/**
+ * The WordOccurrences list, that contains words, their occurrence, and bars reflecting their relative occurrence.
+ *
+ * @param {Array} words The array of objects containing words and occurrences.
+ *
+ * @returns {ReactElement} The list of words, their occurrences, and bars.
+ */
+class WordOccurrences extends React.Component {
+ constructor( props ) {
+ super( props );
+
+ this.state = {
+ words: [ ...props.words ] || [],
+ occurrences: {
+ min: 0,
+ max: 0,
+ },
+ };
+ }
+
+ static getDerivedStateFromProps( props ) {
+ const words = [ ...props.words ];
+ words.sort( ( a, b ) => {
+ return b._occurrences - a._occurrences;
+ } );
+ const allOccurrences = words.map( wordObject => wordObject._occurrences );
+
+ return {
+ occurrences: {
+ min: Math.min( ...allOccurrences ),
+ max: Math.max( ...allOccurrences ),
+ },
+ words: words,
+ };
+ }
+
+ render() {
+ return (
+
+ {
+ this.state.words.map(
+ ( wordObject, index ) => {
+ const width = `${ ( wordObject._occurrences / this.state.occurrences.max ) * 100 }%`;
+ return ;
+ }
+ )
+ }
+
+ );
+ }
+}
+
+WordOccurrences.propTypes = {
+ words: PropTypes.array.isRequired,
+};
+
+export default WordOccurrences;
diff --git a/packages/components/src/index.js b/packages/components/src/index.js
index 0fc1e9e961..402c7368a3 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -70,6 +70,7 @@ export { default as Tabs } from "./Tabs";
export { default as Warning } from "./Warning";
export { default as YouTubeVideo } from "./YouTubeVideo";
export { default as WordList } from "./WordList";
+export { default as WordOccurrences } from "./WordOccurrences";
export { default as Checkbox } from "./Checkbox";
export { ListTable, ZebrafiedListTable } from "./table/ListTable";