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

Add external link functionality to the Button component #6786

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions components/button/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
This component is used to implement dang sweet buttons.
# Button

#### Props
This component is used to implement buttons and links.
It's also used to implement links that open in a new browser's tab.

The following props are used to control the display of the component. Any additional props will be passed to the rendered `<a />` or `<button />` element. The presence of a `href` prop determines whether an anchor element is rendered instead of a button.
## Props

The component accepts the following props. Any additional props will be passed to the rendered `<a />` or `<button />` element. The presence of a `href` prop determines whether an anchor element is rendered instead of a button.

* `isPrimary`: (bool) whether the button is styled as a primary button.
* `isLarge`: (bool) whether the button is styled as a large button.
* `isSmall`: (bool) whether the button is styled as a small button.
* `isToggled`: (bool) whether the button is styled with an inversion of colors.
* `isBusy`: (bool) whether the button is styled with a "busy" animation.
* `disabled`: (bool) whether the `<button />` element is disabled.
* `href`: (string) if this property is added, it will use an `a` rather than a `button` element.
* `rel`: (string) the `rel` attribute for the `<a />` element.
Copy link
Member

@aduth aduth May 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rel has specific logic when assigned as null, which is not (but should be) documented here.

* `target`: (string) the `target` attribute for the `<a />` element.
44 changes: 40 additions & 4 deletions components/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@
* External dependencies
*/
import classnames from 'classnames';
import { compact, uniq, includes, omit } from 'lodash';

/**
* WordPress dependencies
*/
import { createElement, forwardRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import Dashicon from '../dashicon';
import './style.scss';

export function Button( props, ref ) {
const {
href,
target,
rel = '',
isPrimary,
isLarge,
isSmall,
Expand All @@ -25,28 +29,60 @@ export function Button( props, ref ) {
className,
disabled,
focus,
children,
...additionalProps
} = props;

const tag = href !== undefined && ! disabled ? 'a' : 'button';
const tagProps = tag === 'a' ? { href, target, rel } : { type: 'button', disabled };
const { icon = true } = additionalProps;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I see it, additionalProps are those which are not explicitly handled by the component itself. In this case, we are handling icon, and it should be included in the destructuring which occurs at the top of this function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should document icon. In particular, it's unclear to me whether this is always a boolean (where I might expect a prefix to indicate this such as hasIcon, showIcon) or an overloaded value where a boolean overrides a default.

const isExternalLink = href && ( target && ! includes( [ '_self', '_parent', '_top' ], target ) );

const classes = classnames( 'components-button', className, {
button: ( isPrimary || isLarge || isSmall ),
'button-primary': isPrimary,
'button-large': isLarge,
'button-small': isSmall,
'is-toggled': isToggled,
'is-busy': isBusy,
'external-link': isExternalLink,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we inheriting a core style here? If this is our own modifier class defined only within the Button component, it should be named as such with an appropriate prefix, e.g. is-external-link (relevant guideline).

'components-icon-button': isExternalLink && icon && ( isPrimary || isLarge || isSmall ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One component shouldn't inherit the styles of another. In this case, I wonder if Button could render an itself as an IconButton when the condition applies (which would trickle down to rendering both the icon and another Button where maybe we omit the properties which would satisfy the condition to avoid infinite recursion).

Another idea is to bake in icon behavior into Button.

} );

const tag = href !== undefined && ! disabled ? 'a' : 'button';
const tagProps = tag === 'a' ? { href, target } : { type: 'button', disabled };
const getRel = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While technically fine as-is, defining this as a function seems a bit excessive when we could just assign as rel as a variable and override in an if condition:

let { rel } = this.props;
if ( isExternalLink && rel !== null ) {
	rel = uniq( compact( [
		...rel.split( ' ' ),
		'external',
		'noreferrer',
		'noopener',
	] ) ).join( ' ' );
}

// ...

return createElement( tag, {
	// ...
	rel,
} );

// Allow to omit the `rel` attribute passing a `null` value.
return rel === null ? rel : uniq( compact( [
...rel.split( ' ' ),
'external',
'noreferrer',
'noopener',
] ) ).join( ' ' );
};

const { opensInNewTabText = __(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same note re: additionalProps and assigning in initial destructuring.

/* translators: accessibility text */
'(opens in a new tab)'
) } = additionalProps;

const OpensInNewTabScreenReaderText = isExternalLink && (
<span className="screen-reader-text">
{
// We need a space to separate this from previous text.
' ' + opensInNewTabText
}
</span>
);

const ExternalIcon = icon && isExternalLink && <Dashicon icon="external" />;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will behave funny if you assign icon={ 0 }, since the behavior of JS in an && assignment is to defer to the left value if it is falsey, i.e.

const ExternalIcon = 0 && true && <Dashicon icon="external" />;

... will evaluate to:

const ExternalIcon = 0;

And React happily outputs a literal 0 text (Example)

(Only false, "", null, or an undefined child will not be rendered)

Unless you know that all members of the condition are boolean values, I'd suggest a ternary instead, where the fallback value is one of the above unrendered (most likely null).


return createElement( tag, {
...tagProps,
...additionalProps,
...omit( additionalProps, [ 'icon', 'opensInNewTabText' ] ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another advantage of destructuring icon and opensInNewTabText in the original props destructuring is that it avoids this omit.

className: classes,
autoFocus: focus,
rel: isExternalLink && getRel(),
ref,
} );
}, children, OpensInNewTabScreenReaderText, ExternalIcon );
}

export default forwardRef( Button );
14 changes: 13 additions & 1 deletion components/button/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
background: none;
border: none;
outline: none;
text-decoration: none;
margin: 0;
border-radius: 0;

&.button {
text-decoration: none;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this change for?

}

&:active {
color: currentColor;
}
Expand Down Expand Up @@ -41,6 +44,15 @@
.wp-core-ui.gutenberg-editor-page & {
font-size: $default-font-size;
}

&.external-link {
.dashicon {
width: 1.4em;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see these existed previously, but shouldn't these styles be inherited from IconButton ? It's not obvious to me why we would we want the external link icon to be displayed any differently than any other icon button?

height: 1.4em;
margin: 0 .2em;
vertical-align: top;
}
}
}

@keyframes components-button__busy-animation {
Expand Down
40 changes: 0 additions & 40 deletions components/external-link/index.js

This file was deleted.

8 changes: 0 additions & 8 deletions components/external-link/style.scss

This file was deleted.

1 change: 0 additions & 1 deletion components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export { default as DropZone } from './drop-zone';
export { default as DropZoneProvider } from './drop-zone/provider';
export { default as Dropdown } from './dropdown';
export { default as DropdownMenu } from './dropdown-menu';
export { default as ExternalLink } from './external-link';
export { default as FocusableIframe } from './focusable-iframe';
export { default as FontSizePicker } from './font-size-picker';
export { default as FormFileUpload } from './form-file-upload';
Expand Down
11 changes: 9 additions & 2 deletions core-blocks/categories/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { Component, Fragment } from '@wordpress/element';
import { PanelBody, Placeholder, Spinner, ToggleControl } from '@wordpress/components';
import { PanelBody, Placeholder, Spinner, ToggleControl, Button } from '@wordpress/components';
import { withSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { times, unescape } from 'lodash';
Expand Down Expand Up @@ -88,10 +88,17 @@ class CategoriesEdit extends Component {
renderCategoryListItem( category, level ) {
const { showHierarchy, showPostCounts } = this.props.attributes;
const childCategories = this.getCategories( category.id );
const opensInNewTabText = __( '(opens in a new tab from this preview)' );

return (
<li key={ category.id }>
<a href={ category.link } target="_blank">{ this.renderCategoryName( category ) }</a>
<Button href={ category.link }
target="_blank"
icon={ false }
opensInNewTabText={ opensInNewTabText }
>
{ this.renderCategoryName( category ) }
</Button>
{ showPostCounts &&
<span className={ `${ this.props.className }__post-count` }>
{ ' ' }({ category.count })
Expand Down
12 changes: 11 additions & 1 deletion core-blocks/latest-posts/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ToggleControl,
Toolbar,
withAPIData,
Button,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/utils';
Expand Down Expand Up @@ -123,6 +124,8 @@ class LatestPostsEdit extends Component {
},
];

const opensInNewTabText = __( '(opens in a new tab from this preview)' );

return (
<Fragment>
{ inspectorControls }
Expand All @@ -144,7 +147,14 @@ class LatestPostsEdit extends Component {
>
{ displayPosts.map( ( post, i ) =>
<li key={ i }>
<a href={ post.link } target="_blank">{ decodeEntities( post.title.rendered.trim() ) || __( '(Untitled)' ) }</a>
<Button
href={ post.link }
target="_blank"
icon={ false }
opensInNewTabText={ opensInNewTabText }
>
{ decodeEntities( post.title.rendered.trim() ) || __( '(Untitled)' ) }
</Button>
{ displayPostDate && post.date_gmt &&
<time dateTime={ moment( post.date_gmt ).utc().format() } className={ `${ this.props.className }__post-date` }>
{ moment( post.date_gmt ).local().format( 'MMMM DD, Y' ) }
Expand Down
5 changes: 5 additions & 0 deletions editor/components/block-list/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@
padding-left: 0;
padding-right: 0;
}

// Let buttons in the post content inherit the content font-size.
.wp-core-ui & .components-button.external-link {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the .wp-core-ui context?

font-size: inherit;
}
}

.editor-block-list__layout .editor-block-list__block {
Expand Down
6 changes: 3 additions & 3 deletions editor/components/post-excerpt/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { ExternalLink, TextareaControl } from '@wordpress/components';
import { Button, TextareaControl } from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
import { compose } from '@wordpress/element';

Expand All @@ -20,9 +20,9 @@ function PostExcerpt( { excerpt, onUpdateExcerpt } ) {
onChange={ ( value ) => onUpdateExcerpt( value ) }
value={ excerpt }
/>
<ExternalLink href="https://codex.wordpress.org/Excerpt">
<Button href="https://codex.wordpress.org/Excerpt" target="_blank">
{ __( 'Learn more about manual excerpts' ) }
</ExternalLink>
</Button>
</div>
);
}
Expand Down
1 change: 1 addition & 0 deletions editor/components/post-last-revision/style.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.editor-post-last-revision__title {
width: 100%;
font-weight: 600;
text-decoration: none;

.dashicon {
margin-right: 5px;
Expand Down
3 changes: 3 additions & 0 deletions editor/components/post-permalink/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class PostPermalink extends Component {
href={ ! isPublished ? previewLink : samplePermalink }
target="_blank"
ref={ ( permalinkButton ) => this.permalinkButton = permalinkButton }
icon={ false }
>
{ decodeURI( samplePermalink ) }
&lrm;
Expand Down Expand Up @@ -112,6 +113,8 @@ class PostPermalink extends Component {
href={ getWPAdminURL( 'options-permalink.php' ) }
onClick={ this.addVisibilityCheck }
target="_blank"
icon={ false }
rel={ null }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, as I recall, setting rel as null to override default behavior is needed because we don't want the external values to apply for links which open in a new tab but are within the admin? Can we infer this programmatically such that we don't impose this requirement on the developer to understand?

Example: https://github.com/aduth/dones/blob/0450c88429deb4546f72d20f915551ec62a33de3/src/components/link/index.js#L44-L47

>
{ __( 'Change Permalinks' ) }
</Button>
Expand Down
1 change: 1 addition & 0 deletions editor/components/post-preview-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class PostPreviewButton extends Component {
onClick={ this.saveForPreview }
target={ this.getWindowTarget() }
disabled={ ! isSaveable }
icon={ false }
>
{ _x( 'Preview', 'imperative verb' ) }
</Button>
Expand Down
6 changes: 4 additions & 2 deletions editor/components/rich-text/format-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ToggleControl,
Toolbar,
withSpokenMessages,
Button,
} from '@wordpress/components';
import { keycodes } from '@wordpress/utils';
import { prependHTTP } from '@wordpress/url';
Expand Down Expand Up @@ -240,13 +241,14 @@ class FormatToolbar extends Component {
onKeyPress={ stopKeyPropagation }
>
<div className="editor-format-toolbar__link-modal-line">
<a
<Button
className="editor-format-toolbar__link-value"
href={ formats.link.value }
target="_blank"
icon={ false }
>
{ formats.link.value && filterURLForDisplay( decodeURI( formats.link.value ) ) }
</a>
</Button>
<IconButton icon="edit" label={ __( 'Edit' ) } onClick={ this.editLink } />
<IconButton
className="editor-format-toolbar__link-settings-toggle"
Expand Down
3 changes: 2 additions & 1 deletion editor/components/rich-text/format-toolbar/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
min-width: 0;
align-items: flex-start;

.components-button {

.components-icon-button {
flex-shrink: 0;
width: $icon-button-size;
height: $icon-button-size;
Expand Down