Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
})
})