Skip to content

Commit

Permalink
adds ListFormat component
Browse files Browse the repository at this point in the history
  • Loading branch information
langz committed Nov 7, 2024
1 parent d4e4334 commit 904d799
Show file tree
Hide file tree
Showing 22 changed files with 1,127 additions and 84 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: 'ListFormat'
description: 'A ready to use DNB list formatter.'
showTabs: true
tabs:
- title: Info
key: /uilib/components/list-format/info
- title: Demos
key: /uilib/components/list-format/demos
- title: Properties
key: /uilib/components/list-format/properties
theme: 'sbanken'
status: 'new'
---

import ListFormatInfo from 'Docs/uilib/components/list-format/info'
import ListFormatDemos from 'Docs/uilib/components/list-format/demos'

<ListFormatInfo />
<ListFormatDemos />
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* UI lib Component Example
*
*/

import React from 'react'
import ComponentBox from '../../../../shared/tags/ComponentBox'
import { Provider } from '@dnb/eufemia/src/shared'
import { ListFormat, P } from '@dnb/eufemia/src'

export const WithValue = () => {
return (
<ComponentBox data-visual-test="list-format-default">
<ListFormat value={['Foo', 'Bar', 'Baz']} />
</ComponentBox>
)
}

export const WithCustomFormat = () => {
return (
<ComponentBox data-visual-test="list-format-custom-format">
<Provider locale="en-GB" data={{ myPath: [123, 456, 789] }}>
<ListFormat
value={[123, 456, 789]}
format={{ type: 'disjunction' }}
/>
</Provider>
</ComponentBox>
)
}

export const Inline = () => {
return (
<ComponentBox data-visual-test="list-format-inline">
<P>
This is before the component{' '}
<ListFormat value={['Foo', 'Bar', 'Baz']} /> This is after the
component
</P>
</ComponentBox>
)
}

export const ListVariants = () => {
return (
<ComponentBox data-visual-test="list-format-variants">
<P>Ordered List:</P>
<ListFormat value={['Foo', 'Bar', 'Baz']} variant="ol" />
<P>Unordered List:</P>
<ListFormat value={['Foo', 'Bar', 'Baz']} variant="ul" />
</ComponentBox>
)
}

export const ListTypes = () => {
return (
<ComponentBox data-visual-test="list-format-types">
<P>Ordered List a:</P>
<ListFormat
value={['Foo', 'Bar', 'Baz']}
variant="ol"
listType="a"
/>
<P>Ordered List A:</P>
<ListFormat
value={['Foo', 'Bar', 'Baz']}
variant="ol"
listType="A"
/>
<P>Ordered List i:</P>
<ListFormat
value={['Foo', 'Bar', 'Baz']}
variant="ol"
listType="i"
/>
<P>Ordered List I:</P>
<ListFormat
value={['Foo', 'Bar', 'Baz']}
variant="ol"
listType="I"
/>
<P>Unordered List square:</P>
<ListFormat
value={['Foo', 'Bar', 'Baz']}
variant="ul"
listType="square"
/>
<P>Unordered List circle:</P>
<ListFormat
value={['Foo', 'Bar', 'Baz']}
variant="ul"
listType="circle"
/>
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
showTabs: true
---

import * as Examples from './Examples'

## Demos

### Value

<Examples.WithValue />

### Custom format

<Examples.WithCustomFormat />

### Inline

<Examples.Inline />

### List variants

<Examples.ListVariants />

### List types

<Examples.ListTypes />
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
showTabs: true
---

## Import

```tsx
import { ListFormat } from '@dnb/eufemia'
```

## Description

A ready-to-use list formatter. Use it wherever you have to display a list of strings, numbers, or React components (JSX).

Good reasons for why we have this is to:

- uniform the creation and formatting of lists.
- Supports translation and localization.
- Built on top of web standards.

The component is designed to maximum display 10-20 items.
If you need to display more items than that, consider a different design, and perhaps using [Pagination](/uilib/components/pagination) and/or [InfinityScroller](/uilib/components/pagination/infinity-scroller)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
showTabs: true
---

import TranslationsTable from 'dnb-design-system-portal/src/shared/parts/TranslationsTable'
import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
import { ListFormatProperties } from '@dnb/eufemia/src/components/list-format/ListFormatDocs'

## Properties

<PropertiesTable props={ListFormatProperties} />
2 changes: 2 additions & 0 deletions packages/dnb-eufemia/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import IconPrimary from './icon-primary/IconPrimary'
import InfoCard from './info-card/InfoCard'
import Input from './input/Input'
import InputMasked from './input-masked/InputMasked'
import ListFormat from './list-format/ListFormat'
import Logo from './logo/Logo'
import Modal from './modal/Modal'
import NumberFormat from './number-format/NumberFormat'
Expand Down Expand Up @@ -99,6 +100,7 @@ export {
InfoCard,
Input,
InputMasked,
ListFormat,
Logo,
Modal,
NumberFormat,
Expand Down
2 changes: 2 additions & 0 deletions packages/dnb-eufemia/src/components/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import IconPrimary from './icon-primary/IconPrimary'
import InfoCard from './info-card/InfoCard'
import Input from './input/Input'
import InputMasked from './input-masked/InputMasked'
import ListFormat from './list-format/ListFormat'
import Logo from './logo/Logo'
import Modal from './modal/Modal'
import NumberFormat from './number-format/NumberFormat'
Expand Down Expand Up @@ -157,6 +158,7 @@ export const getComponents = () => {
InfoCard,
Input,
InputMasked,
ListFormat,
Logo,
Modal,
NumberFormat,
Expand Down
160 changes: 160 additions & 0 deletions packages/dnb-eufemia/src/components/list-format/ListFormat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import React, { useContext, useMemo } from 'react'
import { LOCALE } from '../../shared/defaults'
import {
convertJsxToString,
extendPropsWithContext,
} from '../../shared/component-helper'
import SharedContext, { InternalLocale } from '../../shared/Context'
import { Li, Ol, Ul } from '../../elements'

export type ListFormatProps = {
/**
* Formatting options for the value.
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat
*/
format?: Intl.ListFormatOptions
/**
* Defines if the value should be displayed in list format or regular text format on one line.
* Default: `text`
*/
variant?: 'ol' | 'ul' | 'text'
/**
* Defines the type of list styling used for list variants. Used together with variant `ol` and `ul`.
* Variant `ol`: `a`, `A`, `i`, `I` and `1`.
* Variant `ul`: `circle`, `disc` and `square`.
* Default: `undefined`
*/
listType?:
| 'a'
| 'A'
| 'i'
| 'I'
| '1'
| 'circle'
| 'disc'
| 'square'
| undefined

/**
* The value to format as list.
* Default: null
*/
value?: Array<number | string | React.ReactNode>

/**
* The children to format as list.
* Default: null
*/
children?: React.ReactNode
}

export const defaultProps = {}

function ListFormat(localProps: ListFormatProps) {
const { locale, ListFormat } = useContext(SharedContext)

// Extract additional props from global context
const allProps = extendPropsWithContext(
localProps,
defaultProps,
ListFormat
)
const { value, format, variant = 'text', listType, children } = allProps

const list = useMemo(() => {
const isListVariant = variant !== 'text'
if (children) {
return isListVariant
? React.Children.map(children, (child: React.ReactNode, index) => {
return <Li key={index}>{child}</Li>
})
: children
}
return isListVariant
? value.map((value, index) => (
<Li key={index}>
{React.isValidElement(value)
? value
: convertJsxToString(value)}
</Li>
))
: value
}, [value, children, variant])

const result = useMemo(() => {
if (variant === 'text') {
return listFormat(list, { locale, format })
}

const ListElement = variant.startsWith('ol') ? Ol : Ul

return <ListElement type={listType}>{list}</ListElement>
}, [format, list, locale, variant, listType])

return result
}

// Support for "ListFormat.format(list)" for non-React usage
ListFormat.format = listFormat

export function listFormat(
list: Array<React.ReactNode> | React.ReactNode,
{
locale = LOCALE,
format = {
style: 'long',
type: 'conjunction',
},
}: {
locale?: InternalLocale
format?: Intl.ListFormatOptions
} = {}
) {
if (!Array.isArray(list)) {
return list
}

const buffer = new Map()
const hasJSX = list.some((v) => typeof v === 'object')
const shadow = list.map((v, i) => {
if (hasJSX) {
const id = `id-${i}`
buffer.set(id, v)
return `{${id}}`
}

return String(v)
})

try {
const formatter = new Intl.ListFormat(locale, format)
const formattedList = formatter.format(shadow)

if (hasJSX) {
return formattedList.split(/\{(id-[0-9]+)\}/).map((v, i) => {
if (v.startsWith('id-')) {
const element = buffer.get(v)

return element.key
? element
: // Support lists without a key
React.createElement(React.Fragment, { key: i }, element)
}

return v
})
}

return formattedList
} catch (error) {
if (hasJSX) {
return list
}

return list.join(', ')
}
}

ListFormat._supportsSpacingProps = true

export default ListFormat
Loading

0 comments on commit 904d799

Please sign in to comment.