import React, { useContext, useLayoutEffect, useMemo, useRef } from 'react'; import flattenChildren from 'react-keyed-flatten-children'; import classNames from 'classnames'; import { isTruthyReactNode } from '../../utils/isTruthyNode'; import type { AnchorProps } from '../Anchor/Anchor'; import { Anchor } from '../Anchor/Anchor'; import type { IconProps } from '../Icon/Icon'; import { Icon } from '../Icon/Icon'; import { Paragraph } from '../Typography/Paragraph/Paragraph'; import { listItemTouchesBottom, selectFromElementProps } from './utils'; const defaultIconProps: Omit = { size: 'sm', variant: 'default' }; /** * Context to pass the list configuration to the list items. */ export interface ListContextType { columns: boolean | 'dense'; dividers: boolean; ellipsis: boolean; icon: IconProps | boolean | undefined; dense: boolean; verticalAlignment: 'top' | 'center' | 'bottom'; } const ListContext = React.createContext({ columns: false, ellipsis: true, dividers: true, icon: undefined, dense: false, verticalAlignment: 'top', }); export interface ListProps { children: React.ReactNode; /** * Displays the list in columns. * * Items are distributed in columns filling the first column, then second, and so on. * * @default false */ columns?: boolean | 'dense'; /** * Displays dividers between list items. * @default true */ dividers?: boolean; /** * Displays ellipsis for long text in list items. * @default true */ ellipsis?: boolean; /** * Icon to be displayed in the list. * * - `IconName` displays icon with the provided name * - `IconProps` displays icon defined by the icon props * - `true` displays empty icon placeholder, extra space (padding) * - `false` to not display any icon or icon placeholder padding * * @default undefined */ icon?: IconProps['icon'] | Omit | boolean; /** * If true, the list items have no minimum height. * @default false */ dense?: boolean; /** * Aligns the content of the list items vertically. * @default 'top' * */ verticalAlignment?: 'top' | 'center' | 'bottom'; } export interface ListItemKeyValueProps { /** * Value of the list item. */ value: React.ReactNode; /** * Children are not allowed in this case. */ children?: never; } export interface ListItemWithChildrenProps { /** * Value is not allowed in this case. */ value?: never; /** * Children of the list item. */ children: React.ReactNode; } export type ListItemProps = { /** * Action to be displayed in the list item. * * @default undefined */ action?: { href?: string; onClick: React.MouseEventHandler; value: string; target?: AnchorProps['target']; }; /** * Icon to be displayed in the list item. * * - `IconName` displays icon with the provided name * - `IconProps` displays icon defined by the icon props * - `true` displays empty icon placeholder, extra space (padding) * - `false` to not display any icon or icon placeholder padding * * @default undefined */ icon?: IconProps['icon'] | Omit | boolean; /** * Displays ellipsis for long text in list items. * @default true */ ellipsis?: boolean; /** * Label of the list item. */ label?: string; /** * If true, the list items have no minimum height. * @default false */ dense?: boolean; /** * Aligns the content of the list items vertically. * @default 'top' * */ verticalAlignment?: 'top' | 'center' | 'bottom'; /** * Displays dividers at the bottom of the list item. * @default false */ divider?: boolean; } & (ListItemKeyValueProps | ListItemWithChildrenProps); function ListComponent({ children, columns = false, dividers = true, ellipsis = true, dense = false, verticalAlignment = 'top', icon, ...rest }: ListProps) { const flatChildren = flattenChildren(children); const withIcons = flatChildren.some(child => !!selectFromElementProps(child, 'icon')); return ( ({ columns, dividers, ellipsis, icon: typeof icon === 'string' ? { ...defaultIconProps, icon: icon ?? withIcons } : (icon ?? withIcons), dense, verticalAlignment, }), [columns, dense, dividers, ellipsis, icon, verticalAlignment, withIcons] )} >
    {children}
); } ListComponent.displayName = 'List'; function ListItem({ action, ellipsis: ellipsisProp, icon: propsIcon, dense: denseProp, verticalAlignment: verticalAlignmentProp, divider, ...rest }: ListItemProps) { const ref = useRef(null); const [touchesBottom, setTouchesBottom] = React.useState(false); const timeoutIdRef = React.useRef(undefined); const { icon: contextIcon, dividers, columns, ellipsis: contextEllipsis, dense: contextDense, verticalAlignment: contextVerticalAlignment, } = useContext(ListContext); let icon = propsIcon ?? contextIcon; if (typeof icon === 'string') { icon = { ...defaultIconProps, ...(typeof contextIcon === 'object' ? contextIcon : undefined), icon }; } else if (typeof icon === 'object') { icon = { ...defaultIconProps, ...(typeof contextIcon === 'object' ? contextIcon : undefined), ...icon }; } const ellipsis = ellipsisProp ?? contextEllipsis; const dense = denseProp ?? contextDense; const verticalAlignment = verticalAlignmentProp ?? contextVerticalAlignment; useLayoutEffect(() => { const element = ref.current; if (element) { if (dividers) { if (columns) { const timeoutId = timeoutIdRef.current; const observer = new ResizeObserver(() => { window.clearTimeout(timeoutId); timeoutIdRef.current = window.setTimeout(() => { setTouchesBottom(listItemTouchesBottom(element)); }, 100); }); observer.observe(element); return () => { window.clearTimeout(timeoutId); observer.disconnect(); }; } else { setTouchesBottom(listItemTouchesBottom(element)); } } else { setTouchesBottom(false); } } }, [columns, dividers, ref, divider]); const label = 'label' in rest ? rest.label : undefined; const value = 'value' in rest ? rest.value : rest.children; const hasRawChildren = 'children' in rest; return (
  • {icon &&
    {icon && typeof icon === 'object' && }
    }
    {isTruthyReactNode(label) && (
    {label}
    )}
    {(!hasRawChildren && typeof value === 'string') || typeof value === 'number' ? ( {value} ) : ( value )}
    {action && (
    {action.value}
    )}
  • ); } export interface BasicListItemProps { children: React.ReactNode; } function BasicListItem({ children, ...rest }: BasicListItemProps) { return (
  • {children}
  • ); } export const List = Object.assign(ListComponent, { Item: ListItem, BasicItem: BasicListItem, });