-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Conversation
</span> | ||
); | ||
|
||
const ExternalIcon = icon && isExternalLink && <Dashicon icon="external" />; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = __( |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 ), |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 } ) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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?
* `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. |
There was a problem hiding this comment.
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.
@aduth thanks for the detailed review 🙂 As I wrote in the initial comment, I was more expecting an initial feedback on the general approach 😉
Personally, I'm not so convinced that putting all these different behaviors into one single component is a good thing in the first place. |
In that case: Sure, this looks perfectly reasonable to pursue. Can you elaborate on your hesitations? |
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:
There's also the IconButton component that doesn't help in making things clearer... |
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 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 " Would it be enough to separate this to two: A
A
A few notable advantages here:
|
Not arguing, but the original scope of this PR and the previous #6245 was to just make the usage of the accessible message Jokes apart 😉this:
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:
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 |
Sometimes it's the seemingly small changes that bring to light big underlying issues 😄
Agreed 💯 .
I think this could make sense. How we deduplicate could have a few options:
const VisuallyButton = ( { className, ...props } ) => (
<span className={ classnames( 'components-visually-button', className ) } { ...props } />
);
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. |
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. |
WCEU could be a good opportunity to have a look at this. |
Issue at #7534 |
Replaces #6245
As agreed, this PR tries to bring in the functionalities formerly provided by the
ExternalLink
component into theButton
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 ahref
andtarget
(not '_self', '_parent', '_top') props, it's considered an "external" link.Fixes #1105