diff --git a/.eslintrc.js b/.eslintrc.js
index ebdbaa72c6f..4142fa78221 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -20,40 +20,35 @@ const APACHE_2_0_LICENSE_HEADER = `
`;
module.exports = {
- parser: "@typescript-eslint/parser",
+ parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
- jsx: true
- }
+ jsx: true,
+ },
},
settings: {
- "import/resolver": {
- node: {
- extensions: [".ts", ".tsx", ".js", ".json"]
+ 'import/resolver': {
+ node: {
+ extensions: ['.ts', '.tsx', '.js', '.json'],
},
webpack: {
- config: "./src-docs/webpack.config.js"
- }
+ config: './src-docs/webpack.config.js',
+ },
},
react: {
- version: "detect"
- }
+ version: 'detect',
+ },
},
extends: [
- "@elastic/eslint-config-kibana",
- "plugin:@typescript-eslint/recommended",
+ '@elastic/eslint-config-kibana',
+ 'plugin:@typescript-eslint/recommended',
// Prettier options need to come last, in order to override other style
// rules.
- "prettier/react",
- "prettier/standard",
- "plugin:prettier/recommended"
- ],
- plugins: [
- "jsx-a11y",
- "prettier",
- "local",
- "react-hooks"
+ 'prettier/react',
+ 'prettier/standard',
+ 'plugin:prettier/recommended',
],
+ plugins: ['jsx-a11y', 'prettier', 'local', 'react-hooks'],
rules: {
"prefer-template": "error",
"local/i18n": "error",
@@ -67,7 +62,6 @@ module.exports = {
],
"no-use-before-define": "off",
"quotes": ["warn", "single", "avoid-escape"],
-
"jsx-a11y/accessible-emoji": "error",
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-has-content": "error",
@@ -118,6 +112,16 @@ module.exports = {
"@typescript-eslint/no-inferrable-types": "off",
},
env: {
- jest: true
- }
+ jest: true,
+ },
+ overrides: [
+ {
+ files: ['*.d.ts'],
+ rules: {
+ 'react/no-multi-comp': 'off',
+ 'react/prefer-es6-class': 'off',
+ 'react/prefer-stateless-function': 'off',
+ },
+ },
+ ],
};
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c78669d6f07..005bf726e5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## [`master`](https://github.com/elastic/eui/tree/master)
+- Added `useEuiTextDiff` react hook utility ([#3288](https://github.com/elastic/eui/pull/3288))
+
**Bug fixes**
- Fixed `EuiCodeBlockImpl` testenv mock pass-through of `data-test-subj` attribute ([#3560](https://github.com/elastic/eui/pull/3560))
diff --git a/package.json b/package.json
index d4f0703153e..5ec0d33e562 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"react-virtualized": "^9.21.2",
"resize-observer-polyfill": "^1.5.0",
"tabbable": "^3.0.0",
+ "text-diff": "^1.0.1",
"uuid": "^3.1.0"
},
"devDependencies": {
diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js
index 6882010051d..e4b7790de8e 100644
--- a/src-docs/src/routes.js
+++ b/src-docs/src/routes.js
@@ -195,6 +195,8 @@ import { TableInMemoryExample } from './views/tables/tables_in_memory_example';
import { TabsExample } from './views/tabs/tabs_example';
+import { TextDiffExample } from './views/text_diff/text_diff_example';
+
import { TextExample } from './views/text/text_example';
import { TitleExample } from './views/title/title_example';
@@ -383,6 +385,7 @@ const navigation = [
LoadingExample,
ProgressExample,
StatExample,
+
TextExample,
TitleExample,
ToastExample,
@@ -444,6 +447,7 @@ const navigation = [
PrettyDurationExample,
ResizeObserverExample,
ResponsiveExample,
+ TextDiffExample,
ToggleExample,
WindowEventExample,
].map(example => createExample(example)),
diff --git a/src-docs/src/views/html_id_generator/html_id_generator_example.js b/src-docs/src/views/html_id_generator/html_id_generator_example.js
index 07c652cd538..d882d9efde4 100644
--- a/src-docs/src/views/html_id_generator/html_id_generator_example.js
+++ b/src-docs/src/views/html_id_generator/html_id_generator_example.js
@@ -27,7 +27,7 @@ const PrefixSufixHtml = renderToHtml(PrefixSufix);
const prefixSuffixSnippet = " htmlIdGenerator('prefix')('suffix')";
export const HtmlIdGeneratorExample = {
- title: 'HTML ID Generator',
+ title: 'HTML ID generator',
sections: [
{
source: [
diff --git a/src-docs/src/views/overlay_mask/overlay_mask_example.js b/src-docs/src/views/overlay_mask/overlay_mask_example.js
index e7cbebf23ca..4df9912a382 100644
--- a/src-docs/src/views/overlay_mask/overlay_mask_example.js
+++ b/src-docs/src/views/overlay_mask/overlay_mask_example.js
@@ -16,7 +16,7 @@ const overlayMaskSnippet = ``;
export const OverlayMaskExample = {
- title: 'Overlay Mask',
+ title: 'Overlay mask',
sections: [
{
source: [
diff --git a/src-docs/src/views/text_diff/props.tsx b/src-docs/src/views/text_diff/props.tsx
new file mode 100644
index 00000000000..859d46b5a6f
--- /dev/null
+++ b/src-docs/src/views/text_diff/props.tsx
@@ -0,0 +1,7 @@
+import React, { FunctionComponent } from 'react';
+
+import { EuiTextDiffProps } from '../../../../src/components/text_diff';
+
+export const useEuiTextDiffProp: FunctionComponent = () => {
+ return ;
+};
diff --git a/src-docs/src/views/text_diff/text_diff.js b/src-docs/src/views/text_diff/text_diff.js
new file mode 100644
index 00000000000..297638dad2b
--- /dev/null
+++ b/src-docs/src/views/text_diff/text_diff.js
@@ -0,0 +1,49 @@
+import React, { useState, useEffect } from 'react';
+
+import {
+ useEuiTextDiff,
+ EuiCode,
+ EuiSpacer,
+ EuiTextColor,
+ EuiText,
+} from '../../../../src/components';
+
+export default () => {
+ const [del, setDel] = useState(0);
+ const [ins, setIns] = useState(0);
+
+ const beforeText =
+ 'Orbiting this at a distance of roughly ninety-two million miles is an utterly insignificant little blue green planet whose ape- descended life forms are so amazingly primitive that they still think digital watches are a pretty neat idea.';
+ const afterText =
+ 'Orbiting those at a distance of roughly ninety-nine billion yards is not insignificant dwaf red green planet whose ape- ascended life forms are so amazingly primitive that they still think digital clocks are a pretty neat idea.';
+
+ const [rendered, textDiffObject] = useEuiTextDiff({
+ beforeText,
+ afterText,
+ });
+
+ useEffect(() => {
+ textDiffObject.forEach(el => {
+ if (el[0] === 1) {
+ setIns(add => add + 1);
+ } else if (el[0] === -1) {
+ setDel(sub => sub + 1);
+ }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+ <>
+
+
{rendered}
+
+
+
+ {ins} Insertions,
+ {del}
+ Deletions
+
+ >
+ );
+};
diff --git a/src-docs/src/views/text_diff/text_diff_custom_components.js b/src-docs/src/views/text_diff/text_diff_custom_components.js
new file mode 100644
index 00000000000..7460c1e01d9
--- /dev/null
+++ b/src-docs/src/views/text_diff/text_diff_custom_components.js
@@ -0,0 +1,22 @@
+import React from 'react';
+
+import { useEuiTextDiff, EuiCodeBlock } from '../../../../src/components';
+
+export default () => {
+ const beforeText =
+ 'Orbiting this at a distance of roughly ninety-two million miles is an utterly insignificant little blue green planet whose ape- descended life forms are so amazingly primitive that they still think digital watches are a pretty neat idea.';
+ const afterText =
+ 'Orbiting those at a distance of roughly ninety-nine billion yards is not insignificant dwaf red green planet whose ape- ascended life forms are so amazingly primitive that they still think digital clocks are a pretty neat idea.';
+ const [rendered] = useEuiTextDiff({
+ beforeText,
+ afterText,
+ insertComponent: 'strong',
+ deleteComponent: 's',
+ });
+
+ return (
+
+ {rendered}
+
+ );
+};
diff --git a/src-docs/src/views/text_diff/text_diff_example.js b/src-docs/src/views/text_diff/text_diff_example.js
new file mode 100644
index 00000000000..a4e20e776ee
--- /dev/null
+++ b/src-docs/src/views/text_diff/text_diff_example.js
@@ -0,0 +1,108 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import { renderToHtml } from '../../services';
+
+import { GuideSectionTypes } from '../../components';
+
+import { EuiCode } from '../../../../src/components';
+import { useEuiTextDiffProp } from './props';
+import TextDiff from './text_diff';
+const textDiffSource = require('!!raw-loader!./text_diff');
+const textDiffHtml = renderToHtml(TextDiff);
+
+import TextDiffCustomComponents from './text_diff_custom_components';
+const customComponentsSource = require('!!raw-loader!./text_diff_custom_components');
+const customComponentsHtml = renderToHtml(TextDiffCustomComponents);
+
+export const TextDiffExample = {
+ title: 'Text diff',
+ isNew: true,
+ sections: [
+ {
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: textDiffSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: textDiffHtml,
+ },
+ ],
+ text: (
+ <>
+
+ The hook, useEuiTextDiff, generates a set of
+ changes between two strings. It returns both React elements for
+ displaying the diff and an object representing the identified
+ changes. The timeout prop is used to set how many
+ seconds any diff's exploration phase may take. The default
+ value is 0.1, a value of 0 disables the timeout and lets diff run
+ until completion. The higher the timeout, the more detailed the
+ comparison.
+
+ By default, the hook will wrap deletions with{' '}
+ {''} and insertions with{' '}
+ {''} elements. You can replace these
+ elements with the deleteComponent and{' '}
+ insertComponent
+ props respectively.
+
+
+ Also, since rendered is simple html string, you
+ can wrap it in any contextual element like{' '}
+
+ EuiText
+ {' '}
+ or{' '}
+
+ EuiCodeBlock
+
+ .
+
+ Orbiting th
+
+ i
+
+
+ o
+
+ s
+
+ e
+
+ at a distance of roughly ninety-
+
+ two
+
+
+ nine
+
+
+
+ m
+
+
+ b
+
+ illion
+
+ mile
+
+
+ yard
+
+ s is
+
+ a
+
+ n
+
+ ut
+
+
+ o
+
+ t
+
+ erly
+
+ insignificant
+
+ little
+
+
+ dwaf
+
+
+
+ blu
+
+
+ r
+
+ e
+
+ d
+
+ green planet whose ape-
+
+ de
+
+
+ a
+
+ scended life forms are so amazingly primitive that they still think digital
+
+ wat
+
+
+ clo
+
+ c
+
+ he
+
+
+ k
+
+ s are a pretty neat idea.
+
+ scended life forms are so amazingly primitive that they still think digital
+
+
+ wat
+
+
+ clo
+
+
+ c
+
+
+ he
+
+
+ k
+
+
+ s are a pretty neat idea.
+
+
+
+`;
diff --git a/src/components/text_diff/_index.scss b/src/components/text_diff/_index.scss
new file mode 100644
index 00000000000..e4b6a8c0360
--- /dev/null
+++ b/src/components/text_diff/_index.scss
@@ -0,0 +1 @@
+@import 'text_diff';
diff --git a/src/components/text_diff/_text_diff.scss b/src/components/text_diff/_text_diff.scss
new file mode 100644
index 00000000000..6e38229934d
--- /dev/null
+++ b/src/components/text_diff/_text_diff.scss
@@ -0,0 +1,9 @@
+.euiTextDiff {
+ del {
+ color: $euiColorDangerText;
+ }
+
+ ins {
+ color: $euiColorSuccessText;
+ }
+}
diff --git a/src/components/text_diff/index.ts b/src/components/text_diff/index.ts
new file mode 100644
index 00000000000..4a733d52cc9
--- /dev/null
+++ b/src/components/text_diff/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { useEuiTextDiff, EuiTextDiffProps } from './text_diff';
diff --git a/src/components/text_diff/text-diff.d.ts b/src/components/text_diff/text-diff.d.ts
new file mode 100644
index 00000000000..1462884fd85
--- /dev/null
+++ b/src/components/text_diff/text-diff.d.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+declare module 'text-diff' {
+ interface ConstructorProps {
+ timeout: number;
+ }
+
+ type DiffElement = [-1 | 0 | 1, string];
+
+ class Diff {
+ constructor({ timeout }: ConstructorProps);
+ main: (initialText: string, currentText: string) => DiffElement[];
+ }
+ export = Diff;
+}
diff --git a/src/components/text_diff/text-diff.test.tsx b/src/components/text_diff/text-diff.test.tsx
new file mode 100644
index 00000000000..3f8b19e2c8d
--- /dev/null
+++ b/src/components/text_diff/text-diff.test.tsx
@@ -0,0 +1,64 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { useEuiTextDiff } from './text_diff';
+const beforeText =
+ 'Orbiting this at a distance of roughly ninety-two million miles is an utterly insignificant little blue green planet whose ape- descended life forms are so amazingly primitive that they still think digital watches are a pretty neat idea.';
+const afterText =
+ 'Orbiting those at a distance of roughly ninety-nine billion yards is not insignificant dwaf red green planet whose ape- ascended life forms are so amazingly primitive that they still think digital clocks are a pretty neat idea.';
+
+describe('useEuiTextDiff', () => {
+ test('is rendered', () => {
+ const Element = () => {
+ const renderedComponent = useEuiTextDiff({
+ beforeText,
+ afterText,
+ timeout: 0,
+ })[0];
+ return <>{renderedComponent}>;
+ };
+ const component = shallow();
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('props', () => {
+ describe('custom components', () => {
+ test('is rendered', () => {
+ const Element = () => {
+ const renderedComponent = useEuiTextDiff({
+ beforeText,
+ afterText,
+ timeout: 0,
+ insertComponent: 'strong',
+ deleteComponent: 's',
+ sameComponent: 'p',
+ })[0];
+ return <>{renderedComponent}>;
+ };
+ const component = shallow();
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/src/components/text_diff/text_diff.tsx b/src/components/text_diff/text_diff.tsx
new file mode 100644
index 00000000000..d600a3d6767
--- /dev/null
+++ b/src/components/text_diff/text_diff.tsx
@@ -0,0 +1,99 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { HTMLAttributes, useMemo, ElementType } from 'react';
+import Diff from 'text-diff';
+import classNames from 'classnames';
+import { CommonProps } from '../common';
+
+interface Props {
+ /**
+ * The starting string
+ */
+ beforeText: string;
+ /**
+ * The string used to compare against `beforeText`
+ */
+ afterText: string;
+ /**
+ * HTML element to wrap insertion differences.
+ * Defaults to `ins`
+ */
+ insertComponent?: ElementType;
+ /**
+ * HTML element to wrap deletion differences.
+ * Defaults to `del`
+ */
+ deleteComponent?: ElementType;
+ /**
+ * HTML element to wrap text with no differences.
+ * Doesn't wrap with anything by default
+ */
+ sameComponent?: ElementType;
+ /**
+ * Time in milliseconds. Passing a timeout of value '0' disables the timeout state
+ */
+ timeout?: number;
+}
+
+export type EuiTextDiffProps = CommonProps &
+ Props &
+ HTMLAttributes;
+
+export const useEuiTextDiff = ({
+ className,
+ insertComponent = 'ins',
+ deleteComponent = 'del',
+ sameComponent,
+ beforeText = '',
+ afterText = '',
+ timeout = 0.1,
+ ...rest
+}: EuiTextDiffProps) => {
+ const textDiff = useMemo(() => {
+ const diff = new Diff({ timeout }); // options may be passed to constructor
+
+ return diff.main(beforeText, afterText);
+ }, [beforeText, afterText, timeout]); // produces diff array
+
+ const classes = classNames('euiTextDiff', className);
+
+ const rendereredHtml = useMemo(() => {
+ const html = [];
+ if (textDiff)
+ for (let i = 0; i < textDiff.length; i++) {
+ let Element;
+ const el = textDiff[i];
+ if (el[0] === 1) Element = insertComponent;
+ else if (el[0] === -1) Element = deleteComponent;
+ else if (sameComponent) Element = sameComponent;
+ if (Element) html.push({el[1]});
+ else html.push(el[1]);
+ }
+
+ return html;
+ }, [textDiff, deleteComponent, insertComponent, sameComponent]); // produces diff array
+
+ return [
+