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( <Rating rating={ rating } size={ size } /> ); 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.