Try   HackMD

Vertical Apps - V9 Migration

The purpose of this design doc is to present and describe the different decisions and research required to achieve a smooth migration from Fluent UI Northstar (v0) to Fluent UI v9 in the Teams verticals monorepo.

Encapsulating v9 usage

The verticals repository has all reusable UI code in the @microsoft/modernworkplace-ui-core repo. All northstar exports are done through @microsoft/modernworkplace-ui-core/src/components/northstar. Although the deep import is considered an antipattern in the JS ecosystem, the good thing is that this import is used consistently throughout.

We can simply create a new export path @microsoft/modernworkplace-ui-core/src/components/fluentui as the primary export path of v9 components and do a gradual migration.

The existence of this export path if no code is used should not impact any of the production code.

Styling

Currently the packages inside the verticals apps repo are using mergeStyleSets from @uifabric:

import { mergeStyleSets } from "@uifabric/merge-styles";

It's convenient, since the syntax is very similar to @griffel

All style files are under the pattern ComponentName.styles.ts, what can facilitate automation with codemodes.

Let's take the FacePile styles for example, currently we have:

import { avatarClassName, pxToRem } from "@fluentui/react-northstar";
import { mergeStyleSets } from "@uifabric/merge-styles";

import { ComponentClassNames } from "../../styles/classNames";

type IFacePileClasses = ComponentClassNames<"facePile" | "userName">;

/**
 * Get class names.
 * @returns Class names.
 */
export const getClassNames = (): IFacePileClasses => {
    return mergeStyleSets({
        facePile: {
            selectors: {
                [`.${avatarClassName}`]: {
                    marginRight: pxToRem(3)
                },
                [`.${avatarClassName}.disabled`]: {
                    filter: "grayscale(1)"
                }
            }
        },
        userName: {
            marginLeft: pxToRem(2),
            display: "inline-block",
            overflow: "hidden",
            textOverflow: "ellipsis"
        }
    });
};

By convernting it to griffel it will look like:

import { avatarClassName, pxToRem } from "@fluentui/react-northstar";
import { makeStyles, shorthands } from "@griffel/react";

/**
 * Get class names.
 * @returns Class names.
 */
export const getClassNames = makeStyles({
    facePile: {
        selectors: {
            [`.${avatarClassName}`]: {
                ...shorthands.margin("0", pxToRem(3), "0", "0")
            },
            [`.${avatarClassName}.disabled`]: {
                filter: "grayscale(1)"
            }
        }
    },
    userName: {
        ...shorthands.margin("0", "0", "0", pxToRem(2)),
        ...shorthands.overflow("hidden"),
        display: "inline-block",
        textOverflow: "ellipsis"
    }
});

Automated migration

There is already a library that will automatically try to migrate northstar styles to v9 styles: https://github.com/YuanboXue-Amber/style-transform-tool

A demo of this library can be seen: https://yellow-meadow-005156603.1.azurestaticapps.net/

This library will not work out of the box on mergeStyleSets but the expected format is similar so we can reuse some of the code in there to help create tooling that will.

Since the styles are all consistently in .styles.ts files, once we have tooling in place to do this automatically, it should be fairly easy to run.

Potential performance gain

The modern workplace repo currently uses @uifabric/merge-styles@7.19.1 as the main styling solution of choice. Running some benchmarks that were implemented in https://github.com/microsoft/griffel/pull/252 we can see the following results

NOTE: That build time optimizations were not applied for Griffel (v9) styling, the values could potentially be further improved by applying build time optimizations

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

You can see that in the purely runtime scenario Griffel could already be ~30% increase to styling performance.

Deprecating northstar usage

Northstar will be deprecated since Teams is heavily investing into the migration of northstar to v9 for improved performance and other API related benefits.

We can create an eslint rule in eslint-plugin-modernworkplace that will throw an error for any new imports of northstar components to promote new code using v9 components.

Gradual migration

v9 can can be used side by side with northstar without any problems. If there are any problems discovered with using the two component libraries together, that is a bug that should be reported to the Fluent UI team.

Components

Below is the usage statistics of northstar component usage at the moment of 11/10/2022

Count Component
149 Flex
99 Text
67 Button
26 Box
17 Divider
14 Input
13 Dropdown
12 Header
12 Avatar
11 Dialog
10 Skeleton
10 FormField
10 Ref
9 FlexItem
9 Checkbox
8 Provider
8 MenuButton
8 Table
8 Tooltip
6 TextArea
6 List
6 Loader
6 Popup
5 FormLabel
5 FormTextArea
5 Accordion
4 ListItem
4 Label
4 Image
3 Datepicker
2 Breadcrumb
2 Pill
2 FormInput
1 Grid
1 Segment
1 FormDatepicker
1 Alert
1 SplitButton
1 Portal
1 Status
1 RadioGroup
1 Toolbar

It's quite clear the the most often used components are simpler layout components. The top three components add up in usages more than all other compnents combined:

  • Flex
  • Text
  • Button

For simpler components, (especially the most used components as they are quite simple) we should look into the possibility of creating codemods for automatic migration to avoid as much developer manual work as possible.

Icons

Below is a snapshot of all icons used in the MWT repo as of 11/10/2022

Current Icon V9 Icon Equivalent
CloseIcon
CalendarIcon
AcceptIcon
ChevronDownIcon
ParticipantAddIcon
AddIcon
SearchIcon
LocationIcon
CheckmarkCircleIcon
CircleIcon
ChevronStartIcon
RedbangIcon
ExclamationCircleIcon
EditIcon
MoreIcon
ArrowDownIcon
FilesEmptyIcon
TrashCanIcon
InfoIcon
OpenOutsideIcon
ArrowRightIcon
ChevronEndIcon
ErrorIcon
ArchiveIcon
LinkIcon
SettingsIcon
ChevronEndMediumIcon
UserFriendsIcon
ArrowUpIcon
ApprovalsAppbarIcon
TeamCreateIcon
CalendarAgendaIcon
QuestionCircleIcon
BookmarkIcon
FormatIcon
FilesUploadIcon
OneDriveIcon
ThumbtackIcon
ThumbtackSlashIcon
UndoIcon
ExcelColorIcon
FilesCodeIcon
FilesHtmlColoredIcon
FilesPdfColoredIcon
FilesPictureColoredIcon
FilesSoundIcon
FilesTextColoredIcon
FilesVideoIcon
FilesZipIcon
FluidFileIcon
OneNoteColorIcon
PowerPointColorIcon
VisioColorIcon
WordColorIcon
SyncIcon
TabsIcon
ExclamationTriangleIcon
UrgentIcon
AttachmentIcon
RetryIcon
FilterIcon
ShiftActivityIcon
WorkOrSchoolIcon

We should finish the above mapping of V9 icons to northstar icons.

Icon migration can be done separately to any components and the intended design of V9 icons is that they can be used with northstar components.

We should also look into codemods for icon migration once we have a complete mapping.

A catalog of v9 icons is available https://react.fluentui.dev/?path=/docs/concepts-developer-icons-icons-catalogpage

Component Migration

We can assume that we have the encapsulation of v9 setup described earlier.

Extending TeamsContextProvider

The TeamsContextProvider componnet is responsible for applying all providers that is used by modern workplace controls. In this way, we can simply add the FluentProvider to the main provider. This would incur some minor bundlesize, but would not impact any northstar code and enables the use of v9 components in the entire repo.

packages\modernworkplace-react-hooks\src\useTeamsContext\TeamsContextProvider.tsx

import { Provider as NorthstarThemeProvider, RendererContext } from "@fluentui/react-northstar"; +import { FluentProvider, teamsLightTheme } from "@microsoft/modernworkplace-ui-core-v9"; import { createEmotionRenderer } from "@fluentui/react-northstar-emotion-renderer"; import React from "react"; import { TextDirection, useTextDirection } from "../useTextDirection"; import { getClassNames } from "./TeamsContextProvider.styles"; import { TeamsThemeType, getComposedTeamsTheme } from "./getComposedTeamsTheme"; import { TeamsContext } from "./useTeamsContext"; import { useTeamsTheme } from "./useTeamsTheme"; /** * Component that connects to Teams and fetches its context. * @param props Children. * @param props.children Children of this component. * @returns Child component wrapped in Teams Context Provider. */ export const TeamsContextProvider: React.FC = props => { - /* Other code removed for simplicity */ return ( <RendererContext.Provider value={createEmotionRenderer()}> <TeamsContext.Provider value={{ theme: themeWithoutStaticStyles, themeType: theme as TeamsThemeType }}> + {/* We can also map v9 themes to current themes */} + <FluentProvider theme={teamsLightTheme}> <NorthstarThemeProvider className={containerStyles} theme={themeWithoutStaticStyles} rtl={direction === TextDirection.RightToLeft}> {props.children} </NorthstarThemeProvider> + </FluentProvider> </TeamsContext.Provider> </RendererContext.Provider> ); };

Example

Here is an example of a more complex migration.
lets take FacePile as example and see what would need to be done in order to migrate the Avatar Component used inside.

First we would need to use FluentProvider in the following places:

So the replacement in packages\modernworkplace-ui-core\src\components\facePile\FacePile.tsx is simple enough as:

-import { Avatar, Flex, Text } from "@fluentui/react-northstar"; +import { Avatar, AvatarSizes } from "@fluentui/react-components"; +import { Flex, SizeValue, Text } from "@fluentui/react-northstar"; import { useTeamsContext } from "@microsoft/modernworkplace-react-hooks"; import * as React from "react"; @@ -14,6 +15,16 @@ import { IFacePile } from "./IFacePile"; // Default, maximum number of users to display. const defaultVisibleMax = 11; +const sizesMap: Record<SizeValue, AvatarSizes> = { + smallest: 20, + smaller: 24, + small: 28, + medium: 32, + large: 64, + larger: 64, + largest: 96 +}; + /** * Renders face pile component. * @param props Component properties. @@ -27,7 +38,7 @@ export const FacePile: React.FC<IFacePile> = props => { const classes = getClassNames(); const maxVisible = props.maxNumberOfVisibleAvatars ?? defaultVisibleMax; const overflow = props.users.length > maxVisible; - const overflowLabel = (name: string) => `+${props.users.length - maxVisible}`; + const overflowLabel = `+${props.users.length - maxVisible}`; const avatarClass = props.disabled ? "disabled" : ""; const size = props.size ?? "medium"; @@ -39,11 +50,9 @@ export const FacePile: React.FC<IFacePile> = props => { return ( <Flex vAlign="center" className={classes.facePile}> {props.users.slice(0, maxVisible).map((user, idx) => { - return ( - <Avatar className={avatarClass} key={`${idx}-${user.id}`} name={user.displayName} image={user.imageUrl} size={size} /> - ); + return <Avatar key={`${idx}-${user.id}`} name={user.displayName} image={{ src: user.imageUrl }} size={sizesMap[size]} />; })} - {overflow && <Avatar getInitials={overflowLabel} size={size} className={avatarClass} />} + {overflow && <Avatar initials={overflowLabel} size={sizesMap[size]} className={avatarClass} />} {props.users.length === 1 && ( <Text variables={textVariables}

One final touch, we need one more update in the styles since we are not exporting avatarClassName in v9 but avatarClassNames which is an object with class names for each slots. So the final change in the styles file would be:

- import { avatarClassName } from "@fluentui/react-northstar"; + import { avatarClassNames } from "@microsoft/modernworkplace-ui-core-v9"; - [`.${avatarClassName}`]: { + [`.${avatarClassNames.root}`]: {

We have migration instructions for several components, here we used Avatar Migration

Accessibility

Check the accessibility migration doc where it exaplains how to migrate the 4 possible properties that an accessibility behavior has. For attributes we should add them directly to the component.

Let's take packages\modernworkplace-ui-members\src\Members.tsx as example, where it's using gridBehavior:

+import { useTabsterAttributes } from "@fluentui/react-tabster"; export const Members: React.FC<IMembersProps> = props => { const { members } = useTranslations(); + const arrowKeyNavigationAttributes = useTabsterAttributes({ + mover: { + direction: 3 // grid + } + }); + <Table - accessibility={gridNestedBehavior} + {...arrowKeyNavigationAttributes}

Here we are replacing the focus zone property by using react-tabster

If we had a custom behavior such has:

const customBehavior = (props) => { attributes: { role="dialog", disabled: props.disabled } } <Component accessibility={customBehavior} />

That could be replaced by:

<Component role="dialog" disabled={props.disabled} />

Validation

Visual Regression

There is no visual regression testing in the modern workplace repo, therefore any visual regression will have to be done manually during migration. However what level of visual regression allowed should be discussed with designers since v9 is using the Fluent language which is intended to be the future design of all M365 experiences

Unit tests

All unit tests that break as a result of migration should be investigated and fixed.

Performance

There are performance tests setup for the LoanManager scenario. In order have an accurate perf tests between northstar and v0 we should try to quickly migrate LoanManager related controls to v9 and run the existing performance tests

Bundle size

Bundle size is measured by looking at the final size of the js bundle that is built with the production webpack config. We can measure the bundle size of just integrating the FluentProvider at the start to see the initial impact and keep measuring as components as migrated