Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.

Commit e4bed08

Browse files
author
Anton
authored
feat(expandableList): add accordion property to the component (#125)
-Adds a new `accordion` property to the component in which only one item could be expanded. - Use expanded tooltip for the cropped labels in the expandable list - Re-use `useHasOverflow` hooks from `react-hooks-shareable`
1 parent ea0dfbe commit e4bed08

File tree

4 files changed

+145
-42
lines changed

4 files changed

+145
-42
lines changed

packages/core/src/ExpandableList/ExpandableListItem.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, FC, Dispatch, SetStateAction } from 'react'
1+
import React, { useCallback, Dispatch, SetStateAction } from 'react'
22
import styled from 'styled-components'
33

44
import { FoldTransition } from '../Transition'
@@ -34,6 +34,7 @@ export interface ExpandableListItemProps extends BaseProps {
3434
readonly expandedItems: ReadonlyArray<string>
3535
readonly setExpandedItems: Dispatch<SetStateAction<ReadonlyArray<string>>>
3636
readonly isNestedItem: boolean
37+
readonly isAccordion: boolean
3738
}
3839

3940
/**
@@ -45,23 +46,34 @@ export interface ExpandableListItemProps extends BaseProps {
4546
*
4647
*/
4748

48-
export const ExpandableListItem: FC<ExpandableListItemProps> = ({
49+
export const ExpandableListItem: React.VFC<ExpandableListItemProps> = ({
4950
item,
5051
expandedItems,
5152
setExpandedItems,
5253
isNestedItem,
54+
isAccordion,
5355
...props
5456
}) => {
5557
const { id, label, icon, selected = false, onClick, items } = item
5658
const onChildClick = useCallback(
5759
() =>
5860
((itemId: string) => {
59-
const nextExpandedItems = expandedItems.includes(itemId)
60-
? expandedItems.filter(i => i !== itemId)
61-
: [...expandedItems, id]
61+
let nextExpandedItems = []
62+
63+
if (expandedItems.includes(itemId)) {
64+
// Close the expanded item
65+
nextExpandedItems = expandedItems.filter(i => i !== itemId)
66+
} else if (isAccordion) {
67+
// Only add one expanded item when accordion
68+
nextExpandedItems = [id]
69+
} else {
70+
// Extend expanded items with a new one
71+
nextExpandedItems = [...expandedItems, id]
72+
}
73+
6274
setExpandedItems(nextExpandedItems)
6375
})(id),
64-
[expandedItems, id, setExpandedItems]
76+
[expandedItems, id, isAccordion, setExpandedItems]
6577
)
6678
const hasChildren = items !== undefined
6779
const onItemClick = hasChildren ? onChildClick : onClick
@@ -93,6 +105,7 @@ export const ExpandableListItem: FC<ExpandableListItemProps> = ({
93105
expandedItems={expandedItems}
94106
setExpandedItems={setExpandedItems}
95107
isNestedItem={true}
108+
isAccordion={isAccordion}
96109
/>
97110
))}
98111
</ExpandableListContainer>

packages/core/src/ExpandableList/ListItemContainer.tsx

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React, { useCallback, useRef, useState, useLayoutEffect } from 'react'
2-
1+
import React, { useCallback } from 'react'
32
import styled, { css } from 'styled-components'
4-
import { shape, spacing } from '../designparams'
3+
import { useHasOverflow } from 'react-hooks-shareable'
54

5+
import { shape, spacing } from '../designparams'
66
import { Icon, IconType } from '../Icon'
77
import { Typography } from '../Typography'
8-
import { Tooltip } from '../Tooltip'
8+
import { Tooltip, ExpandedTooltipTypography } from '../Tooltip'
99

1010
interface ListItemMarkerBaseProps {
1111
readonly selected: boolean
@@ -113,7 +113,9 @@ const ListItemMarker = styled.div<ListItemMarkerProps>`
113113
`}
114114
`
115115

116-
const Label = styled(Typography)`
116+
const Label = styled(Typography).attrs({
117+
variant: 'default-text',
118+
})`
117119
margin-right: ${spacing.huge};
118120
white-space: nowrap;
119121
`
@@ -160,27 +162,20 @@ interface OverflowTooltipProps {
160162
* Used to show a tooltip if label is too long to be fully shown in container.
161163
*/
162164
const LabelOverflowTooltip: React.FC<OverflowTooltipProps> = ({ label }) => {
163-
const [hasOverflow, setHasOverflow] = useState(false)
164-
const labelRef = useRef<HTMLDivElement>(null)
165-
166-
useLayoutEffect(() => {
167-
if (labelRef.current === null) {
168-
return
169-
}
170-
171-
setHasOverflow(
172-
labelRef.current.offsetHeight < labelRef.current.scrollHeight ||
173-
labelRef.current.offsetWidth < labelRef.current.scrollWidth
174-
)
175-
}, [labelRef])
176-
177-
const text = (
178-
<Label ref={labelRef} variant="default-text">
179-
{label}
180-
</Label>
181-
)
165+
const { hasOverflow, ref } = useHasOverflow()
182166

183-
return hasOverflow ? <Tooltip text={label}>{text}</Tooltip> : text
167+
const text = <Label ref={ref}>{label}</Label>
168+
169+
return hasOverflow ? (
170+
<Tooltip
171+
variant="expanded"
172+
contents={<ExpandedTooltipTypography>{label}</ExpandedTooltipTypography>}
173+
>
174+
<Label>{label}</Label>
175+
</Tooltip>
176+
) : (
177+
text
178+
)
184179
}
185180

186181
/**
@@ -193,7 +188,7 @@ const LabelOverflowTooltip: React.FC<OverflowTooltipProps> = ({ label }) => {
193188
*
194189
*/
195190

196-
export const ListItemContainer: React.FC<ListItemContainerProps> = ({
191+
export const ListItemContainer: React.VFC<ListItemContainerProps> = ({
197192
selected,
198193
isNestedItem,
199194
hasChildren,

packages/core/src/ExpandableList/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, FC } from 'react'
1+
import React, { useState } from 'react'
22

33
import {
44
ExpandableListContainer,
@@ -20,10 +20,16 @@ interface ExpandableListProps extends BaseProps {
2020
* Used to create an array of items.
2121
*/
2222
readonly items: ReadonlyArray<ExpandableListItemType>
23+
/**
24+
* Whether the list should work as accordion
25+
* and allows only one expanded item at once.
26+
*/
27+
readonly accordion?: boolean
2328
}
2429

25-
export const ExpandableList: FC<ExpandableListProps> = ({
30+
export const ExpandableList: React.VFC<ExpandableListProps> = ({
2631
items,
32+
accordion,
2733
...props
2834
}) => {
2935
const [expandedItems, setExpandedItems] = useState<ReadonlyArray<string>>([])
@@ -36,6 +42,7 @@ export const ExpandableList: FC<ExpandableListProps> = ({
3642
item={item}
3743
expandedItems={expandedItems}
3844
setExpandedItems={setExpandedItems}
45+
isAccordion={accordion === true}
3946
isNestedItem={false}
4047
/>
4148
))}

packages/docs/src/mdx/coreComponents/ExpandableList.mdx

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useState } from 'react'
88
import styled from 'styled-components'
99

1010
import { DeviceIcon } from 'practical-react-components-icons'
11-
import { ExpandableList } from 'practical-react-components-core'
11+
import { ExpandableList, Typography } from 'practical-react-components-core'
1212

1313
# ExpandableList
1414

@@ -29,32 +29,32 @@ export const ITEMS = [
2929
selected: true,
3030
items: [
3131
{
32-
id: 'item-1',
32+
id: 'category-item-1',
3333
label: 'Item 1',
3434
icon: DeviceIcon,
3535
},
3636
{
37-
id: 'something-2',
37+
id: 'category-something-2',
3838
label: 'Something 2',
3939
icon: DeviceIcon,
4040
},
4141
{
42-
id: 'something-else-3',
42+
id: 'category-something-else-3',
4343
label: 'Something else 3',
4444
icon: DeviceIcon,
4545
},
4646
{
47-
id: 'entirely-different-thing',
47+
id: 'category-entirely-different-thing',
4848
label: 'Entirely different thing',
4949
icon: DeviceIcon,
5050
},
5151
{
52-
id: 'and-this-thing',
52+
id: 'category-and-this-thing',
5353
label: 'And this thing',
5454
icon: DeviceIcon,
5555
},
5656
{
57-
id: 'also-this',
57+
id: 'category-also-this',
5858
label: 'Also this',
5959
icon: DeviceIcon,
6060
},
@@ -64,13 +64,62 @@ export const ITEMS = [
6464
id: 'site-log',
6565
label: 'Site log',
6666
icon: DeviceIcon,
67+
items: [
68+
{
69+
id: 'site-log-item-1',
70+
label: 'Item 1',
71+
icon: DeviceIcon,
72+
},
73+
{
74+
id: 'site-log-something-2',
75+
label: 'Something 2',
76+
icon: DeviceIcon,
77+
},
78+
{
79+
id: 'site-log-something-else-3',
80+
label: 'Something else 3',
81+
icon: DeviceIcon,
82+
},
83+
{
84+
id: 'site-log-entirely-different-thing',
85+
label: 'Entirely different thing',
86+
icon: DeviceIcon,
87+
},
88+
{
89+
id: 'site-log-and-this-thing',
90+
label: 'And this thing',
91+
icon: DeviceIcon,
92+
},
93+
{
94+
id: 'site-log-also-this',
95+
label: 'Also this',
96+
icon: DeviceIcon,
97+
},
98+
],
99+
},
100+
{
101+
id: 'something-else',
102+
label: 'Something else',
103+
icon: DeviceIcon,
67104
},
68105
]
69106

70107
export const Container = styled.div`
71108
width: 256px;
72109
`
73110

111+
export const DemoContainer = styled.div`
112+
display: grid;
113+
grid-template-columns: 1fr 1fr;
114+
grid-gap: 32px;
115+
`
116+
117+
export const DemoContainerItem = styled.div`
118+
display: grid;
119+
grid-template-rows: auto 1fr;
120+
grid-gap: 16px;
121+
`
122+
74123
export const DemoComponent = ({}) => {
75124
const [selectedItem, setSelectedItem] = useState('none')
76125
const listItems = ITEMS.map(item => {
@@ -101,9 +150,48 @@ export const DemoComponent = ({}) => {
101150
)
102151
}
103152

153+
export const DemoComponentAccordion = ({}) => {
154+
const [selectedItem, setSelectedItem] = useState('none')
155+
const listItems = ITEMS.map(item => {
156+
return {
157+
...item,
158+
selected: selectedItem === item.id,
159+
onClick: () => {
160+
setSelectedItem(item.id)
161+
},
162+
items:
163+
item.items !== undefined
164+
? item.items.map(subItem => {
165+
return {
166+
...subItem,
167+
selected: selectedItem === subItem.id,
168+
onClick: () => {
169+
setSelectedItem(subItem.id)
170+
},
171+
}
172+
})
173+
: undefined,
174+
}
175+
})
176+
return (
177+
<Container>
178+
<ExpandableList items={listItems} accordion={true} />
179+
</Container>
180+
)
181+
}
182+
104183
export const onClick = () => {}
105184

106-
<DemoComponent />
185+
<DemoContainer>
186+
<DemoContainerItem>
187+
<Typography variant="card-title">Expandable list</Typography>
188+
<DemoComponent />
189+
</DemoContainerItem>
190+
<DemoContainerItem>
191+
<Typography variant="card-title">Expandable list accordion</Typography>
192+
<DemoComponentAccordion />
193+
</DemoContainerItem>
194+
</DemoContainer>
107195

108196
## Basic usage
109197

0 commit comments

Comments
 (0)