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

feat: add useCSS hook #14470

Merged
merged 17 commits into from
Aug 14, 2020
Merged

feat: add useCSS hook #14470

merged 17 commits into from
Aug 14, 2020

Conversation

layershifter
Copy link
Member

@layershifter layershifter commented Aug 11, 2020

An alternative for #13641.


This PR adds useCSS() hook for Fluent UI Northstar. It allows to use CSS-in-JS to generate a className that can be applied to any component. To merge styles I used an approach inspired by Emotion's cx() function which allows merging styles and/or class names.

const red = useCSS({ color: 'red' })

const greenWins = useCSS(red, { color: 'green' })
const redWins = useCSS({ color: 'green' }, red)

All features like nested & preudo selectors are supported.


I've added 3 perf examples to this PR. The first is representative of usage in Teams today. The second two are for comparison to that baseline, both using useCSS. This was done to ensure useCSS would not be slower than what is in react-northstar usage today.

Example min avg median max
ButtonOverridesMiss.perf.tsx - Applies boolean variable overrides 25.34 30.05 28.85 47.01
ButtonUseCss.perf.tsx - Applies style object overrides 13.67 19.42 19.4 27.64
ButtonUseCssNesting.perf.tsx - Merges generated class names 22.41 26.5 25.11 37.53

@DustyTheBot
Copy link

DustyTheBot commented Aug 11, 2020

Warnings
⚠️ There are no updates provided to CHANGELOG. Ensure there are no publicly visible changes introduced by this PR.

Generated by 🚫 dangerJS against d0d327c

@codesandbox-ci
Copy link

codesandbox-ci bot commented Aug 11, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit d0d327c:

Sandbox Source
Fluent UI Button Configuration
microsoft/fluentui: codesandbox-react-template Configuration
microsoft/fluentui: codesandbox-react-next-template Configuration
microsoft/fluentui: codesandbox-react-northstar-template Configuration

@msft-github-bot
Copy link
Contributor

msft-github-bot commented Aug 11, 2020

Perf Analysis

No significant results to display.

All results

Scenario Render type Master Ticks PR Ticks Iterations Status
BaseButton mount 901 885 5000
ButtonNext mount 599 581 5000
Checkbox mount 1617 1619 5000
CheckboxBase mount 1297 1301 5000
CheckboxNext mount 1722 1635 5000
ChoiceGroup mount 4877 4976 5000
ComboBox mount 914 919 1000
CommandBar mount 7534 7629 1000
ContextualMenu mount 13573 13664 1000
DefaultButton mount 1114 1097 5000
DetailsRow mount 3480 3598 5000
DetailsRowFast mount 3495 3591 5000
DetailsRowNoStyles mount 3356 3397 5000
Dialog mount 1497 1481 1000
DocumentCardTitle mount 1836 1867 1000
Dropdown mount 2578 2571 5000
FocusZone mount 1903 1834 5000
IconButton mount 1741 1726 5000
Label mount 332 358 5000
Link mount 433 456 5000
LinkNext mount 467 479 5000
MenuButton mount 1425 1459 5000
Nav mount 3222 3216 1000
Panel mount 1477 1423 1000
Persona mount 870 870 1000
Pivot mount 1433 1433 1000
PivotNext mount 1408 1404 1000
PrimaryButton mount 1273 1229 5000
SearchBox mount 1309 1283 5000
SearchBoxNext mount 1349 1339 5000
Slider mount 1461 1508 5000
SliderNext mount 1918 1922 5000
SpinButton mount 5017 5027 5000
SpinButtonNext mount 5095 5096 5000
Spinner mount 413 447 5000
SplitButton mount 3093 3158 5000
Stack mount 498 531 5000
StackWithIntrinsicChildren mount 1967 1990 5000
StackWithTextChildren mount 5051 5023 5000
TagPicker mount 2717 2735 5000
Text mount 406 435 5000
TextField mount 1416 1444 5000
ThemeProvider mount 2906 3002 5000
ThemeProvider virtual-rerender 461 450 5000
Toggle mount 823 807 5000
ToggleNext mount 814 834 5000
button mount 112 120 5000

Perf Analysis (Fluent)

⚠️ 2 potential perf regressions detected

Potential regressions comparing to master

Scenario Current PR Ticks Baseline Ticks Ratio Regression Analysis
ButtonOverridesMissPerf.default 116 44 2.64:1 analysis
ButtonUseCssNestingPerf.default 45 44 1.02:1 analysis
Perf comparison
Status Scenario Fluent TPI Fabric TPI Ratio Iterations Ticks
🎯 Avatar.Fluent 0.45 0.48 0.94:1 2000 909
🦄 Button.Fluent 0.11 0.19 0.58:1 5000 538
🔧 Checkbox.Fluent 0.66 0.36 1.83:1 1000 657
🎯 Dialog.Fluent 0.16 0.23 0.7:1 5000 786
🔧 Dropdown.Fluent 2.98 0.48 6.21:1 1000 2977
🔧 Icon.Fluent 0.14 0.05 2.8:1 5000 716
🎯 Image.Fluent 0.08 0.11 0.73:1 5000 379
🔧 Slider.Fluent 1.7 0.38 4.47:1 1000 1696
🔧 Text.Fluent 0.07 0.03 2.33:1 5000 355
🦄 Tooltip.Fluent 0.11 17.35 0.01:1 5000 531

🔧 Needs work     🎯 On target     🦄 Amazing

Perf tests with no regressions
Scenario Current PR Ticks Baseline Ticks Ratio
AttachmentMinimalPerf.default 169 136 1.24:1
ButtonUseCssPerf.default 51 44 1.16:1
ButtonSlotsPerf.default 593 542 1.09:1
Avatar.Fluent 909 833 1.09:1
Text.Fluent 355 327 1.09:1
AlertMinimalPerf.default 308 286 1.08:1
CardMinimalPerf.default 550 507 1.08:1
ChatDuplicateMessagesPerf.default 427 396 1.08:1
LabelMinimalPerf.default 410 381 1.08:1
PortalMinimalPerf.default 129 120 1.08:1
AnimationMinimalPerf.default 395 369 1.07:1
GridMinimalPerf.default 337 316 1.07:1
HeaderMinimalPerf.default 360 337 1.07:1
RadioGroupMinimalPerf.default 417 390 1.07:1
TreeWith60ListItems.default 230 215 1.07:1
ButtonMinimalPerf.default 169 160 1.06:1
SkeletonMinimalPerf.default 392 369 1.06:1
Image.Fluent 379 358 1.06:1
DividerMinimalPerf.default 378 361 1.05:1
TextAreaMinimalPerf.default 463 440 1.05:1
ToolbarMinimalPerf.default 975 927 1.05:1
ChatMinimalPerf.default 589 566 1.04:1
ChatWithPopoverPerf.default 455 437 1.04:1
ListNestedPerf.default 911 872 1.04:1
TableManyItemsPerf.default 2221 2137 1.04:1
TooltipMinimalPerf.default 797 767 1.04:1
AttachmentSlotsPerf.default 1117 1088 1.03:1
DialogMinimalPerf.default 804 779 1.03:1
FormMinimalPerf.default 400 390 1.03:1
LayoutMinimalPerf.default 399 388 1.03:1
ListCommonPerf.default 1005 974 1.03:1
ProviderMergeThemesPerf.default 1973 1918 1.03:1
ProviderMinimalPerf.default 967 940 1.03:1
ReactionMinimalPerf.default 381 370 1.03:1
SplitButtonMinimalPerf.default 3765 3670 1.03:1
TextMinimalPerf.default 356 345 1.03:1
TreeMinimalPerf.default 848 824 1.03:1
Icon.Fluent 716 697 1.03:1
Slider.Fluent 1696 1644 1.03:1
AccordionMinimalPerf.default 141 138 1.02:1
BoxMinimalPerf.default 330 323 1.02:1
EmbedMinimalPerf.default 1953 1916 1.02:1
MenuButtonMinimalPerf.default 1563 1537 1.02:1
StatusMinimalPerf.default 670 660 1.02:1
TableMinimalPerf.default 415 406 1.02:1
Button.Fluent 538 527 1.02:1
Dialog.Fluent 786 771 1.02:1
Tooltip.Fluent 531 522 1.02:1
IconMinimalPerf.default 645 636 1.01:1
CarouselMinimalPerf.default 432 433 1:1
HeaderSlotsPerf.default 751 754 1:1
InputMinimalPerf.default 1326 1327 1:1
ItemLayoutMinimalPerf.default 1258 1258 1:1
ListWith60ListItems.default 1132 1134 1:1
MenuMinimalPerf.default 859 858 1:1
CustomToolbarPrototype.default 3690 3698 1:1
Checkbox.Fluent 657 655 1:1
Dropdown.Fluent 2977 2966 1:1
DropdownMinimalPerf.default 3005 3049 0.99:1
FlexMinimalPerf.default 276 278 0.99:1
ImageMinimalPerf.default 352 356 0.99:1
ListMinimalPerf.default 485 491 0.99:1
LoaderMinimalPerf.default 741 758 0.98:1
RefMinimalPerf.default 208 213 0.98:1
CheckboxMinimalPerf.default 2796 2893 0.97:1
AvatarMinimalPerf.default 450 470 0.96:1
PopupMinimalPerf.default 681 706 0.96:1
SegmentMinimalPerf.default 326 338 0.96:1
VideoMinimalPerf.default 586 618 0.95:1
DropdownManyItemsPerf.default 780 830 0.94:1
SliderMinimalPerf.default 1568 1670 0.94:1

@size-auditor
Copy link

size-auditor bot commented Aug 11, 2020

Asset size changes

Size Auditor did not detect a change in bundle size for any component!

Baseline commit: feb91b97860c47dbe0b4eac542f6ac615dc06a47 (build)

@mnajdova
Copy link
Contributor

image

import _Stylis from 'stylis';
// @ts-ignore No typings :(
import focusVisiblePlugin from '@quid/stylis-plugin-focus-visible';
// @ts-ignore No typings :(
Copy link
Contributor

Choose a reason for hiding this comment

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

:(

// Types
//

export type UseCSSStyle = Omit<ICSSInJSStyle, 'animationName'>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we support our animation definition in useCss? If not we do not have to omit this prop.

Copy link
Member Author

Choose a reason for hiding this comment

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

No we don't support, changed to:

// Inline keyframe definitions are not supported by useCSS() hook
export type UseCSSStyle = Omit<ICSSInJSStyle, 'animationName'> & { animationName?: string };


// serializeStyles() will concat all passed styles and will resolve functions
const serializedStyles = serializeStyles(resolvedStyles, stylesCache, theme);
// ".name" is not a valid CSS classname
Copy link
Contributor

Choose a reason for hiding this comment

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

Why? And why adding an "f" before it makes it valid?

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to // ".name" is not a valid CSS classname as it can start from a digit, now it should be cleaner 🐱

mount(<TestComponent styles={styles} />, getMountOptions(renderGlobal, false));
mount(<TestComponent styles={styles} />, getMountOptions(renderGlobal, true));

expect(renderGlobal).toMatchInlineSnapshot(`
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't it be better if we test with window.getComputerStyle? Feels like more stable that snapshot

Copy link
Member Author

Choose a reason for hiding this comment

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

It means that we will need to actually invoke Fela/Emotion to the process that can make results depend on a renderer...

const styles = [{ color: 'red' }];
const wrapper = mount(<TestComponent styles={styles} />);

expect(wrapper.find('div').prop('className')).toBeDefined();
Copy link
Contributor

Choose a reason for hiding this comment

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

On the other hand shouldn't we also test what that className is, or what styles the component has.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed it as className is stable

Copy link
Contributor

@mnajdova mnajdova left a comment

Choose a reason for hiding this comment

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

Let's add some documentation for this hook under the guides. This is pretty important new feature that we do not to be overlooked by just adding it to some component's example page.

// Definitions
//

const SPECIFICITY_CLASSNAME = 'fcss';
Copy link
Member

Choose a reason for hiding this comment

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

I find the previous .increase-specificity more informative than fcss. We could also consider .use-css or something as well.

Having been told to consider production size of the CSS, we could use .css which would still correlate to the useCSS call.

Not a blocker

Copy link
Member

@levithomason levithomason left a comment

Choose a reason for hiding this comment

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

👍 Only minor comments, merge at will.

@dzearing
Copy link
Member

dzearing commented Aug 12, 2020

Overall, this is great, it helps to abstract the css in js calls to use any centralized renderer, and extracts the important parts (rtl processing, merging, classname to style destructuring, etc).

I'm wondering if there is a way to avoid all of the work on every render when it's not necessary, like deps array or string key. React memo would work for local cache but really you need more of a global cache. Without it, you'd need to build everything on every render and let the renderer use the entire object (keys/values) as the cache key which is expensive.

The perf issue is pretty much the exact thing we had to fix in getClassNames (and i think Marija addressed a while ago) and still find the abuses add up (cache key invalidation cases.) If the key is a string, there's 1 key, and a bunch of edge cases would be removed.

// Types
//

// Inline keyframe definitions are not supported by useCSS() hook
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we put that note in the docs as well?

@levithomason
Copy link
Member

Perf definitely needs to be tested and proven before we can publicly ship this. For now, let's not let premature optimization hold up testing it. We need to merge and test against a couple component pages in the docs and in some production app code.

Once we do this, it may fail entirely, in which case there's no need to spend time optimizing. If it passes those pragmatic checks, let's hit the perf hard.

Copy link
Contributor

@mnajdova mnajdova left a comment

Choose a reason for hiding this comment

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

LGTM!

const firstWrapper = mount(<TestComponent styles={[{ left: '20px' }]} />);
const firstClassName = firstWrapper.find('div').prop('className') as string;

mount(<TestComponent styles={[firstClassName, { color: 'red', left: '30px' }]} />, getMountOptions(renderGlobal));
Copy link
Contributor

Choose a reason for hiding this comment

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

Would suggest testing as part of this case another component that has those classes reversed, so we can potentially find issues about this in the future if we change the logic.

…eat/use-css

� Conflicts:
�	packages/fluentui/docs/src/components/Sidebar/Sidebar.tsx
@layershifter layershifter merged commit f00a6cd into master Aug 14, 2020
@layershifter layershifter deleted the feat/use-css branch August 14, 2020 11:39
levithomason pushed a commit to levithomason/fluentui that referenced this pull request Aug 24, 2020
* feat: add useCSS hook

* fix example path

* fix example name

* fix example names

* fix lint issues

* fix comment

* Update packages/fluentui/react-bindings/test/hooks/useCSS-test.tsx

Co-authored-by: Marija Najdova <mnajdova@gmail.com>

* fix an assertion

* use different selector for dev

* add initial docs

* improve examples, add RTL example

* add a UT

* fix merge issue

Co-authored-by: Marija Najdova <mnajdova@gmail.com>
layershifter added a commit that referenced this pull request Sep 22, 2020
(cherry picked from commit f00a6cd)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fluent UI react-northstar (v0) Work related to Fluent UI V0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants