Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

Commit

Permalink
chore(DropdownItem): refactor to functional component with hooks (#2382)
Browse files Browse the repository at this point in the history
* start component refactor

* add a11y props

* update styles

* add isConformant test

* add variables to dropdownVariables

* changelog
  • Loading branch information
silviuaavram authored Feb 25, 2020
1 parent 22af6f9 commit 9d4d87b
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 91 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Restricted prop sets in the `Chat`, `ChatItem`, `ChatMessage` components which are passed to styles functions @layershifter ([#2366](https://github.com/microsoft/fluent-ui-react/pull/2366))
- `sanitize-css` plugin is disabled for production mode by default @layershifter ([#2340](https://github.com/microsoft/fluent-ui-react/pull/2340))
- Standardise component onChange callback names and test them in `isConformant` @silviuavram ([#2293](https://github.com/microsoft/fluent-ui-react/pull/2293))
- Restricted prop set in the `DropdownItem` styles @silviuavram ([#2382](https://github.com/microsoft/fluent-ui-react/pull/2382))

### Fixes
- Remove dependency on Lodash in TypeScript typings @layershifter ([#2323](https://github.com/microsoft/fluent-ui-react/pull/2323))
Expand Down
230 changes: 143 additions & 87 deletions packages/react/src/components/Dropdown/DropdownItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@ import * as customPropTypes from '@fluentui/react-proptypes'
import * as React from 'react'
import * as PropTypes from 'prop-types'
import * as _ from 'lodash'

// @ts-ignore
import { ThemeContext } from 'react-fela'
import {
getElementType,
getUnhandledProps,
useStyles,
useTelemetry,
} from '@fluentui/react-bindings'
import cx from 'classnames'

import { createShorthandFactory, commonPropTypes } from '../../utils'
import {
UIComponent,
RenderResultConfig,
createShorthandFactory,
commonPropTypes,
ShorthandFactory,
} from '../../utils'
import { ShorthandValue, ComponentEventHandler, WithAsProp, withSafeTypeForAs } from '../../types'
ShorthandValue,
ComponentEventHandler,
WithAsProp,
withSafeTypeForAs,
FluentComponentStaticProps,
ProviderContextPrepared,
} from '../../types'
import { UIComponentProps } from '../../utils/commonPropInterfaces'
import ListItem from '../List/ListItem'
import Icon, { IconProps } from '../Icon/Icon'
import Image, { ImageProps } from '../Image/Image'
import Box, { BoxProps } from '../Box/Box'
Expand All @@ -22,6 +31,7 @@ export interface DropdownItemSlotClassNames {
header: string
image: string
checkableIndicator: string
main: string
}

export interface DropdownItemProps extends UIComponentProps<DropdownItemProps> {
Expand Down Expand Up @@ -61,93 +71,139 @@ export interface DropdownItemProps extends UIComponentProps<DropdownItemProps> {
selected?: boolean
}

class DropdownItem extends UIComponent<WithAsProp<DropdownItemProps>> {
static displayName = 'DropdownItem'

static create: ShorthandFactory<DropdownItemProps>

static className = 'ui-dropdown__item'

static slotClassNames: DropdownItemSlotClassNames

static propTypes = {
...commonPropTypes.createCommon({
accessibility: false,
children: false,
content: false,
const DropdownItem: React.FC<WithAsProp<DropdownItemProps> & { index: number }> &
FluentComponentStaticProps<DropdownItemProps> & {
slotClassNames: DropdownItemSlotClassNames
} = props => {
const context: ProviderContextPrepared = React.useContext(ThemeContext)
const { setStart, setEnd } = useTelemetry(DropdownItem.displayName, context.telemetry)

setStart()

const {
active,
accessibilityItemProps,
className,
content,
design,
header,
image,
isFromKeyboard,
styles,
checkable,
checkableIndicator,
selected,
variables,
} = props

const { classes, styles: resolvedStyles } = useStyles(DropdownItem.displayName, {
className: DropdownItem.className,
mapPropsToStyles: () => ({
active,
isFromKeyboard,
selected,
hasContent: !!content,
hasHeader: !!header,
}),
accessibilityItemProps: PropTypes.object,
active: PropTypes.bool,
content: customPropTypes.itemShorthand,
checkable: PropTypes.bool,
checkableIndicator: customPropTypes.itemShorthandWithoutJSX,
header: customPropTypes.itemShorthand,
image: customPropTypes.itemShorthandWithoutJSX,
onClick: PropTypes.func,
isFromKeyboard: PropTypes.bool,
selected: PropTypes.bool,
}
mapPropsToInlineStyles: () => ({ className, design, styles, variables }),
rtl: context.rtl,
})

const ElementType = getElementType(props)
const unhandledProps = getUnhandledProps(DropdownItem.handledProps, props)

handleClick = e => {
_.invoke(this.props, 'onClick', e, this.props)
const handleClick = (e: React.MouseEvent | React.KeyboardEvent) => {
_.invoke(props, 'onClick', e, props)
}

renderComponent({ classes, styles, unhandledProps }: RenderResultConfig<DropdownItemProps>) {
const {
content,
header,
image,
accessibilityItemProps,
selected,
checkable,
checkableIndicator,
} = this.props
return (
<ListItem
className={DropdownItem.className}
styles={styles.root}
onClick={this.handleClick}
header={Box.create(header, {
defaultProps: () => ({
className: DropdownItem.slotClassNames.header,
styles: styles.header,
}),
})}
media={Image.create(image, {
defaultProps: () => ({
avatar: true,
className: DropdownItem.slotClassNames.image,
styles: styles.image,
}),
})}
content={Box.create(content, {
const contentElement = Box.create(content, {
defaultProps: () => ({
className: DropdownItem.slotClassNames.content,
styles: resolvedStyles.content,
}),
})
const headerElement = Box.create(header, {
defaultProps: () => ({
className: DropdownItem.slotClassNames.header,
styles: resolvedStyles.header,
}),
})
const endMediaElement =
selected && checkable
? Icon.create(checkableIndicator, {
defaultProps: () => ({
className: DropdownItem.slotClassNames.content,
styles: styles.content,
className: DropdownItem.slotClassNames.checkableIndicator,
styles: resolvedStyles.checkableIndicator,
}),
})}
endMedia={
selected &&
checkable && {
content: Icon.create(checkableIndicator, {
defaultProps: () => ({
className: DropdownItem.slotClassNames.checkableIndicator,
styles: styles.checkableIndicator,
}),
}),
styles: styles.endMedia,
}
}
truncateContent
truncateHeader
{...accessibilityItemProps}
{...unhandledProps}
/>
)
}
})
: null
const imageElement = Box.create(
Image.create(image, {
defaultProps: () => ({
avatar: true,
className: DropdownItem.slotClassNames.image,
styles: resolvedStyles.image,
}),
}),
{
defaultProps: () => ({
className: DropdownItem.slotClassNames.image,
styles: resolvedStyles.media,
}),
},
)

const element = (
<ElementType
className={classes.root}
onClick={handleClick}
{...accessibilityItemProps}
{...unhandledProps}
>
{imageElement}

<div className={cx(DropdownItem.slotClassNames.main, classes.main)}>
{headerElement}
{contentElement}
</div>

{endMediaElement}
</ElementType>
)

setEnd()

return element
}

DropdownItem.className = 'ui-dropdown__item'
DropdownItem.displayName = 'DropdownItem'

DropdownItem.defaultProps = {
as: 'li',
}

DropdownItem.propTypes = {
...commonPropTypes.createCommon({
accessibility: false,
children: false,
content: false,
}),
accessibilityItemProps: PropTypes.object,
active: PropTypes.bool,
content: customPropTypes.itemShorthand,
checkable: PropTypes.bool,
checkableIndicator: customPropTypes.itemShorthandWithoutJSX,
header: customPropTypes.itemShorthand,
image: customPropTypes.itemShorthandWithoutJSX,
onClick: PropTypes.func,
isFromKeyboard: PropTypes.bool,
selected: PropTypes.bool,
}
DropdownItem.handledProps = Object.keys(DropdownItem.propTypes) as any

DropdownItem.slotClassNames = {
main: `${DropdownItem.className}__main`,
content: `${DropdownItem.className}__content`,
header: `${DropdownItem.className}__header`,
image: `${DropdownItem.className}__image`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@ import DropdownItem, { DropdownItemProps } from '../../../../components/Dropdown
import getBorderFocusStyles from '../../getBorderFocusStyles'
import { pxToRem } from '../../../../utils'

const dropdownItemStyles: ComponentSlotStylesPrepared<DropdownItemProps, DropdownVariables> = {
export type DropdownItemStylesProps = Pick<
DropdownItemProps,
'selected' | 'active' | 'isFromKeyboard'
> & {
hasContent?: boolean
hasHeader?: boolean
}

const dropdownItemStyles: ComponentSlotStylesPrepared<
DropdownItemStylesProps,
DropdownVariables
> = {
root: ({ props: p, variables: v, theme: { siteVariables } }): ICSSInJSStyle => ({
display: 'flex',
alignItems: 'center',
minHeight: 0,
padding: `${pxToRem(4)} ${pxToRem(11)}`,
whiteSpace: 'nowrap',
Expand All @@ -22,12 +35,12 @@ const dropdownItemStyles: ComponentSlotStylesPrepared<DropdownItemProps, Dropdow
...(!p.isFromKeyboard && {
color: v.listItemColorHover,
backgroundColor: v.listItemBackgroundColorHover,
...(p.header && {
...(p.hasHeader && {
[`& .${DropdownItem.slotClassNames.header}`]: {
color: v.listItemColorHover,
},
}),
...(p.content && {
...(p.hasContent && {
[`& .${DropdownItem.slotClassNames.content}`]: {
color: v.listItemColorHover,
},
Expand All @@ -39,10 +52,13 @@ const dropdownItemStyles: ComponentSlotStylesPrepared<DropdownItemProps, Dropdow
margin: `${pxToRem(3)} ${pxToRem(12)} ${pxToRem(3)} ${pxToRem(4)}`,
}),
header: ({ props: p, variables: v }): ICSSInJSStyle => ({
flexGrow: 1,
lineHeight: v.listItemHeaderLineHeight,

fontSize: v.listItemHeaderFontSize,
// if the item doesn't have content - i.e. it is header only - then it should use the content color
color: v.listItemContentColor,
...(p.content && {
...(p.hasContent && {
// if there is content it needs to be "tightened up" to the header
marginBottom: pxToRem(-1),
color: v.listItemHeaderColor,
Expand All @@ -53,6 +69,8 @@ const dropdownItemStyles: ComponentSlotStylesPrepared<DropdownItemProps, Dropdow
}),
}),
content: ({ variables: v }): ICSSInJSStyle => ({
flexGrow: 1,
lineHeight: v.listItemContentLineHeight,
fontSize: v.listItemContentFontSize,
color: v.listItemContentColor,
}),
Expand All @@ -61,8 +79,15 @@ const dropdownItemStyles: ComponentSlotStylesPrepared<DropdownItemProps, Dropdow
left: pxToRem(3),
}),
endMedia: () => ({
flexShrink: 0,
lineHeight: pxToRem(16),
}),
main: () => ({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
minWidth: 0, // needed for the truncate styles to work
}),
}

export default dropdownItemStyles
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface DropdownVariables {
listItemColorActive: string
listItemSelectedFontWeight: number
listItemSelectedColor: string
listItemHeaderLineHeight: string
listItemContentLineHeight: string
selectedItemColor: string
selectedItemBackgroundColor: string
selectedItemColorFocus: string
Expand Down Expand Up @@ -76,6 +78,9 @@ export default (siteVars): DropdownVariables => ({
listItemColorActive: siteVars.colors.grey[750],
listItemSelectedFontWeight: siteVars.fontWeightSemibold,
listItemSelectedColor: siteVars.colors.grey[750],
// TODO: prod app uses 17.5px here, it should be 16px per the design guide!
listItemHeaderLineHeight: siteVars.lineHeightSmall,
listItemContentLineHeight: siteVars.lineHeightSmall,
selectedItemBackgroundColor: 'undefined',
selectedItemColorFocus: siteVars.bodyColor,
selectedItemBackgroundColorFocus: siteVars.colors.brand[200],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import DropdownItem from 'src/components/Dropdown/DropdownItem'
import { isConformant } from 'test/specs/commonTests'

describe('DropdownItem', () => {
isConformant(DropdownItem, {
constructorName: 'DropdownItem',
hasAccessibilityProp: false,
})
})

0 comments on commit 9d4d87b

Please sign in to comment.