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

Adds 'nofollow' setting to Button block #54110

Merged
merged 9 commits into from Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/block-library/src/button/constants.js
@@ -0,0 +1,3 @@
export const NEW_TAB_REL = 'noreferrer noopener';
export const NEW_TAB_TARGET = '_blank';
export const NOFOLLOW_REL = 'nofollow';
59 changes: 30 additions & 29 deletions packages/block-library/src/button/edit.js
Expand Up @@ -3,6 +3,12 @@
*/
import classnames from 'classnames';

/**
* Internal dependencies
*/
import { NEW_TAB_TARGET, NOFOLLOW_REL } from './constants';
import { getUpdatedLinkAttributes } from './get-updated-link-attributes';

/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -32,9 +38,14 @@ import { displayShortcut, isKeyboardEvent } from '@wordpress/keycodes';
import { link, linkOff } from '@wordpress/icons';
import { createBlock } from '@wordpress/blocks';
import { useMergeRefs } from '@wordpress/compose';
import { prependHTTP } from '@wordpress/url';

const NEW_TAB_REL = 'noreferrer noopener';
const LINK_SETTINGS = [
bangank36 marked this conversation as resolved.
Show resolved Hide resolved
...LinkControl.DEFAULT_LINK_SETTINGS,
{
id: 'nofollow',
title: __( 'Mark as nofollow' ),
},
];

function WidthPanel( { selectedWidth, setAttributes } ) {
function handleChange( newWidth ) {
Expand Down Expand Up @@ -92,22 +103,6 @@ function ButtonEdit( props ) {

const TagName = tagName || 'a';

function onToggleOpenInNewTab( value ) {
const newLinkTarget = value ? '_blank' : undefined;

let updatedRel = rel;
if ( newLinkTarget && ! rel ) {
updatedRel = NEW_TAB_REL;
} else if ( ! newLinkTarget && rel === NEW_TAB_REL ) {
updatedRel = undefined;
}

setAttributes( {
linkTarget: newLinkTarget,
rel: updatedRel,
} );
}

function setButtonText( newText ) {
// Remove anchor tags from button text content.
setAttributes( { text: newText.replace( /<\/?a[^>]*>/g, '' ) } );
Expand Down Expand Up @@ -138,7 +133,8 @@ function ButtonEdit( props ) {

const [ isEditingURL, setIsEditingURL ] = useState( false );
const isURLSet = !! url;
const opensInNewTab = linkTarget === '_blank';
const opensInNewTab = linkTarget === NEW_TAB_TARGET;
const nofollow = !! rel?.includes( NOFOLLOW_REL );
const isLinkTag = 'a' === TagName;

function startEditing( event ) {
Expand All @@ -164,8 +160,8 @@ function ButtonEdit( props ) {
// Memoize link value to avoid overriding the LinkControl's internal state.
// This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/51256.
const linkValue = useMemo(
() => ( { url, opensInNewTab } ),
[ url, opensInNewTab ]
() => ( { url, opensInNewTab, nofollow } ),
[ url, opensInNewTab, nofollow ]
);

return (
Expand Down Expand Up @@ -256,20 +252,25 @@ function ButtonEdit( props ) {
<LinkControl
value={ linkValue }
onChange={ ( {
url: newURL = '',
url: newURL,
opensInNewTab: newOpensInNewTab,
getdave marked this conversation as resolved.
Show resolved Hide resolved
} ) => {
setAttributes( { url: prependHTTP( newURL ) } );

if ( opensInNewTab !== newOpensInNewTab ) {
onToggleOpenInNewTab( newOpensInNewTab );
}
} }
nofollow: newNofollow,
} ) =>
setAttributes(
getUpdatedLinkAttributes( {
rel,
url: newURL,
opensInNewTab: newOpensInNewTab,
nofollow: newNofollow,
} )
)
}
onRemove={ () => {
unlink();
richTextRef.current?.focus();
} }
forceIsEditingLink={ isEditingURL }
settings={ LINK_SETTINGS }
/>
</Popover>
) }
Expand Down
@@ -0,0 +1,54 @@
/**
* Internal dependencies
*/
import { NEW_TAB_REL, NEW_TAB_TARGET, NOFOLLOW_REL } from './constants';

/**
* WordPress dependencies
*/
import { prependHTTP } from '@wordpress/url';

/**
* Updates the link attributes.
*
* @param {Object} attributes The current block attributes.
* @param {string} attributes.rel The current link rel attribute.
* @param {string} attributes.url The current link url.
* @param {boolean} attributes.opensInNewTab Whether the link should open in a new window.
* @param {boolean} attributes.nofollow Whether the link should be marked as nofollow.
*/
export function getUpdatedLinkAttributes( {
rel = '',
url = '',
opensInNewTab,
nofollow,
} ) {
let newLinkTarget;
// Since `rel` is editable attribute, we need to check for existing values and proceed accordingly.
let updatedRel = rel;

if ( opensInNewTab ) {
newLinkTarget = NEW_TAB_TARGET;
updatedRel = updatedRel?.includes( NEW_TAB_REL )
? updatedRel
: updatedRel + ` ${ NEW_TAB_REL }`;
} else {
const relRegex = new RegExp( `\\b${ NEW_TAB_REL }\\s*`, 'g' );
updatedRel = updatedRel?.replace( relRegex, '' ).trim();
}

if ( nofollow ) {
updatedRel = updatedRel?.includes( NOFOLLOW_REL )
? updatedRel
: updatedRel + ` ${ NOFOLLOW_REL }`;
} else {
const relRegex = new RegExp( `\\b${ NOFOLLOW_REL }\\s*`, 'g' );
updatedRel = updatedRel?.replace( relRegex, '' ).trim();
}

return {
url: prependHTTP( url ),
linkTarget: newLinkTarget,
rel: updatedRel || undefined,
};
}
@@ -0,0 +1,113 @@
/**
* Internal dependencies
*/
import { getUpdatedLinkAttributes } from '../get-updated-link-attributes';

describe( 'getUpdatedLinkAttributes method', () => {
it( 'should correctly handle unassigned rel', () => {
const options = {
url: 'example.com',
opensInNewTab: true,
nofollow: false,
};

const result = getUpdatedLinkAttributes( options );

expect( result.url ).toEqual( 'http://example.com' );
expect( result.linkTarget ).toEqual( '_blank' );
expect( result.rel ).toEqual( 'noreferrer noopener' );
} );

it( 'should return empty rel value as undefined', () => {
const options = {
url: 'example.com',
opensInNewTab: false,
nofollow: false,
};

const result = getUpdatedLinkAttributes( options );

expect( result.url ).toEqual( 'http://example.com' );
expect( result.linkTarget ).toEqual( undefined );
expect( result.rel ).toEqual( undefined );
} );

it( 'should correctly handle rel with existing values', () => {
const options = {
url: 'example.com',
opensInNewTab: true,
nofollow: true,
rel: 'rel_value',
};

const result = getUpdatedLinkAttributes( options );

expect( result.url ).toEqual( 'http://example.com' );
expect( result.linkTarget ).toEqual( '_blank' );
expect( result.rel ).toEqual(
'rel_value noreferrer noopener nofollow'
);
} );

it( 'should correctly update link attributes with opensInNewTab', () => {
const options = {
url: 'example.com',
opensInNewTab: true,
nofollow: false,
rel: 'rel_value',
};

const result = getUpdatedLinkAttributes( options );

expect( result.url ).toEqual( 'http://example.com' );
expect( result.linkTarget ).toEqual( '_blank' );
expect( result.rel ).toEqual( 'rel_value noreferrer noopener' );
} );

it( 'should correctly update link attributes with nofollow', () => {
const options = {
url: 'example.com',
opensInNewTab: false,
nofollow: true,
rel: 'rel_value',
};

const result = getUpdatedLinkAttributes( options );

expect( result.url ).toEqual( 'http://example.com' );
expect( result.linkTarget ).toEqual( undefined );
expect( result.rel ).toEqual( 'rel_value nofollow' );
} );

it( 'should correctly handle rel with existing nofollow values and remove duplicates', () => {
const options = {
url: 'example.com',
opensInNewTab: true,
nofollow: true,
rel: 'rel_value nofollow',
};

const result = getUpdatedLinkAttributes( options );

expect( result.url ).toEqual( 'http://example.com' );
expect( result.linkTarget ).toEqual( '_blank' );
expect( result.rel ).toEqual(
'rel_value nofollow noreferrer noopener'
);
} );

it( 'should correctly handle rel with existing new tab values and remove duplicates', () => {
const options = {
url: 'example.com',
opensInNewTab: true,
nofollow: false,
rel: 'rel_value noreferrer noopener',
};

const result = getUpdatedLinkAttributes( options );

expect( result.url ).toEqual( 'http://example.com' );
expect( result.linkTarget ).toEqual( '_blank' );
expect( result.rel ).toEqual( 'rel_value noreferrer noopener' );
} );
} );
93 changes: 93 additions & 0 deletions test/e2e/specs/editor/blocks/buttons.spec.js
Expand Up @@ -162,6 +162,99 @@ test.describe( 'Buttons', () => {
);
} );

test( 'can toggle button link settings', async ( {
editor,
page,
pageUtils,
} ) => {
await editor.insertBlock( { name: 'core/buttons' } );
await page.keyboard.type( 'WordPress' );
await pageUtils.pressKeys( 'primary+k' );
await page.keyboard.type( 'https://www.wordpress.org/' );
await page.keyboard.press( 'Enter' );

// Edit link.
await page.getByRole( 'button', { name: 'Edit' } ).click();

// Open Advanced settings panel.
await page
.getByRole( 'region', {
name: 'Editor content',
} )
.getByRole( 'button', {
name: 'Advanced',
} )
.click();

const newTabCheckbox = page.getByLabel( 'Open in new tab' );
const noFollowCheckbox = page.getByLabel( 'nofollow' );

// Navigate to and toggle the "Open in new tab" checkbox.
await newTabCheckbox.click();

// Toggle should still have focus and be checked.
await expect( newTabCheckbox ).toBeChecked();
await expect( newTabCheckbox ).toBeFocused();

await page
//TODO: change to a better selector when https://github.com/WordPress/gutenberg/issues/51060 is resolved.
.locator( '.block-editor-link-control' )
.getByRole( 'button', { name: 'Save' } )
.click();

// The link should have been inserted.
await expect.poll( editor.getBlocks ).toMatchObject( [
{
name: 'core/buttons',
innerBlocks: [
{
name: 'core/button',
attributes: {
text: 'WordPress',
url: 'https://www.wordpress.org/',
rel: 'noreferrer noopener',
linkTarget: '_blank',
},
},
],
},
] );

// Edit link again.
await page.getByRole( 'button', { name: 'Edit' } ).click();

// Navigate to and toggle the "nofollow" checkbox.
await noFollowCheckbox.click();

// expect settings for `Open in new tab` and `No follow`
await expect( newTabCheckbox ).toBeChecked();
await expect( noFollowCheckbox ).toBeChecked();

await page
//TODO: change to a better selector when https://github.com/WordPress/gutenberg/issues/51060 is resolved.
.locator( '.block-editor-link-control' )
.getByRole( 'button', { name: 'Save' } )
.click();

// Check the content again.
await expect.poll( editor.getBlocks ).toMatchObject( [
{
name: 'core/buttons',
innerBlocks: [
{
name: 'core/button',
attributes: {
text: 'WordPress',
url: 'https://www.wordpress.org/',
rel: 'noreferrer noopener nofollow',
linkTarget: '_blank',
},
},
],
},
] );
} );

test( 'can resize width', async ( { editor, page } ) => {
await editor.insertBlock( { name: 'core/buttons' } );
await page.keyboard.type( 'Content' );
Expand Down