import { useState } from 'react'; import { Meta, StoryObj } from '@storybook/react-webpack5'; import { MultiCurrency, Plus } from '@transferwise/icons'; import { lorem5, lorem10 } from '../../test-utils'; import Emphasis from '../../emphasis'; import Link from '../../link'; import List from '../../list'; import { ListItem, type ListItemProps } from '../ListItem'; import { LISTITEM_CQ } from '../constants'; import { SB_LIST_ITEM_CONTROLS as CONTROLS, SB_LIST_ITEM_ADDITIONAL_INFO as ADDITIONAL_INFO, SB_LIST_ITEM_PROMPTS as PROMPTS, SB_LIST_ITEM_MEDIA as MEDIA, } from './subcomponents'; import { disableControls, storySourceWithoutNoise, withoutKey } from './helpers'; const hideControls = disableControls([ 'additionalInfo', 'control', 'prompt', 'media', 'className', 'id', 'as', ]); /** * List Items let users review or select options from a dynamic list.
* For more details please refer to the [release notes](https://transferwise.atlassian.net/wiki/spaces/DS/pages/3647251055/List+Item+release+notes) and the [design spec](https://wise.design/components/list-item).
* * > This component replaces now deprecated `LegacyListItem`, `Summary` and all `*Option` components (run codemod to migrate: **`npx @wise/wds-codemods@latest info-prompt`**). */ export default { component: ListItem, tags: ['new'], subcomponents: { 'ListItem.AdditionalInfo': ListItem.AdditionalInfo, 'ListItem.Prompt': ListItem.Prompt, 'ListItem.Button': ListItem.Button, 'ListItem.IconButton': ListItem.IconButton, 'ListItem.Navigation': ListItem.Navigation, 'ListItem.Checkbox': ListItem.Checkbox, 'ListItem.Radio': ListItem.Radio, 'ListItem.Switch': ListItem.Switch, 'ListItem.AvatarView': ListItem.AvatarView, 'ListItem.AvatarLayout': ListItem.AvatarLayout, 'ListItem.Image': ListItem.Image, }, title: 'Content/ListItem', args: { title: 'List item title', subtitle: 'Subtitle goes here', valueTitle: '100 GBP', valueSubtitle: '100 USD', disabled: false, disabledPromptMessage: undefined, spotlight: undefined, inverted: false, valueColumnWidth: undefined, }, argTypes: { spotlight: { control: 'radio', options: ['unset (undefined)', 'active', 'inactive'], mapping: { 'unset (undefined)': undefined, active: 'active', inactive: 'inactive', }, }, }, } satisfies Meta; type Story = StoryObj; /** * Convenience controls for previewing rich markup, * not otherwise possible via Storybook */ type PreviewStoryArgs = ListItemProps & { previewInteractivity: 'non-interactive' | 'fully-interactive' | 'partially-interactive'; previewControlFull: ListItemProps['control']; previewControlPartial: ListItemProps['control']; previewAdditionalInfoFull: ListItemProps['additionalInfo']; previewAdditionalInfoPartial: ListItemProps['additionalInfo']; previewPrompt: ListItemProps['prompt']; previewMedia: ListItemProps['media']; }; const previewArgGroup = { category: 'Storybook Preview options', type: { summary: undefined, }, }; const previewArgTypes = { previewInteractivity: { name: 'Preview different interactivity', control: 'radio', options: ['non-interactive', 'fully-interactive', 'partially-interactive'], table: previewArgGroup, }, previewControlFull: { name: 'Preview with `control`', if: { arg: 'previewInteractivity', eq: 'fully-interactive', }, control: 'select', options: [ 'unset (undefined)', 'Button', 'Button as anchor', 'IconButton', 'IconButton as anchor', 'Navigation', 'Navigation as button', 'Checkbox', 'Radio', 'Switch', ], mapping: { 'unset (undefined)': undefined, Button: CONTROLS.button, 'Button as anchor': CONTROLS.buttonAsLink, IconButton: CONTROLS.iconButton, 'IconButton as anchor': CONTROLS.iconButtonAsLink, Navigation: CONTROLS.navigation, 'Navigation as button': CONTROLS.navigationAsButton, Checkbox: CONTROLS.checkbox, Radio: CONTROLS.radio, Switch: CONTROLS.switch, }, table: previewArgGroup, }, previewControlPartial: { name: 'Preview with `control`', if: { arg: 'previewInteractivity', eq: 'partially-interactive', }, control: 'select', options: [ 'unset (undefined)', 'Button', 'Button as anchor', 'IconButton', 'IconButton as anchor', ], mapping: { 'unset (undefined)': undefined, Button: CONTROLS.partialButton, 'Button as anchor': CONTROLS.partialButtonAsLink, IconButton: CONTROLS.partialIconButton, 'IconButton as anchor': CONTROLS.partialIconButtonAsLink, }, table: previewArgGroup, }, previewAdditionalInfoFull: { name: 'Preview with `additionalInfo`', if: { arg: 'previewInteractivity', eq: 'fully-interactive', }, control: 'radio', options: ['unset (undefined)', 'non-interactive'], mapping: { 'unset (undefined)': undefined, 'non-interactive': ADDITIONAL_INFO.nonInteractive, }, table: previewArgGroup, }, previewAdditionalInfoPartial: { name: 'Preview with `additionalInfo`', if: { arg: 'previewInteractivity', eq: 'partially-interactive', }, control: 'radio', options: ['unset (undefined)', 'interactive', 'non-interactive'], mapping: { 'unset (undefined)': undefined, interactive: ADDITIONAL_INFO.interactive, 'non-interactive': ADDITIONAL_INFO.nonInteractive, }, table: previewArgGroup, }, previewPrompt: { name: 'Preview with `prompt`', control: 'radio', options: ['unset (undefined)', 'interactive', 'non-interactive'], mapping: { 'unset (undefined)': undefined, interactive: PROMPTS.interactive, 'non-interactive': PROMPTS.nonInteractive, }, table: previewArgGroup, }, previewMedia: { name: 'Preview with `media`', control: 'select', options: [ 'unset (undefined)', 'image', 'avatar: single', 'avatar: double', 'avatar: double-diagonal', ], mapping: { 'unset (undefined)': undefined, image: MEDIA.image, 'avatar: single': MEDIA.avatarSingle, 'avatar: double': MEDIA.avatarDouble, 'avatar: double-diagonal': MEDIA.avatarDiagonal, }, table: previewArgGroup, }, } as const; const getPropsForPreview = (args: PreviewStoryArgs): [ListItemProps, Partial] => { const { previewInteractivity, previewControlFull, previewControlPartial, previewAdditionalInfoFull, previewAdditionalInfoPartial, previewPrompt, previewMedia, ...props } = args; return [ props, { control: previewControlFull || previewControlPartial, additionalInfo: previewAdditionalInfoFull || previewAdditionalInfoPartial, prompt: previewPrompt, media: previewMedia, }, ]; }; export const Playground: StoryObj = { render: (args: PreviewStoryArgs) => { const [props, previewProps] = getPropsForPreview(args); return ( ); }, argTypes: previewArgTypes, args: { previewInteractivity: 'fully-interactive', previewPrompt: 'interactive', previewMedia: 'avatar: single', previewControlFull: 'Button', previewControlPartial: 'Button', previewAdditionalInfoFull: 'non-interactive', previewAdditionalInfoPartial: 'non-interactive', }, decorators: [withoutKey], }; export const Responsiveness: StoryObj = { parameters: { docs: { canvas: { sourceState: 'hidden', }, description: { story: `ListItem uses container queries under the hood, which means its layout self-adjusts based on the available space. At the 100% zoom level, these breakpoints correspond to containers under \`${LISTITEM_CQ.MIN}px\`, those between \`${LISTITEM_CQ.MIN + 1}px and ${LISTITEM_CQ.MAX}px\`, and finally everything including and above \`${LISTITEM_CQ.MAX + 1}px\`.\n\nThis also supports high levels of zoom for assistive tech users.`, }, }, }, argTypes: { ...hideControls(), ...previewArgTypes, }, args: { subtitle: lorem10, previewInteractivity: 'fully-interactive', previewPrompt: 'interactive', previewMedia: 'image', previewControlFull: 'Button', previewControlPartial: 'Button', previewAdditionalInfoFull: 'non-interactive', previewAdditionalInfoPartial: 'non-interactive', }, decorators: [withoutKey], render: (args) => { const [props, previewProps] = getPropsForPreview(args); return ( `${width}px`) .join(' '), gap: 16, }} > ); }, }; /** * Due to platform limitations, the component is unable to automagically adjust its content to * all possible configurations.
* By default, it'll follow a CSS grid `1fr max-content` ratio of left side content (`title` and * `subtitle`) to the right side content (`valueTitle` and `valueSubtitle`). * * To adjust the width of the right side content, you can use `valueColumnWidth` prop which accepts * 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`. * * **NB:** This behaviour is slightly different on mobile platforms as they have more control over * layout calculations. */ export const ContentRatio: StoryObj = { parameters: { docs: { canvas: { sourceState: 'hidden', }, }, }, args: { valueColumnWidth: 70, subtitle: lorem5, valueTitle: `${lorem5} ${lorem5}`, valueSubtitle: lorem5, previewInteractivity: 'fully-interactive', previewPrompt: 'interactive', previewMedia: 'image', previewControlFull: 'Button', previewControlPartial: 'Button', previewAdditionalInfoFull: 'non-interactive', previewAdditionalInfoPartial: 'non-interactive', as: 'div', }, argTypes: { ...hideControls(['disabled', 'spotlight']), ...previewArgTypes, }, render: (args) => { const [props, previewProps] = getPropsForPreview(args); return ; }, }; /** * List item supports default and inverted title and description to allow for further flexibility * in the component. No other content elements are affected by this prop. * * Refer to the [design documentation](https://wise.design/components/list-item#content-hierarchy) for more details. */ export const ContentHierarchy: StoryObj = { args: { inverted: true, previewInteractivity: 'fully-interactive', previewPrompt: 'interactive', previewMedia: 'image', previewControlFull: 'Button', previewControlPartial: 'Button', previewAdditionalInfoFull: 'non-interactive', previewAdditionalInfoPartial: 'non-interactive', as: 'div', }, argTypes: { ...hideControls(['valueColumnWidth', 'disabled']), ...previewArgTypes, }, render: (args) => { const [props, previewProps] = getPropsForPreview(args); return ; }, }; /** * Interactive list items can alternatively be displayed with a spotlight. * * Refer to the [design documentation](https://wise.design/components/list-item#spotlight) for more details. */ export const Spotlight: StoryObj = { argTypes: { ...hideControls(['spotlight', 'valueColumnWidth']), ...previewArgTypes, }, args: { subtitle: lorem10, previewInteractivity: 'fully-interactive', previewPrompt: 'interactive', previewMedia: 'image', previewControlFull: 'Button', previewControlPartial: 'Button', previewAdditionalInfoFull: 'non-interactive', previewAdditionalInfoPartial: 'non-interactive', }, decorators: [withoutKey], render: (args) => { const [props, previewProps] = getPropsForPreview(args); return ( ); }, }; /** * By default, all list items are fully interactive, meaning that their whole surface triggers the * main action. The only 2 exceptions are those using `ListItem.Button` and `ListItem.IconButton` * as their controls, which can be made partially interactive by toggling `partiallyInteractive` * prop directly on them. * * Fully interactive list items **cannot contain any other interactive elements**, such as links or * buttons, inside them, with the only exception being the `ListItem.Prompt` subcomponent, as it's * rendered on a separate Accessibility Tree branch.
* Partially interactive items can also have interactive `ListItem.AdditionalInfo` which allows * for a single, appended link or inline button. * * All of these limitations were put in place to ensure that the list item is compliant with * accessibility guidance and offers consistent experience across all engineering platforms. * * Please refer to the [design documentation](https://wise.design/components/list-item#interaction) for more details. */ export const Interactivity: StoryObj = storySourceWithoutNoise({ argTypes: { ...hideControls([ 'title', 'subtitle', 'valueTitle', 'valueSubtitle', 'valueColumnWidth', 'inverted', ]), }, args: { subtitle: lorem10, }, decorators: [withoutKey], render: function Render(args) { const [clicked, setClicked] = useState(0); const renderPrompt = () => ( Finally, there is a prompt{' '} referencing some details {' '} for your convenience. ); const renderInteractiveInfo = () => ( Which is augmented with additional info. ); const renderStaticInfo = () => ( Which is augmented with additional info. ); return ( <> Count: {clicked} setClicked((current) => current + 1)} > as Button } additionalInfo={renderStaticInfo()} prompt={renderPrompt()} /> setClicked((current) => current + 1)} > as Button } additionalInfo={renderInteractiveInfo()} prompt={renderPrompt()} /> setClicked((current) => current + 1)} > } additionalInfo={renderStaticInfo()} prompt={renderPrompt()} /> setClicked((current) => current + 1)} > } additionalInfo={renderInteractiveInfo()} prompt={renderPrompt()} /> ); }, }); /** * For backwards compatibility, all of the ListItem's content elements accept `ReactNode`. * That said, most of them should be fed with plain text only. The exception is `valueTitle` * and `valueSubtitle` which can be augmented with sentiment colours, strikethrough or bold * styles.
* * Please refer to the [design documentation](https://wise.design/components/list-item#content) for more details. */ export const StylingLabels: Story = { argTypes: { ...hideControls(['spotlight', 'valueColumnWidth']), }, args: { title: lorem5, subtitle: lorem10, media: MEDIA.image, control: CONTROLS.iconButton, prompt: PROMPTS.interactive, }, decorators: [withoutKey], render: function Render(args) { return ( } valueSubtitle={125 GBP} /> } valueSubtitle={125 GBP} /> ); }, };