Intermediate SVG Icons in React

One of the things Product Designers at Automattic get to do plenty of is to tinker with code. Whether it’s prototyping, writing CSS, or even playing with new tools. And that’s what I normally get up to on Hack/Trash Day – our internal day to work on something that’s not you’re regular job – fixing something that needs fixing.

During last August’s Hack/Trash Day, I picked “replace Noticons with Gridicons in Calypso”. Noticons are our old icon font based icons, and Gridicons are our new SVG-based icons.

One of my icon font to SVG replacements involved creating a half-star Gridicon so our Rating component in Calypso could be updated to use all SVG-based Gridicons.

In my Github PR to present the new icon, my colleague Davide Casali mentioned how there’d been previous discussion to create intermediate icons by overlaying a solid icon over an outline icon and masking out what you don’t need. This was a brilliant solution to help keep our SVG icon library compact and performant, so I got straight to work.

HTML/CSS Prototype

First up, I prototyped the rating component using just HTML and CSS at Codepen. At it’s core, the rating component has two rows of icons, one positioned absolutely over the other. Masking is handled by both the new clip-path property, and the deprecated clip properties. Why use a deprecated property? Unfortunately, ’clip-path is not supported by any Microsoft browsers, but clip is still supported by every browser. And I figure that Microsoft won’t be pulling support for clip until they introduce clip-path.

It’s important to note that there’s two differences in how clip and clip-path calculate the masking rectangles:

clip: rect(0, $mask-position, $height, 0);
clip-path: inset(0 $clip-path-mask-position 0 0);

rect values are positional: top, left, bottom and right; whereas inset values are: from-top, from-right, from-bottom and from-left.

Once I was happy with the result and tested it on our supported browsers, I got started on integrating it into its home, the React component.

Reactify

My JavaScript skills are very far removed from what Automattic’s JavaScript Wranglers are capable of, but I’ve had some exposure to React, so I was determined to give it a good go.

One of my first realizations was that most of what I had written in SASS, would need to be rewritten into JavaScript/React.

For example the SASS mixin to change the outline-only stars:

@mixin outline-grey( $rating ) {
  $inverse-rating: 100 - $rating;
  @if $inverse-rating / 20 >= 1 {
    // need an integer to make this work
    $count: floor($inverse-rating / 20);
    // add a color class to only those stars outside the rating
    @for $i from 1 through $count {
      .gridicon.outline-#{$i} {
        fill: lighten( $gray, 20% );
      }
    }
  }
}

became a JavaScript loop:

let outlineStyles = { fill: '#00aadc' };
if ( inverseRating / 20 >= 1 ) {
	if ( i > ( 5 - outlineCount ) ) {
		outlineStyles = {
			fill: '#c8d7e1',
		};
	}
}<

But the SASS math to calculate the values:

$rating: round($rating / 10) * 10;
$width: $height * 5;
$mask-position: ( ( $rating / 100 ) * $width );
$clip-path-mask-position: ( $width - ( ( $rating / 100 ) * $width ) );</code></pre>

remained math:

const roundRating = Math.round( this.props.rating / 10 ) * 10;
const ratingWidth = ( this.props.size * 5 );
const maskPosition = ( ( roundRating / 100 ) * ratingWidth ) + 'px';
const clipPathMaskPosition = ( ratingWidth - ( ( roundRating / 100 ) * ratingWidth ) ) + 'px';
const overlayHeightPx = ( this.props.size ) + 'px';

And to add an additional CSS class to a DOM object, you firstly create an object to hold the CSS property:

const style = { fill: '#ffffff' }

And then add your additional CSS properties using

Object.assign

like this:

allStyles = Object.assign( {}, style, { color: '#c8d7e1' } );

Testing

We use component testing on Calypso to ensure code and UI components work as they should. As the final part of this update, I needed to rewrite the test for this component which had been written to count the number of font icons being rendered.

Because all SVG icons were being rendered all of the time, I needed to take a different approach, and that was to check that the mask being created was of the correct dimension. Basically I was running the component code in reverse and in one test, inside a loop over each value the component would display (0-100).

const ratingList = [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ];
	it.each( ratingList, 'should render rating %s as %s', [ 'element', 'element' ], function( element ) {
		const rating = element,
			size = 24, // use default size
			wrapper = shallow(
				&lt;Rating
					rating={ rating }
					size={ size }
				/&gt;
			);

		const roundRating = Math.round( rating / 10 ) * 10;
		const ratingWidth = ( size * 5 );
		const maskPosition = ( ( roundRating / 100 ) * ratingWidth );
		const clipPathMaskPosition = ( ratingWidth - ( ( roundRating / 100 ) * ratingWidth ) );
		const component = wrapper.find( 'div.rating__overlay' );
		expect( component.props().style.clipPath ).to.equal( 'inset(0 ' + clipPathMaskPosition + 'px 0 0 )' );
		expect( component.props().style.clip ).to.equal( 'rect(0, ' + maskPosition + 'px, ' + size + 'px, 0)' );
	} );

Challenges

The biggest challenge I faced was that the component was written almost two years ago, and the current preferred JavaScript style is ES6. I don’t know enough about the differences to make changes, so I largely left the linter recommendations alone.

I knew that come the code review, a JavaScript Wrangler would come to the code’s rescue and bring it into 2017. As it happened, that colleague would be Andrija Vucinic, who also gave my code a nice cleanup refactor.

If you have a self-hosted WordPress site and use the Jetpack plugin, you can see the component in action on WordPress.com in the Manage Plugins page.

So there you have it, a new and performant way to do rating stars and also how to bring it nicely into React.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s