Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moves <TextHighlight /> component to @wordpress/components package #18609

Merged
merged 12 commits into from Dec 3, 2019
6 changes: 6 additions & 0 deletions docs/manifest-devhub.json
Expand Up @@ -965,6 +965,12 @@
"markdown_source": "../packages/components/src/text-control/README.md",
"parent": "components"
},
{
"title": "TextHighlight",
"slug": "text-highlight",
"markdown_source": "../packages/components/src/text-highlight/README.md",
"parent": "components"
},
{
"title": "TextareaControl",
"slug": "textarea-control",
Expand Down
Expand Up @@ -3,11 +3,6 @@
*/
import classnames from 'classnames';

/**
* Internal dependencies
*/
import TextHighlight from './text-highlight';

/**
* WordPress dependencies
*/
Expand All @@ -16,6 +11,7 @@ import { __ } from '@wordpress/i18n';

import {
Icon,
TextHighlight,
} from '@wordpress/components';

export const LinkControlSearchItem = ( { itemProps, suggestion, isSelected = false, onClick, isURL = false, searchTerm = '' } ) => {
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/index.js
Expand Up @@ -66,6 +66,7 @@ export { default as Spinner } from './spinner';
export { default as TabPanel } from './tab-panel';
export { default as TextControl } from './text-control';
export { default as TextareaControl } from './textarea-control';
export { default as TextHighlight } from './text-highlight';
export { default as Tip } from './tip';
export { default as ToggleControl } from './toggle-control';
export { default as Toolbar } from './toolbar';
Expand Down
39 changes: 39 additions & 0 deletions packages/components/src/text-highlight/README.md
@@ -0,0 +1,39 @@
# TextHighlight

Highlights occurances of a given string within another string of text. Wraps each match with a [`<mark>` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark) which provides browser default styling.

## Usage

Pass in the `text` and the `highlight` string to be matched against.

In the example below, the string `Gutenberg` would be highlighted twice.

```jsx
import { TextHighlight } from '@wordpress/components';

const MyTextHighlight = () => (
<TextHighlight
text="Why do we like Gutenberg? Because Gutenberg is the best!"
highlight="Gutenberg"
/>
);
```

## Props

The component accepts the following props.

### text

The string of text to be tested for occurances of then given `highlight`.

- Type: `String`
- Required: Yes


### highlight

The string to search for and highlight within the `text`. Case insensitive. Multiple matches.

- Type: `String`
- Required: Yes
Expand Up @@ -7,7 +7,7 @@ import { escapeRegExp } from 'lodash';
* WordPress dependencies
*/
import {
Fragment,
__experimentalCreateInterpolateElement,
} from '@wordpress/element';

const TextHighlight = ( { text = '', highlight = '' } ) => {
Expand All @@ -16,13 +16,12 @@ const TextHighlight = ( { text = '', highlight = '' } ) => {
}

const regex = new RegExp( `(${ escapeRegExp( highlight ) })`, 'gi' );
const parts = text.split( regex );
return (
<Fragment>
{ parts.filter( ( part ) => part ).map( ( part, i ) => (
regex.test( part ) ? <mark key={ i }>{ part }</mark> : <span key={ i }>{ part }</span>
) ) }
</Fragment>

return __experimentalCreateInterpolateElement(
text.replace( regex, '<mark>$&</mark>' ),
{
mark: <mark />,
}
);
};

Expand Down
21 changes: 21 additions & 0 deletions packages/components/src/text-highlight/stories/index.js
@@ -0,0 +1,21 @@
/**
* External dependencies
*/
import { text } from '@storybook/addon-knobs';

/**
* Internal dependencies
*/
import TextHighlight from '../';

export default { title: 'Components|TextHighlight', component: TextHighlight };

export const _default = () => {
const textToMatch = text( 'Text', 'We call the new editor Gutenberg. The entire editing experience has been rebuilt for media rich pages and posts.' );

const textToHighlight = text( 'Text to be highlighted ', 'Gutenberg' );

return (
<TextHighlight text={ textToMatch } highlight={ textToHighlight } />
);
};
112 changes: 112 additions & 0 deletions packages/components/src/text-highlight/test/index.js
@@ -0,0 +1,112 @@
/**
* External dependencies
*/
import { render, unmountComponentAtNode } from 'react-dom';
import { act } from 'react-dom/test-utils';

/**
* Internal dependencies
*/
import TextHighlight from '../index';

let container = null;

beforeEach( () => {
// setup a DOM element as a render target
container = document.createElement( 'div' );
document.body.appendChild( container );
} );

afterEach( () => {
// cleanup on exiting
unmountComponentAtNode( container );
container.remove();
container = null;
} );

const defaultText = 'We call the new editor Gutenberg. The entire editing experience has been rebuilt for media rich pages and posts.';

describe( 'Basic rendering', () => {
it.each( [
[ 'Gutenberg' ],
[ 'media' ],
] )( 'should highlight the singular occurance of the text "%s" in the text if it exists', ( highlight ) => {
act( () => {
render(
<TextHighlight
text={ defaultText }
highlight={ highlight }
/>, container
);
} );

const highlightedEls = Array.from( container.querySelectorAll( 'mark' ) );

highlightedEls.forEach( ( el ) => {
expect( el.innerHTML ).toEqual( expect.stringContaining( highlight ) );
} );
} );

it( 'should highlight multiple occurances of the string every time it exists in the text', () => {
const highlight = 'edit';

act( () => {
render(
<TextHighlight
text={ defaultText }
highlight={ highlight }
/>, container
);
} );

const highlightedEls = Array.from( container.querySelectorAll( 'mark' ) );

expect( highlightedEls ).toHaveLength( 2 );

highlightedEls.forEach( ( el ) => {
expect( el.innerHTML ).toEqual( expect.stringContaining( highlight ) );
} );
} );

it( 'should highlight occurances of a string regardless of capitalisation', () => {
const highlight = 'The'; // note this occurs in both sentance of lowercase forms

act( () => {
render(
<TextHighlight
text={ defaultText }
highlight={ highlight }
/>, container
);
} );

const highlightedEls = Array.from( container.querySelectorAll( 'mark' ) );

// Our component matcher is case insensitive so string.Containing will
// return a false failure
const regex = new RegExp( highlight, 'i' );

expect( highlightedEls ).toHaveLength( 2 );

highlightedEls.forEach( ( el ) => {
expect( el.innerHTML ).toMatch( regex );
} );
} );

it( 'should not highlight a string that is not in the text', () => {
const highlight = 'Antidisestablishmentarianism';

act( () => {
render(
<TextHighlight
text={ defaultText }
highlight={ highlight }
/>, container
);
} );

const highlightedEls = Array.from( container.querySelectorAll( 'mark' ) );

expect( highlightedEls ).toHaveLength( 0 );
} );
} );
10 changes: 10 additions & 0 deletions storybook/test/__snapshots__/index.js.snap
Expand Up @@ -3262,6 +3262,16 @@ exports[`Storyshots Components|TabPanel Default 1`] = `
</div>
`;

exports[`Storyshots Components|TextHighlight Default 1`] = `
Array [
"We call the new editor ",
<mark>
Gutenberg
</mark>,
". The entire editing experience has been rebuilt for media rich pages and posts.",
]
`;

exports[`Storyshots Components|Tip Default 1`] = `
<div
className="components-tip"
Expand Down