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 (
<>
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}
/>
);
},
};