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

Social Icon: Try auto-matching variation based on URL #59303

Open
wants to merge 10 commits into
base: trunk
Choose a base branch
from
60 changes: 41 additions & 19 deletions packages/block-library/src/social-link/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,64 @@ import {
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { keyboardReturn } from '@wordpress/icons';
import { isEmail, prependHTTPS } from '@wordpress/url';

/**
* Internal dependencies
*/
import { getIconBySite, getNameBySite } from './social-list';
import {
getIconBySite,
getNameBySite,
getMatchingService,
} from './social-list';

import isURLLike from './is-url-like';

const SocialLinkURLPopover = ( {
url,
setAttributes,
setPopover,
onClose,
popoverAnchor,
clientId,
} ) => {
const { removeBlock } = useDispatch( blockEditorStore );
return (
<URLPopover
anchor={ popoverAnchor }
aria-label={ __( 'Edit social link' ) }
onClose={ () => {
setPopover( false );
popoverAnchor?.focus();
} }
>
<URLPopover anchor={ popoverAnchor } onClose={ () => onClose( false ) }>
<form
className="block-editor-url-popover__link-editor"
onSubmit={ ( event ) => {
event.preventDefault();
setPopover( false );

if ( isURLLike( url ) ) {
// Append https if user did not include it.
setAttributes( { url: prependHTTPS( url ) } );
}
popoverAnchor?.focus();
onClose( false );
} }
>
<div className="block-editor-url-input">
<URLInput
value={ url }
onChange={ ( nextURL ) =>
setAttributes( { url: nextURL } )
}
onChange={ ( nextURL ) => {
const nextAttributes = {
url: nextURL,
service: undefined,
};

if ( isURLLike( nextURL ) || isEmail( nextURL ) ) {
const matchingService = isEmail( nextURL )
? 'mail'
: getMatchingService(
prependHTTPS( nextURL )
);

nextAttributes.service =
matchingService ?? 'chain';
}

setAttributes( nextAttributes );
} }
Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe we should provide a hit below the URL input for this new feature. Not sure about the design or text thought 😅

placeholder={ __( 'Enter social link' ) }
label={ __( 'Enter social link' ) }
hideLabelFromVision
Expand Down Expand Up @@ -107,14 +128,15 @@ const SocialLinkEdit = ( {
iconBackgroundColor,
iconBackgroundColorValue,
} = context;
const [ showURLPopover, setPopover ] = useState( false );
const classes = clsx( 'wp-social-link', 'wp-social-link-' + service, {
const classes = clsx( 'wp-social-link', {
[ `wp-social-link-${ service }` ]: !! service,
'wp-social-link__is-incomplete': ! url,
Copy link
Contributor

Choose a reason for hiding this comment

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

The way this 'is-incomplete' CSS class works should be adjusted.
When the UI shows the 'link' icon button as a placeholder, the button is actually an actionable control. That is fine but it looks 'disabled' because, technically, it doesn't have the url prop set.
However, if we want to use this button as an actionable, enabled, focusable control then it can't look 'disabled'.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we can remove this class. With update flow, the child block is either a placeholder or matches a service. What do you think?

[ `has-${ iconColor }-color` ]: iconColor,
[ `has-${ iconBackgroundColor }-background-color` ]:
iconBackgroundColor,
} );

const [ showPopover, setShowPopover ] = useState( ! url && ! service );
Copy link
Member Author

Choose a reason for hiding this comment

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

The inital state change allows Popover to be opened for freshly inserted blocks.

// Use internal state instead of a ref to make sure that the component
// re-renders when the popover's anchor updates.
const [ popoverAnchor, setPopoverAnchor ] = useState( null );
Expand Down Expand Up @@ -169,7 +191,7 @@ const SocialLinkEdit = ( {
<button
className="wp-block-social-link-anchor"
ref={ setPopoverAnchor }
onClick={ () => setPopover( true ) }
onClick={ () => setShowPopover( true ) }
aria-haspopup="dialog"
>
<IconComponent />
Expand All @@ -181,11 +203,11 @@ const SocialLinkEdit = ( {
{ socialLinkText }
</span>
</button>
{ isSelected && showURLPopover && (
{ isSelected && showPopover && (
<SocialLinkURLPopover
url={ url }
setAttributes={ setAttributes }
setPopover={ setPopover }
onClose={ setShowPopover }
popoverAnchor={ popoverAnchor }
clientId={ clientId }
/>
Expand Down
39 changes: 39 additions & 0 deletions packages/block-library/src/social-link/is-url-like.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* WordPress dependencies
*/
import { getProtocol, isValidProtocol } from '@wordpress/url';

// Please see packages/block-editor/src/components/link-control/is-url-like.js
export default function isURLLike( val ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we decide to ship this PR, I'm going to raise a PR to get this moved into the @wordpress/url package. Then I'll reuse it in both here and LinkControl.

const hasSpaces = val.includes( ' ' );

if ( hasSpaces ) {
return false;
}

const protocol = getProtocol( val );
const protocolIsValid = isValidProtocol( protocol );

const mayBeTLD = hasPossibleTLD( val );

const isWWW = val?.startsWith( 'www.' );

return protocolIsValid || isWWW || mayBeTLD;
}

// Please see packages/block-editor/src/components/link-control/is-url-like.js
function hasPossibleTLD( url, maxLength = 6 ) {
// Clean the URL by removing anything after the first occurrence of "?" or "#".
const cleanedURL = url.split( /[?#]/ )[ 0 ];

// Regular expression explanation:
// - (?<=\S) : Positive lookbehind assertion to ensure there is at least one non-whitespace character before the TLD
// - \. : Matches a literal dot (.)
// - [a-zA-Z_]{2,maxLength} : Matches 2 to maxLength letters or underscores, representing the TLD
// - (?:\/|$) : Non-capturing group that matches either a forward slash (/) or the end of the string
const regex = new RegExp(
`(?<=\\S)\\.(?:[a-zA-Z_]{2,${ maxLength }})(?:\\/|$)`
);

return regex.test( cleanedURL );
}
14 changes: 14 additions & 0 deletions packages/block-library/src/social-link/social-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,17 @@ export const getNameBySite = ( name ) => {
const variation = variations.find( ( v ) => v.name === name );
return variation ? variation.title : __( 'Social Icon' );
};

/**
* Retrieves the matching social service based on the URL.
*
* @param {string} url URL to match against.
* @return {Object} Social service variation.
*/
export function getMatchingService( url ) {
const variation = variations.find( ( { patterns } ) =>
patterns?.some( ( pattern ) => pattern.test( url ) )
);

return variation?.attributes?.service;
}
62 changes: 62 additions & 0 deletions packages/block-library/src/social-link/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Internal dependencies
*/
import { getMatchingService } from '../social-list';

const fixtures = [
[ 'https://profiles.wordpress.org/exampleuser', 'wordpress' ],
[ 'https://exampleuser.wordpress.com/', 'wordpress' ],
[ 'https://www.instagram.com/exampleuser/', 'instagram' ],
[ 'https://www.tiktok.com/@exampleuser', 'tiktok' ],
[ 'https://twitter.com/exampleuser', 'x' ],
[ 'https://x.com/exampleuser', 'x' ],
[ 'https://www.facebook.com/exampleuser', 'facebook' ],
[ 'https://www.linkedin.com/in/exampleuser', 'linkedin' ],
[ 'https://www.pinterest.com/exampleuser', 'pinterest' ],
[ 'https://www.snapchat.com/add/exampleuser', 'snapchat' ],
[ 'https://exampleuser.tumblr.com/', 'tumblr' ],
[ 'https://www.reddit.com/user/exampleuser', 'reddit' ],
[ 'https://www.goodreads.com/user/show/12345678-exampleuser', 'goodreads' ],
[ 'https://exampleuser.deviantart.com/', 'deviantart' ],
[ 'https://www.flickr.com/photos/exampleuser', 'flickr' ],
[ 'https://www.behance.net/exampleuser', 'behance' ],
[ 'https://soundcloud.com/exampleuser', 'soundcloud' ],
[ 'https://www.twitch.tv/exampleuser', 'twitch' ],
[ 'https://www.youtube.com/user/exampleuser', 'youtube' ],
[ 'https://vimeo.com/exampleuser', 'vimeo' ],
[ 'https://www.amazon.com/gp/profile/exampleuser', 'amazon' ],
[ 'https://www.amazon.de/gp/profile/exampleuser', 'amazon' ],
[ 'https://www.etsy.com/shop/ExampleShop', 'etsy' ],
[ 'https://www.etsy.com/people/exampleuser', 'etsy' ],
[ 'https://exampleuser.bandcamp.com/', 'bandcamp' ],
[ 'https://dribbble.com/exampleuser', 'dribbble' ],
[ 'https://www.dropbox.com/home/Example_User', 'dropbox' ],
[ 'https://codepen.io/exampleuser', 'codepen' ],
[ 'https://www.yelp.com/user_details?userid=exampleuser', 'yelp' ],
[ 'https://vk.com/id12345678', 'vk' ],
[ 'https://t.me/exampleuser', 'telegram' ],
[ 'https://open.spotify.com/user/exampleuser', 'spotify' ],
[ 'https://getpocket.com/@exampleuser', 'pocket' ],
[ 'https://www.patreon.com/exampleuser', 'patreon' ],
[ 'https://medium.com/@exampleuser', 'medium' ],
[ 'https://www.meetup.com/members/12345678/', 'meetup' ],
[ 'https://github.com/exampleuser', 'github' ],
[ 'https://www.gravatar.com/avatar/{example_hash}', 'gravatar' ],
[ 'https://www.last.fm/user/exampleuser', 'lastfm' ],
[ 'https://foursquare.com/user/exampleuser', 'foursquare' ],
[ 'https://join.skype.com/invite/exampleuser', 'skype' ],
[ 'https://www.threads.net/@exampleuser', 'threads' ],
[ 'https://example.com/feed/', 'feed' ],
[ 'https://500px.com/p/exampleuser?view=photos', 'fivehundredpx' ],
];

describe( 'Utils', () => {
describe( 'getMatchingService', () => {
it.each( fixtures )(
'should return the matching service for %s',
( url, expected ) => {
expect( getMatchingService( url ) ).toBe( expected );
}
);
} );
} );
Loading
Loading