From 0ba74759d6af15bdb81a42f4692cc21f43cd5a59 Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Mon, 6 Feb 2023 22:19:21 -0800 Subject: [PATCH 01/17] add avatar as a new component --- ...-1c73e6e8-61a0-46c8-b69e-0820f5a2488d.json | 7 + packages/web-components/.eslintrc.json | 1 + .../src/avatar/avatar.definition.ts | 17 + .../src/avatar/avatar.options.ts | 124 ++++ .../src/avatar/avatar.stories.ts | 193 ++++++ .../src/avatar/avatar.styles.ts | 569 ++++++++++++++++++ .../src/avatar/avatar.template.ts | 41 ++ packages/web-components/src/avatar/avatar.ts | 175 ++++++ packages/web-components/src/avatar/define.ts | 4 + packages/web-components/src/avatar/index.ts | 5 + packages/web-components/src/index.ts | 1 + .../web-components/src/utils/get-initials.ts | 110 ++++ 12 files changed, 1247 insertions(+) create mode 100644 change/@fluentui-web-components-1c73e6e8-61a0-46c8-b69e-0820f5a2488d.json create mode 100644 packages/web-components/src/avatar/avatar.definition.ts create mode 100644 packages/web-components/src/avatar/avatar.options.ts create mode 100644 packages/web-components/src/avatar/avatar.stories.ts create mode 100644 packages/web-components/src/avatar/avatar.styles.ts create mode 100644 packages/web-components/src/avatar/avatar.template.ts create mode 100644 packages/web-components/src/avatar/avatar.ts create mode 100644 packages/web-components/src/avatar/define.ts create mode 100644 packages/web-components/src/avatar/index.ts create mode 100644 packages/web-components/src/utils/get-initials.ts diff --git a/change/@fluentui-web-components-1c73e6e8-61a0-46c8-b69e-0820f5a2488d.json b/change/@fluentui-web-components-1c73e6e8-61a0-46c8-b69e-0820f5a2488d.json new file mode 100644 index 0000000000000..bb7aee06b82f0 --- /dev/null +++ b/change/@fluentui-web-components-1c73e6e8-61a0-46c8-b69e-0820f5a2488d.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "add avatar as a new fluent 2 aligned web component", + "packageName": "@fluentui/web-components", + "email": "chhol@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/.eslintrc.json b/packages/web-components/.eslintrc.json index ca5b46cb7c91c..29a022348e612 100644 --- a/packages/web-components/.eslintrc.json +++ b/packages/web-components/.eslintrc.json @@ -18,6 +18,7 @@ "no-prototype-builtins": "off", "no-fallthrough": "off", "no-unexpected-multiline": "off", + "no-useless-escape": "off", "import/order": "error", "sort-imports": [ "error", diff --git a/packages/web-components/src/avatar/avatar.definition.ts b/packages/web-components/src/avatar/avatar.definition.ts new file mode 100644 index 0000000000000..6f076756e5860 --- /dev/null +++ b/packages/web-components/src/avatar/avatar.definition.ts @@ -0,0 +1,17 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { Avatar } from './avatar.js'; +import { styles } from './avatar.styles.js'; +import { template } from './avatar.template.js'; + +/** + * The Fluent Avatar Element. + * + * @public + * @remarks + * HTML Element: \ + */ +export const definition = Avatar.compose({ + name: `${FluentDesignSystem.prefix}-avatar`, + template, + styles, +}); diff --git a/packages/web-components/src/avatar/avatar.options.ts b/packages/web-components/src/avatar/avatar.options.ts new file mode 100644 index 0000000000000..9574d8fdcb1a3 --- /dev/null +++ b/packages/web-components/src/avatar/avatar.options.ts @@ -0,0 +1,124 @@ +import { ValuesOf } from '@microsoft/fast-foundation'; + +/** + * The Avatar "active" state + */ +export const AvatarActive = { + active: 'active', + inactive: 'inactive', +} as const; + +/** + * The types of Avatar active state + */ +export type AvatarActive = ValuesOf; + +/** + * The Avatar Shape + */ +export const AvatarShape = { + circular: 'circular', + square: 'square', +} as const; + +/** + * The types of Avatar Shape + */ +export type AvatarShape = ValuesOf; + +/** + * The Avatar Appearance when "active" + */ +export const AvatarAppearance = { + ring: 'ring', + shadow: 'shadow', + ringShadow: 'ring-shadow', +} as const; + +/** + * The appearance when "active" + */ +export type AvatarAppearance = ValuesOf; + +/** + * A specific named color for the Avatar + */ +export const AvatarNamedColor = { + darkRed: 'dark-red', + cranberry: 'cranberry', + red: 'red', + pumpkin: 'pumpkin', + peach: 'peach', + marigold: 'marigold', + gold: 'gold', + brass: 'brass', + brown: 'brown', + forest: 'forest', + seafoam: 'seafoam', + darkGreen: 'dark-green', + lighTeal: 'light-teal', + teal: 'teal', + steel: 'steel', + blue: 'blue', + royalBlue: 'royal-blue', + cornflower: 'cornflower', + navy: 'navy', + lavender: 'lavender', + purple: 'purple', + grape: 'grape', + lilac: 'lilac', + pink: 'pink', + magenta: 'magenta', + plum: 'plum', + beige: 'beige', + mink: 'mink', + platinum: 'platinum', + anchor: 'anchor', +} as const; + +/** + * An avatar can be one of named colors + * @public + */ +export type AvatarNamedColor = ValuesOf; + +/** + * Supported Avatar colors + */ +export const AvatarColor = { + neutral: 'neutral', + brand: 'brand', + colorful: 'colorful', + ...AvatarNamedColor, +} as const; + +/** + * The Avatar Color + */ +export type AvatarColor = ValuesOf; + +/** + * The Avatar Sizes + * @public + */ +export const AvatarSize = { + _20: 20, + _24: 24, + _28: 28, + _32: 32, + _36: 36, + _40: 40, + _48: 48, + _56: 56, + _64: 64, + _72: 72, + _96: 96, + _120: 120, + _128: 128, +} as const; + +/** + * A Avatar can be on of several preset sizes. + * @public + */ +export type AvatarSize = ValuesOf; diff --git a/packages/web-components/src/avatar/avatar.stories.ts b/packages/web-components/src/avatar/avatar.stories.ts new file mode 100644 index 0000000000000..32bae0dd512fd --- /dev/null +++ b/packages/web-components/src/avatar/avatar.stories.ts @@ -0,0 +1,193 @@ +import { html } from '@microsoft/fast-element'; +import type { Args, Meta } from '@storybook/html'; +import { renderComponent } from '../__test__/helpers.js'; +import type { Avatar as FluentAvatar } from './avatar.js'; +import { AvatarActive, AvatarAppearance, AvatarColor, AvatarShape, AvatarSize } from './avatar.options.js'; +import './define.js'; + +type AvatarStoryArgs = Args & FluentAvatar; +type AvatarStoryMeta = Meta; + +const storyTemplate = html` + +`; + +export default { + title: 'Components/Avatar', + argTypes: { + active: { + options: Object.values(AvatarActive), + control: { + type: 'select', + }, + }, + appearance: { + options: Object.values(AvatarAppearance), + control: { + type: 'select', + }, + }, + color: { + options: Object.values(AvatarColor), + control: { + type: 'select', + }, + }, + initials: { + control: 'text', + }, + name: { + control: 'text', + }, + shape: { + options: Object.values(AvatarShape), + control: { + type: 'select', + }, + }, + size: { + options: Object.values(AvatarSize), + control: { + type: 'select', + }, + }, + }, +} as AvatarStoryMeta; + +export const Avatar = renderComponent(storyTemplate).bind({}); + +export const Image = renderComponent(html` + +`); + +export const Icon = renderComponent(html` + +`); + +export const Badge = renderComponent(html` `); + +export const ColorBrand = renderComponent(html``); + +export const Color = renderComponent(html` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+`); + +export const Colorful = renderComponent(html` +
+ + + + + + + + + + + + +
+`); + +export const Shape = renderComponent(html` + + +`); + +export const Active = renderComponent(html` +
+ U + A + I +
+
+`); + +export const ActiveAppearance = renderComponent(html` +
+ R + S + RS +
+`); + +export const CustomInitials = renderComponent(html` `); + +export const Size = renderComponent(html` +
+ 20 + 24 + 28 + 32 + 36 + 40 + 48 + 56 + 64 + 72 + 96 + 120 + 128 +
+`); diff --git a/packages/web-components/src/avatar/avatar.styles.ts b/packages/web-components/src/avatar/avatar.styles.ts new file mode 100644 index 0000000000000..beb84ca267f91 --- /dev/null +++ b/packages/web-components/src/avatar/avatar.styles.ts @@ -0,0 +1,569 @@ +import { css } from '@microsoft/fast-element'; +import { display } from '@microsoft/fast-foundation'; +import { + borderRadiusCircular, + borderRadiusLarge, + borderRadiusMedium, + borderRadiusSmall, + borderRadiusXLarge, + colorBrandBackgroundStatic, + colorNeutralBackground1, + colorNeutralBackground6, + colorNeutralForeground3, + colorNeutralForegroundInverted, + colorPaletteAnchorBackground2, + colorPaletteAnchorForeground2, + colorPaletteBeigeBackground2, + colorPaletteBeigeForeground2, + colorPaletteBlueBackground2, + colorPaletteBlueForeground2, + colorPaletteBrassBackground2, + colorPaletteBrassForeground2, + colorPaletteBrownBackground2, + colorPaletteBrownForeground2, + colorPaletteCornflowerBackground2, + colorPaletteCornflowerForeground2, + colorPaletteCranberryBackground2, + colorPaletteCranberryForeground2, + colorPaletteDarkGreenBackground2, + colorPaletteDarkGreenForeground2, + colorPaletteDarkRedBackground2, + colorPaletteDarkRedForeground2, + colorPaletteForestBackground2, + colorPaletteForestForeground2, + colorPaletteGoldBackground2, + colorPaletteGoldForeground2, + colorPaletteGrapeBackground2, + colorPaletteGrapeForeground2, + colorPaletteLavenderBackground2, + colorPaletteLavenderForeground2, + colorPaletteLightTealBackground2, + colorPaletteLightTealForeground2, + colorPaletteLilacBackground2, + colorPaletteLilacForeground2, + colorPaletteMagentaBackground2, + colorPaletteMagentaForeground2, + colorPaletteMarigoldBackground2, + colorPaletteMarigoldForeground2, + colorPaletteMinkBackground2, + colorPaletteMinkForeground2, + colorPaletteNavyBackground2, + colorPaletteNavyForeground2, + colorPalettePeachBackground2, + colorPalettePeachForeground2, + colorPalettePinkBackground2, + colorPalettePinkForeground2, + colorPalettePlatinumBackground2, + colorPalettePlatinumForeground2, + colorPalettePlumBackground2, + colorPalettePlumForeground2, + colorPalettePumpkinBackground2, + colorPalettePumpkinForeground2, + colorPalettePurpleBackground2, + colorPalettePurpleForeground2, + colorPaletteRedBackground2, + colorPaletteRedForeground2, + colorPaletteRoyalBlueBackground2, + colorPaletteRoyalBlueForeground2, + colorPaletteSeafoamBackground2, + colorPaletteSeafoamForeground2, + colorPaletteSteelBackground2, + colorPaletteSteelForeground2, + colorPaletteTealBackground2, + colorPaletteTealForeground2, + fontFamilyBase, + fontSizeBase100, + fontSizeBase200, + fontSizeBase300, + fontSizeBase400, + fontSizeBase500, + fontSizeBase600, + fontWeightRegular, + fontWeightSemibold, + shadow16, + shadow28, + shadow4, + shadow8, + strokeWidthThick, + strokeWidthThicker, + strokeWidthThickest, + strokeWidthThin, +} from '../theme/design-tokens'; + +// TODO: Should all animation constants go to tokens/theme or globals? +// https://github.com/microsoft/fluentui/issues/16372#issuecomment-778240665 + +const animationDuration = { + duration50: '50ms', + duration100: '100ms', + duration150: '150ms', + duration200: '200ms', + duration300: '300ms', + duration400: '400ms', + duration500: '500ms', +}; + +const animationTiming = { + ultraFast: animationDuration.duration50, + faster: animationDuration.duration100, + fast: animationDuration.duration150, + normal: animationDuration.duration200, + slow: animationDuration.duration300, + slower: animationDuration.duration400, + ultraSlow: animationDuration.duration500, +}; + +const animationLines = { + decelerateMax: 'cubic-bezier(0.00,0.00,0.00,1.00)', + decelerateMid: 'cubic-bezier(0.10,0.90,0.20,1.00)', + decelerateMin: 'cubic-bezier(0.33,0.00,0.10,1.00)', + accelerateMax: 'cubic-bezier(1.00,0.00,1.00,1.00)', + accelerateMid: 'cubic-bezier(0.90,0.10,1.00,0.20)', + accelerateMin: 'cubic-bezier(0.80,0.00,0.78,1.00)', + maxEasyEase: 'cubic-bezier(0.80,0.00,0.20,1.00)', + easyEase: 'cubic-bezier(0.33,0.00,0.67,1.00)', + linear: 'linear', +}; + +const animations = { + fastOutSlowInMax: animationLines.decelerateMax, + fastOutSlowInMid: animationLines.decelerateMid, + fastOutSlowInMin: animationLines.decelerateMin, + slowOutFastInMax: animationLines.accelerateMax, + slowOutFastInMid: animationLines.accelerateMid, + slowOutFastInMin: animationLines.accelerateMin, + fastEase: animationLines.maxEasyEase, + normalEase: animationLines.easyEase, + nullEasing: animationLines.linear, +}; + +/** Avatar styles + * @public + */ +export const styles = css` + ${display('inline-flex')} :host { + position: relative; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 32px; + height: 32px; + font-family: ${fontFamilyBase}; + font-weight: ${fontWeightSemibold}; + font-size: ${fontSizeBase300}; + border-radius: ${borderRadiusCircular}; + color: ${colorNeutralForeground3}; + background-color: ${colorNeutralBackground6}; + } + + .default-icon, + ::slotted(svg) { + width: 20px; + height: 20px; + font-size: 20px; + } + + ::slotted(img) { + box-sizing: border-box; + width: 100%; + height: 100%; + border-radius: ${borderRadiusCircular}; + } + + ::slotted(fluent-presence-badge), + ::slotted(fluent-badge) { + position: absolute; + bottom: 0; + right: 0; + box-shadow: 0 0 0 ${strokeWidthThin} ${colorNeutralBackground1}; + } + + :host([size='20']), + :host([size='24']) { + font-size: ${fontSizeBase100}; + font-weight: ${fontWeightRegular}; + } + + :host([size='20']) { + width: 20px; + height: 20px; + } + + :host([size='24']) { + width: 24px; + height: 24px; + } + + :host([size='20']) .default-icon, + :host([size='24']) .default-icon, + :host([size='20']) ::slotted(svg), + :host([size='24']) ::slotted(svg) { + width: 16px; + height: 16px; + font-size: 16px; + } + + :host([size='28']) { + width: 28px; + height: 28px; + font-size: ${fontSizeBase200}; + } + + :host([size='36']) { + width: 36px; + height: 36px; + } + + :host([size='40']) { + width: 40px; + height: 40px; + } + + :host([size='48']), + :host([size='56']) { + font-size: ${fontSizeBase400}; + } + + :host([size='48']) { + width: 48px; + height: 48px; + } + + :host([size='48']) .default-icon, + :host([size='48']) ::slotted(svg) { + width: 24px; + height: 24px; + font-size: 24px; + } + + :host([size='56']) { + width: 56px; + height: 56px; + } + + :host([size='56']) .default-icon, + :host([size='56']) ::slotted(svg) { + width: 28px; + height: 28px; + font-size: 28px; + } + + :host([size='64']), + :host([size='72']), + :host([size='96']) { + font-size: ${fontSizeBase500}; + } + + :host([size='64']) .default-icon, + :host([size='72']) .default-icon, + :host([size='64']) ::slotted(svg), + :host([size='72']) ::slotted(svg) { + width: 32px; + height: 32px; + font-size: 32px; + } + + :host([size='64']) { + width: 64px; + height: 64px; + } + + :host([size='72']) { + width: 72px; + height: 72px; + } + + :host([size='96']) { + width: 96px; + height: 96px; + } + + :host([size='96']) .default-icon, + :host([size='120']) .default-icon, + :host([size='128']) .default-icon, + :host([size='96']) ::slotted(svg), + :host([size='120']) ::slotted(svg), + :host([size='128']) ::slotted(svg) { + width: 48px; + height: 48px; + font-size: 48px; + } + + :host([size='120']), + :host([size='128']) { + font-size: ${fontSizeBase600}; + } + + :host([size='120']) { + width: 120px; + height: 120px; + } + + :host([size='128']) { + width: 128px; + height: 128px; + } + + :host([shape='square']) { + border-radius: ${borderRadiusMedium}; + } + + :host([shape='square'][size='20']), + :host([shape='square'][size='24']) { + border-radius: ${borderRadiusSmall}; + } + + :host([shape='square'][size='56']), + :host([shape='square'][size='64']), + :host([shape='square'][size='72']) { + border-radius: ${borderRadiusLarge}; + } + :host([shape='square'][size='96']), + :host([shape='square'][size='120']), + :host([shape='square'][size='128']) { + border-radius: ${borderRadiusXLarge}; + } + + :host([data-color='brand']) { + color: ${colorNeutralForegroundInverted}; + background-color: ${colorBrandBackgroundStatic}; + } + + :host([data-color='dark-red']) { + color: ${colorPaletteDarkRedForeground2}; + background-color: ${colorPaletteDarkRedBackground2}; + } + + :host([data-color='cranberry']) { + color: ${colorPaletteCranberryForeground2}; + background-color: ${colorPaletteCranberryBackground2}; + } + + :host([data-color='red']) { + color: ${colorPaletteRedForeground2}; + background-color: ${colorPaletteRedBackground2}; + } + + :host([data-color='pumpkin']) { + color: ${colorPalettePumpkinForeground2}; + background-color: ${colorPalettePumpkinBackground2}; + } + + :host([data-color='peach']) { + color: ${colorPalettePeachForeground2}; + background-color: ${colorPalettePeachBackground2}; + } + + :host([data-color='marigold']) { + color: ${colorPaletteMarigoldForeground2}; + background-color: ${colorPaletteMarigoldBackground2}; + } + + :host([data-color='gold']) { + color: ${colorPaletteGoldForeground2}; + background-color: ${colorPaletteGoldBackground2}; + } + + :host([data-color='brass']) { + color: ${colorPaletteBrassForeground2}; + background-color: ${colorPaletteBrassBackground2}; + } + + :host([data-color='brown']) { + color: ${colorPaletteBrownForeground2}; + background-color: ${colorPaletteBrownBackground2}; + } + + :host([data-color='forest']) { + color: ${colorPaletteForestForeground2}; + background-color: ${colorPaletteForestBackground2}; + } + + :host([data-color='seafoam']) { + color: ${colorPaletteSeafoamForeground2}; + background-color: ${colorPaletteSeafoamBackground2}; + } + + :host([data-color='dark-green']) { + color: ${colorPaletteDarkGreenForeground2}; + background-color: ${colorPaletteDarkGreenBackground2}; + } + + :host([data-color='light-teal']) { + color: ${colorPaletteLightTealForeground2}; + background-color: ${colorPaletteLightTealBackground2}; + } + + :host([data-color='teal']) { + color: ${colorPaletteTealForeground2}; + background-color: ${colorPaletteTealBackground2}; + } + + :host([data-color='steel']) { + color: ${colorPaletteSteelForeground2}; + background-color: ${colorPaletteSteelBackground2}; + } + + :host([data-color='blue']) { + color: ${colorPaletteBlueForeground2}; + background-color: ${colorPaletteBlueBackground2}; + } + + :host([data-color='royal-blue']) { + color: ${colorPaletteRoyalBlueForeground2}; + background-color: ${colorPaletteRoyalBlueBackground2}; + } + + :host([data-color='cornflower']) { + color: ${colorPaletteCornflowerForeground2}; + background-color: ${colorPaletteCornflowerBackground2}; + } + + :host([data-color='navy']) { + color: ${colorPaletteNavyForeground2}; + background-color: ${colorPaletteNavyBackground2}; + } + + :host([data-color='lavender']) { + color: ${colorPaletteLavenderForeground2}; + background-color: ${colorPaletteLavenderBackground2}; + } + + :host([data-color='purple']) { + color: ${colorPalettePurpleForeground2}; + background-color: ${colorPalettePurpleBackground2}; + } + + :host([data-color='grape']) { + color: ${colorPaletteGrapeForeground2}; + background-color: ${colorPaletteGrapeBackground2}; + } + + :host([data-color='lilac']) { + color: ${colorPaletteLilacForeground2}; + background-color: ${colorPaletteLilacBackground2}; + } + + :host([data-color='pink']) { + color: ${colorPalettePinkForeground2}; + background-color: ${colorPalettePinkBackground2}; + } + + :host([data-color='magenta']) { + color: ${colorPaletteMagentaForeground2}; + background-color: ${colorPaletteMagentaBackground2}; + } + + :host([data-color='plum']) { + color: ${colorPalettePlumForeground2}; + background-color: ${colorPalettePlumBackground2}; + } + + :host([data-color='beige']) { + color: ${colorPaletteBeigeForeground2}; + background-color: ${colorPaletteBeigeBackground2}; + } + + :host([data-color='mink']) { + color: ${colorPaletteMinkForeground2}; + background-color: ${colorPaletteMinkBackground2}; + } + + :host([data-color='platinum']) { + color: ${colorPalettePlatinumForeground2}; + background-color: ${colorPalettePlatinumBackground2}; + } + + :host([data-color='anchor']) { + color: ${colorPaletteAnchorForeground2}; + background-color: ${colorPaletteAnchorBackground2}; + } + + :host([active]) { + /* Work-around for text pixel snapping at the end of the animation */ + transform: perspective(1px); + transition-property: transform, opacity; + transition-duration: ${animationTiming.ultraSlow}, ${animationTiming.faster}; + transition-delay: ${animations.fastEase}, ${animations.nullEasing}; + } + + :host([active])::before { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + border-radius: inherit; + transition-property: margin, opacity; + transition-duration: ${animationTiming.ultraSlow}, ${animationTiming.slower}; + transition-delay: ${animations.fastEase}, ${animations.nullEasing}; + } + :host([active])::before { + box-shadow: ${shadow8}; + border-style: solid; + border-color: ${colorBrandBackgroundStatic}; + } + + :host([active][appearance='shadow']) { + border-style: none; + border-color: none; + } + + :host([active]:not([appearance='shadow']))::before { + margin: calc(-2 * ${strokeWidthThick}); + border-width: ${strokeWidthThick}; + } + + :host([size='56'][active]:not([appearance='shadow']))::before, + :host([size='64'][active]:not([appearance='shadow']))::before { + margin: calc(-2 * ${strokeWidthThicker}); + border-width: ${strokeWidthThicker}; + } + + :host([size='72'][active]:not([appearance='shadow']))::before, + :host([size='96'][active]:not([appearance='shadow']))::before, + :host([size='120'][active]:not([appearance='shadow']))::before, + :host([size='128'][active]:not([appearance='shadow']))::before { + margin: calc(-2 * ${strokeWidthThickest}); + border-width: ${strokeWidthThickest}; + } + + :host([size='20'][active][appearance])::before, + :host([size='24'][active][appearance])::before, + :host([size='28'][active][appearance])::before { + box-shadow: ${shadow4}; + } + + :host([size='56'][active][appearance])::before, + :host([size='64'][active][appearance])::before { + box-shadow: ${shadow16}; + } + + :host([size='72'][active][appearance])::before, + :host([size='96'][active][appearance])::before, + :host([size='120'][active][appearance])::before, + :host([size='128'][active][appearance])::before { + box-shadow: ${shadow28}; + } + + :host([active][appearance='ring'])::before { + box-shadow: none; + } + + :host([active='inactive']) { + opacity: 0.8; + transform: scale(0.875); + transition-property: transform, opacity; + transition-duration: ${animationTiming.ultraSlow}, ${animationTiming.faster}; + transition-delay: ${animations.fastOutSlowInMin}, ${animations.nullEasing}; + } + + :host([active='inactive'])::before { + margin: 0; + opacity: 0; + transition-property: margin, opacity; + transition-duration: ${animationTiming.ultraSlow}, ${animationTiming.slower}; + transition-delay: ${animations.fastOutSlowInMin}, ${animations.nullEasing}; + } +`; diff --git a/packages/web-components/src/avatar/avatar.template.ts b/packages/web-components/src/avatar/avatar.template.ts new file mode 100644 index 0000000000000..963ac4a699044 --- /dev/null +++ b/packages/web-components/src/avatar/avatar.template.ts @@ -0,0 +1,41 @@ +import { ElementViewTemplate, html } from '@microsoft/fast-element'; +import type { Avatar } from './avatar.js'; + +const defaultIconTemplate = html``; + +/** + * The template for the Avatar component. + * @public + */ +export function avatarTemplate(): ElementViewTemplate { + return html` + + `; +} + +export const template: ElementViewTemplate = avatarTemplate(); diff --git a/packages/web-components/src/avatar/avatar.ts b/packages/web-components/src/avatar/avatar.ts new file mode 100644 index 0000000000000..57ffa73adfa89 --- /dev/null +++ b/packages/web-components/src/avatar/avatar.ts @@ -0,0 +1,175 @@ +import { attr, FASTElement, nullableNumberConverter } from '@microsoft/fast-element'; +import { getInitials } from '../utils/get-initials.js'; +import { + AvatarActive, + AvatarAppearance, + AvatarColor, + AvatarNamedColor, + AvatarShape, + AvatarSize, +} from './avatar.options.js'; + +/** + * The base class used for constructing a fluent-avatar custom element + * @public + */ +export class Avatar extends FASTElement { + /** + * The name of the person or entity represented by this Avatar. This should always be provided if it is available. + * + * @public + * @remarks + * HTML Attribute: name + */ + @attr + public name: string | undefined; + protected nameChanged(previous: string, next: string): void { + if (next !== undefined && !this.initials) { + this.setInitials(); + } + } + + /** + * Provide custom initials rather than one generated via the name + * + * @public + * @remarks + * HTML Attribute: name + */ + @attr + public initials: string | undefined; + protected initialsChanged(previous: string | undefined, next: string | undefined): void { + if (next !== undefined) { + this.setInitials(); + } + } + + /** + * Size of the avatar in pixels. + * + * Size is restricted to a limited set of supported values recommended for most uses (see `AvatarSizeValue`) and + * based on design guidelines for the Avatar control. + * + * If a non-supported size is neeeded, set `size` to the next-smaller supported size, and set `width` and `height` + * to override the rendered size. + * + * @public + * @remarks + * HTML Attribute: size + * + */ + @attr({ converter: nullableNumberConverter }) + public size: AvatarSize | undefined; + + /** + * The avatar can have a circular or square shape. + * + * @public + * @remarks + * HTML Attribute: shape + */ + @attr + public shape: AvatarShape | undefined; + + /** + * Optional activity indicator + * * active: the avatar will be decorated according to activeAppearance + * * inactive: the avatar will be reduced in size and partially transparent + * * undefined: normal display + * + * @public + * @remarks + * HTML Attribute: active + */ + @attr + public active?: AvatarActive | undefined; + + /** + * The appearance when `active="active"` + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance?: AvatarAppearance | undefined; + + /** + * The color when displaying either an icon or initials. + * * neutral (default): gray + * * brand: color from the brand palette + * * colorful: picks a color from a set of pre-defined colors, based on a hash of the name (or colorId if provided) + * * [AvatarNamedColor]: a specific color from the theme + * + * @public + * @remarks + * HTML Attribute: color + */ + @attr + public color: AvatarColor = 'neutral'; + protected colorChanged(previous: string, next: string): void { + this.setColor(); + } + + /** + * Specify a string to be used instead of the name, to determine which color to use when color="colorful". + * Use this when a name is not available, but there is another unique identifier that can be used instead. + */ + @attr({ attribute: 'color-id' }) + public colorId: AvatarNamedColor | undefined; + public colorIdChanged(): void { + if (!this.colorId) { + return; + } + + this.setColor(); + } + + /** + * Sets the data-color attribute used for the visual presentation + * @internal + */ + public setColor(): AvatarColor { + if (!this.color) { + return; + } + + const colors = Object.keys(AvatarNamedColor); + + return this.color === 'colorful' + ? colors[getHashCode(this.colorId ?? this.name ?? '') % colors.length] + : this.color; + } + + /** + * Generates and sets the initials for the template + * @internal + */ + public setInitials(): string | void { + if (!this.name && !this.initials) { + return; + } + + // size can be undefined since we default it in CSS only + const size = this.size ?? 32; + + return ( + this.initials ?? + getInitials(this.name, window.getComputedStyle((this as unknown) as HTMLElement).direction === 'rtl', { + firstInitialOnly: size <= 16, + }) + ); + } +} + +// copied from React avatar +const getHashCode = (str: string): number => { + let hashCode = 0; + for (let len: number = str.length - 1; len >= 0; len--) { + const ch = str.charCodeAt(len); + const shift = len % 8; + hashCode ^= (ch << shift) + (ch >> (8 - shift)); // eslint-disable-line no-bitwise + } + + return hashCode; +}; diff --git a/packages/web-components/src/avatar/define.ts b/packages/web-components/src/avatar/define.ts new file mode 100644 index 0000000000000..34933837173dc --- /dev/null +++ b/packages/web-components/src/avatar/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { definition } from './avatar.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/packages/web-components/src/avatar/index.ts b/packages/web-components/src/avatar/index.ts new file mode 100644 index 0000000000000..8bb35aee4dc2a --- /dev/null +++ b/packages/web-components/src/avatar/index.ts @@ -0,0 +1,5 @@ +export * from './avatar.js'; +export * from './avatar.options.js'; +export { template as AvatarTemplate } from './avatar.template.js'; +export { styles as AvatarStyles } from './avatar.styles.js'; +export { definition as AvatarDefinition } from './avatar.definition.js'; diff --git a/packages/web-components/src/index.ts b/packages/web-components/src/index.ts index f8394d0a1cf52..041cf1b698364 100644 --- a/packages/web-components/src/index.ts +++ b/packages/web-components/src/index.ts @@ -1,5 +1,6 @@ export * from './accordion/index.js'; export * from './accordion-item/index.js'; +export * from './avatar/index.js'; export * from './badge/index.js'; export * from './counter-badge/index.js'; export * from './divider/index.js'; diff --git a/packages/web-components/src/utils/get-initials.ts b/packages/web-components/src/utils/get-initials.ts new file mode 100644 index 0000000000000..7de388f103ec9 --- /dev/null +++ b/packages/web-components/src/utils/get-initials.ts @@ -0,0 +1,110 @@ +/* TODO: This file is a direct copy of the React Avatar utils */ + +/** + * Regular expressions matching characters to ignore when calculating the initials. + */ + +/** + * Regular expression matching characters within various types of enclosures, including the enclosures themselves + * so for example, (xyz) [xyz] {xyz} all would be ignored + */ +const UNWANTED_ENCLOSURES_REGEX: RegExp = /[\(\[\{][^\)\]\}]*[\)\]\}]/g; + +/** + * Regular expression matching special ASCII characters except space, plus some unicode special characters. + * Applies after unwanted enclosures have been removed + */ +// eslint-disable-next-line no-control-regex +const UNWANTED_CHARS_REGEX: RegExp = /[\0-\u001F\!-/:-@\[-`\{-\u00BF\u0250-\u036F\uD800-\uFFFF]/g; + +/** + * Regular expression matching phone numbers. Applied after chars matching UNWANTED_CHARS_REGEX have been removed + * and number has been trimmed for whitespaces + */ +const PHONENUMBER_REGEX: RegExp = /^\d+[\d\s]*(:?ext|x|)\s*\d+$/i; + +/** Regular expression matching one or more spaces. */ +const MULTIPLE_WHITESPACES_REGEX: RegExp = /\s+/g; + +/** + * Regular expression matching languages for which we currently don't support initials. + * Arabic: Arabic, Arabic Supplement, Arabic Extended-A. + * Korean: Hangul Jamo, Hangul Compatibility Jamo, Hangul Jamo Extended-A, Hangul Syllables, Hangul Jamo Extended-B. + * Japanese: Hiragana, Katakana. + * CJK: CJK Unified Ideographs Extension A, CJK Unified Ideographs, CJK Compatibility Ideographs, + * CJK Unified Ideographs Extension B + */ +// eslint-disable-next-line +const UNSUPPORTED_TEXT_REGEX: RegExp = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u1100-\u11FF\u3130-\u318F\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]|[\uD840-\uD869][\uDC00-\uDED6]/; + +function getInitialsLatin(displayName: string, isRtl: boolean, firstInitialOnly?: boolean): string { + let initials = ''; + + const splits: string[] = displayName.split(' '); + if (splits.length !== 0) { + initials += splits[0].charAt(0).toUpperCase(); + } + + if (!firstInitialOnly) { + if (splits.length === 2) { + initials += splits[1].charAt(0).toUpperCase(); + } else if (splits.length === 3) { + initials += splits[2].charAt(0).toUpperCase(); + } + } + + if (isRtl && initials.length > 1) { + return initials.charAt(1) + initials.charAt(0); + } + + return initials; +} + +function cleanupDisplayName(displayName: string): string { + displayName = displayName.replace(UNWANTED_ENCLOSURES_REGEX, ''); + displayName = displayName.replace(UNWANTED_CHARS_REGEX, ''); + displayName = displayName.replace(MULTIPLE_WHITESPACES_REGEX, ' '); + displayName = displayName.trim(); + + return displayName; +} + +/** + * Get (up to 2 characters) initials based on display name of the persona. + * + * @param displayName - The full name of the person or entity + * @param isRtl - Whether the display is in RTL + * @param options - Extra options to control the behavior of getInitials + * + * @returns The 1 or 2 character initials based on the name. Or an empty string if no initials + * could be derived from the name. + * + * @internal + */ +export function getInitials( + displayName: string | undefined | null, + isRtl: boolean, + options?: { + /** Should initials be generated from phone numbers (default false) */ + allowPhoneInitials?: boolean; + + /** Returns only the first initial */ + firstInitialOnly?: boolean; + }, +): string { + if (!displayName) { + return ''; + } + + displayName = cleanupDisplayName(displayName); + + // For names containing CJK characters, and phone numbers, we don't display initials + if ( + UNSUPPORTED_TEXT_REGEX.test(displayName) || + (!options?.allowPhoneInitials && PHONENUMBER_REGEX.test(displayName)) + ) { + return ''; + } + + return getInitialsLatin(displayName, isRtl, options?.firstInitialOnly); +} From 4ae8cc47e763ff6c8d55f334309c6dc3c7439c47 Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Mon, 6 Feb 2023 23:22:00 -0800 Subject: [PATCH 02/17] add export path --- packages/web-components/docs/api-report.md | 158 ++++++++++++++++++ packages/web-components/package.json | 4 + .../src/avatar/avatar.styles.ts | 4 +- packages/web-components/src/avatar/avatar.ts | 6 +- 4 files changed, 167 insertions(+), 5 deletions(-) diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index 4563c8b9e70f4..bff461ae96b51 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -17,6 +17,164 @@ import { StaticallyComposableHTML } from '@microsoft/fast-foundation'; import type { Theme } from '@fluentui/tokens'; import { ValuesOf } from '@microsoft/fast-foundation'; +// @public +export class Avatar extends FASTElement { + active?: AvatarActive | undefined; + appearance?: AvatarAppearance | undefined; + color: AvatarColor; + // (undocumented) + protected colorChanged(previous: string, next: string): void; + colorId: AvatarNamedColor | undefined; + // (undocumented) + colorIdChanged(): void; + initials: string | undefined; + // (undocumented) + protected initialsChanged(previous: string | undefined, next: string | undefined): void; + name: string | undefined; + // (undocumented) + protected nameChanged(previous: string, next: string): void; + // @internal + setColor(): AvatarColor | void; + // @internal + setInitials(): string | void; + shape: AvatarShape | undefined; + size: AvatarSize | undefined; +} + +// @public +export const AvatarActive: { + readonly active: "active"; + readonly inactive: "inactive"; +}; + +// @public +export type AvatarActive = ValuesOf; + +// @public +export const AvatarAppearance: { + readonly ring: "ring"; + readonly shadow: "shadow"; + readonly ringShadow: "ring-shadow"; +}; + +// @public +export type AvatarAppearance = ValuesOf; + +// @public +export const AvatarColor: { + readonly darkRed: "dark-red"; + readonly cranberry: "cranberry"; + readonly red: "red"; + readonly pumpkin: "pumpkin"; + readonly peach: "peach"; + readonly marigold: "marigold"; + readonly gold: "gold"; + readonly brass: "brass"; + readonly brown: "brown"; + readonly forest: "forest"; + readonly seafoam: "seafoam"; + readonly darkGreen: "dark-green"; + readonly lighTeal: "light-teal"; + readonly teal: "teal"; + readonly steel: "steel"; + readonly blue: "blue"; + readonly royalBlue: "royal-blue"; + readonly cornflower: "cornflower"; + readonly navy: "navy"; + readonly lavender: "lavender"; + readonly purple: "purple"; + readonly grape: "grape"; + readonly lilac: "lilac"; + readonly pink: "pink"; + readonly magenta: "magenta"; + readonly plum: "plum"; + readonly beige: "beige"; + readonly mink: "mink"; + readonly platinum: "platinum"; + readonly anchor: "anchor"; + readonly neutral: "neutral"; + readonly brand: "brand"; + readonly colorful: "colorful"; +}; + +// @public +export type AvatarColor = ValuesOf; + +// @public +export const AvatarDefinition: FASTElementDefinition; + +// @public +export const AvatarNamedColor: { + readonly darkRed: "dark-red"; + readonly cranberry: "cranberry"; + readonly red: "red"; + readonly pumpkin: "pumpkin"; + readonly peach: "peach"; + readonly marigold: "marigold"; + readonly gold: "gold"; + readonly brass: "brass"; + readonly brown: "brown"; + readonly forest: "forest"; + readonly seafoam: "seafoam"; + readonly darkGreen: "dark-green"; + readonly lighTeal: "light-teal"; + readonly teal: "teal"; + readonly steel: "steel"; + readonly blue: "blue"; + readonly royalBlue: "royal-blue"; + readonly cornflower: "cornflower"; + readonly navy: "navy"; + readonly lavender: "lavender"; + readonly purple: "purple"; + readonly grape: "grape"; + readonly lilac: "lilac"; + readonly pink: "pink"; + readonly magenta: "magenta"; + readonly plum: "plum"; + readonly beige: "beige"; + readonly mink: "mink"; + readonly platinum: "platinum"; + readonly anchor: "anchor"; +}; + +// @public +export type AvatarNamedColor = ValuesOf; + +// @public +export const AvatarShape: { + readonly circular: "circular"; + readonly square: "square"; +}; + +// @public +export type AvatarShape = ValuesOf; + +// @public +export const AvatarSize: { + readonly _20: 20; + readonly _24: 24; + readonly _28: 28; + readonly _32: 32; + readonly _36: 36; + readonly _40: 40; + readonly _48: 48; + readonly _56: 56; + readonly _64: 64; + readonly _72: 72; + readonly _96: 96; + readonly _120: 120; + readonly _128: 128; +}; + +// @public +export type AvatarSize = ValuesOf; + +// @public +export const AvatarStyles: ElementStyles; + +// @public (undocumented) +export const AvatarTemplate: ElementViewTemplate; + // Warning: (ae-internal-mixed-release-tag) Mixed release tags are not allowed for "Badge" because one of its declarations is marked as @internal // // @public diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 14de3ce922e06..7093c1856539d 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -32,6 +32,10 @@ "types": "./dist/esm/accordion-item/define.d.ts", "default": "./dist/esm/accordion-item/define.js" }, + "./avatar": { + "types": "./dist/esm/avatar/define.d.ts", + "default": "./dist/esm/avatar/define.js" + }, "./badge": { "types": "./dist/esm/badge/define.d.ts", "default": "./dist/esm/badge/define.js" diff --git a/packages/web-components/src/avatar/avatar.styles.ts b/packages/web-components/src/avatar/avatar.styles.ts index beb84ca267f91..9378e03b85286 100644 --- a/packages/web-components/src/avatar/avatar.styles.ts +++ b/packages/web-components/src/avatar/avatar.styles.ts @@ -88,7 +88,7 @@ import { strokeWidthThicker, strokeWidthThickest, strokeWidthThin, -} from '../theme/design-tokens'; +} from '../theme/design-tokens.js'; // TODO: Should all animation constants go to tokens/theme or globals? // https://github.com/microsoft/fluentui/issues/16372#issuecomment-778240665 @@ -505,7 +505,7 @@ export const styles = css` border-color: ${colorBrandBackgroundStatic}; } - :host([active][appearance='shadow']) { + :host([active][appearance='shadow'])::before { border-style: none; border-color: none; } diff --git a/packages/web-components/src/avatar/avatar.ts b/packages/web-components/src/avatar/avatar.ts index 57ffa73adfa89..c22304315d994 100644 --- a/packages/web-components/src/avatar/avatar.ts +++ b/packages/web-components/src/avatar/avatar.ts @@ -129,15 +129,15 @@ export class Avatar extends FASTElement { * Sets the data-color attribute used for the visual presentation * @internal */ - public setColor(): AvatarColor { + public setColor(): AvatarColor | void { if (!this.color) { return; } const colors = Object.keys(AvatarNamedColor); - return this.color === 'colorful' - ? colors[getHashCode(this.colorId ?? this.name ?? '') % colors.length] + return this.color === AvatarColor.colorful + ? (colors[getHashCode(this.colorId ?? this.name ?? '') % colors.length] as AvatarColor) : this.color; } From 723932a8876549b7070ae417797f6d717e16f689 Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Mon, 20 Feb 2023 16:29:31 -0800 Subject: [PATCH 03/17] update helpers path in avatar stories --- packages/web-components/src/avatar/avatar.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-components/src/avatar/avatar.stories.ts b/packages/web-components/src/avatar/avatar.stories.ts index 32bae0dd512fd..1629d56f3aaa8 100644 --- a/packages/web-components/src/avatar/avatar.stories.ts +++ b/packages/web-components/src/avatar/avatar.stories.ts @@ -1,6 +1,6 @@ import { html } from '@microsoft/fast-element'; import type { Args, Meta } from '@storybook/html'; -import { renderComponent } from '../__test__/helpers.js'; +import { renderComponent } from '../helpers.stories.js'; import type { Avatar as FluentAvatar } from './avatar.js'; import { AvatarActive, AvatarAppearance, AvatarColor, AvatarShape, AvatarSize } from './avatar.options.js'; import './define.js'; From 80afc416642a9662b91bb710bf84c23a26389c89 Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Tue, 21 Feb 2023 15:40:10 -0800 Subject: [PATCH 04/17] create public static value for named colors --- packages/web-components/src/avatar/avatar.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/web-components/src/avatar/avatar.ts b/packages/web-components/src/avatar/avatar.ts index c22304315d994..cd180df947020 100644 --- a/packages/web-components/src/avatar/avatar.ts +++ b/packages/web-components/src/avatar/avatar.ts @@ -134,10 +134,8 @@ export class Avatar extends FASTElement { return; } - const colors = Object.keys(AvatarNamedColor); - return this.color === AvatarColor.colorful - ? (colors[getHashCode(this.colorId ?? this.name ?? '') % colors.length] as AvatarColor) + ? (Avatar.colors[getHashCode(this.colorId ?? this.name ?? '') % Avatar.colors.length] as AvatarColor) : this.color; } @@ -160,6 +158,11 @@ export class Avatar extends FASTElement { }) ); } + + /** + * An array of the available Avatar named colors + */ + public static colors = Object.keys(AvatarNamedColor); } // copied from React avatar From dce5d5538207a1601ad071b01f4f8a6e48b46142 Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Tue, 21 Feb 2023 15:40:55 -0800 Subject: [PATCH 05/17] update styles to support any slotted badge --- packages/web-components/src/avatar/avatar.styles.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/web-components/src/avatar/avatar.styles.ts b/packages/web-components/src/avatar/avatar.styles.ts index 9378e03b85286..5e14d38bdbca6 100644 --- a/packages/web-components/src/avatar/avatar.styles.ts +++ b/packages/web-components/src/avatar/avatar.styles.ts @@ -170,8 +170,7 @@ export const styles = css` border-radius: ${borderRadiusCircular}; } - ::slotted(fluent-presence-badge), - ::slotted(fluent-badge) { + ::slotted([slot='badge']) { position: absolute; bottom: 0; right: 0; From 84ecbccb54eaf11e29cec24ad11087353cb5266a Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Tue, 21 Feb 2023 15:52:52 -0800 Subject: [PATCH 06/17] add size 16 styles and stories --- .../web-components/src/avatar/avatar.options.ts | 1 + .../web-components/src/avatar/avatar.stories.ts | 1 + packages/web-components/src/avatar/avatar.styles.ts | 13 +++++++++++++ .../web-components/src/avatar/avatar.template.ts | 12 +----------- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/web-components/src/avatar/avatar.options.ts b/packages/web-components/src/avatar/avatar.options.ts index 9574d8fdcb1a3..893aefbee899b 100644 --- a/packages/web-components/src/avatar/avatar.options.ts +++ b/packages/web-components/src/avatar/avatar.options.ts @@ -102,6 +102,7 @@ export type AvatarColor = ValuesOf; * @public */ export const AvatarSize = { + _16: 16, _20: 20, _24: 24, _28: 28, diff --git a/packages/web-components/src/avatar/avatar.stories.ts b/packages/web-components/src/avatar/avatar.stories.ts index 1629d56f3aaa8..8765138662cca 100644 --- a/packages/web-components/src/avatar/avatar.stories.ts +++ b/packages/web-components/src/avatar/avatar.stories.ts @@ -176,6 +176,7 @@ export const CustomInitials = renderComponent(html` `
+ 16 20 24 28 diff --git a/packages/web-components/src/avatar/avatar.styles.ts b/packages/web-components/src/avatar/avatar.styles.ts index 5e14d38bdbca6..b1b474daf716f 100644 --- a/packages/web-components/src/avatar/avatar.styles.ts +++ b/packages/web-components/src/avatar/avatar.styles.ts @@ -177,12 +177,18 @@ export const styles = css` box-shadow: 0 0 0 ${strokeWidthThin} ${colorNeutralBackground1}; } + :host([size='16']), :host([size='20']), :host([size='24']) { font-size: ${fontSizeBase100}; font-weight: ${fontWeightRegular}; } + :host([size='16']) { + width: 16px; + height: 16px; + } + :host([size='20']) { width: 20px; height: 20px; @@ -193,6 +199,13 @@ export const styles = css` height: 24px; } + :host([size='16']) .default-icon, + :host([size='16']) ::slotted(svg) { + width: 12px; + height: 12px; + font-size: 12px; + } + :host([size='20']) .default-icon, :host([size='24']) .default-icon, :host([size='20']) ::slotted(svg), diff --git a/packages/web-components/src/avatar/avatar.template.ts b/packages/web-components/src/avatar/avatar.template.ts index 963ac4a699044..04e9306e9d4d7 100644 --- a/packages/web-components/src/avatar/avatar.template.ts +++ b/packages/web-components/src/avatar/avatar.template.ts @@ -22,17 +22,7 @@ const defaultIconTemplate = html`(): ElementViewTemplate { return html` `; From 566d983fd57c9633699d869ae87637612815b811 Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Tue, 21 Feb 2023 16:00:47 -0800 Subject: [PATCH 07/17] update animations to use tokens --- .../src/avatar/avatar.styles.ts | 73 +++++++------------ 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/packages/web-components/src/avatar/avatar.styles.ts b/packages/web-components/src/avatar/avatar.styles.ts index b1b474daf716f..2d9b478eb7085 100644 --- a/packages/web-components/src/avatar/avatar.styles.ts +++ b/packages/web-components/src/avatar/avatar.styles.ts @@ -71,6 +71,18 @@ import { colorPaletteSteelForeground2, colorPaletteTealBackground2, colorPaletteTealForeground2, + curveAccelerateMax, + curveAccelerateMid, + curveAccelerateMin, + curveDecelerateMax, + curveDecelerateMid, + curveDecelerateMin, + curveEasyEase, + curveEasyEaseMax, + curveLinear, + durationFaster, + durationSlower, + durationUltraSlow, fontFamilyBase, fontSizeBase100, fontSizeBase200, @@ -90,51 +102,16 @@ import { strokeWidthThin, } from '../theme/design-tokens.js'; -// TODO: Should all animation constants go to tokens/theme or globals? -// https://github.com/microsoft/fluentui/issues/16372#issuecomment-778240665 - -const animationDuration = { - duration50: '50ms', - duration100: '100ms', - duration150: '150ms', - duration200: '200ms', - duration300: '300ms', - duration400: '400ms', - duration500: '500ms', -}; - -const animationTiming = { - ultraFast: animationDuration.duration50, - faster: animationDuration.duration100, - fast: animationDuration.duration150, - normal: animationDuration.duration200, - slow: animationDuration.duration300, - slower: animationDuration.duration400, - ultraSlow: animationDuration.duration500, -}; - -const animationLines = { - decelerateMax: 'cubic-bezier(0.00,0.00,0.00,1.00)', - decelerateMid: 'cubic-bezier(0.10,0.90,0.20,1.00)', - decelerateMin: 'cubic-bezier(0.33,0.00,0.10,1.00)', - accelerateMax: 'cubic-bezier(1.00,0.00,1.00,1.00)', - accelerateMid: 'cubic-bezier(0.90,0.10,1.00,0.20)', - accelerateMin: 'cubic-bezier(0.80,0.00,0.78,1.00)', - maxEasyEase: 'cubic-bezier(0.80,0.00,0.20,1.00)', - easyEase: 'cubic-bezier(0.33,0.00,0.67,1.00)', - linear: 'linear', -}; - const animations = { - fastOutSlowInMax: animationLines.decelerateMax, - fastOutSlowInMid: animationLines.decelerateMid, - fastOutSlowInMin: animationLines.decelerateMin, - slowOutFastInMax: animationLines.accelerateMax, - slowOutFastInMid: animationLines.accelerateMid, - slowOutFastInMin: animationLines.accelerateMin, - fastEase: animationLines.maxEasyEase, - normalEase: animationLines.easyEase, - nullEasing: animationLines.linear, + fastOutSlowInMax: curveDecelerateMax, + fastOutSlowInMid: curveDecelerateMid, + fastOutSlowInMin: curveDecelerateMin, + slowOutFastInMax: curveAccelerateMax, + slowOutFastInMid: curveAccelerateMid, + slowOutFastInMin: curveAccelerateMin, + fastEase: curveEasyEaseMax, + normalEase: curveEasyEase, + nullEasing: curveLinear, }; /** Avatar styles @@ -495,7 +472,7 @@ export const styles = css` /* Work-around for text pixel snapping at the end of the animation */ transform: perspective(1px); transition-property: transform, opacity; - transition-duration: ${animationTiming.ultraSlow}, ${animationTiming.faster}; + transition-duration: ${durationUltraSlow}, ${durationFaster}; transition-delay: ${animations.fastEase}, ${animations.nullEasing}; } @@ -508,7 +485,7 @@ export const styles = css` right: 0; border-radius: inherit; transition-property: margin, opacity; - transition-duration: ${animationTiming.ultraSlow}, ${animationTiming.slower}; + transition-duration: ${durationUltraSlow}, ${durationSlower}; transition-delay: ${animations.fastEase}, ${animations.nullEasing}; } :host([active])::before { @@ -567,7 +544,7 @@ export const styles = css` opacity: 0.8; transform: scale(0.875); transition-property: transform, opacity; - transition-duration: ${animationTiming.ultraSlow}, ${animationTiming.faster}; + transition-duration: ${durationUltraSlow}, ${durationFaster}; transition-delay: ${animations.fastOutSlowInMin}, ${animations.nullEasing}; } @@ -575,7 +552,7 @@ export const styles = css` margin: 0; opacity: 0; transition-property: margin, opacity; - transition-duration: ${animationTiming.ultraSlow}, ${animationTiming.slower}; + transition-duration: ${durationUltraSlow}, ${durationSlower}; transition-delay: ${animations.fastOutSlowInMin}, ${animations.nullEasing}; } `; From 186695837bb5da17e8e036922a3a1e7e41072a62 Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Tue, 21 Feb 2023 16:04:18 -0800 Subject: [PATCH 08/17] add support for prefers reduced motion on active and inactive transitions --- packages/web-components/src/avatar/avatar.styles.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/web-components/src/avatar/avatar.styles.ts b/packages/web-components/src/avatar/avatar.styles.ts index 2d9b478eb7085..6f7b4fc74a737 100644 --- a/packages/web-components/src/avatar/avatar.styles.ts +++ b/packages/web-components/src/avatar/avatar.styles.ts @@ -555,4 +555,15 @@ export const styles = css` transition-duration: ${durationUltraSlow}, ${durationSlower}; transition-delay: ${animations.fastOutSlowInMin}, ${animations.nullEasing}; } + + @media screen and (prefers-reduced-motion: reduce) { + :host([active]) { + transition-duration: 0.01ms; + } + + :host([active])::before { + transition-duration: 0.01ms; + transition-delay: 0.01ms; + } + } `; From 34aa07758d9a92f4eea71fcb671fe80e97de524f Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Tue, 21 Feb 2023 16:31:42 -0800 Subject: [PATCH 09/17] change set to generate for color and initials fns --- packages/web-components/docs/api-report.md | 10 ++++++---- .../web-components/src/avatar/avatar.template.ts | 4 ++-- packages/web-components/src/avatar/avatar.ts | 12 ++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index bff461ae96b51..3b2c2308d01cf 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -27,16 +27,17 @@ export class Avatar extends FASTElement { colorId: AvatarNamedColor | undefined; // (undocumented) colorIdChanged(): void; + static colors: string[]; + // @internal + generateColor(): AvatarColor | void; + // @internal + generateInitials(): string | void; initials: string | undefined; // (undocumented) protected initialsChanged(previous: string | undefined, next: string | undefined): void; name: string | undefined; // (undocumented) protected nameChanged(previous: string, next: string): void; - // @internal - setColor(): AvatarColor | void; - // @internal - setInitials(): string | void; shape: AvatarShape | undefined; size: AvatarSize | undefined; } @@ -151,6 +152,7 @@ export type AvatarShape = ValuesOf; // @public export const AvatarSize: { + readonly _16: 16; readonly _20: 20; readonly _24: 24; readonly _28: 28; diff --git a/packages/web-components/src/avatar/avatar.template.ts b/packages/web-components/src/avatar/avatar.template.ts index 04e9306e9d4d7..3e9f595d71405 100644 --- a/packages/web-components/src/avatar/avatar.template.ts +++ b/packages/web-components/src/avatar/avatar.template.ts @@ -21,8 +21,8 @@ const defaultIconTemplate = html`(): ElementViewTemplate { return html` -
+ +## **Engineering Spec** + +Fluent WC3 Avatar has feature parity with the Fluent UI React 9 Accordion implementation but not direct parity. + +
+ +## Class: `Avatar` + +
+ +### **Component Name** + +`` + +
+ +## **Preparation** + +
+ +### **Fluent Web Component v3 v.s Fluent React 9** + +
+ +**Component and Slot Mapping** + +| Fluent UI React 9 | Fluent Web Components 3 | +| ----------------- | ----------------------- | +| `` | `` | + +
+ +**Property Mapping** +| Fluent UI React 9 | Fluent Web Components 3 | Description of difference | +| ------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------- | +| `idForColor: string`| `colorId: string` | both are strings, the delta here is primarily verbosity. In web components attributes need to follow HTML syntax for attributes. The property of `colorId` maps to `color-id`. Were we to map directly we would have an attribute of `id-for-color` which seems overly verbose. Almost all HTML attributes are at max hyphenated once. This proposes an attribute which is less verbose and only requires a single `-`. | +| `size` | `size` | | +| `shape` | `shape` | +| `active` | `active` | The only delta here is that the web components are aligning to the resolved RFC to use `undefined` for fields which are intended to "unset" attributes | +| `activeAppearance` | `appearance` | The delta here is semantic only, unless we need to reserve the appearance namespace, brevity seems preferred here | +| `name` | `name` | +| `initials` | `initials` | + +**Additional Deltas:** + +The FUIR9 implementation seems to utilize several "slots", whereas with the web component implementation includes two primary slots. + +1. Default slot - When a name or initials is provided, the default slot projects the initials generated via name or initials. If an image is slotted into the default slot, the image will be shown. If an SVG is slotted into the default slot an SVG will project overriding any other value. If name and initials are not provided and nothing is slotted, the default avatar svg is projected. +2. Badge - The slot for the badge From cee198cd6fcb429e0589899030f9072ba405644e Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Fri, 24 Feb 2023 16:47:30 -0800 Subject: [PATCH 16/17] update attributes as optional --- packages/web-components/src/avatar/avatar.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/web-components/src/avatar/avatar.ts b/packages/web-components/src/avatar/avatar.ts index 56c5e39e56eb1..da94ce2626f6e 100644 --- a/packages/web-components/src/avatar/avatar.ts +++ b/packages/web-components/src/avatar/avatar.ts @@ -22,7 +22,7 @@ export class Avatar extends FASTElement { * HTML Attribute: name */ @attr - public name: string | undefined; + public name?: string | undefined; /** * Provide custom initials rather than one generated via the name @@ -32,7 +32,7 @@ export class Avatar extends FASTElement { * HTML Attribute: name */ @attr - public initials: string | undefined; + public initials?: string | undefined; /** * Size of the avatar in pixels. @@ -49,7 +49,7 @@ export class Avatar extends FASTElement { * */ @attr({ converter: nullableNumberConverter }) - public size: AvatarSize | undefined; + public size?: AvatarSize | undefined; /** * The avatar can have a circular or square shape. @@ -59,7 +59,7 @@ export class Avatar extends FASTElement { * HTML Attribute: shape */ @attr - public shape: AvatarShape | undefined; + public shape?: AvatarShape | undefined; /** * Optional activity indicator @@ -96,14 +96,14 @@ export class Avatar extends FASTElement { * HTML Attribute: color */ @attr - public color: AvatarColor = 'neutral'; + public color?: AvatarColor = 'neutral'; /** * Specify a string to be used instead of the name, to determine which color to use when color="colorful". * Use this when a name is not available, but there is another unique identifier that can be used instead. */ @attr({ attribute: 'color-id' }) - public colorId: AvatarNamedColor | undefined; + public colorId?: AvatarNamedColor | undefined; /** * Sets the data-color attribute used for the visual presentation From 34ea6e8ae27c43cf1a5bf350ab22ee7e134ff27f Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Fri, 24 Feb 2023 16:50:54 -0800 Subject: [PATCH 17/17] update api report --- packages/web-components/docs/api-report.md | 122 ++++++++++++++++++++- 1 file changed, 116 insertions(+), 6 deletions(-) diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index ba91f2b2e1380..2e81845851dce 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -5,14 +5,18 @@ ```ts import { CSSDesignToken } from '@microsoft/fast-foundation'; +import { DividerOrientation } from '@microsoft/fast-foundation'; +import { DividerRole } from '@microsoft/fast-foundation'; import { ElementStyles } from '@microsoft/fast-element'; import { ElementViewTemplate } from '@microsoft/fast-element'; import { FASTAccordion } from '@microsoft/fast-foundation'; import { FASTAccordionItem } from '@microsoft/fast-foundation'; +import { FASTDivider } from '@microsoft/fast-foundation'; import { FASTElement } from '@microsoft/fast-element'; import { FASTElementDefinition } from '@microsoft/fast-element'; import { FASTProgress } from '@microsoft/fast-foundation'; import { FASTProgressRing } from '@microsoft/fast-foundation'; +import { FASTSwitch } from '@microsoft/fast-foundation'; import { StartEnd } from '@microsoft/fast-foundation'; import { StartEndOptions } from '@microsoft/fast-foundation'; import { StaticallyComposableHTML } from '@microsoft/fast-foundation'; @@ -81,17 +85,17 @@ export const accordionTemplate: ElementViewTemplate; export class Avatar extends FASTElement { active?: AvatarActive | undefined; appearance?: AvatarAppearance | undefined; - color: AvatarColor; - colorId: AvatarNamedColor | undefined; + color?: AvatarColor; + colorId?: AvatarNamedColor | undefined; static colors: ("anchor" | "dark-red" | "cranberry" | "red" | "pumpkin" | "peach" | "marigold" | "gold" | "brass" | "brown" | "forest" | "seafoam" | "dark-green" | "light-teal" | "teal" | "steel" | "blue" | "royal-blue" | "cornflower" | "navy" | "lavender" | "purple" | "grape" | "lilac" | "pink" | "magenta" | "plum" | "beige" | "mink" | "platinum")[]; // @internal generateColor(): AvatarColor | void; // @internal generateInitials(): string | void; - initials: string | undefined; - name: string | undefined; - shape: AvatarShape | undefined; - size: AvatarSize | undefined; + initials?: string | undefined; + name?: string | undefined; + shape?: AvatarShape | undefined; + size?: AvatarSize | undefined; } // @public @@ -1313,6 +1317,50 @@ export const curveEasyEaseMax: CSSDesignToken; // @public (undocumented) export const curveLinear: CSSDesignToken; +// @public +export const definition: FASTElementDefinition; + +// @public +export class Divider extends FASTDivider { + alignContent?: DividerAlignContent; + appearance?: DividerAppearance; + inset?: boolean; +} + +// @public +export const DividerAlignContent: { + readonly center: "center"; + readonly start: "start"; + readonly end: "end"; +}; + +// @public +export type DividerAlignContent = ValuesOf; + +// @public +export const DividerAppearance: { + readonly strong: "strong"; + readonly brand: "brand"; + readonly subtle: "subtle"; + readonly default: "default"; +}; + +// @public +export type DividerAppearance = ValuesOf; + +// @public +export const DividerDefinition: FASTElementDefinition; + +export { DividerOrientation } + +export { DividerRole } + +// @public +export const DividerStyles: ElementStyles; + +// @public +export const DividerTemplate: ElementViewTemplate; + // @public (undocumented) export const durationFast: CSSDesignToken; @@ -1385,6 +1433,47 @@ export const fontWeightRegular: CSSDesignToken; // @public (undocumented) export const fontWeightSemibold: CSSDesignToken; +// @public +class Image_2 extends FASTElement { + block?: boolean; + bordered?: boolean; + fit?: ImageFit; + shadow?: boolean; + shape?: ImageShape; +} +export { Image_2 as Image } + +// @public +export const ImageDefinition: FASTElementDefinition; + +// @public +export const ImageFit: { + readonly none: "none"; + readonly center: "center"; + readonly contain: "contain"; + readonly cover: "cover"; + readonly default: "default"; +}; + +// @public +export type ImageFit = ValuesOf; + +// @public +export const ImageShape: { + readonly circular: "circular"; + readonly rounded: "rounded"; + readonly square: "square"; +}; + +// @public (undocumented) +export type ImageShape = ValuesOf; + +// @public +export const ImageStyles: ElementStyles; + +// @public +export const ImageTemplate: ElementViewTemplate; + // @public (undocumented) export const lineHeightBase100: CSSDesignToken; @@ -1615,6 +1704,27 @@ export const strokeWidthThickest: CSSDesignToken; // @public (undocumented) export const strokeWidthThin: CSSDesignToken; +// @public (undocumented) +export class Switch extends FASTSwitch { + labelPosition: SwitchLabelPosition | undefined; +} + +// @public +export const SwitchLabelPosition: { + readonly above: "above"; + readonly after: "after"; + readonly before: "before"; +}; + +// @public +export type SwitchLabelPosition = ValuesOf; + +// @public (undocumented) +export const switchStyles: ElementStyles; + +// @public (undocumented) +export const switchTemplate: ElementViewTemplate; + // @public class Text_2 extends FASTElement { align: TextAlign;