# Office Ribbon convergence integration (July) ## Problem As we've built out reworked `Button`, `ToggleButton`, `MenuButton`, `SplitMenuButton`, `Checkbox`, `Toggle`, `Slider`, and `Link`, we'd must continue to prove new components in the wild. In June, we started an effort to help WAC with `Ribbon` perf. In doing so, we created a branch integrating the new `Button`. With that work and with additional optimizations to remove CSS in JS and HOC wrapper overhead, we're seeing over [50% improvement](https://hackmd.io/2npOW1kRSUe39MRZQKwUgg) in initial render times. We need to get this work shipping in prod code. In the process, we found many hurdles in using the `Button` and replacing the component layers in `Ribbon`: 1. Component features were still pending (focus imperative api, a11y fixes, `KeyTips` support, `SplitButton`, `Tooltips`) 2. `compose` shortcomings and needed improvements to make it a viable option for recomposing components (integrate theming, simplify API.) 3. Some themable variant support (need to scaffold RibbonButton with its own token defaults.) We will focus on fixing all shortcomings and working with WAC to ship these improvements before releasing v8. ### Appetite The project should take 2-3 weeks for this initial implementation. We expect to deliver the following: * compose updates checked in * existing components use updates * WAC Ribbon branch updated with improvements * Scaffolding for ribbon flighting in progress (will likely be completed in august) Much of the improvements listed above are already prototyped and simply need review, tests, and cleanup. We will finish this over the next few weeks. ### Solution #### Ribbon ship plan We are coordinating with the WAC team, who will be helping us set up a separate flight for the new Ribbon updates. We'd like to have a single flight to try a separate copy of the Ribbon with the improvements so that we can compare initial render times with the least amount of overhead. The expectation set up in long term support is that we will scaffold this work and get it rolling, and they will take over once it's all in place. We also plan to either move the generalized Ribbon component code into a package within our repo, or a replica of it, so that we can ensure the performance is super fast and that any changes to our utilities or design patterns can continue to stay fast and ergonomic. We have a branch right now with improvements staged, but it is not integrated into `office-online-ui` master. We will work with the WAC team to know where the new Ribbon code will be checked into. #### Component work * Wrap up `SplitButton` convergence in accordance with design spec * Review how we support imperative APIs and update `focus` calls to use that * Ensure accessibility helpers integrated into `Button` variants ##### Possible additional work * Add Button `vertical` modifier for Button (pending design) * Add Button `stealth` modifier (pending design) * Integrate Pivot into Ribbon * Integrate Checkbox into RibbonCheckbox * Move Ribbon into FluentUI as perf test #### Compose improvements In vetting the `compose` work, we found a lot of confusion and friction in building the components, knowing when to use slots, how to use them, and what the role of `compose` was in general. The utility is not a one-size-fits all and we need to realize there are 3 types of roles to support: * Atomic/platform component developers building Buttons and such. * HVC component developers building Ribbons or ItemScope or other composite components which use the building blocks to build more ellaborate things * App developers who are a large majority. They are similar to HVC devs, but really need simplicity and rails so that things stay consistent, resulting in large app development scalability. The `compose` helper is really needed for scenarios like the Ribbon - component developers want to build HVCs. They want to use our components, but would like different default settings. The Ribbon developers intuitively built HOC wrappers around every component, injecting new settings. And when new developers came online and needed slight styling tweaks, they copied the pattern and created HOC wrappers around those, recomputing styles at multiple layers. This is exactly what recomposition is for. By reducing the React component layers and providing a way to merge settings at definition time rather than at runtime, overhead is reduced considerably. To make `compose` usage more straightforward and understandable however, we are proposing a number of changes: * Improve typings for compose to be simpler (Just `<TProps, TState>`) * Reduce `slots` and `slotProps` down to simply `defaultProps`, slots defined as defaults for shorthand props: Before: ``` const Button = compose(..., { slots: { icon: 'span' // Confusion: is this the container or the content? }, slotProps: { icon: { as: 'div', // Ok... is the icon prop a span or a div? children: <AddIcon /> // Hmm. What's going to render now? } }, ) ``` After: ``` const CheckboxBase = compose(..., { defaultProps: { checkmark: { as: 'span' // The container } } }) const Checkbox = compose(CheckboxBase, { defaultProps: { checkmark: { children: <CheckIcon /> // The content } } }) ``` * Remove `handledProps`, move to `getNativeProps`. (Perf issues have been addressed!) * Reduce render complexity by replacing props with state, and moving ref into state: Before: ``` compose( (props, ref, options) => { const { state } = options; // use props or state? // manually mix in ref (error prone) return <div ref={ref} /> } ) ``` After: ``` compose( (state, options) => { // Helper to extract slots and slotProps. (calls getNativeProps) const { slots, slotProps } = getSlots(state); return <slots.root {...slotProps.root} /> } ) ``` * State management improved to reduce spread overhead of multiple hooks Hooks should be considered as prop transforms. That is they modify a clone of the props in some way, or create side effects. Compose can now manage a single a 1-time deep clone of props to pass into hooks for manipulation. This reduces the spread overhead and keeps things rendering fast. Hooks would manipulate the state object however needed to pull together the final state object. The hooks chain would be moved into the composition settings and resolved by compose, before rendering: ```tsx compose( (finalState) => { return finalState.foo /* 3 */ }, { useHooks: [ (state) => { /* state.foo = 1; */ }, (state) => { /* state.foo++ */ }, (state) => { /* state.foo++ */ }, ] } ) ``` ### Theme improvements needed Another change required for Office is how tokens are provided to components. Today, we support inline variables. Example: ```tsx <Button tokens={{ background: 'red' }} /> // renders: <button style={{ '--button-background': 'red' }} ``` We also support this provided in the ThemeProvider theme: ```tsx <ThemeProvider theme={{ tokens: { button: { background: 'red' }}}}> <Button /> // Red button </ThemeProvider> ``` This isn't enough however. We need the ability to create variants for components. Specifically, the `Ribbon` has a number of button variants, each needing different token defaults than what we would provide: | Variant | Description | |-|-| | RibbonButton | Typical click buttons in the ribbon. | | RibbonCheckbox | Standalone checkbox. No padding needed | | RibbonCompoundButton | Vertical buttons | | RibbonFlyoutAnchor | Small condensed chevron buttons at the end of groups | | RibbonInkButton | Variant of the RibbonToggleButton | | RibbonInkFlyoutAnchor | Variant of the RibbonFlyoutAnchor | | RibbonToggleButton | Same as RibbonButton with a toggle state | | RibbonInput | Styled TextField | | RibbonSlider | Styled Slider | | RibbonLabel | Styled AppLabel (which is a styled Label) | In all of these cases, the differences from default aren't far and mostly involve only a small set of tweaks to existing tokens. Proposed way to recompose component variants that come with new default settings: ```tsx const RibbonButton = compose(Button, { defaultProps: { tokens: { background: 'red' } } }) ``` ### Keeping it fast We also saw in performance evaluation that injecting the variable values as inline style props was overhead to avoid (faster than constant css-in-js computation, but still needs optimization.) We will collect the default values and create a classname to reuse when rendering the component with new defaults: * At first render, we request from the theme and defaultProps the tokens for the given component. * We request a classname from a context provider provided by the `ThemeProvider`, given those variable values. We provide an optional dependency list (variant name only) * We cache the classname in the scope of the theme based on the dep list. * We render future instances using that classname. ## Risks Feature creep; the most critical work here is to get `compose` and `variant` updates scalable to everything else, and to get Ribbon prod.