diff --git a/package-lock.json b/package-lock.json index 9db5b061..a36690c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aboutbits/react-ui", - "version": "2.6.6", + "version": "2.6.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@aboutbits/react-ui", - "version": "2.6.6", + "version": "2.6.8", "dependencies": { "@aboutbits/pagination": "^2.0.0", "@aboutbits/react-material-icons": "^1.1.2", diff --git a/package.json b/package.json index a297f66f..2d2cf6bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aboutbits/react-ui", - "version": "2.6.6", + "version": "2.6.8", "types": "./dist/types/index.d.ts", "module": "./dist/esm/index.js", "main": "./dist/cjs/index.js", diff --git a/src/components/content/WithPlaceholder/WithPlacehoder.stories.tsx b/src/components/content/WithPlaceholder/WithPlacehoder.stories.tsx new file mode 100644 index 00000000..eb1147c4 --- /dev/null +++ b/src/components/content/WithPlaceholder/WithPlacehoder.stories.tsx @@ -0,0 +1,52 @@ +import { + Controls, + Description, + Primary, + Stories, + Subheading, + Title, +} from '@storybook/blocks' +import { Meta, StoryObj } from '@storybook/react' +import { WithPlaceholder } from './WithPlaceholder' + +const children = { + options: ['Null', 'Undefined', 'Empty text', 'ReactNode'], + mapping: { + Null: null, + Undefined: undefined, + 'Empty text': '', + ReactNode: 'John Doe', + }, + control: { type: 'select' }, +} + +const meta = { + title: 'Components/Content/WithPlaceHolder', + component: WithPlaceholder, + args: { + placeholder: '-', + children: 'John Doe', + }, + argTypes: { + children, + }, + parameters: { + docs: { + page: () => ( + <> + + <Description /> + <Primary /> + <Subheading>Props</Subheading> + <Controls /> + <Stories /> + </> + ), + }, + }, +} satisfies Meta<typeof WithPlaceholder> +export default meta + +type Story = StoryObj<typeof meta> + +export const Default: Story = {} diff --git a/src/components/content/WithPlaceholder/WithPlaceholder.tsx b/src/components/content/WithPlaceholder/WithPlaceholder.tsx new file mode 100644 index 00000000..8dd068f3 --- /dev/null +++ b/src/components/content/WithPlaceholder/WithPlaceholder.tsx @@ -0,0 +1,26 @@ +import { PropsWithChildren, ReactNode } from 'react' + +export type WithPlaceholderProps = PropsWithChildren<{ + /** + * Defines the placeholder to be rendered if the children is not valid. + */ + placeholder?: ReactNode +}> + +/** + * This component validates the content and displays a placeholder if the content is empty, null or undefined. + */ +export function WithPlaceholder({ + placeholder = '-', + children, +}: WithPlaceholderProps) { + return ( + <> + {typeof children === 'number' + ? isNaN(children) + ? placeholder + : children + : children || placeholder} + </> + ) +} diff --git a/src/components/content/index.ts b/src/components/content/index.ts index 617ae4a0..959a5cb9 100644 --- a/src/components/content/index.ts +++ b/src/components/content/index.ts @@ -11,3 +11,4 @@ export * from './DescriptionItem/DescriptionItemTitle' export * from './DescriptionItem/DescriptionItemContent' export * from './DescriptionItem/LoadingDescriptionItem' export * from './DescriptionItem/types' +export * from './WithPlaceholder/WithPlaceholder' diff --git a/src/components/section/SectionItem/SectionListItem.tsx b/src/components/section/SectionItem/SectionListItem.tsx index 50bd72c1..96d7dcde 100644 --- a/src/components/section/SectionItem/SectionListItem.tsx +++ b/src/components/section/SectionItem/SectionListItem.tsx @@ -22,19 +22,32 @@ export function SectionListItem({ className, children }: SectionListItemProps) { ) } +function SectionListItemIcon() { + const { section } = useTheme() + + return ( + <IconKeyboardArrowRight + width="24" + height="24" + className={section.listItemButtonLink.icon} + /> + ) +} + export type SectionListItemButtonProps = ClassNameProps & { /** * On click handler for the button. */ onClick: () => void children?: ReactNode + withIcon?: boolean } export const SectionListItemButton = forwardRef< HTMLButtonElement, SectionListItemButtonProps >(function SectionListItemButton( - { children, onClick, className, ...props }, + { children, onClick, className, withIcon = true, ...props }, ref, ) { const { section } = useTheme() @@ -43,30 +56,28 @@ export const SectionListItemButton = forwardRef< <button onClick={onClick} className={classNames( - section.listItemButton.base, section.listItem.base, + section.listItemButtonLink.base, className, )} ref={ref} {...props} > {children} - <IconKeyboardArrowRight - width="24" - height="24" - className={section.listItemButton.icon} - /> + {withIcon && <SectionListItemIcon />} </button> ) }) -export type SectionListItemLinkProps = LinkComponentProps +export type SectionListItemLinkProps = LinkComponentProps & { + withIcon?: boolean +} export const SectionListItemLink = forwardRef< HTMLAnchorElement, SectionListItemLinkProps >(function SectionListItemLink( - { children, className, internal = true, ...props }, + { children, className, internal = true, withIcon = true, ...props }, ref, ) { const LinkComponent = useLinkComponent() @@ -75,8 +86,8 @@ export const SectionListItemLink = forwardRef< return ( <LinkComponent className={classNames( - section.listItemLink.base, section.listItem.base, + section.listItemButtonLink.base, className, )} internal={internal} @@ -84,11 +95,7 @@ export const SectionListItemLink = forwardRef< {...props} > {children} - <IconKeyboardArrowRight - width="24" - height="24" - className={section.listItemButton.icon} - /> + {withIcon && <SectionListItemIcon />} </LinkComponent> ) }) diff --git a/src/components/section/theme.ts b/src/components/section/theme.ts index 464e1325..4c8c187a 100644 --- a/src/components/section/theme.ts +++ b/src/components/section/theme.ts @@ -30,7 +30,7 @@ export default { base: 'grid xl:grid-cols-2 xl:gap-x-11 gap-y-6 px-4 md:px-6 pt-4 md:pt-6 pb-8 md:pb-9', }, listItem: { - base: 'flex border-b border-neutral-200 last:border-0 items-center min-h-[3.5rem] bg-white px-4 md:px-6', + base: 'flex border-b border-neutral-200 last:border-0 items-center py-4 bg-white px-4 md:px-6', }, listItemWithAction: { base: 'justify-between space-x-4', @@ -38,13 +38,10 @@ export default { base: 'flex flex-shrink-0', }, }, - listItemButton: { + listItemButtonLink: { base: 'block w-full focus:outline-neutral-800 justify-between space-x-4 hover:bg-neutral-100 active:bg-neutral-100', icon: 'fill-current', }, - listItemLink: { - base: 'block focus:outline-neutral-800 justify-between space-x-4 hover:bg-neutral-100 active:bg-neutral-100', - }, subsectionTitle: { base: 'bg-neutral-100 px-4 md:px-6 py-1 border-b border-t first:border-t-none border-neutral-200 text-sm text-neutral-600', }, diff --git a/src/examples/List.stories.tsx b/src/examples/List.stories.tsx index decaaab7..1a7597ff 100644 --- a/src/examples/List.stories.tsx +++ b/src/examples/List.stories.tsx @@ -29,6 +29,8 @@ import { Option, useFilter, SearchField, + SectionListItemButtonProps, + SectionListItemLink, } from '../components' const meta = { @@ -66,7 +68,13 @@ function useMockedList(numberOfTotalItems: number) { ) } -const List = ({ numberOfTotalItems = 5 }: { numberOfTotalItems?: number }) => { +const List = ({ + numberOfTotalItems = 5, + withIcon, +}: { numberOfTotalItems?: number } & Pick< + SectionListItemButtonProps, + 'withIcon' +>) => { const content = useMockedList(numberOfTotalItems) return ( <Section> @@ -79,14 +87,25 @@ const List = ({ numberOfTotalItems = 5 }: { numberOfTotalItems?: number }) => { /> ) : ( <SectionContentList> - {content.map((item) => ( - <SectionListItemButton - key={item.name} - onClick={action('onItemClick')} - > - {`${item.name} (${item.role} - ${item.department})`} - </SectionListItemButton> - ))} + {content.map((item, index) => + index % 2 === 0 ? ( + <SectionListItemButton + key={item.name} + onClick={action('onItemClick')} + withIcon={withIcon} + > + {`Button ${item.name} (${item.role} - ${item.department})`} + </SectionListItemButton> + ) : ( + <SectionListItemLink + key={item.name} + href="#" + withIcon={withIcon} + > + {`Link ${item.name} (${item.role} - ${item.department})`} + </SectionListItemLink> + ), + )} </SectionContentList> )} </SectionContainer> @@ -98,6 +117,8 @@ export const SimpleList: Story = () => <List /> export const EmptySimpleList: Story = () => <List numberOfTotalItems={0} /> +export const ListWithoutIcon: Story = () => <List withIcon={false} /> + /** * The following example shows how multiple section components and the in memory pagination are used to create an overview list with filters. */