Skip to content

Commit

Permalink
Components: Update ExternalLink to support onClick handler (#45214)
Browse files Browse the repository at this point in the history
* Update ExternalLink to support onClick handler

* Add unit tests for ExternalLink component

It includes test cases for onClick event handler and preventing default action when clicking internal anchor links.

* Test using getByRole and userEvent

* Add CHANGELOG entry

* Add comments for defaultPrevented approach

* Remove unnecessary data-testid attributes
  • Loading branch information
Initsogar authored Oct 31, 2022
1 parent 3bce35a commit 19186e5
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
- `TabPanel`: Call `onSelect()` on every tab selection, regardless of whether it was triggered by user interaction ([#44028](https://github.com/WordPress/gutenberg/pull/44028)).
- `FontSizePicker`: Fallback to font size `slug` if `name` is undefined ([#45041](https://github.com/WordPress/gutenberg/pull/45041)).
- `AutocompleterUI`: fix issue where autocompleter UI would appear on top of other UI elements ([#44795](https://github.com/WordPress/gutenberg/pull/44795/))
- `ExternalLink`: Fix to re-enable support for `onClick` event handler ([#45214](https://github.com/WordPress/gutenberg/pull/45214)).

### Internal

Expand Down
18 changes: 13 additions & 5 deletions packages/components/src/external-link/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,25 @@ function UnforwardedExternalLink(
to prevent them from being opened in the editor. */
const isInternalAnchor = !! href?.startsWith( '#' );

const onClickHandler = (
event: React.MouseEvent< HTMLAnchorElement, MouseEvent >
) => {
if ( isInternalAnchor ) {
event.preventDefault();
}

if ( props.onClick ) {
props.onClick( event );
}
};

return (
/* eslint-disable react/jsx-no-target-blank */
<a
{ ...additionalProps }
className={ classes }
href={ href }
onClick={
isInternalAnchor
? ( event ) => event.preventDefault()
: undefined
}
onClick={ onClickHandler }
target="_blank"
rel={ optimizedRel }
ref={ ref }
Expand Down
106 changes: 106 additions & 0 deletions packages/components/src/external-link/test/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

/**
* Internal dependencies
*/
import { ExternalLink } from '..';

const setupUser = () =>
userEvent.setup( {
advanceTimers: jest.advanceTimersByTime,
} );

describe( 'ExternalLink', () => {
test( 'should call function passed in onClick handler when clicking the link', async () => {
const user = setupUser();
const onClickMock = jest.fn();

render(
<ExternalLink href="https://wordpress.org" onClick={ onClickMock }>
WordPress.org
</ExternalLink>
);

const link = screen.getByRole( 'link', {
name: 'WordPress.org (opens in a new tab)',
} );

await user.click( link );

expect( onClickMock ).toHaveBeenCalledTimes( 1 );
} );

test( 'should prevent default action when clicking an internal anchor link without passing onClick prop', async () => {
const user = setupUser();

render(
<ExternalLink href="#test">I&apos;m an anchor link!</ExternalLink>
);

const link = screen.getByRole( 'link', {
name: "I'm an anchor link! (opens in a new tab)",
} );

// We are using this approach so we can test the defaultPrevented
// without passing an onClick prop to the component.
const onClickMock = jest.fn();
link.onclick = onClickMock;

await user.click( link );
expect( onClickMock ).toHaveBeenCalledTimes( 1 );
expect( onClickMock ).toHaveBeenLastCalledWith(
expect.objectContaining( { defaultPrevented: true } )
);
} );

test( 'should call function passed in onClick handler and prevent default action when clicking an internal anchor link', async () => {
const user = setupUser();
const onClickMock = jest.fn();

render(
<ExternalLink href="#test" onClick={ onClickMock }>
I&apos;m an anchor link!
</ExternalLink>
);

const link = screen.getByRole( 'link', {
name: "I'm an anchor link! (opens in a new tab)",
} );

await user.click( link );
expect( onClickMock ).toHaveBeenCalledTimes( 1 );
expect( onClickMock ).toHaveBeenLastCalledWith(
expect.objectContaining( { defaultPrevented: true } )
);
} );

test( 'should not prevent default action when clicking a non anchor link without passing onClick prop', async () => {
const user = setupUser();

render(
<ExternalLink href="https://wordpress.org">
I&apos;m not an anchor link!
</ExternalLink>
);

const link = screen.getByRole( 'link', {
name: "I'm not an anchor link! (opens in a new tab)",
} );

// We are using this approach so we can test the defaultPrevented
// without passing an onClick prop to the component.
const onClickMock = jest.fn();
link.onclick = onClickMock;

await user.click( link );

expect( onClickMock ).toHaveBeenCalledTimes( 1 );
expect( onClickMock ).toHaveBeenLastCalledWith(
expect.objectContaining( { defaultPrevented: false } )
);
} );
} );

0 comments on commit 19186e5

Please sign in to comment.