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

Conversation

afercia
Copy link
Contributor

@afercia afercia commented May 16, 2018

Replaces #6245

As agreed, this PR tries to bring in the functionalities formerly provided by the ExternalLink component into the Button component.

Before going ahead with polishing, some minor styling issues, documentation, and maybe tests, I'd greatly appreciate some feedback if this is really the desired behavior.

Worth reminding the Button component. already renders a button or a link, depending on the passed props. Now, when it has a href and target (not '_self', '_parent', '_top') props, it's considered an "external" link.

Fixes #1105

</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).

...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.

] ) ).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.

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).

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,
'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.


&.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?

import Dashicon from '../dashicon';
import './style.scss';

function ExternalLink( { href, children, className, rel = '', ...additionalProps } ) {
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 probably leave this for two versions per deprecation policy, with an added deprecated warning.

https://wordpress.org/gutenberg/handbook/reference/deprecated/
https://github.com/WordPress/gutenberg/blob/master/utils/deprecation.js

@@ -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?

@@ -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

* `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.

@afercia
Copy link
Contributor Author

afercia commented May 23, 2018

@aduth thanks for the detailed review 🙂 As I wrote in the initial comment, I was more expecting an initial feedback on the general approach 😉

Before going ahead with polishing, some minor styling issues, documentation, and maybe tests, I'd greatly appreciate some feedback if this is really the desired behavior.

Personally, I'm not so convinced that putting all these different behaviors into one single component is a good thing in the first place.

@aduth
Copy link
Member

aduth commented May 23, 2018

In that case: Sure, this looks perfectly reasonable to pursue. Can you elaborate on your hesitations?

@afercia
Copy link
Contributor Author

afercia commented May 23, 2018

Mostly ease of use by developers. It's a component named Button that actually renders either a button or a link or an external link with some logic (and assumptions) to render important attributes like rel, target, etc.

From an ease of use perspective, these are 3 very different behaviors that maybe should go in 3 separate components, regardless of how they look visually:

  • Button
  • Link
  • ExternalLink

There's also the IconButton component that doesn't help in making things clearer...

@aduth
Copy link
Member

aduth commented May 25, 2018

What is it about the abstraction of IconButton that makes it unclear that won't also be suffered by a separation of Button, Link, and ExternalLink components?

As noted previously in #6245 (review) , it's difficult to reconcile the various requirements here; things like navigable links which appear as buttons, or behavioral buttons which appear as navigable links. The hope with consolidating all behaviors into a single Button component is that if we can rely on a predictable set of assumptions, we can make it easier for the end-developer to pass certain attributes and it will be rendered "correct" on their behalf.

But this only works when we're consistent about how we're using the component. That's not the case currently (as a search for "<a " makes evident). Further, I worry it's not natural for a developer to intuitively consider a plain link as <Button href="/" isLink />. Between the inconsistent usage and unintuitiveness, it does appear the abstraction is failing us.

Would it be enough to separate this to two: Button and Link ?

A Button...

  • Is semantically: an element with Click/Enter/Space behavior (onClick)
  • Default appearance of button (see below open question in Link requiring href)
  • Handles icon behavior by assumptions based on incoming props (icon)
  • May be navigable (by presence of href), in which case it defers its rendering to an underlying Link

A Link...

  • Is semantically: a navigable element (href)
  • Default appearance of a link (<Button href="..."> may override)
  • Handles rel behavior by assumptions based on incoming props (href, target)
  • Open question: Must receive href prop?
    • Could warn about incorrect usage otherwise, encouraging use of Button instead (avoid href="#" onClick="..."). But this requires that Button can assume the appearance of a link, which may be okay (as it does today with isLink prop).

A few notable advantages here:

  • Link is more intuitive to use in place of <a />, helping promote consistency
  • rel behavior is more natural to occur in Link (navigable), not Button
  • Eliminate IconButton as a separate component

@afercia
Copy link
Contributor Author

afercia commented May 25, 2018

Not arguing, but the original scope of this PR and the previous #6245 was to just make the usage of the accessible message (Opens in a new tab) easier, and now I'm scared 😱

Jokes apart 😉this:

Further, I worry it's not natural for a developer to intuitively consider a plain link as . Between the inconsistent usage and unintuitiveness, it does appear the abstraction is failing us.

Would it be enough to separate this to two: Button and Link ?

Makes a lot of sense to me. Personally, I'm used to think at meaning and semantics first, so to me a button is a button, a link is a link. Instead, naming a component based on its appearance doesn't seem so ideal to me.

I'm a bit concerned about the amount of work needed for such a big refactoring, and the potential temporary breakage it would introduce. I'm not sure I have the time to address all the necessary changes.

One more concern is about styling. Instead of deferring / overriding, I'd keep them well separated. However, this way the two components would share some styling:

  • a Button may look like a button
  • a Link may look like a button
    _ (and vice-versa, with all the size variants, etc.)

This would lead to duplicating a lot of styles, with consequent maintainability issues. Wouldn't be possible to extend from a base styling (or a base component)?

Overall, I'd totally second your proposal as it would make developers life easier. Imagine a coding workflow... I need a link, well I just type a <Link ... component passing the props that are the most intuitive for a link. Yes, I should get warnings for incorrect usage. Same for buttons. I need a button? Yeah, let me just type <Button ... with some props. How they look should be a separate concern.

@aduth
Copy link
Member

aduth commented May 29, 2018

Not arguing, but the original scope of this PR and the previous #6245 was to just make the usage of the accessible message (Opens in a new tab) easier, and now I'm scared 😱

Sometimes it's the seemingly small changes that bring to light big underlying issues 😄

Makes a lot of sense to me. Personally, I'm used to think at meaning and semantics first, so to me a button is a button, a link is a link. Instead, naming a component based on its appearance doesn't seem so ideal to me.

Agreed 💯 .

This would lead to duplicating a lot of styles, with consequent maintainability issues. Wouldn't be possible to extend from a base styling (or a base component)?

I think this could make sense. How we deduplicate could have a few options:

  • Base styles defined in a common stylesheet, like what we have already in edit-popt/assets/stylesheets
    • Should be stated that these are not intended to be specific to the post editor.
  • A "visually button" component, e.g.
const VisuallyButton = ( { className, ...props } ) => (
	<span className={ classnames( 'components-visually-button', className ) } { ...props } />
);

I'm a bit concerned about the amount of work needed for such a big refactoring, and the potential temporary breakage it would introduce. I'm not sure I have the time to address all the necessary changes.

If we can come to an agreement on the proposed implementation, would it be enough to create an issue with the proposed plan for another developer to pick up? I'm of course willing to help, though likewise with my current workload cannot commit to giving it the attention it deserves in a reasonable timeframe.

@afercia
Copy link
Contributor Author

afercia commented May 30, 2018

If we can come to an agreement on the proposed implementation,

FWIW seems to me the plan makes sense and would bring in clear advantages. I'd totally second it. I'm not the one who make decisions though. If the team agrees, I'd be happy to close this PR.

@afercia
Copy link
Contributor Author

afercia commented Jun 6, 2018

WCEU could be a good opportunity to have a look at this.

@aduth
Copy link
Member

aduth commented Jun 25, 2018

Issue at #7534

@aduth
Copy link
Member

aduth commented Sep 13, 2018

Closing as superseded by #7534, #9702

@aduth aduth closed this Sep 13, 2018
@aduth aduth deleted the add/button-external-link-behavior branch September 13, 2018 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants