diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c8bbf30e..1ce3fb2495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Remove `animation` prop from all components @joschect ([#2239](https://github.com/microsoft/fluent-ui-react/pull/2239)) - `mode` property from `focusZone` configuration in accessibility behaviors is no longer supported - the focus zone will always be in embed mode @layershifter ([#2265](https://github.com/microsoft/fluent-ui-react/pull/2265)) - `FocusZoneMode` and `FOCUSZONE_WRAP_ATTRIBUTE` are no longer exported @layershifter ([#2265](https://github.com/microsoft/fluent-ui-react/pull/2265)) +- Changed `avatarBorderWidth` and `statusBorderWidth` avatar variables types from number to string and updated styles in Teams theme @mnajdova ([#2238](https://github.com/microsoft/fluent-ui-react/pull/2238)) ### Fixes - Fix event listener leak in `FocusZone` @miroslavstastny ([#2227](https://github.com/microsoft/fluent-ui-react/pull/2227)) diff --git a/packages/accessibility/src/behaviors/Image/imageBehavior.ts b/packages/accessibility/src/behaviors/Image/imageBehavior.ts index 00bdc141f1..ec03e2548a 100644 --- a/packages/accessibility/src/behaviors/Image/imageBehavior.ts +++ b/packages/accessibility/src/behaviors/Image/imageBehavior.ts @@ -18,7 +18,7 @@ const imageBehavior: Accessibility = props => ({ export default imageBehavior -type ImageBehaviorProps = { +export type ImageBehaviorProps = { /** Alternative text. */ alt?: string } & Pick diff --git a/packages/accessibility/src/behaviors/index.ts b/packages/accessibility/src/behaviors/index.ts index a8584fe8dc..fb2a0b1dca 100644 --- a/packages/accessibility/src/behaviors/index.ts +++ b/packages/accessibility/src/behaviors/index.ts @@ -3,6 +3,7 @@ export { default as alertWarningBehavior } from './Alert/alertWarningBehavior' export { default as attachmentBehavior } from './Attachment/attachmentBehavior' export { default as buttonBehavior } from './Button/buttonBehavior' export { default as toggleButtonBehavior } from './Button/toggleButtonBehavior' +export * from './Image/imageBehavior' export { default as imageBehavior } from './Image/imageBehavior' export { default as menuBehavior } from './Menu/menuBehavior' export { default as menuItemBehavior } from './Menu/menuItemBehavior' diff --git a/packages/react-proptypes/src/index.ts b/packages/react-proptypes/src/index.ts index 8bc39cc8c2..714b9df0e4 100644 --- a/packages/react-proptypes/src/index.ts +++ b/packages/react-proptypes/src/index.ts @@ -476,17 +476,11 @@ export const deprecate = (help: string, validator?: Function) => ( return error } -export const accessibility = PropTypes.oneOfType([PropTypes.func, PropTypes.object]) - -export const size = PropTypes.oneOf([ - 'smallest', - 'smaller', - 'small', - 'medium', - 'large', - 'larger', - 'largest', -]) +export const accessibility = PropTypes.func + +export const size = PropTypes.oneOf< + 'smallest' | 'smaller' | 'small' | 'medium' | 'large' | 'larger' | 'largest' +>(['smallest', 'smaller', 'small', 'medium', 'large', 'larger', 'largest']) export const align = PropTypes.oneOf(['start', 'end', 'center', 'justify']) diff --git a/packages/react/src/components/Avatar/Avatar.tsx b/packages/react/src/components/Avatar/Avatar.tsx index ec13bca42d..bf50e2cc18 100644 --- a/packages/react/src/components/Avatar/Avatar.tsx +++ b/packages/react/src/components/Avatar/Avatar.tsx @@ -1,25 +1,34 @@ import { Accessibility } from '@fluentui/accessibility' +import { + getElementType, + getUnhandledProps, + useAccessibility, + useStyles, + useTelemetry, +} from '@fluentui/react-bindings' import * as customPropTypes from '@fluentui/react-proptypes' import * as PropTypes from 'prop-types' import * as React from 'react' +// @ts-ignore +import { ThemeContext } from 'react-fela' + import Image, { ImageProps } from '../Image/Image' import Label, { LabelProps } from '../Label/Label' import Status, { StatusProps } from '../Status/Status' -import { WithAsProp, ShorthandValue, withSafeTypeForAs } from '../../types' import { - createShorthandFactory, - UIComponent, - UIComponentProps, - commonPropTypes, - SizeValue, - ShorthandFactory, -} from '../../utils' + WithAsProp, + ShorthandValue, + withSafeTypeForAs, + FluentComponentStaticProps, + ProviderContextPrepared, +} from '../../types' +import { createShorthandFactory, UIComponentProps, commonPropTypes, SizeValue } from '../../utils' export interface AvatarProps extends UIComponentProps { /** * Accessibility behavior if overridden by the user. */ - accessibility?: Accessibility + accessibility?: Accessibility /** Shorthand for the image. */ image?: ShorthandValue @@ -40,87 +49,121 @@ export interface AvatarProps extends UIComponentProps { getInitials?: (name: string) => string } -class Avatar extends UIComponent, any> { - static create: ShorthandFactory - - static className = 'ui-avatar' +const Avatar: React.FC> & + FluentComponentStaticProps = props => { + const context: ProviderContextPrepared = React.useContext(ThemeContext) + const { setStart, setEnd } = useTelemetry(Avatar.displayName, context.telemetry) + setStart() + + const { + accessibility, + className, + design, + getInitials, + label, + image, + name, + size, + status, + styles, + variables, + } = props + + const getA11Props = useAccessibility(accessibility, { + debugName: Avatar.displayName, + rtl: context.rtl, + }) + const { classes, styles: resolvedStyles } = useStyles(Avatar.displayName, { + className: Avatar.className, + mapPropsToStyles: () => ({ size }), + mapPropsToInlineStyles: () => ({ + className, + design, + styles, + variables, + }), + }) - static displayName = 'Avatar' + const ElementType = getElementType(props) + const unhandledProps = getUnhandledProps(Avatar.handledProps, props) - static propTypes = { - ...commonPropTypes.createCommon({ - children: false, - content: false, - }), - name: PropTypes.string, - image: customPropTypes.itemShorthandWithoutJSX, - label: customPropTypes.itemShorthand, - size: customPropTypes.size, - status: customPropTypes.itemShorthand, - getInitials: PropTypes.func, - } - - static defaultProps = { - size: 'medium', - getInitials(name: string) { - if (!name) { - return '' - } - - const reducedName = name - .replace(/\s*\(.*?\)\s*/g, ' ') - .replace(/\s*{.*?}\s*/g, ' ') - .replace(/\s*\[.*?]\s*/g, ' ') - - const initials = reducedName - .split(' ') - .filter(item => item !== '') - .map(item => item.charAt(0)) - .reduce((accumulator, currentValue) => accumulator + currentValue) - - if (initials.length > 2) { - return initials.charAt(0) + initials.charAt(initials.length - 1) - } - return initials - }, - } as AvatarProps - - renderComponent({ accessibility, ElementType, classes, unhandledProps, styles, variables }) { - const { name, status, image, label, getInitials, size } = this.props as AvatarProps - - return ( - - {Image.create(image, { - defaultProps: () => ({ + const result = ( + + {Image.create(image, { + defaultProps: () => + getA11Props('image', { fluid: true, avatar: true, title: name, - styles: styles.image, + styles: resolvedStyles.image, }), - })} - {!image && - Label.create(label || {}, { - defaultProps: () => ({ + })} + {!image && + Label.create(label || {}, { + defaultProps: () => + getA11Props('label', { content: getInitials(name), circular: true, title: name, - styles: styles.label, + styles: resolvedStyles.label, }), - })} - {Status.create(status, { - defaultProps: () => ({ + })} + {Status.create(status, { + defaultProps: () => + getA11Props('status', { size, - styles: styles.status, - variables: { - borderColor: variables.statusBorderColor, - borderWidth: variables.statusBorderWidth, - }, + styles: resolvedStyles.status, }), - })} - - ) - } + })} + + ) + + setEnd() + + return result +} + +Avatar.className = 'ui-avatar' +Avatar.displayName = 'Avatar' + +Avatar.defaultProps = { + size: 'medium', + getInitials(name: string) { + if (!name) { + return '' + } + + const reducedName = name + .replace(/\s*\(.*?\)\s*/g, ' ') + .replace(/\s*{.*?}\s*/g, ' ') + .replace(/\s*\[.*?]\s*/g, ' ') + + const initials = reducedName + .split(' ') + .filter(item => item !== '') + .map(item => item.charAt(0)) + .reduce((accumulator, currentValue) => accumulator + currentValue) + + if (initials.length > 2) { + return initials.charAt(0) + initials.charAt(initials.length - 1) + } + return initials + }, +} + +Avatar.propTypes = { + ...commonPropTypes.createCommon({ + children: false, + content: false, + }), + name: PropTypes.string, + image: customPropTypes.itemShorthandWithoutJSX, + label: customPropTypes.itemShorthand, + size: customPropTypes.size, + status: customPropTypes.itemShorthand, + getInitials: PropTypes.func, } +Avatar.handledProps = Object.keys(Avatar.propTypes) as any Avatar.create = createShorthandFactory({ Component: Avatar, mappedProp: 'name' }) diff --git a/packages/react/src/components/Box/Box.tsx b/packages/react/src/components/Box/Box.tsx index 536b5bf898..a4b9c8c33b 100644 --- a/packages/react/src/components/Box/Box.tsx +++ b/packages/react/src/components/Box/Box.tsx @@ -1,45 +1,75 @@ +import { + getElementType, + getUnhandledProps, + useStyles, + useTelemetry, +} from '@fluentui/react-bindings' import * as React from 'react' +// @ts-ignore +import { ThemeContext } from 'react-fela' + import { childrenExist, createShorthandFactory, - UIComponentProps, ContentComponentProps, ChildrenComponentProps, commonPropTypes, rtlTextContainer, + UIComponentProps, } from '../../utils' -import createComponentInternal from '../../utils/createComponentInternal' -import { WithAsProp, withSafeTypeForAs } from '../../types' +import { + ProviderContextPrepared, + WithAsProp, + withSafeTypeForAs, + FluentComponentStaticProps, +} from '../../types' export interface BoxProps extends UIComponentProps, ContentComponentProps, ChildrenComponentProps {} -const Box = createComponentInternal>({ - displayName: 'Box', - - className: 'ui-box', - - propTypes: { - ...commonPropTypes.createCommon(), - }, - - render(config, props) { - const { ElementType, classes, unhandledProps } = config - const { children, content } = props - - return ( - - {childrenExist(children) ? children : content} - - ) - }, -}) +const Box: React.FC> & FluentComponentStaticProps = props => { + const context: ProviderContextPrepared = React.useContext(ThemeContext) + const { setStart, setEnd } = useTelemetry(Box.displayName, context.telemetry) + setStart() + + const { className, design, styles, variables, children, content } = props + + const { classes } = useStyles(Box.displayName, { + className: Box.className, + mapPropsToInlineStyles: () => ({ + className, + design, + styles, + variables, + }), + rtl: context.rtl, + }) + + const unhandledProps = getUnhandledProps(Box.handledProps, props) + const ElementType = getElementType(props) + + const result = ( + + {childrenExist(children) ? children : content} + + ) + + setEnd() + + return result +} + +Box.className = 'ui-box' +Box.displayName = 'Box' + +Box.propTypes = commonPropTypes.createCommon({ accessibility: false }) +Box.handledProps = Object.keys(Box.propTypes) as any Box.create = createShorthandFactory({ Component: Box }) diff --git a/packages/react/src/components/Button/Button.tsx b/packages/react/src/components/Button/Button.tsx index d1633e3c05..8278b95c4d 100644 --- a/packages/react/src/components/Button/Button.tsx +++ b/packages/react/src/components/Button/Button.tsx @@ -5,7 +5,6 @@ import * as React from 'react' import * as _ from 'lodash' import { - UIComponent, childrenExist, createShorthandFactory, UIComponentProps, @@ -13,15 +12,29 @@ import { ChildrenComponentProps, commonPropTypes, rtlTextContainer, - applyAccessibilityKeyHandlers, SizeValue, - ShorthandFactory, } from '../../utils' import Icon, { IconProps } from '../Icon/Icon' import Box, { BoxProps } from '../Box/Box' import Loader, { LoaderProps } from '../Loader/Loader' -import { ComponentEventHandler, WithAsProp, ShorthandValue, withSafeTypeForAs } from '../../types' +import { + ComponentEventHandler, + WithAsProp, + ShorthandValue, + withSafeTypeForAs, + FluentComponentStaticProps, + ProviderContextPrepared, +} from '../../types' import ButtonGroup from './ButtonGroup' +import { + getElementType, + getUnhandledProps, + useAccessibility, + useStyles, + useTelemetry, +} from '@fluentui/react-bindings' +// @ts-ignore +import { ThemeContext } from 'react-fela' export interface ButtonProps extends UIComponentProps, @@ -84,120 +97,179 @@ export interface ButtonProps size?: SizeValue } -class Button extends UIComponent> { - static create: ShorthandFactory - - static displayName = 'Button' - - static className = 'ui-button' +const Button: React.FC> & + FluentComponentStaticProps & { Group: typeof ButtonGroup } = props => { + const context: ProviderContextPrepared = React.useContext(ThemeContext) + const { setStart, setEnd } = useTelemetry(Button.displayName, context.telemetry) + setStart() - static propTypes = { - ...commonPropTypes.createCommon({ - content: 'shorthand', - }), - circular: PropTypes.bool, - disabled: PropTypes.bool, - fluid: PropTypes.bool, - icon: customPropTypes.itemShorthandWithoutJSX, - iconOnly: PropTypes.bool, - iconPosition: PropTypes.oneOf(['before', 'after']), - loader: customPropTypes.itemShorthandWithoutJSX, - loading: PropTypes.bool, - onClick: PropTypes.func, - onFocus: PropTypes.func, - primary: customPropTypes.every([customPropTypes.disallow(['secondary']), PropTypes.bool]), - text: PropTypes.bool, - secondary: customPropTypes.every([customPropTypes.disallow(['primary']), PropTypes.bool]), - size: customPropTypes.size, - } - - static defaultProps = { - as: 'button', - accessibility: buttonBehavior as Accessibility, - size: 'medium', - } - - static Group = ButtonGroup - - actionHandlers = { - performClick: event => { - event.preventDefault() - this.handleClick(event) - }, - } - - renderComponent({ - ElementType, - classes, + const { accessibility, - variables, + // @ts-ignore + active, + as, + children, + content, + icon, + loader, + disabled, + iconPosition, + loading, + text, + primary, + inverted, + size, + iconOnly, + fluid, + circular, + className, styles, - unhandledProps, - }): React.ReactNode { - const { children, content, disabled, iconPosition, loading } = this.props - const hasChildren = childrenExist(children) - - return ( - - {hasChildren && children} - {!hasChildren && loading && this.renderLoader(variables, styles)} - {!hasChildren && iconPosition !== 'after' && this.renderIcon(variables, styles)} - {Box.create(!hasChildren && content, { - defaultProps: () => ({ as: 'span', styles: styles.content }), - })} - {!hasChildren && iconPosition === 'after' && this.renderIcon(variables, styles)} - - ) - } + variables, + design, + } = props + + const hasChildren = childrenExist(children) + + const getA11Props = useAccessibility(accessibility, { + debugName: Button.displayName, + mapPropsToBehavior: () => ({ + as, + active, + disabled, + loading, + }), + actionHandlers: { + performClick: event => { + event.preventDefault() + handleClick(event) + }, + }, + rtl: context.rtl, + }) + const { classes, styles: resolvedStyles } = useStyles(Button.displayName, { + className: Button.className, + mapPropsToStyles: () => ({ + text, + primary, + disabled, + circular, + size, + loading, + inverted, + iconOnly, + fluid, + hasContent: !!content, + }), + mapPropsToInlineStyles: () => ({ + className, + design, + styles, + variables, + }), + rtl: context.rtl, + }) - renderIcon = (variables, styles) => { - const { icon, iconPosition, content } = this.props + const unhandledProps = getUnhandledProps(Button.handledProps, props) + const ElementType = getElementType(props) + const renderIcon = () => { return Icon.create(icon, { - defaultProps: () => ({ - styles: styles.icon, - xSpacing: !content ? 'none' : iconPosition === 'after' ? 'before' : 'after', - variables: variables.icon, - }), + defaultProps: () => + getA11Props('icon', { + styles: resolvedStyles.icon, + xSpacing: !content ? 'none' : iconPosition === 'after' ? 'before' : 'after', + }), }) } - renderLoader = (variables, styles) => { - const { loader } = this.props - + const renderLoader = () => { return Loader.create(loader || {}, { - defaultProps: () => ({ - role: undefined, - styles: styles.loader, - }), + defaultProps: () => + getA11Props('loader', { + role: undefined, + styles: resolvedStyles.loader, + }), }) } - handleClick = (e: React.SyntheticEvent) => { - const { disabled } = this.props - + const handleClick = (e: React.SyntheticEvent) => { if (disabled) { e.preventDefault() return } - _.invoke(this.props, 'onClick', e, this.props) + _.invoke(props, 'onClick', e, props) } - handleFocus = (e: React.SyntheticEvent) => { - _.invoke(this.props, 'onFocus', e, this.props) + const handleFocus = (e: React.SyntheticEvent) => { + _.invoke(props, 'onFocus', e, props) } + + const result = ( + + {hasChildren ? ( + children + ) : ( + <> + {loading && renderLoader()} + {iconPosition !== 'after' && renderIcon()} + {Box.create(content, { + defaultProps: () => + getA11Props('content', { as: 'span', styles: resolvedStyles.content }), + })} + {iconPosition === 'after' && renderIcon()} + + )} + + ) + + setEnd() + + return result +} + +Button.defaultProps = { + as: 'button', + accessibility: buttonBehavior, + size: 'medium', } +Button.displayName = 'Button' +Button.className = 'ui-button' + +Button.propTypes = { + ...commonPropTypes.createCommon({ + content: 'shorthand', + }), + circular: PropTypes.bool, + disabled: PropTypes.bool, + fluid: PropTypes.bool, + icon: customPropTypes.itemShorthandWithoutJSX, + iconOnly: PropTypes.bool, + iconPosition: PropTypes.oneOf(['before', 'after']), + loader: customPropTypes.itemShorthandWithoutJSX, + loading: PropTypes.bool, + onClick: PropTypes.func, + onFocus: PropTypes.func, + primary: customPropTypes.every([customPropTypes.disallow(['secondary']), PropTypes.bool]), + text: PropTypes.bool, + secondary: customPropTypes.every([customPropTypes.disallow(['primary']), PropTypes.bool]), + size: customPropTypes.size, +} + +Button.handledProps = Object.keys(Button.propTypes) as any + +Button.Group = ButtonGroup + Button.create = createShorthandFactory({ Component: Button, mappedProp: 'content' }) /** diff --git a/packages/react/src/components/Image/Image.tsx b/packages/react/src/components/Image/Image.tsx index f9876c98ec..805bc7a780 100644 --- a/packages/react/src/components/Image/Image.tsx +++ b/packages/react/src/components/Image/Image.tsx @@ -1,19 +1,37 @@ -import { Accessibility, imageBehavior } from '@fluentui/accessibility' +import { + Accessibility, + AccessibilityAttributes, + imageBehavior, + ImageBehaviorProps, +} from '@fluentui/accessibility' +import { + getElementType, + getUnhandledProps, + useAccessibility, + useStyles, + useTelemetry, +} from '@fluentui/react-bindings' import * as PropTypes from 'prop-types' import * as React from 'react' +// @ts-ignore +import { ThemeContext } from 'react-fela' +import { createShorthandFactory, UIComponentProps, commonPropTypes } from '../../utils' import { - createShorthandFactory, - UIComponent, - UIComponentProps, - commonPropTypes, - ShorthandFactory, -} from '../../utils' -import { WithAsProp, withSafeTypeForAs } from '../../types' - -export interface ImageProps extends UIComponentProps { + FluentComponentStaticProps, + ProviderContextPrepared, + WithAsProp, + withSafeTypeForAs, +} from '../../types' + +export interface ImageProps extends UIComponentProps, ImageBehaviorProps { + /** Alternative text. */ + alt?: string + + 'aria-label'?: AccessibilityAttributes['aria-label'] + /** Accessibility behavior if overridden by the user. */ - accessibility?: Accessibility + accessibility?: Accessibility /** An image may be formatted to appear inline with text as an avatar. */ avatar?: boolean @@ -28,39 +46,79 @@ export interface ImageProps extends UIComponentProps { src?: string } -class Image extends UIComponent, any> { - static create: ShorthandFactory +const Image: React.FC> & FluentComponentStaticProps = props => { + const context: ProviderContextPrepared = React.useContext(ThemeContext) + const { setStart, setEnd } = useTelemetry(Image.displayName, context.telemetry) + setStart() - static className = 'ui-image' + const { + accessibility, + alt, + 'aria-label': ariaLabel, + avatar, + circular, + className, + design, + fluid, + styles, + variables, + } = props - static displayName = 'Image' - - static propTypes = { - ...commonPropTypes.createCommon({ - children: false, - content: false, + const getA11Props = useAccessibility(accessibility, { + debugName: Image.displayName, + mapPropsToBehavior: () => ({ + alt, + 'aria-label': ariaLabel, + }), + rtl: context.rtl, + }) + const { classes } = useStyles(Image.displayName, { + className: Image.className, + mapPropsToStyles: () => ({ + avatar, + circular, + fluid, }), - avatar: PropTypes.bool, - circular: PropTypes.bool, - fluid: PropTypes.bool, - } - - static defaultProps = { - as: 'img', - accessibility: imageBehavior as Accessibility, - } - - renderComponent({ ElementType, classes, accessibility, unhandledProps }) { - return ( - - ) - } + mapPropsToInlineStyles: () => ({ + className, + design, + styles, + variables, + }), + rtl: context.rtl, + }) + + const ElementType = getElementType(props) + const unhandledProps = getUnhandledProps(Image.handledProps, props) + + const result = ( + + ) + + setEnd() + + return result +} + +Image.className = 'ui-image' +Image.displayName = 'Image' +Image.defaultProps = { + as: 'img', + accessibility: imageBehavior, +} + +Image.propTypes = { + ...commonPropTypes.createCommon({ + children: false, + content: false, + }), + avatar: PropTypes.bool, + circular: PropTypes.bool, + fluid: PropTypes.bool, } +Image.handledProps = Object.keys(Image.propTypes) as any + Image.create = createShorthandFactory({ Component: Image, mappedProp: 'src', allowsJSX: false }) /** diff --git a/packages/react/src/themes/teams-high-contrast/components/Avatar/avatarVariables.ts b/packages/react/src/themes/teams-high-contrast/components/Avatar/avatarVariables.ts index 08cf6cc6b9..047326ca10 100644 --- a/packages/react/src/themes/teams-high-contrast/components/Avatar/avatarVariables.ts +++ b/packages/react/src/themes/teams-high-contrast/components/Avatar/avatarVariables.ts @@ -2,6 +2,6 @@ import { AvatarVariables } from '../../../teams/components/Avatar/avatarVariable export default (siteVariables: any): Partial => ({ avatarBorderColor: siteVariables.colors.white, - avatarBorderWidth: 2, + avatarBorderWidth: '2px', statusBorderColor: siteVariables.colors.black, }) diff --git a/packages/react/src/themes/teams-high-contrast/components/Button/buttonStyles.ts b/packages/react/src/themes/teams-high-contrast/components/Button/buttonStyles.ts index 5615392659..e362fe06a1 100644 --- a/packages/react/src/themes/teams-high-contrast/components/Button/buttonStyles.ts +++ b/packages/react/src/themes/teams-high-contrast/components/Button/buttonStyles.ts @@ -1,13 +1,13 @@ import { ComponentSlotStylesPrepared, ICSSInJSStyle } from '@fluentui/styles' -import { ButtonProps } from '../../../../components/Button/Button' +import { ButtonStylesProps } from '../../../teams/components/Button/buttonStyles' import { ButtonVariables } from '../../../teams/components/Button/buttonVariables' import { ButtonHighContrastVariables } from './buttonVariables' const buttonStyles: ComponentSlotStylesPrepared< - ButtonProps, + ButtonStylesProps, ButtonVariables & ButtonHighContrastVariables > = { - root: ({ props: p, variables: v, theme: { siteVariables } }): ICSSInJSStyle => { + root: ({ props: p, variables: v }): ICSSInJSStyle => { return { // rectangular button defaults ...(!p.text && { diff --git a/packages/react/src/themes/teams/components/Avatar/avatarStyles.ts b/packages/react/src/themes/teams/components/Avatar/avatarStyles.ts index 94cea677c9..cd88754beb 100644 --- a/packages/react/src/themes/teams/components/Avatar/avatarStyles.ts +++ b/packages/react/src/themes/teams/components/Avatar/avatarStyles.ts @@ -1,6 +1,9 @@ import { pxToRem } from '../../../../utils' import { ComponentSlotStylesPrepared, ICSSInJSStyle } from '@fluentui/styles' import { AvatarProps } from '../../../../components/Avatar/Avatar' +import { AvatarVariables } from './avatarVariables' + +export type AvatarStylesProps = Pick const sizeToPxValue = { smallest: 24, @@ -12,7 +15,7 @@ const sizeToPxValue = { largest: 48, } -const avatarStyles: ComponentSlotStylesPrepared = { +const avatarStyles: ComponentSlotStylesPrepared = { root: ({ props: { size } }): ICSSInJSStyle => { const sizeInRem = pxToRem(sizeToPxValue[size]) @@ -28,7 +31,7 @@ const avatarStyles: ComponentSlotStylesPrepared = { image: ({ variables: v }): ICSSInJSStyle => ({ borderColor: v.avatarBorderColor, borderStyle: 'solid', - borderWidth: `${v.avatarBorderWidth}px`, + borderWidth: v.avatarBorderWidth, height: '100%', objectFit: 'cover', @@ -50,8 +53,9 @@ const avatarStyles: ComponentSlotStylesPrepared = { }, status: ({ variables: v }): ICSSInJSStyle => ({ position: 'absolute', - bottom: `-${v.statusBorderWidth}px`, - right: `-${v.statusBorderWidth}px`, + bottom: 0, + right: 0, + boxShadow: `0 0 0 ${v.statusBorderWidth} ${v.statusBorderColor}`, }), } diff --git a/packages/react/src/themes/teams/components/Avatar/avatarVariables.ts b/packages/react/src/themes/teams/components/Avatar/avatarVariables.ts index f952d65e12..0bdaa8c0bd 100644 --- a/packages/react/src/themes/teams/components/Avatar/avatarVariables.ts +++ b/packages/react/src/themes/teams/components/Avatar/avatarVariables.ts @@ -1,13 +1,13 @@ export interface AvatarVariables { avatarBorderColor: string - avatarBorderWidth: number + avatarBorderWidth: string statusBorderColor: string - statusBorderWidth: number + statusBorderWidth: string } export default (siteVariables): AvatarVariables => ({ avatarBorderColor: '', - avatarBorderWidth: 0, + avatarBorderWidth: '0', statusBorderColor: siteVariables.bodyBackground, - statusBorderWidth: 2, + statusBorderWidth: '2px', }) diff --git a/packages/react/src/themes/teams/components/Button/buttonStyles.ts b/packages/react/src/themes/teams/components/Button/buttonStyles.ts index 205a72c6a7..b87518eccf 100644 --- a/packages/react/src/themes/teams/components/Button/buttonStyles.ts +++ b/packages/react/src/themes/teams/components/Button/buttonStyles.ts @@ -8,7 +8,22 @@ import { ButtonVariables } from './buttonVariables' import getBorderFocusStyles from '../../getBorderFocusStyles' import getIconFillOrOutlineStyles from '../../getIconFillOrOutlineStyles' -const buttonStyles: ComponentSlotStylesPrepared = { +export type ButtonStylesProps = Pick< + ButtonProps, + | 'text' + | 'primary' + | 'disabled' + | 'circular' + | 'size' + | 'loading' + | 'inverted' + | 'iconOnly' + | 'fluid' +> & { + hasContent?: boolean +} + +const buttonStyles: ComponentSlotStylesPrepared = { root: ({ props: p, variables: v, theme }): ICSSInJSStyle => { const { siteVariables } = theme const { borderWidth } = siteVariables @@ -236,7 +251,7 @@ const buttonStyles: ComponentSlotStylesPrepared = }), }), - icon: ({ props: p, variables: v }) => ({ + icon: ({ props: p }) => ({ // when loading, hide the icon ...(p.loading && { margin: 0, @@ -265,7 +280,7 @@ const buttonStyles: ComponentSlotStylesPrepared = }, }, - ...(p.content && { + ...(p.hasContent && { marginRight: pxToRem(4), }), }), diff --git a/packages/react/src/themes/teams/components/Image/imageStyles.ts b/packages/react/src/themes/teams/components/Image/imageStyles.ts index 01b1ed3a8a..f23037f521 100644 --- a/packages/react/src/themes/teams/components/Image/imageStyles.ts +++ b/packages/react/src/themes/teams/components/Image/imageStyles.ts @@ -1,7 +1,10 @@ import { ComponentSlotStylesPrepared, ICSSInJSStyle } from '@fluentui/styles' import { ImageProps } from '../../../../components/Image/Image' +import { ImageVariables } from './imageVariables' -export default { +export type ImageStylesProps = Pick + +const imageStyles: ComponentSlotStylesPrepared = { root: ({ props, variables }): ICSSInJSStyle => ({ boxSizing: 'border-box', display: 'inline-block', @@ -14,4 +17,6 @@ export default { borderRadius: variables.avatarRadius, }), }), -} as ComponentSlotStylesPrepared +} + +export default imageStyles diff --git a/packages/react/src/themes/teams/components/Image/imageVariables.ts b/packages/react/src/themes/teams/components/Image/imageVariables.ts index 580023877d..e967a17799 100644 --- a/packages/react/src/themes/teams/components/Image/imageVariables.ts +++ b/packages/react/src/themes/teams/components/Image/imageVariables.ts @@ -1,6 +1,14 @@ import { pxToRem } from '../../../../utils' -export default () => ({ +export type ImageVariables = { + width?: string + height?: string + avatarRadius: string + avatarSize: string + circularRadius: string +} + +export default (): ImageVariables => ({ width: undefined, height: undefined, avatarRadius: pxToRem(9999), diff --git a/packages/react/src/themes/teams/types.ts b/packages/react/src/themes/teams/types.ts index f3f20fa2f3..a93507b1db 100644 --- a/packages/react/src/themes/teams/types.ts +++ b/packages/react/src/themes/teams/types.ts @@ -13,9 +13,9 @@ import { AccordionTitleProps } from '../../components/Accordion/AccordionTitle' import { AlertProps } from '../../components/Alert/Alert' import { AnimationProps } from '../../components/Animation/Animation' import { AttachmentProps } from '../../components/Attachment/Attachment' -import { AvatarProps } from '../../components/Avatar/Avatar' +import { AvatarStylesProps } from './components/Avatar/avatarStyles' import { ButtonGroupProps } from '../../components/Button/ButtonGroup' -import { ButtonProps } from '../../components/Button/Button' +import { ButtonStylesProps } from './components/Button/buttonStyles' import { ChatItemProps } from '../../components/Chat/ChatItem' import { ChatMessageProps } from '../../components/Chat/ChatMessage' import { ChatProps } from '../../components/Chat/Chat' @@ -31,7 +31,7 @@ import { GridProps } from '../../components/Grid/Grid' import { HeaderDescriptionProps } from '../../components/Header/HeaderDescription' import { HeaderProps } from '../../components/Header/Header' import { IconProps } from '../../components/Icon/Icon' -import { ImageProps } from '../../components/Image/Image' +import { ImageStylesProps } from './components/Image/imageStyles' import { InputProps } from '../../components/Input/Input' import { ItemLayoutProps } from '../../components/ItemLayout/ItemLayout' import { LabelProps } from '../../components/Label/Label' @@ -70,8 +70,8 @@ export type TeamsThemeStylesProps = { Alert?: AlertProps Animation?: AnimationProps Attachment?: AttachmentProps - Avatar?: AvatarProps - Button?: ButtonProps + Avatar?: AvatarStylesProps + Button?: ButtonStylesProps ButtonGroup?: ButtonGroupProps Chat?: ChatProps ChatItem?: ChatItemProps @@ -88,7 +88,7 @@ export type TeamsThemeStylesProps = { Header?: HeaderProps HeaderDescription?: HeaderDescriptionProps Icon?: IconProps - Image?: ImageProps + Image?: ImageStylesProps Input?: InputProps ItemLayout?: ItemLayoutProps Label?: LabelProps diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index d625631182..576f58259d 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -1,6 +1,8 @@ import { StylesContextInputValue, StylesContextValue, Telemetry } from '@fluentui/react-bindings' import * as React from 'react' +import { ShorthandFactory } from './utils/factories' + // ======================================================== // Utilities // ======================================================== @@ -11,6 +13,16 @@ export type ObjectOf = { [key: string]: T } export type Omit = Pick> +// ======================================================== +// Components +// ======================================================== + +export type FluentComponentStaticProps

= { + className: string + handledProps: (keyof P)[] + create: ShorthandFactory

+} + // ======================================================== // Props // ======================================================== diff --git a/packages/react/test/specs/commonTests/handlesAccessibility.tsx b/packages/react/test/specs/commonTests/handlesAccessibility.tsx index 3ab60058a9..6ac1ce9707 100644 --- a/packages/react/test/specs/commonTests/handlesAccessibility.tsx +++ b/packages/react/test/specs/commonTests/handlesAccessibility.tsx @@ -124,19 +124,23 @@ export default ( const wrapper = mountWithProvider() const component = wrapper.find(Component) const instance = component.instance() as UIComponent - if (instance.actionHandlers) { - instance.actionHandlers.mockAction = actionHandler + + if (instance) { + if (instance.actionHandlers) { + instance.actionHandlers.mockAction = actionHandler + } + // Force render component to apply updated key handlers + wrapper.setProps({}) } - // Force render component to apply updated key handlers - wrapper.setProps({}) getEventTargetComponent(component, 'onKeyDown').simulate('keydown', { keyCode: keyboardKey.Enter, }) - if (instance.actionHandlers) { + if (instance && instance.actionHandlers) { expect(actionHandler).toBeCalledTimes(1) } + expect(eventHandler).toBeCalledTimes(1) }) } diff --git a/packages/react/test/specs/commonTests/isConformant.tsx b/packages/react/test/specs/commonTests/isConformant.tsx index 917e9ec44c..7738bdcf98 100644 --- a/packages/react/test/specs/commonTests/isConformant.tsx +++ b/packages/react/test/specs/commonTests/isConformant.tsx @@ -21,6 +21,7 @@ import * as FluentUI from 'src/index' import { getEventTargetComponent, EVENT_TARGET_ATTRIBUTE } from './eventTarget' export interface Conformant { + constructorName?: string /** Map of events and the child component to target. */ eventTargets?: object hasAccessibilityProp?: boolean @@ -47,6 +48,7 @@ export default function isConformant( options: Conformant = {}, ) { const { + constructorName = Component.prototype.constructor.name, eventTargets = {}, exportedAtTopLevel = true, hasAccessibilityProp = true, @@ -93,7 +95,6 @@ export default function isConformant( } // tests depend on Component constructor names, enforce them - const constructorName = Component.prototype.constructor.name if (!constructorName) { throwError( [ @@ -299,7 +300,7 @@ export default function isConformant( expect({ message, - handledProps: Component.handledProps, + handledProps: Component.handledProps.sort(), }).toEqual({ message, handledProps: expectedProps, @@ -454,6 +455,8 @@ export default function isConformant( // Handles className // ---------------------------------------- describe('static className (common)', () => { + const componentClassName = + info.componentClassName || `ui-${Component.displayName}`.toLowerCase() const getClassesOfRootElement = component => { const classes = component .find('[className]') @@ -463,8 +466,8 @@ export default function isConformant( return classes } - test(`is a static equal to "${info.componentClassName}"`, () => { - expect(Component.className).toEqual(info.componentClassName) + test(`is a static equal to "${componentClassName}"`, () => { + expect(Component.className).toEqual(componentClassName) }) test(`is applied to the root element`, () => { @@ -472,9 +475,7 @@ export default function isConformant( // only test components that implement className if (component.find('[className]').hostNodes().length > 0) { - expect( - _.includes(getClassesOfRootElement(component), `${info.componentClassName}`), - ).toEqual(true) + expect(_.includes(getClassesOfRootElement(component), componentClassName)).toEqual(true) } }) @@ -537,7 +538,7 @@ export default function isConformant( // ---------------------------------------- describe('static displayName (common)', () => { test('matches constructor name', () => { - expect(Component.displayName).toEqual(info.constructorName) + expect(Component.displayName).toEqual(constructorName) }) }) diff --git a/packages/react/test/specs/components/Avatar/Avatar-test.tsx b/packages/react/test/specs/components/Avatar/Avatar-test.tsx index 2a38004fea..e9d9f49e40 100644 --- a/packages/react/test/specs/components/Avatar/Avatar-test.tsx +++ b/packages/react/test/specs/components/Avatar/Avatar-test.tsx @@ -8,7 +8,9 @@ const avatarImplementsShorthandProp = implementsShorthandProp(Avatar) const { getInitials } = (Avatar as any).defaultProps describe('Avatar', () => { - isConformant(Avatar) + isConformant(Avatar, { + constructorName: 'Avatar', + }) avatarImplementsShorthandProp('label', Label) avatarImplementsShorthandProp('image', Image, { mapsValueToProp: 'src' }) diff --git a/packages/react/test/specs/components/Button/Button-test.tsx b/packages/react/test/specs/components/Button/Button-test.tsx index 2d2809acc9..aa0ea7686b 100644 --- a/packages/react/test/specs/components/Button/Button-test.tsx +++ b/packages/react/test/specs/components/Button/Button-test.tsx @@ -16,7 +16,9 @@ import Icon from 'src/components/Icon/Icon' const buttonImplementsShorthandProp = implementsShorthandProp(Button) describe('Button', () => { - isConformant(Button) + isConformant(Button, { + constructorName: 'Button', + }) buttonImplementsShorthandProp('icon', Icon, { mapsValueToProp: 'name', requiredShorthandProps: { name: 'at' }, diff --git a/packages/react/test/specs/components/Image/Image-test.tsx b/packages/react/test/specs/components/Image/Image-test.tsx index 6d8c7cd8fc..ff7c14a120 100644 --- a/packages/react/test/specs/components/Image/Image-test.tsx +++ b/packages/react/test/specs/components/Image/Image-test.tsx @@ -5,7 +5,9 @@ import Image from 'src/components/Image/Image' import { mountWithProviderAndGetComponent } from 'test/utils' describe('Image', () => { - isConformant(Image) + isConformant(Image, { + constructorName: 'Image', + }) describe('accessibility', () => { handlesAccessibility(Image, { diff --git a/packages/react/test/utils/withProvider.tsx b/packages/react/test/utils/withProvider.tsx index 9e897f2d7a..03ce491b36 100644 --- a/packages/react/test/utils/withProvider.tsx +++ b/packages/react/test/utils/withProvider.tsx @@ -1,12 +1,24 @@ -import { ThemeInput } from '@fluentui/styles' +import { emptyTheme, ThemeInput } from '@fluentui/styles' import * as React from 'react' import { mount } from 'enzyme' import { ThemeProvider } from 'react-fela' + import { felaRenderer } from 'src/utils' +import { ProviderContextPrepared } from 'src/types' + +export const EmptyThemeProvider: React.FunctionComponent = ({ children }) => { + const theme: ProviderContextPrepared = { + renderer: felaRenderer, + target: document, + _internal_resolvedComponentVariables: {}, + disableAnimations: false, + rtl: false, + theme: emptyTheme, + telemetry: undefined, + } -export const EmptyThemeProvider: React.FunctionComponent = ({ children }) => ( - {children} -) + return {children} +} export const mountWithProvider = (node, options?, theme?: ThemeInput) => { return mount(node, {