-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Require an initial click on embed previews for interactivity #12981
Changes from 8 commits
1e46e7e
ebd88ee
4053879
6435f68
bb1336e
b2f4481
b37acae
391e7ee
a17ede1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,58 +17,99 @@ import classnames from 'classnames/dedupe'; | |
import { __, sprintf } from '@wordpress/i18n'; | ||
import { Placeholder, SandBox } from '@wordpress/components'; | ||
import { RichText, BlockIcon } from '@wordpress/editor'; | ||
import { Component } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import WpEmbedPreview from './wp-embed-preview'; | ||
|
||
const EmbedPreview = ( props ) => { | ||
const { preview, url, type, caption, onCaptionChange, isSelected, className, icon, label } = props; | ||
const { scripts } = preview; | ||
class EmbedPreview extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.hideOverlay = this.hideOverlay.bind( this ); | ||
this.state = { | ||
interactive: false, | ||
}; | ||
} | ||
|
||
const html = 'photo' === type ? getPhotoHtml( preview ) : preview.html; | ||
const parsedHost = parse( url ).host.split( '.' ); | ||
const parsedHostBaseUrl = parsedHost.splice( parsedHost.length - 2, parsedHost.length - 1 ).join( '.' ); | ||
const cannotPreview = includes( HOSTS_NO_PREVIEWS, parsedHostBaseUrl ); | ||
// translators: %s: host providing embed content e.g: www.youtube.com | ||
const iframeTitle = sprintf( __( 'Embedded content from %s' ), parsedHostBaseUrl ); | ||
const sandboxClassnames = classnames( type, className, 'wp-block-embed__wrapper' ); | ||
static getDerivedStateFromProps( nextProps ) { | ||
if ( ! nextProps.isSelected ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's worth noting that this function will be called more than just at the point where it transitions from a state of being selected to being unselected: It could be called hundreds of times for a block which isn't (and may never be) selected. I'm not too familiar with whether React does anything internally to optimize for when the state value doesn't change, but it may be a good idea to also consider the current state when returning a non-
https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops To this point as well, I'm not sure whether There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The two points of this preceding comment were not addressed. I've verified that a warning would be logged by allowing this function to return
Of my own curiosity, I dug deeper to see the work involved in generating a new state value for every render of the component. At the very least, it involves a wasteful shallow clone of the state object: Instead, if the intention is that state only needs to update if the block is both not selected, but has the interactive state, this ought to be expressed as: static getDerivedStateFromProps( props, state ) {
if ( ! props.isSelected && state.interactive ) {
// ...
return { interactive: false };
}
return null;
} This ensures that we only update state when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, apologies, this one didn't make it on to the todo list. WIll address and update. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be correct now. Thank you :) |
||
// We only want to change this when the block is not selected, because changing it when | ||
// the block becomes selected makes the overlap disappear too early. Hiding the overlay | ||
// happens on mouseup when the overlay is clicked. | ||
return { interactive: false }; | ||
} | ||
} | ||
|
||
const embedWrapper = 'wp-embed' === type ? ( | ||
<WpEmbedPreview | ||
html={ html } | ||
/> | ||
) : ( | ||
<div className="wp-block-embed__wrapper"> | ||
<SandBox | ||
hideOverlay() { | ||
// This is called onMouseUp on the overlay. We can't respond to the `isSelected` prop | ||
// changing, because that happens on mouse down, and the overlay immediately disappears, | ||
// and the mouse event can end up in the preview content. We can't use onClick on | ||
// the overlay to hide it either, because then the editor misses the mouseup event, and | ||
// thinks we're multi-selecting blocks. | ||
this.setState( { interactive: true } ); | ||
} | ||
|
||
render() { | ||
const { preview, url, type, caption, onCaptionChange, isSelected, className, icon, label } = this.props; | ||
const { scripts } = preview; | ||
const { interactive } = this.state; | ||
|
||
const html = 'photo' === type ? getPhotoHtml( preview ) : preview.html; | ||
const parsedHost = parse( url ).host.split( '.' ); | ||
const parsedHostBaseUrl = parsedHost.splice( parsedHost.length - 2, parsedHost.length - 1 ).join( '.' ); | ||
const cannotPreview = includes( HOSTS_NO_PREVIEWS, parsedHostBaseUrl ); | ||
// translators: %s: host providing embed content e.g: www.youtube.com | ||
const iframeTitle = sprintf( __( 'Embedded content from %s' ), parsedHostBaseUrl ); | ||
const sandboxClassnames = classnames( type, className, 'wp-block-embed__wrapper' ); | ||
|
||
// Disabled because the overlay div doesn't actually have a role or functionality | ||
// as far as the user is concerned. We're just catching the first click so that | ||
// the block can be selected without interacting with the embed preview that the overlay covers. | ||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you provide as a preceding inline comment a "Disable reason" explaining to a future maintainer why the rule was not needed to be respected here. Otherwise, it will be difficult to track whether it can ever be removed (and often, forcing oneself to explain the reason can serve to deter one from actually disabling, though in this case I think it's fine/reasonable). |
||
/* eslint-disable jsx-a11y/no-static-element-interactions */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: You can disable multiple rules in a single There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, just for readability :) |
||
const embedWrapper = 'wp-embed' === type ? ( | ||
<WpEmbedPreview | ||
html={ html } | ||
scripts={ scripts } | ||
title={ iframeTitle } | ||
type={ sandboxClassnames } | ||
/> | ||
</div> | ||
); | ||
|
||
return ( | ||
<figure className={ classnames( className, 'wp-block-embed', { 'is-type-video': 'video' === type } ) }> | ||
{ ( cannotPreview ) ? ( | ||
<Placeholder icon={ <BlockIcon icon={ icon } showColors /> } label={ label }> | ||
<p className="components-placeholder__error"><a href={ url }>{ url }</a></p> | ||
<p className="components-placeholder__error">{ __( 'Sorry, this embedded content cannot be previewed in the editor.' ) }</p> | ||
</Placeholder> | ||
) : embedWrapper } | ||
{ ( ! RichText.isEmpty( caption ) || isSelected ) && ( | ||
<RichText | ||
tagName="figcaption" | ||
placeholder={ __( 'Write caption…' ) } | ||
value={ caption } | ||
onChange={ onCaptionChange } | ||
inlineToolbar | ||
) : ( | ||
<div className="wp-block-embed__wrapper"> | ||
<SandBox | ||
html={ html } | ||
scripts={ scripts } | ||
title={ iframeTitle } | ||
type={ sandboxClassnames } | ||
onFocus={ this.hideOverlay } | ||
/> | ||
) } | ||
</figure> | ||
); | ||
}; | ||
{ ! interactive && <div | ||
className="block-library-embed__interactive-overlay" | ||
onMouseUp={ this.hideOverlay } /> } | ||
</div> | ||
); | ||
/* eslint-enable jsx-a11y/no-static-element-interactions */ | ||
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ | ||
|
||
return ( | ||
<figure className={ classnames( className, 'wp-block-embed', { 'is-type-video': 'video' === type } ) }> | ||
{ ( cannotPreview ) ? ( | ||
<Placeholder icon={ <BlockIcon icon={ icon } showColors /> } label={ label }> | ||
<p className="components-placeholder__error"><a href={ url }>{ url }</a></p> | ||
<p className="components-placeholder__error">{ __( 'Sorry, this embedded content cannot be previewed in the editor.' ) }</p> | ||
</Placeholder> | ||
) : embedWrapper } | ||
{ ( ! RichText.isEmpty( caption ) || isSelected ) && ( | ||
<RichText | ||
tagName="figcaption" | ||
placeholder={ __( 'Write caption…' ) } | ||
value={ caption } | ||
onChange={ onCaptionChange } | ||
inlineToolbar | ||
/> | ||
) } | ||
</figure> | ||
); | ||
} | ||
} | ||
|
||
export default EmbedPreview; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -198,7 +198,7 @@ class Sandbox extends Component { | |
} | ||
|
||
render() { | ||
const { title } = this.props; | ||
const { title, onFocus } = this.props; | ||
|
||
return ( | ||
<FocusableIframe | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aside: It's curious to me that we would have opted to use |
||
|
@@ -207,6 +207,7 @@ class Sandbox extends Component { | |
className="components-sandbox" | ||
sandbox="allow-scripts allow-same-origin allow-presentation" | ||
onLoad={ this.trySandbox } | ||
onFocus={ onFocus } | ||
width={ Math.ceil( this.state.width ) } | ||
height={ Math.ceil( this.state.height ) } /> | ||
); | ||
|
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.
Minor: For consistency's sake, could you keep methods which extend the
Component
interface (e.g. constructor, lifecycle,getDerivedStateFromProps
, etc) grouped together toward the top of the component definition? While not yet part of a coding standard, it's a useful convention which has developed around component definitions. I'd like to circle around to adding it as a lint rule at some point in the future.