Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions packages/core/src/ExpandableList/ExpandableListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, FC, Dispatch, SetStateAction } from 'react'
import React, { useCallback, Dispatch, SetStateAction } from 'react'
import styled from 'styled-components'

import { FoldTransition } from '../Transition'
Expand Down Expand Up @@ -34,6 +34,7 @@ export interface ExpandableListItemProps extends BaseProps {
readonly expandedItems: ReadonlyArray<string>
readonly setExpandedItems: Dispatch<SetStateAction<ReadonlyArray<string>>>
readonly isNestedItem: boolean
readonly isAccordion: boolean
}

/**
Expand All @@ -45,23 +46,34 @@ export interface ExpandableListItemProps extends BaseProps {
*
*/

export const ExpandableListItem: FC<ExpandableListItemProps> = ({
export const ExpandableListItem: React.VFC<ExpandableListItemProps> = ({
item,
expandedItems,
setExpandedItems,
isNestedItem,
isAccordion,
...props
}) => {
const { id, label, icon, selected = false, onClick, items } = item
const onChildClick = useCallback(
() =>
((itemId: string) => {
const nextExpandedItems = expandedItems.includes(itemId)
? expandedItems.filter(i => i !== itemId)
: [...expandedItems, id]
let nextExpandedItems = []

if (expandedItems.includes(itemId)) {
// Close the expanded item
nextExpandedItems = expandedItems.filter(i => i !== itemId)
} else if (isAccordion) {
// Only add one expanded item when accordion
nextExpandedItems = [id]
} else {
// Extend expanded items with a new one
nextExpandedItems = [...expandedItems, id]
}

setExpandedItems(nextExpandedItems)
})(id),
[expandedItems, id, setExpandedItems]
[expandedItems, id, isAccordion, setExpandedItems]
)
const hasChildren = items !== undefined
const onItemClick = hasChildren ? onChildClick : onClick
Expand Down Expand Up @@ -93,6 +105,7 @@ export const ExpandableListItem: FC<ExpandableListItemProps> = ({
expandedItems={expandedItems}
setExpandedItems={setExpandedItems}
isNestedItem={true}
isAccordion={isAccordion}
/>
))}
</ExpandableListContainer>
Expand Down
47 changes: 21 additions & 26 deletions packages/core/src/ExpandableList/ListItemContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useCallback, useRef, useState, useLayoutEffect } from 'react'

import React, { useCallback } from 'react'
import styled, { css } from 'styled-components'
import { shape, spacing } from '../designparams'
import { useHasOverflow } from 'react-hooks-shareable'

import { shape, spacing } from '../designparams'
import { Icon, IconType } from '../Icon'
import { Typography } from '../Typography'
import { Tooltip } from '../Tooltip'
import { Tooltip, ExpandedTooltipTypography } from '../Tooltip'

interface ListItemMarkerBaseProps {
readonly selected: boolean
Expand Down Expand Up @@ -113,7 +113,9 @@ const ListItemMarker = styled.div<ListItemMarkerProps>`
`}
`

const Label = styled(Typography)`
const Label = styled(Typography).attrs({
variant: 'default-text',
})`
margin-right: ${spacing.huge};
white-space: nowrap;
`
Expand Down Expand Up @@ -160,27 +162,20 @@ interface OverflowTooltipProps {
* Used to show a tooltip if label is too long to be fully shown in container.
*/
const LabelOverflowTooltip: React.FC<OverflowTooltipProps> = ({ label }) => {
const [hasOverflow, setHasOverflow] = useState(false)
const labelRef = useRef<HTMLDivElement>(null)

useLayoutEffect(() => {
if (labelRef.current === null) {
return
}

setHasOverflow(
labelRef.current.offsetHeight < labelRef.current.scrollHeight ||
labelRef.current.offsetWidth < labelRef.current.scrollWidth
)
}, [labelRef])

const text = (
<Label ref={labelRef} variant="default-text">
{label}
</Label>
)
const { hasOverflow, ref } = useHasOverflow()

return hasOverflow ? <Tooltip text={label}>{text}</Tooltip> : text
const text = <Label ref={ref}>{label}</Label>

return hasOverflow ? (
<Tooltip
variant="expanded"
contents={<ExpandedTooltipTypography>{label}</ExpandedTooltipTypography>}
>
<Label>{label}</Label>
</Tooltip>
) : (
text
)
}

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

export const ListItemContainer: React.FC<ListItemContainerProps> = ({
export const ListItemContainer: React.VFC<ListItemContainerProps> = ({
selected,
isNestedItem,
hasChildren,
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/ExpandableList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, FC } from 'react'
import React, { useState } from 'react'

import {
ExpandableListContainer,
Expand All @@ -20,10 +20,16 @@ interface ExpandableListProps extends BaseProps {
* Used to create an array of items.
*/
readonly items: ReadonlyArray<ExpandableListItemType>
/**
* Whether the list should work as accordion
* and allows only one expanded item at once.
*/
readonly accordion?: boolean
}

export const ExpandableList: FC<ExpandableListProps> = ({
export const ExpandableList: React.VFC<ExpandableListProps> = ({
items,
accordion,
...props
}) => {
const [expandedItems, setExpandedItems] = useState<ReadonlyArray<string>>([])
Expand All @@ -36,6 +42,7 @@ export const ExpandableList: FC<ExpandableListProps> = ({
item={item}
expandedItems={expandedItems}
setExpandedItems={setExpandedItems}
isAccordion={accordion === true}
isNestedItem={false}
/>
))}
Expand Down
104 changes: 96 additions & 8 deletions packages/docs/src/mdx/coreComponents/ExpandableList.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useState } from 'react'
import styled from 'styled-components'

import { DeviceIcon } from 'practical-react-components-icons'
import { ExpandableList } from 'practical-react-components-core'
import { ExpandableList, Typography } from 'practical-react-components-core'

# ExpandableList

Expand All @@ -29,32 +29,32 @@ export const ITEMS = [
selected: true,
items: [
{
id: 'item-1',
id: 'category-item-1',
label: 'Item 1',
icon: DeviceIcon,
},
{
id: 'something-2',
id: 'category-something-2',
label: 'Something 2',
icon: DeviceIcon,
},
{
id: 'something-else-3',
id: 'category-something-else-3',
label: 'Something else 3',
icon: DeviceIcon,
},
{
id: 'entirely-different-thing',
id: 'category-entirely-different-thing',
label: 'Entirely different thing',
icon: DeviceIcon,
},
{
id: 'and-this-thing',
id: 'category-and-this-thing',
label: 'And this thing',
icon: DeviceIcon,
},
{
id: 'also-this',
id: 'category-also-this',
label: 'Also this',
icon: DeviceIcon,
},
Expand All @@ -64,13 +64,62 @@ export const ITEMS = [
id: 'site-log',
label: 'Site log',
icon: DeviceIcon,
items: [
{
id: 'site-log-item-1',
label: 'Item 1',
icon: DeviceIcon,
},
{
id: 'site-log-something-2',
label: 'Something 2',
icon: DeviceIcon,
},
{
id: 'site-log-something-else-3',
label: 'Something else 3',
icon: DeviceIcon,
},
{
id: 'site-log-entirely-different-thing',
label: 'Entirely different thing',
icon: DeviceIcon,
},
{
id: 'site-log-and-this-thing',
label: 'And this thing',
icon: DeviceIcon,
},
{
id: 'site-log-also-this',
label: 'Also this',
icon: DeviceIcon,
},
],
},
{
id: 'something-else',
label: 'Something else',
icon: DeviceIcon,
},
]

export const Container = styled.div`
width: 256px;
`

export const DemoContainer = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 32px;
`

export const DemoContainerItem = styled.div`
display: grid;
grid-template-rows: auto 1fr;
grid-gap: 16px;
`

export const DemoComponent = ({}) => {
const [selectedItem, setSelectedItem] = useState('none')
const listItems = ITEMS.map(item => {
Expand Down Expand Up @@ -101,9 +150,48 @@ export const DemoComponent = ({}) => {
)
}

export const DemoComponentAccordion = ({}) => {
const [selectedItem, setSelectedItem] = useState('none')
const listItems = ITEMS.map(item => {
return {
...item,
selected: selectedItem === item.id,
onClick: () => {
setSelectedItem(item.id)
},
items:
item.items !== undefined
? item.items.map(subItem => {
return {
...subItem,
selected: selectedItem === subItem.id,
onClick: () => {
setSelectedItem(subItem.id)
},
}
})
: undefined,
}
})
return (
<Container>
<ExpandableList items={listItems} accordion={true} />
</Container>
)
}

export const onClick = () => {}

<DemoComponent />
<DemoContainer>
<DemoContainerItem>
<Typography variant="card-title">Expandable list</Typography>
<DemoComponent />
</DemoContainerItem>
<DemoContainerItem>
<Typography variant="card-title">Expandable list accordion</Typography>
<DemoComponentAccordion />
</DemoContainerItem>
</DemoContainer>

## Basic usage

Expand Down