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

chore(List): move to hooks #2267

Merged
merged 20 commits into from
Jan 28, 2020
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- `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))
- Returned function from `useAccessibility` no longer keeps the same reference @layershifter ([#2268](https://github.com/microsoft/fluent-ui-react/pull/2268))
- 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))
- Add logic for mounting/removing elements when they are shown/hidden using the `Animation` component; `Animation` component is not rendering element anymore, just applying classes to it's children @mnajdova ([#2115](https://github.com/microsoft/fluent-ui-react/pull/2115))
- Restricted prop set in the `Button`, `Avatar`, `Box` and `Image` styles; 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))
- Restricted prop set in the `List` & `ListItem` @layershifter ([#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))
Expand Down
7 changes: 7 additions & 0 deletions packages/accessibility/src/behaviors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ export { default as menuBehavior } from './Menu/menuBehavior'
export { default as menuItemBehavior } from './Menu/menuItemBehavior'
export { default as menuDividerBehavior } from './Menu/menuDividerBehavior'
export { default as submenuBehavior } from './Menu/submenuBehavior'

export { default as basicListBehavior } from './List/listBehavior'
export { default as basicListItemBehavior } from './List/basicListItemBehavior'

export { default as listBehavior } from './List/listBehavior'
export * from './List/listBehavior'
export { default as listItemBehavior } from './List/listItemBehavior'
export * from './List/listItemBehavior'

export { default as navigableListBehavior } from './List/navigableListBehavior'
export { default as navigableListItemBehavior } from './List/navigableListItemBehavior'

export { default as selectableListBehavior } from './List/selectableListBehavior'
export { default as selectableListItemBehavior } from './List/selectableListItemBehavior'

export { default as loaderBehavior } from './Loader/loaderBehavior'
export { default as inputBehavior } from './Input/inputBehavior'
export { default as iconBehavior } from './Icon/iconBehavior'
Expand Down
266 changes: 146 additions & 120 deletions packages/react/src/components/List/List.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
import { Accessibility, listBehavior } from '@fluentui/accessibility'
import { Accessibility, listBehavior, ListBehaviorProps } from '@fluentui/accessibility'
import {
getElementType,
getUnhandledProps,
useAccessibility,
useStateManager,
useStyles,
useTelemetry,
} from '@fluentui/react-bindings'
import { createListManager } from '@fluentui/state'
import * as customPropTypes from '@fluentui/react-proptypes'
import * as _ from 'lodash'
import * as React from 'react'
import * as PropTypes from 'prop-types'
import * as React from 'react'
// @ts-ignore
import { ThemeContext } from 'react-fela'

import {
WithAsProp,
ComponentEventHandler,
withSafeTypeForAs,
ShorthandCollection,
ReactChildren,
ProviderContextPrepared,
FluentComponentStaticProps,
} from '../../types'
import {
childrenExist,
AutoControlledComponent,
UIComponentProps,
ChildrenComponentProps,
commonPropTypes,
rtlTextContainer,
applyAccessibilityKeyHandlers,
createShorthandFactory,
ShorthandFactory,
} from '../../utils'
import ListItem, { ListItemProps } from './ListItem'
import {
WithAsProp,
ComponentEventHandler,
withSafeTypeForAs,
ShorthandCollection,
ReactChildren,
} from '../../types'

export interface ListSlotClassNames {
item: string
}

export interface ListProps extends UIComponentProps, ChildrenComponentProps {
/** Accessibility behavior if overridden by the user. */
accessibility?: Accessibility
accessibility?: Accessibility<ListBehaviorProps>

/** Toggle debug mode */
debug?: boolean
Expand Down Expand Up @@ -70,122 +76,142 @@ export interface ListProps extends UIComponentProps, ChildrenComponentProps {
wrap?: (children: ReactChildren) => React.ReactNode
}

export interface ListState {
selectedIndex?: number
}
// List props that are passed to each individual Item props
const itemProps = [
'debug',
'selectable',
'navigable',
'truncateContent',
'truncateHeader',
'variables',
]

const List: React.FC<WithAsProp<ListProps>> &
FluentComponentStaticProps<ListProps> & {
Item: typeof ListItem
} = props => {
const context: ProviderContextPrepared = React.useContext(ThemeContext)
const { setStart, setEnd } = useTelemetry(List.displayName, context.telemetry)
setStart()

const {
accessibility,
as,
children,
className,
debug,
defaultSelectedIndex,
design,
horizontal,
navigable,
items,
selectable,
selectedIndex,
styles,
variables,
wrap,
} = props

const { state, actions } = useStateManager(createListManager, {
mapPropsToInitialState: () => ({ selectedIndex: defaultSelectedIndex }),
mapPropsToState: () => ({ selectedIndex }),
})
const getA11Props = useAccessibility(accessibility, {
debugName: List.displayName,
mapPropsToBehavior: () => ({
horizontal,
navigable,
selectable,
}),
rtl: context.rtl,
})
const { classes } = useStyles(List.displayName, {
className: List.className,
mapPropsToStyles: () => ({ isListTag: as === 'ol' || as === 'ul', debug, horizontal }),
mapPropsToInlineStyles: () => ({ className, design, styles, variables }),
rtl: context.rtl,
})

class List extends AutoControlledComponent<WithAsProp<ListProps>, ListState> {
static displayName = 'List'
const ElementType = getElementType(props)
const unhandledProps = getUnhandledProps(List.handledProps, props)

static className = 'ui-list'
const hasContent = childrenExist(children) || (items && items.length > 0)

static slotClassNames: ListSlotClassNames = {
item: `${List.className}__item`,
}
const handleItemOverrides = (predefinedProps: ListItemProps) => ({
onClick: (e: React.SyntheticEvent, itemProps: ListItemProps) => {
_.invoke(predefinedProps, 'onClick', e, itemProps)

static propTypes = {
...commonPropTypes.createCommon({
content: false,
}),
debug: PropTypes.bool,
items: customPropTypes.collectionShorthand,
selectable: customPropTypes.every([customPropTypes.disallow(['navigable']), PropTypes.bool]),
navigable: customPropTypes.every([customPropTypes.disallow(['selectable']), PropTypes.bool]),
truncateContent: PropTypes.bool,
truncateHeader: PropTypes.bool,
selectedIndex: PropTypes.number,
defaultSelectedIndex: PropTypes.number,
onSelectedIndexChange: PropTypes.func,
horizontal: PropTypes.bool,
wrap: PropTypes.func,
}

static defaultProps = {
as: 'ul',
accessibility: listBehavior as Accessibility,
wrap: children => children,
}

static autoControlledProps = ['selectedIndex']

getInitialAutoControlledState() {
return { selectedIndex: -1 }
}

static Item = ListItem

// List props that are passed to each individual Item props
static itemProps = [
'debug',
'selectable',
'navigable',
'truncateContent',
'truncateHeader',
'variables',
]

static create: ShorthandFactory<ListProps>

handleItemOverrides = (predefinedProps: ListItemProps) => {
const { selectable } = this.props

return {
onClick: (e: React.SyntheticEvent, itemProps: ListItemProps) => {
_.invoke(predefinedProps, 'onClick', e, itemProps)

if (selectable) {
this.setState({ selectedIndex: itemProps.index })
_.invoke(this.props, 'onSelectedIndexChange', e, {
...this.props,
...{ selectedIndex: itemProps.index },
})
}
},
}
}

renderComponent({ ElementType, classes, accessibility, unhandledProps }) {
const { children, items, wrap } = this.props
const hasContent = childrenExist(children) || (items && items.length > 0)

return (
<ElementType
{...accessibility.attributes.root}
{...rtlTextContainer.getAttributes({ forElements: [children] })}
{...unhandledProps}
{...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)}
className={classes.root}
>
{hasContent && wrap(childrenExist(children) ? children : this.renderItems())}
</ElementType>
)
}

renderItems() {
const { items, selectable } = this.props
const { selectedIndex } = this.state

return _.map(items, (item, index) => {
if (selectable) {
actions.select(itemProps.index)
_.invoke(props, 'onSelectedIndexChange', e, {
...props,
...{ selectedIndex: itemProps.index },
})
}
},
})

const renderItems = () =>
_.map(items, (item, index) => {
const maybeSelectableItemProps = {} as any

if (selectable) {
maybeSelectableItemProps.selected = index === selectedIndex
maybeSelectableItemProps.selected = index === state.selectedIndex
}

const itemProps = () => ({
className: List.slotClassNames.item,
..._.pick(this.props, List.itemProps),
...maybeSelectableItemProps,
index,
})

return ListItem.create(item, {
defaultProps: itemProps,
overrideProps: this.handleItemOverrides,
defaultProps: () => ({
..._.pick(props, itemProps),
...maybeSelectableItemProps,
index,
}),
overrideProps: handleItemOverrides,
})
})
}

const element = getA11Props.unstable_wrapWithFocusZone(
<ElementType
{...getA11Props('root', {
className: classes.root,
...rtlTextContainer.getAttributes({ forElements: [children] }),
...unhandledProps,
})}
>
{hasContent && wrap(childrenExist(children) ? children : renderItems())}
</ElementType>,
)
setEnd()

return element
}

List.className = 'ui-list'
List.displayName = 'List'

List.defaultProps = {
as: 'ul',
accessibility: listBehavior,
wrap: children => children,
}
List.propTypes = {
...commonPropTypes.createCommon({
content: false,
}),
debug: PropTypes.bool,
items: customPropTypes.collectionShorthand,
selectable: customPropTypes.every([customPropTypes.disallow(['navigable']), PropTypes.bool]),
navigable: customPropTypes.every([customPropTypes.disallow(['selectable']), PropTypes.bool]),
truncateContent: PropTypes.bool,
truncateHeader: PropTypes.bool,
selectedIndex: PropTypes.number,
defaultSelectedIndex: PropTypes.number,
onSelectedIndexChange: PropTypes.func,
horizontal: PropTypes.bool,
wrap: PropTypes.func,
}

List.handledProps = Object.keys(List.propTypes) as any
List.Item = ListItem

List.create = createShorthandFactory({ Component: List, mappedArrayProp: 'items' })

Expand Down
Loading