- {loading ? (
+ {loading && !visibleColumns.length ? (
- {isBulkSelectionConfigured ?
: null}
+ {showIconOrExpandActionGutter ?
: null}
{SHIMMER_DUMMY_ARRAY.map((label) => (
))}
@@ -337,6 +538,32 @@ const TableContent = <
gridTemplateColumns,
}}
>
+ {isAnyRowExpandable ? (
+
+
+ }
+ ariaLabel="Expand/Collapse all rows"
+ showAriaLabelInTippy={false}
+ variant={ButtonVariantType.borderLess}
+ size={ComponentSizeType.xxs}
+ style={ButtonStyleType.neutral}
+ onClick={toggleExpandAll}
+ />
+
+ ) : null}
+
+ {!isAnyRowExpandable && rowStartIconConfig &&
}
+
{visibleColumns.map(
(
{
@@ -346,6 +573,7 @@ const TableContent = <
size,
showTippyOnTruncate,
horizontallySticky: isStickyColumn,
+ infoTooltipText,
},
index,
) => {
@@ -353,7 +581,12 @@ const TableContent = <
const isBulkActionGutter = field === BULK_ACTION_GUTTER_LABEL
const horizontallySticky = isStickyColumn || isBulkActionGutter
const { className: stickyClassName = '', left: stickyLeftValue = '' } =
- horizontallySticky ? getStickyColumnConfig(gridTemplateColumns, index) : {}
+ horizontallySticky
+ ? getStickyColumnConfig(
+ gridTemplateColumns,
+ index + (isAnyRowExpandable ? 1 : 0),
+ )
+ : {}
if (field === BULK_ACTION_GUTTER_LABEL) {
return (
@@ -379,7 +612,7 @@ const TableContent = <
return (
@@ -392,6 +625,7 @@ const TableContent = <
triggerSorting={getTriggerSortingHandler(field)}
showTippyOnTruncate={showTippyOnTruncate}
disabled={areFilteredRowsLoading}
+ infoTooltipText={infoTooltipText}
{...(isResizable
? { isResizable, handleResize, id: label }
: { isResizable: false })}
diff --git a/src/Shared/Components/Table/constants.ts b/src/Shared/Components/Table/constants.ts
index 1b7ed8b0f..63005f738 100644
--- a/src/Shared/Components/Table/constants.ts
+++ b/src/Shared/Components/Table/constants.ts
@@ -29,3 +29,5 @@ export const DRAG_SELECTOR_IDENTIFIER = 'table-drag-selector'
export const SHIMMER_DUMMY_ARRAY = [1, 2, 3]
export const NO_ROWS_OR_GET_ROWS_ERROR = new Error('Neither rows nor getRows function provided')
+
+export const ACTION_GUTTER_SIZE = 24
diff --git a/src/Shared/Components/Table/styles.scss b/src/Shared/Components/Table/styles.scss
index ed2fb7c1e..16bfa0e00 100644
--- a/src/Shared/Components/Table/styles.scss
+++ b/src/Shared/Components/Table/styles.scss
@@ -40,6 +40,13 @@
left: -20px;
width: 20px;
}
+
+ &.expand-row-btn::before,
+ &.row-start-icon::before,
+ &.expanded-tree-line::before {
+ left: -24px;
+ width: 24px;
+ }
}
&--scrolled {
@@ -69,6 +76,10 @@
outline: none;
}
+ &--expanded-row:has(+ .generic-table__row--expanded-row) {
+ border-bottom: 0px;
+ }
+
&:hover,
&:hover > *,
&--active,
@@ -94,6 +105,33 @@
display: inherit;
}
}
+
+ &.with-start-icon-and-bulk-or-expand-action {
+ .bulk-action-checkbox {
+ display: none;
+ }
+
+ .expand-row-btn {
+ display: none;
+ }
+
+ &:hover,
+ &.generic-table__row--active,
+ &.generic-table__row--bulk-selected,
+ &.generic-table__row--is-expanded {
+ .row-start-icon {
+ display: none;
+ }
+
+ .bulk-action-checkbox {
+ display: flex;
+ }
+
+ .expand-row-btn {
+ display: flex;
+ }
+ }
+ }
}
.sortable-table-header__resize-btn:hover,
@@ -105,4 +143,14 @@
transform: scaleY(var(--resize-btn-scale-multiplier));
}
}
+
+ .expanded-tree-line::after {
+ content: '';
+ width: 1px;
+ height: 100%;
+ background: var(--N200);
+ left: calc(50% - 1px); // offset to left by width for perfect centering
+ top: 0;
+ position: absolute;
+ }
}
diff --git a/src/Shared/Components/Table/types.ts b/src/Shared/Components/Table/types.ts
index 0d729b82a..a45d2b7f8 100644
--- a/src/Shared/Components/Table/types.ts
+++ b/src/Shared/Components/Table/types.ts
@@ -26,12 +26,15 @@ import {
import { GenericEmptyStateType } from '@Common/index'
import { PageSizeOption } from '@Common/Pagination/types'
import { SortableTableHeaderCellProps, useResizableTableConfig } from '@Common/SortableTableHeaderCell'
+import { IconsProps } from '@Shared/Components/Icon'
import { useBulkSelection, UseBulkSelectionProps } from '../BulkSelection'
export interface UseFiltersReturnType extends UseStateFiltersReturnType {}
export enum SignalEnum {
+ COLLAPSE_ROW = 'collapse-row',
+ EXPAND_ROW = 'expand-row',
ENTER_PRESSED = 'enter-pressed',
DELETE_PRESSED = 'delete-pressed',
ESCAPE_PRESSED = 'escape-pressed',
@@ -87,13 +90,23 @@ type BaseColumnType = {
size: SizeType
horizontallySticky?: boolean
-}
+} & Pick
-export type RowType = {
+type CommonRowType = {
id: string
data: Data
}
+export type ExpandedRowPrefixType = 'expanded-row-'
+
+export type ExpandedRowType = CommonRowType & {
+ id: `${ExpandedRowPrefixType}${string}`
+}
+
+export type RowType = CommonRowType & {
+ expandableRows?: Array>
+}
+
export type RowsType = RowType[]
export enum FiltersTypeEnum {
@@ -117,6 +130,10 @@ export type CellComponentProps<
? UseFiltersReturnType
: UseUrlFiltersReturnType
isRowActive: boolean
+ isExpandedRow: boolean
+ isRowInExpandState: boolean
+ // NOTE: no action if the row is not expandable
+ expandRowCallback: (e: MouseEvent) => void
}
export type RowActionsOnHoverComponentProps<
@@ -222,7 +239,11 @@ export type ViewWrapperProps<
: {})
>
-type FilterConfig = {
+type FilterConfig<
+ FilterVariant extends FiltersTypeEnum,
+ RowData extends unknown,
+ AdditionalProps extends Record,
+> = {
filtersVariant: FilterVariant
/**
* Props for useUrlFilters/useStateFilters hooks
@@ -236,12 +257,14 @@ type FilterConfig, filterData: UseFiltersReturnType) => boolean
+ : (row: RowType, filterData: UseFiltersReturnType, additionalProps: AdditionalProps) => boolean
clearFilters?: FilterVariant extends FiltersTypeEnum.URL
? () => void
: FilterVariant extends FiltersTypeEnum.STATE
? never
: never
+
+ areFiltersApplied?: FilterVariant extends FiltersTypeEnum.NONE ? never : boolean
}
export type InternalTableProps<
@@ -307,6 +330,14 @@ export type InternalTableProps<
handleToggleBulkSelectionOnRow: (row: RowType) => void
ViewWrapper?: FunctionComponent>
+
+ /**
+ * An icon as the first element of the row, that hides actions like expand or bulk select icons
+ * until user hovers over the row or the row has focus from keyboard navigation
+ */
+ rowStartIconConfig?: Omit
+
+ onRowClick?: (row: RowType, isExpandedRow: boolean) => void
} & (
| {
/**
@@ -337,7 +368,7 @@ export type InternalTableProps<
pageSizeOptions?: never
}
) &
- FilterConfig
+ FilterConfig
export type UseResizableTableConfigWrapperProps<
RowData extends unknown,
@@ -390,6 +421,9 @@ export type TableProps<
| 'ViewWrapper'
| 'pageSizeOptions'
| 'clearFilters'
+ | 'rowStartIconConfig'
+ | 'onRowClick'
+ | 'areFiltersApplied'
>
export type BulkActionStateType = string | null
@@ -440,6 +474,8 @@ export interface TableContentProps<
| 'rowActionOnHoverConfig'
| 'pageSizeOptions'
| 'getRows'
+ | 'rowStartIconConfig'
+ | 'onRowClick'
>,
RowsResultType {
areFilteredRowsLoading: boolean
diff --git a/src/Shared/Components/Table/useTableWithKeyboardShortcuts.ts b/src/Shared/Components/Table/useTableWithKeyboardShortcuts.ts
index a7a67a14b..cb51044c9 100644
--- a/src/Shared/Components/Table/useTableWithKeyboardShortcuts.ts
+++ b/src/Shared/Components/Table/useTableWithKeyboardShortcuts.ts
@@ -101,6 +101,20 @@ const useTableWithKeyboardShortcuts = <
)
useEffect(() => {
+ registerShortcut({
+ keys: ['ArrowLeft'],
+ callback: () => {
+ dispatchEvent(SignalEnum.COLLAPSE_ROW)
+ },
+ })
+
+ registerShortcut({
+ keys: ['ArrowRight'],
+ callback: () => {
+ dispatchEvent(SignalEnum.EXPAND_ROW)
+ },
+ })
+
registerShortcut({
keys: ['ArrowDown'],
callback: () => {
@@ -142,6 +156,8 @@ const useTableWithKeyboardShortcuts = <
unregisterShortcut(['Enter'])
unregisterShortcut(['Backspace'])
unregisterShortcut(['.'])
+ unregisterShortcut(['ArrowLeft'])
+ unregisterShortcut(['ArrowRight'])
}
}, [getMoveFocusToNextRowHandler, getMoveFocusToPreviousRowHandler, dispatchEvent])
diff --git a/src/Shared/Components/Table/utils.ts b/src/Shared/Components/Table/utils.ts
index 8e9ff9580..5d0f4b46a 100644
--- a/src/Shared/Components/Table/utils.ts
+++ b/src/Shared/Components/Table/utils.ts
@@ -42,11 +42,12 @@ export const searchAndSortRows = <
rows: TableProps['rows'],
filter: TableProps['filter'],
filterData: UseFiltersReturnType,
+ additionalProps: AdditionalProps,
comparator?: Column['comparator'],
): Awaited['getRows']>> => {
const { sortBy, sortOrder } = filterData ?? {}
- const filteredRows = filter ? rows.filter((row) => filter(row, filterData)) : rows
+ const filteredRows = filter ? rows.filter((row) => filter(row, filterData, additionalProps)) : rows
return {
rows: