import { useContext, useId, useMemo, useState, type PropsWithChildren, type ReactNode, } from 'react'; import { Sentiment, Typography } from '../common'; import Body from '../body'; import { AdditionalInfo } from './AdditionalInfo'; import { IconButton, type ListItemIconButtonProps } from './IconButton'; import { Checkbox, type ListItemCheckboxProps } from './Checkbox'; import { Navigation, type ListItemNavigationProps } from './Navigation'; import { clsx } from 'clsx'; import { Button, type ListItemButtonProps } from './Button'; import { Radio, type ListItemRadioProps } from './Radio'; import { Switch, type ListItemSwitchProps } from './Switch'; import { AvatarLayout } from './AvatarLayout'; import { AvatarView } from './AvatarView'; import { Image } from './Image'; import { Prompt } from './Prompt'; import { PrimitiveAnchor, type PrimitiveAnchorProps } from '../primitives'; import { ListItemContext, type ListItemContextData, type ListItemMediaSize, } from './ListItemContext'; export type ListItemTypes = | 'non-interactive' | 'navigation' | 'radio' | 'checkbox' | 'switch' | 'button' | 'icon-button'; export type ListItemControlProps = | ListItemNavigationProps | ListItemCheckboxProps | ListItemButtonProps | ListItemIconButtonProps | ListItemRadioProps | ListItemSwitchProps; export type ListItemProps = { /** @default 'li' */ as?: 'li' | 'div'; /** * Swaps vertical hierarchy of title and subtitle and their corresponding right values. */ inverted?: boolean; /** * Disables the control and renders the ListItem in greyscale and with slightly decreased opacity. */ disabled?: boolean; /** * If set, it'll extend the `disabled` state, overriding existing or injecting uniquely styled prompt with the message provided via this prop.
* **NB:** This message cannot house more than **1** link or inline button.
* **NB:** It must be used together with `disabled` prop and will be disregarded otherwise. */ disabledPromptMessage?: ReactNode; /** * Highlights the list item as an action to be taken or already taken.
*/ spotlight?: 'active' | 'inactive'; title: ReactNode; subtitle?: ReactNode; /** * Requires `` component as a sole child.
* Can be only rendered if `subtitle` is also provided. */ additionalInfo?: ReactNode; valueTitle?: ReactNode; valueSubtitle?: ReactNode; /** * Requires one of the following as a sole child:
* ``, * `` or * `` */ media?: ReactNode; /** * Requires one of the following as a sole child:
* ``,
* ``,
* ``,
* ``,
* ``, or * `` * @default null */ control?: ReactNode; /** * Requires `` component as a sole child. */ prompt?: ReactNode; className?: string; /** * A number between `0–100` which resolves to a `fr` value of a `grid-template-columns` declaration. E.g. `valueColumnWidth={25}` will result in a `75fr 25fr`.
* Controls the width ratio of left side content (title and subtitle) to the right side content. */ valueColumnWidth?: number; id?: string; }; /** * @see [Design documentation](https://wise.design/components/list-item) * @see [Storybook documentation](https://storybook.wise.design/?path=/docs/content-listitem--docs) */ export const ListItem = ({ as: ListItemElement = 'li', title, subtitle, additionalInfo, prompt, inverted, media, spotlight, valueTitle, valueSubtitle, control = null, disabled, disabledPromptMessage, className, valueColumnWidth, id, }: ListItemProps) => { const idPrefix = useId(); const [controlProps, setControlProps] = useState({}); const [controlType, setControlType] = useState('non-interactive'); const [mediaSize, setMediaSize] = useState(); const ids: ListItemContextData['ids'] = { title: `${idPrefix}_title`, ...(subtitle ? { subtitle: `${idPrefix}_subtitle` } : {}), ...(valueTitle ? { valueTitle: `${idPrefix}_value-title` } : {}), ...(valueSubtitle ? { valueSubtitle: `${idPrefix}_value-subtitle` } : {}), control: `${idPrefix}_control`, ...(prompt || (disabled && disabledPromptMessage) ? { prompt: `${idPrefix}_prompt` } : {}), ...(additionalInfo ? { additionalInfo: `${idPrefix}_additional-info` } : {}), }; const isPartiallyInteractive = Boolean( (controlType === 'button' || controlType === 'icon-button') && (controlProps as ListItemButtonProps | ListItemIconButtonProps)?.partiallyInteractive, ); const isFullyInteractive = controlType !== 'non-interactive' && !isPartiallyInteractive; const isButtonAsLink = (controlType === 'button' || controlType === 'icon-button') && Boolean((controlProps as ListItemButtonProps | ListItemIconButtonProps)?.href); const titlesAndValues = [ inverted ? ids.subtitle : ids.title, inverted ? ids.title : ids.subtitle, inverted ? ids.valueSubtitle : ids.valueTitle, inverted ? ids.valueTitle : ids.valueSubtitle, ].join(' '); const additionalInfoPrompt = [ids.additionalInfo, ids.prompt].filter(Boolean).join(' '); const describedByIds = useMemo(() => { return isFullyInteractive && !isButtonAsLink ? additionalInfoPrompt : `${titlesAndValues} ${additionalInfoPrompt}`; }, [isFullyInteractive]); const listItemContext = useMemo( () => ({ setControlType, setControlProps, setMediaSize, ids, props: { disabled, inverted, disabledPromptMessage }, mediaSize, isPartiallyInteractive, describedByIds, }), [describedByIds, mediaSize], ); const gridColumnsStyle = { '--wds-list-item-body-left': valueColumnWidth ? `${100 - valueColumnWidth}fr` : '50fr', '--wds-list-item-body-right': valueColumnWidth ? `${valueColumnWidth}fr` : '50fr', } as React.CSSProperties; const getFeatureClassName = () => { const partials = []; const hasMedia = Boolean(media); const hasControl = Boolean(control); const hasInfo = Boolean(additionalInfo); const hasPrompt = Boolean(prompt) || (disabled && Boolean(disabledPromptMessage)); /* eslint-disable functional/immutable-data */ if (hasMedia && hasControl) { partials.push('wds-list-item-hasMedia-hasControl'); } if (hasMedia && !hasControl) { partials.push('wds-list-item-hasMedia-noControl'); } if (!hasMedia && hasControl) { partials.push('wds-list-item-noMedia-hasControl'); } if (!hasMedia && !hasControl) { partials.push('wds-list-item-noMedia-noControl'); } if (hasInfo && hasPrompt) { partials.push('wds-list-item-hasInfo-hasPrompt'); } if (hasInfo && !hasPrompt) { partials.push('wds-list-item-hasInfo-noPrompt'); } if (!hasInfo && hasPrompt) { partials.push('wds-list-item-noInfo-hasPrompt'); } if (!hasInfo && !hasPrompt) { partials.push('wds-list-item-noInfo-noPrompt'); } /* eslint-enable functional/immutable-data */ return partials.join(' '); }; return ( {isFullyInteractive && spotlight === 'inactive' && ( )} {media &&
{media}
} {/* Title + Subtitle + Values - Group */}
{/* Title + Subtitle + Values - Group */} {(() => { const titles = [ {title} , ]; if (subtitle) { titles.push( {subtitle} , ); } return inverted ? [...titles].reverse() : titles; })()} {(valueTitle || valueSubtitle) && ( {(() => { const values = []; if (valueTitle) { values.push( {valueTitle} , ); } if (valueSubtitle) { values.push( {valueSubtitle} , ); } return inverted ? [...values].reverse() : values; })()} )}
{control === null ? null : ( {control} )}
); }; type ViewProps = PropsWithChildren<{ isPartiallyInteractive: boolean; controlType?: ListItemTypes; controlProps?: ListItemControlProps; }> & Pick< ListItemProps, 'subtitle' | 'additionalInfo' | 'prompt' | 'disabled' | 'disabledPromptMessage' | 'className' >; function View({ children, additionalInfo, prompt, disabled, disabledPromptMessage, isPartiallyInteractive, controlType = 'non-interactive', controlProps, className = '', }: ViewProps) { const { ids, describedByIds } = useContext(ListItemContext); const isLinkControl = ['navigation'].includes(controlType); const isHrefProvided = isLinkControl && !!(controlProps as ListItemNavigationProps)?.href; const renderExtras = () => { const resolvedPrompt = disabled && disabledPromptMessage && !prompt ? ( {disabledPromptMessage} ) : ( prompt ); return ( <> {additionalInfo} {resolvedPrompt} ); }; if (isLinkControl && isHrefProvided) { return ( // for link instances of .Navigation, .IconButton, .Button
{children} {renderExtras()}
); } if (isPartiallyInteractive || controlType === 'non-interactive') { return (
{children}
{renderExtras()}
); } // for form control instances of .Radio, .Checkbox, .Switch, .Button, .Navigation etc // Radio cannot be wrapped in a
element to announce it as a group. const InputWrapper = controlType === 'radio' ? 'div' : 'fieldset'; return ( {renderExtras()} ); } ListItem.Image = Image; ListItem.AvatarView = AvatarView; ListItem.AvatarLayout = AvatarLayout; ListItem.AdditionalInfo = AdditionalInfo; ListItem.Checkbox = Checkbox; ListItem.Radio = Radio; ListItem.IconButton = IconButton; ListItem.Navigation = Navigation; ListItem.Button = Button; ListItem.Switch = Switch; ListItem.Prompt = Prompt; export default ListItem;