Skip to content

Commit 969ee58

Browse files
lisalupimatthprost
andcommitted
feat: offerList (#5330)
* feat: offer list * fix: use offer name * fix: rebase * fix: feedback * fix: export offer list * fix: rebase * fix: feedback * fix: feedback and add badge * fix: feedback and add badge * fix: add 'selected' prop and tests * fix: visual feedbacks --------- Co-authored-by: Matthias <[email protected]>
1 parent 9f2defc commit 969ee58

26 files changed

+14135
-2
lines changed

.changeset/whole-rules-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ultraviolet/plus": patch
3+
---
4+
5+
New component `OfferList`
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use client'
2+
3+
import styled from '@emotion/styled'
4+
import { List } from '@ultraviolet/ui'
5+
import { useEffect, useState } from 'react'
6+
import type { ComponentProps } from 'react'
7+
import { OfferListProvider } from './OfferListProvider'
8+
import { Cell } from './components/Cell'
9+
import { Row } from './components/Row'
10+
11+
const StyledList = styled(List)`
12+
td:first-child,
13+
th:first-child {
14+
width: ${({ theme }) => theme.sizing[700]};
15+
min-width: ${({ theme }) => theme.sizing[700]};
16+
max-width: ${({ theme }) => theme.sizing[700]};
17+
}
18+
`
19+
20+
type OfferListProps = Omit<
21+
ComponentProps<typeof List>,
22+
'selectable' | 'onSelectedChange'
23+
> & {
24+
/**
25+
* Make offerList selectable by choosing its type
26+
*/
27+
type?: 'radio' | 'checkbox'
28+
onChangeSelect?: (selected: string | string[]) => void
29+
}
30+
31+
export const OfferList = ({
32+
expandable,
33+
type = 'radio',
34+
columns,
35+
children,
36+
loading,
37+
autoCollapse,
38+
onChangeSelect,
39+
}: OfferListProps) => {
40+
const [selectedRows, setSelectedRows] = useState<string[]>([])
41+
const computedColumns = [
42+
{
43+
label: '',
44+
},
45+
expandable ? { label: '' } : null,
46+
...columns,
47+
].filter(element => !!element)
48+
49+
useEffect(
50+
() => onChangeSelect?.(selectedRows),
51+
[selectedRows, onChangeSelect],
52+
)
53+
54+
return (
55+
<OfferListProvider
56+
selectable={type}
57+
expandable={expandable}
58+
loading={loading}
59+
onChangeSelect={onChangeSelect}
60+
autoCollapse={autoCollapse}
61+
>
62+
<StyledList
63+
expandable={false}
64+
columns={computedColumns}
65+
autoCollapse={autoCollapse}
66+
onSelectedChange={setSelectedRows}
67+
selectable={false}
68+
>
69+
{children}
70+
</StyledList>
71+
</OfferListProvider>
72+
)
73+
}
74+
75+
OfferList.Row = Row
76+
OfferList.Cell = Cell
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { createContext, useContext, useState } from 'react'
2+
import type { Dispatch, ReactNode, SetStateAction } from 'react'
3+
4+
type OfferListContextValue = {
5+
selectable: 'radio' | 'checkbox'
6+
radioSelectedRow: string | undefined
7+
setRadioSelectedRow: Dispatch<SetStateAction<string | undefined>>
8+
expandable?: boolean
9+
disabled?: boolean
10+
loading?: boolean
11+
onChangeSelect?: (selected: string | string[]) => void
12+
autoCollapse?: boolean
13+
checkboxSelectedRows: Record<string | number, boolean>
14+
setCheckboxSelectedRows: Dispatch<
15+
SetStateAction<Record<string | number, boolean>>
16+
>
17+
}
18+
const OfferListContext = createContext<OfferListContextValue | undefined>(
19+
undefined,
20+
)
21+
22+
type OfferListProviderProps = {
23+
selectable: 'radio' | 'checkbox'
24+
children: ReactNode
25+
expandable?: boolean
26+
disabled?: boolean
27+
loading?: boolean
28+
onChangeSelect?: (selected: string | string[]) => void
29+
autoCollapse?: boolean
30+
}
31+
32+
export const OfferListProvider = ({
33+
selectable,
34+
children,
35+
expandable,
36+
disabled,
37+
loading,
38+
onChangeSelect,
39+
autoCollapse,
40+
}: OfferListProviderProps) => {
41+
const [radioSelectedRow, setRadioSelectedRow] = useState<string>()
42+
const [checkboxSelectedRows, setCheckboxSelectedRows] = useState<
43+
Record<string | number, boolean>
44+
>({})
45+
46+
return (
47+
<OfferListContext.Provider
48+
value={{
49+
selectable,
50+
radioSelectedRow,
51+
setRadioSelectedRow,
52+
checkboxSelectedRows,
53+
setCheckboxSelectedRows,
54+
expandable,
55+
disabled,
56+
loading,
57+
onChangeSelect,
58+
autoCollapse,
59+
}}
60+
>
61+
{children}
62+
</OfferListContext.Provider>
63+
)
64+
}
65+
66+
export const useOfferListContext = () => {
67+
const context = useContext(OfferListContext)
68+
69+
if (!context) {
70+
throw new Error(
71+
'useOfferListContext should be used inside a OfferList component',
72+
)
73+
}
74+
75+
return context
76+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { StoryFn } from '@storybook/react-vite'
2+
import type { ComponentProps } from 'react'
3+
import { OfferList } from '../OfferList'
4+
import { columns, data } from './resources'
5+
6+
export const Badge: StoryFn<ComponentProps<typeof OfferList>> = props => (
7+
<OfferList {...props} expandable>
8+
{data.map((planet, index) =>
9+
index < 3 ? (
10+
<OfferList.Row
11+
key={planet.id}
12+
id={planet.id}
13+
offerName={planet.id}
14+
disabled={index === 2}
15+
expandable="Some text"
16+
badge={{
17+
text: 'I am a badge',
18+
sentiment: index === 1 ? 'primary' : 'neutral',
19+
}}
20+
>
21+
<OfferList.Cell>{planet.name}</OfferList.Cell>
22+
<OfferList.Cell>{planet.perihelion}AU</OfferList.Cell>
23+
<OfferList.Cell>{planet.aphelion}AU</OfferList.Cell>
24+
</OfferList.Row>
25+
) : null,
26+
)}
27+
</OfferList>
28+
)
29+
30+
Badge.args = {
31+
columns,
32+
}
33+
34+
Badge.parameters = {
35+
docs: {
36+
description: {
37+
story:
38+
'Use props `badge` to add a badge to the row. When a row is disabled, its badge is also disabled.',
39+
},
40+
},
41+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { StoryFn } from '@storybook/react-vite'
2+
import type { ComponentProps } from 'react'
3+
import { OfferList } from '../OfferList'
4+
import { columns, data } from './resources'
5+
6+
export const Banner: StoryFn<ComponentProps<typeof OfferList>> = props => (
7+
<OfferList {...props} expandable>
8+
{data.map((planet, index) =>
9+
index < 3 ? (
10+
<OfferList.Row
11+
key={planet.id}
12+
id={planet.id}
13+
offerName={planet.id}
14+
disabled={index === 2}
15+
expandable="Some text"
16+
banner={{
17+
text:
18+
index === 2
19+
? 'Disabled banner because row is disabled'
20+
: 'This is a banner',
21+
sentiment: planet.id === 'mercury' ? 'primary' : undefined,
22+
}}
23+
>
24+
<OfferList.Cell>{planet.name}</OfferList.Cell>
25+
<OfferList.Cell>{planet.perihelion}AU</OfferList.Cell>
26+
<OfferList.Cell>{planet.aphelion}AU</OfferList.Cell>
27+
</OfferList.Row>
28+
) : null,
29+
)}
30+
</OfferList>
31+
)
32+
33+
Banner.args = {
34+
columns,
35+
}
36+
37+
Banner.parameters = {
38+
docs: {
39+
description: {
40+
story:
41+
'Use props `banner` to add a banner. When a row is disabled, its banner is also disabled.',
42+
},
43+
},
44+
}

0 commit comments

Comments
 (0)