Skip to content

Commit 54dd39d

Browse files
Amrit BorahAmrit Borah
authored andcommitted
feat: add expandable rows support in table
1 parent bc1bafd commit 54dd39d

3 files changed

Lines changed: 119 additions & 18 deletions

File tree

src/Shared/Components/Table/TableContent.tsx

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ import { useEffectAfterMount } from '@Common/Helper'
2222
import { Pagination } from '@Common/Pagination'
2323
import { SortableTableHeaderCell } from '@Common/SortableTableHeaderCell'
2424
import { CHECKBOX_VALUE } from '@Common/Types'
25+
import { Button, ButtonStyleType, ButtonVariantType } from '@Shared/Components/Button'
26+
import { Icon } from '@Shared/Components/Icon'
27+
import { ComponentSizeType } from '@Shared/constants'
2528

2629
import { BulkSelection } from '../BulkSelection'
2730
import BulkSelectionActionWidget from './BulkSelectionActionWidget'
2831
import { BULK_ACTION_GUTTER_LABEL, EVENT_TARGET, SHIMMER_DUMMY_ARRAY } from './constants'
29-
import { BulkActionStateType, FiltersTypeEnum, PaginationEnum, SignalsType, TableContentProps } from './types'
32+
import { BulkActionStateType, FiltersTypeEnum, PaginationEnum, RowType, SignalsType, TableContentProps } from './types'
3033
import useTableWithKeyboardShortcuts from './useTableWithKeyboardShortcuts'
3134
import { getStickyColumnConfig, scrollToShowActiveElementIfNeeded } from './utils'
3235

@@ -61,6 +64,7 @@ const TableContent = <
6164

6265
const [bulkActionState, setBulkActionState] = useState<BulkActionStateType>(null)
6366
const [showBorderRightOnStickyElements, setShowBorderRightOnStickyElements] = useState(false)
67+
const [expandState, setExpandState] = useState<Record<string, boolean>>({})
6468

6569
const { width: rowOnHoverComponentWidth, Component: RowOnHoverComponent } = rowActionOnHoverConfig || {}
6670

@@ -92,10 +96,50 @@ const TableContent = <
9296
.join(' '),
9397
} = resizableConfig ?? {}
9498

95-
const gridTemplateColumns = rowOnHoverComponentWidth
99+
const { visibleRows, areAllRowsExpanded, isAnyRowExpandable } = useMemo(() => {
100+
const normalizedFilteredRows = filteredRows ?? []
101+
102+
const paginatedRows =
103+
paginationVariant !== PaginationEnum.PAGINATED ||
104+
(paginationVariant === PaginationEnum.PAGINATED && getRows)
105+
? normalizedFilteredRows
106+
: normalizedFilteredRows.slice(offset, offset + pageSize)
107+
108+
const _isAnyRowExpandable = paginatedRows.some((row) => !!row.expandableRows)
109+
110+
const _areAllRowsExpanded =
111+
_isAnyRowExpandable &&
112+
paginatedRows.reduce((acc, row) => {
113+
if (row.expandableRows) {
114+
return acc && !!expandState[row.id]
115+
}
116+
117+
return acc
118+
}, true)
119+
120+
const paginatedRowsWithExpandedRows = paginatedRows.flatMap((row) => {
121+
if (row.expandableRows && expandState[row.id]) {
122+
return [row, ...row.expandableRows]
123+
}
124+
125+
return [row]
126+
})
127+
128+
return {
129+
visibleRows: paginatedRowsWithExpandedRows,
130+
areAllRowsExpanded: _areAllRowsExpanded,
131+
isAnyRowExpandable: _isAnyRowExpandable,
132+
}
133+
}, [paginationVariant, offset, pageSize, filteredRows, expandState])
134+
135+
const gridTemplateColumnsWithoutExpandButton = rowOnHoverComponentWidth
96136
? `${initialGridTemplateColumns} ${typeof rowOnHoverComponentWidth === 'number' ? `minmax(${rowOnHoverComponentWidth}px, 1fr)` : rowOnHoverComponentWidth}`
97137
: initialGridTemplateColumns
98138

139+
const gridTemplateColumns = isAnyRowExpandable
140+
? `16px ${gridTemplateColumnsWithoutExpandButton}`
141+
: gridTemplateColumnsWithoutExpandButton
142+
99143
useEffect(() => {
100144
const scrollEventHandler = () => {
101145
setShowBorderRightOnStickyElements(rowsContainerRef.current?.scrollLeft > 0)
@@ -113,18 +157,6 @@ const TableContent = <
113157

114158
const bulkSelectionCount = isBulkSelectionApplied ? totalRows : (getSelectedIdentifiersCount?.() ?? 0)
115159

116-
const visibleRows = useMemo(() => {
117-
const normalizedFilteredRows = filteredRows ?? []
118-
119-
const paginatedRows =
120-
paginationVariant !== PaginationEnum.PAGINATED ||
121-
(paginationVariant === PaginationEnum.PAGINATED && getRows)
122-
? normalizedFilteredRows
123-
: normalizedFilteredRows.slice(offset, offset + pageSize)
124-
125-
return paginatedRows
126-
}, [paginationVariant, offset, pageSize, filteredRows])
127-
128160
const isBEPagination = !!getRows
129161

130162
const showPagination =
@@ -155,6 +187,18 @@ const TableContent = <
155187
handleSorting(newSortBy)
156188
}
157189

190+
const toggleExpandAll = () => {
191+
setExpandState(
192+
visibleRows.reduce((acc, row) => {
193+
if ((row as RowType<RowData>).expandableRows) {
194+
acc[row.id] = !areAllRowsExpanded
195+
}
196+
197+
return acc
198+
}, {}),
199+
)
200+
}
201+
158202
const focusActiveRow = (node: HTMLDivElement) => {
159203
if (
160204
node &&
@@ -216,6 +260,7 @@ const TableContent = <
216260
return visibleRows.map((row, visibleRowIndex) => {
217261
const isRowActive = activeRowIndex === visibleRowIndex
218262
const isRowBulkSelected = !!bulkSelectionState[row.id] || isBulkSelectionApplied
263+
const isExpandedRow = row.id.startsWith('expanded-row')
219264

220265
const handleChangeActiveRowIndex = () => {
221266
setActiveRowIndex(visibleRowIndex)
@@ -225,6 +270,13 @@ const TableContent = <
225270
handleToggleBulkSelectionOnRow(row)
226271
}
227272

273+
const toggleExpandRow = () => {
274+
setExpandState({
275+
...expandState,
276+
[row.id]: !expandState[row.id],
277+
})
278+
}
279+
228280
return (
229281
<div
230282
key={row.id}
@@ -236,22 +288,37 @@ const TableContent = <
236288
isRowActive ? 'generic-table__row--active checkbox__parent-container--active' : ''
237289
} ${rowActionOnHoverConfig ? 'dc__opacity-hover dc__opacity-hover--parent' : ''} ${
238290
isRowBulkSelected ? 'generic-table__row--bulk-selected' : ''
239-
}`}
291+
} ${isExpandedRow ? 'generic-table__row--expanded-row' : ''}`}
240292
style={{
241293
gridTemplateColumns,
242294
}}
243295
data-active={isRowActive}
244296
// NOTE: by giving it a negative tabIndex we can programmatically focus it through .focus()
245297
tabIndex={-1}
246298
>
299+
{!isExpandedRow && !!(row as RowType<RowData>).expandableRows ? (
300+
<Button
301+
dataTestId={`expand-row-${row.id}`}
302+
icon={<Icon name="ic-expand-right-sm" color={null} />}
303+
ariaLabel="Expand row"
304+
showAriaLabelInTippy={false}
305+
variant={ButtonVariantType.borderLess}
306+
size={ComponentSizeType.xs}
307+
style={ButtonStyleType.neutral}
308+
onClick={toggleExpandRow}
309+
/>
310+
) : null}
311+
312+
{isAnyRowExpandable && (isExpandedRow || !(row as RowType<RowData>).expandableRows) && <div />}
313+
247314
{visibleColumns.map(({ field, horizontallySticky: isStickyColumn, CellComponent }, index) => {
248315
const isBulkActionGutter = field === BULK_ACTION_GUTTER_LABEL
249316
const horizontallySticky = isStickyColumn || isBulkActionGutter
250317
const { className: stickyClassName = '', left: stickyLeftValue = '' } = horizontallySticky
251318
? getStickyColumnConfig(gridTemplateColumns, index)
252319
: {}
253320

254-
if (isBulkActionGutter) {
321+
if (isBulkActionGutter && !isExpandedRow) {
255322
return (
256323
<div
257324
className={`flexbox dc__align-items-center ${stickyClassName}`}
@@ -282,6 +349,8 @@ const TableContent = <
282349
row={row}
283350
filterData={filterData as any}
284351
isRowActive={isRowActive}
352+
isExpandedRow={isExpandedRow}
353+
isRowInExpandState={expandState[row.id]}
285354
{...additionalProps}
286355
/>
287356
) : (
@@ -295,7 +364,7 @@ const TableContent = <
295364
)
296365
})}
297366

298-
{RowOnHoverComponent && (
367+
{!isExpandedRow && RowOnHoverComponent && (
299368
<div
300369
className={`dc__position-sticky dc__right-0 dc__zi-1 ${!isRowActive ? 'dc__opacity-hover--child' : ''}`}
301370
>
@@ -337,6 +406,19 @@ const TableContent = <
337406
gridTemplateColumns,
338407
}}
339408
>
409+
{isAnyRowExpandable ? (
410+
<Button
411+
dataTestId="expand-all-rows"
412+
icon={<Icon name="ic-expand-right-sm" color={null} />}
413+
ariaLabel="Expand row"
414+
showAriaLabelInTippy={false}
415+
variant={ButtonVariantType.borderLess}
416+
size={ComponentSizeType.xs}
417+
style={ButtonStyleType.neutral}
418+
onClick={toggleExpandAll}
419+
/>
420+
) : null}
421+
340422
{visibleColumns.map(
341423
(
342424
{

src/Shared/Components/Table/styles.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
outline: none;
7070
}
7171

72+
&--expanded-row,
73+
&--expanded-row > * {
74+
background-color: var(--N100);
75+
}
76+
7277
&:hover,
7378
&:hover > *,
7479
&--active,
@@ -105,4 +110,8 @@
105110
transform: scaleY(var(--resize-btn-scale-multiplier));
106111
}
107112
}
113+
114+
&__expanse {
115+
background-color: var(--bg-hover-opaque);
116+
}
108117
}

src/Shared/Components/Table/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,19 @@ type BaseColumnType = {
8989
horizontallySticky?: boolean
9090
}
9191

92-
export type RowType<Data extends unknown> = {
92+
type CommonRowType<Data extends unknown> = {
9393
id: string
9494
data: Data
9595
}
9696

97+
export type ExpandedRowType<Data extends unknown> = CommonRowType<Data> & {
98+
id: `expanded-row-${string}`
99+
}
100+
101+
export type RowType<Data extends unknown> = CommonRowType<Data> & {
102+
expandableRows?: Array<ExpandedRowType<Data>>
103+
}
104+
97105
export type RowsType<Data extends unknown> = RowType<Data>[]
98106

99107
export enum FiltersTypeEnum {
@@ -117,6 +125,8 @@ export type CellComponentProps<
117125
? UseFiltersReturnType
118126
: UseUrlFiltersReturnType<string>
119127
isRowActive: boolean
128+
isExpandedRow: boolean
129+
isRowInExpandState: boolean
120130
}
121131

122132
export type RowActionsOnHoverComponentProps<

0 commit comments

Comments
 (0)