import { Accessibility, listBehavior, ListBehaviorProps } from '@fluentui/accessibility'; import { ComponentWithAs, getElementType, useUnhandledProps, useAccessibility, useAutoControlled, useFluentContext, useStyles, useTelemetry, } from '@fluentui/react-bindings'; import * as customPropTypes from '@fluentui/react-proptypes'; import * as _ from 'lodash'; import * as PropTypes from 'prop-types'; import * as React from 'react'; import { ComponentEventHandler, ShorthandCollection, ReactChildren, FluentComponentStaticProps } from '../../types'; import { childrenExist, UIComponentProps, ChildrenComponentProps, commonPropTypes, rtlTextContainer, createShorthandFactory, } from '../../utils'; import { ListContextProvider, ListContextValue } from './listContext'; import { ListItem, ListItemProps } from './ListItem'; export interface ListProps extends UIComponentProps, ChildrenComponentProps { /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility; /** Toggle debug mode */ debug?: boolean; /** Shorthand array of props for ListItem. */ items?: ShorthandCollection; /** A selectable list formats list items as possible choices. */ selectable?: boolean; /** A navigable list allows user to navigate through items. */ navigable?: boolean; /** Index of the currently selected item. */ selectedIndex?: number; /** Initial selectedIndex value. */ defaultSelectedIndex?: number; /** * Event for request to change 'selectedIndex' value. * @param event - React's original SyntheticEvent. * @param data - All props and proposed value. */ onSelectedIndexChange?: ComponentEventHandler; /** Truncates content */ truncateContent?: boolean; /** Truncates header */ truncateHeader?: boolean; /** A horizontal list displays elements horizontally. */ horizontal?: boolean; /** An optional wrapper function. */ wrap?: (children: ReactChildren) => React.ReactNode; } export type ListStylesProps = Pick & { isListTag: boolean }; export const listClassName = 'ui-list'; /** * A List displays a group of related sequential items. * * @accessibility * List may follow one of the following accessibility semantics: * - Static non-navigable list. Implements [ARIA list](https://www.w3.org/TR/wai-aria-1.1/#list) role. * - Selectable list: allows the user to select item from a list of choices. Implements [ARIA Listbox](https://www.w3.org/TR/wai-aria-practices-1.1/#Listbox) design pattern. */ export const List: ComponentWithAs<'ul', ListProps> & FluentComponentStaticProps & { Item: typeof ListItem; } = props => { const context = useFluentContext(); const { setStart, setEnd } = useTelemetry(List.displayName, context.telemetry); setStart(); const { accessibility, as, children, className, debug, design, horizontal, items, navigable, selectable, styles, truncateContent, truncateHeader, variables, wrap, } = props; const [selectedIndex, setSelectedIndex] = useAutoControlled({ defaultValue: props.defaultSelectedIndex, value: props.selectedIndex, initialValue: -1, }); const getA11Props = useAccessibility(accessibility, { debugName: List.displayName, mapPropsToBehavior: () => ({ horizontal, navigable, selectable, }), rtl: context.rtl, }); const { classes } = useStyles(List.displayName, { className: listClassName, mapPropsToStyles: () => ({ isListTag: as === 'ol' || as === 'ul', debug, horizontal }), mapPropsToInlineStyles: () => ({ className, design, styles, variables }), rtl: context.rtl, }); const latestProps = React.useRef(props); latestProps.current = props; const ElementType = getElementType(props); const unhandledProps = useUnhandledProps(List.handledProps, props); const hasContent = childrenExist(children) || (items && items.length > 0); const onItemClick = React.useCallback( (e, itemIndex) => { if (latestProps.current.selectable) { setSelectedIndex(itemIndex); _.invoke(latestProps.current, 'onSelectedIndexChange', e, { ...latestProps.current, selectedIndex: itemIndex, }); } }, [latestProps, setSelectedIndex], ); const childProps: ListContextValue = { debug, navigable, onItemClick, selectable, selectedIndex, truncateContent, truncateHeader, variables, }; const renderItems = () => _.map(items, (item, index) => ListItem.create(item, { defaultProps: () => ({ index }) })); const element = getA11Props.unstable_wrapWithFocusZone( {hasContent && wrap(childrenExist(children) ? children : renderItems())} , ); setEnd(); return element; }; 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' });