Skip to content

Commit

Permalink
refactor: Componentizes table cell types and adds permutations (#3017)
Browse files Browse the repository at this point in the history
pan-kot authored Nov 20, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 8c1bdca commit 8f977eb
Showing 13 changed files with 547 additions and 166 deletions.
305 changes: 305 additions & 0 deletions pages/table/cell-permutations.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useContext, useState } from 'react';
import { range } from 'lodash';

import {
Box,
Button,
Checkbox,
Container,
ExpandableSection,
FormField,
Header,
Input,
Slider,
SpaceBetween,
StatusIndicator,
Table,
TableProps,
} from '~components';

import AppContext, { AppContextType } from '../app/app-context';
import ScreenshotArea from '../utils/screenshot-area';

type PageContext = React.Context<
AppContextType<{
wrapLines?: boolean;
verticalAlignTop?: boolean;
sortingDisabled?: boolean;
resizableColumns?: boolean;
isExpandable?: boolean;
isExpanded?: boolean;
isEditable?: boolean;
stripedRows?: boolean;
hasSelection?: boolean;
multiSelection?: boolean;
hasFooter?: boolean;
stickyColumnsFirst?: string;
stickyColumnsLast?: string;
tableEmpty?: boolean;
tableLoading?: boolean;
}>
>;

const columns = range(0, 10).map(index => index + 1);

export default function InlineEditorPermutations() {
const { settings, setUrlParams } = usePageSettings();
return (
<Box margin="m">
<h1>Table cell permutations</h1>

<ScreenshotArea gutters={false}>
<ExpandableSection variant="stacked" headerText="Settings" headingTagOverride="h2" defaultExpanded={true}>
<SpaceBetween size="m">
<SpaceBetween size="m" direction="horizontal" alignItems="center">
<Checkbox
checked={settings.wrapLines}
onChange={event => setUrlParams({ wrapLines: event.detail.checked })}
>
Wrap lines
</Checkbox>

<Checkbox
checked={settings.isExpandable}
onChange={event => setUrlParams({ isExpandable: event.detail.checked })}
>
Is expandable
</Checkbox>

<Checkbox
checked={settings.isExpanded}
onChange={event => setUrlParams({ isExpanded: event.detail.checked })}
>
Is expanded
</Checkbox>

<Checkbox
checked={settings.isEditable}
onChange={event => setUrlParams({ isEditable: event.detail.checked })}
>
Editable
</Checkbox>

<Checkbox
checked={settings.sortingDisabled}
onChange={event => setUrlParams({ sortingDisabled: event.detail.checked })}
>
Sorting disabled
</Checkbox>

<Checkbox
checked={settings.verticalAlign === 'top'}
onChange={event => setUrlParams({ verticalAlignTop: event.detail.checked })}
>
Vertical align top
</Checkbox>

<Checkbox
checked={settings.stripedRows}
onChange={event => setUrlParams({ stripedRows: event.detail.checked })}
>
Striped rows
</Checkbox>

<Checkbox
checked={settings.hasSelection}
onChange={event => setUrlParams({ hasSelection: event.detail.checked })}
>
Has selection
</Checkbox>

<Checkbox
checked={settings.multiSelection}
onChange={event => setUrlParams({ multiSelection: event.detail.checked })}
>
Multi selection
</Checkbox>

<Checkbox
checked={settings.hasFooter}
onChange={event => setUrlParams({ hasFooter: event.detail.checked })}
>
Has footer
</Checkbox>

<Checkbox
checked={settings.resizableColumns}
onChange={event => setUrlParams({ resizableColumns: event.detail.checked })}
>
Resizable columns
</Checkbox>

<Checkbox
checked={settings.tableEmpty}
onChange={event => setUrlParams({ tableEmpty: event.detail.checked })}
>
Table empty
</Checkbox>

<Checkbox
checked={settings.tableLoading}
onChange={event => setUrlParams({ tableLoading: event.detail.checked })}
>
Table loading
</Checkbox>
</SpaceBetween>

<SpaceBetween size="m" direction="horizontal" alignItems="center">
<FormField label="Sticky columns first">
<Slider
value={settings.stickyColumnsFirst}
onChange={event => setUrlParams({ stickyColumnsFirst: event.detail.value.toString() })}
min={0}
max={3}
/>
</FormField>

<FormField label="Sticky columns last">
<Slider
value={settings.stickyColumnsLast}
onChange={event => setUrlParams({ stickyColumnsLast: event.detail.value.toString() })}
min={0}
max={3}
/>
</FormField>
</SpaceBetween>
</SpaceBetween>
</ExpandableSection>

<Container variant="stacked" header={<Header variant="h2">Actual table demo</Header>}>
<TableCellsDemo />
</Container>
</ScreenshotArea>
</Box>
);
}

function TableCellsDemo() {
const { settings } = usePageSettings();
const [editedValues, setEditedValues] = useState<Record<string, string>>({});

const items = [0, 1, 2, 3, 4, 5, 6, 7];
const itemLevels = [1, 2, 3, 4, 5, 1, 2, 3];
const itemChildren: Record<number, number[]> = { 0: [1], 1: [2], 2: [3], 3: [4], 5: [6], 6: [7] };
const itemLoading = new Map<null | number, TableProps.LoadingStatus>([
[3, 'pending'],
[6, 'error'],
[null, 'loading'],
]);
const selectedItems = [3, 5, 6];

const columnDefinitions: TableProps.ColumnDefinition<number>[] = columns.map(index => {
const columnId = index.toString();
const cellContent = (item: number) =>
editedValues[`${columnId}:${item}`] ??
`Body cell content ${item}:${index}${index === 1 ? ` (L=${itemLevels[item]})` : ''}${index === 8 ? ' with longer text' : ''}`;
return {
id: columnId,
header: `Header cell content ${index}${index === 8 ? ' with longer text' : ''}`,
sortingField: index === 2 ? 'field-1' : index === 3 ? 'field-2' : undefined,
activeSorting: index === 3,
cell: cellContent,
verticalAlign: settings.verticalAlign,
editConfig: settings.isEditable
? {
ariaLabel: 'Edit dialog aria label',
editIconAriaLabel: 'Edit icon label',
errorIconAriaLabel: 'Edit error icon label',
validation(_item, value = '') {
if (value.trim() && value.toLowerCase().includes('content')) {
return 'Must not include "content"';
}
},
editingCell(item, { currentValue, setValue }: TableProps.CellContext<string>) {
return (
<Input
autoFocus={true}
value={currentValue ?? cellContent(item)}
onChange={event => setValue(event.detail.value)}
/>
);
},
}
: undefined,
};
});

let expandableRows: undefined | TableProps.ExpandableRows<number> = undefined;
if (settings.isExpandable) {
expandableRows = {
getItemChildren: item => itemChildren[item] ?? [],
isItemExpandable: item => !!itemChildren[item],
expandedItems: settings.isExpanded ? items : [],
onExpandableItemToggle: () => {},
};
}

let selectionType: undefined | TableProps.SelectionType = undefined;
if (settings.hasSelection) {
selectionType = settings.multiSelection ? 'multi' : 'single';
}

return (
<Table
ariaLabels={{
selectionGroupLabel: 'selectionGroupLabel',
activateEditLabel: () => 'activateEditLabel',
cancelEditLabel: () => 'cancelEditLabel',
submitEditLabel: () => 'submitEditLabel',
allItemsSelectionLabel: () => 'allItemsSelectionLabel',
itemSelectionLabel: () => 'itemSelectionLabel',
tableLabel: 'tableLabel',
expandButtonLabel: () => 'expand row',
collapseButtonLabel: () => 'collapse row',
}}
columnDefinitions={columnDefinitions}
items={settings.tableEmpty ? [] : settings.isExpandable ? [0, 5] : items}
wrapLines={settings.wrapLines}
sortingDisabled={settings.sortingDisabled}
sortingColumn={{ sortingField: 'field-2' }}
resizableColumns={settings.resizableColumns}
stripedRows={settings.stripedRows}
footer={settings.hasFooter ? <Box>Table footer</Box> : null}
stickyColumns={{ first: settings.stickyColumnsFirst, last: settings.stickyColumnsLast }}
selectionType={selectionType}
selectedItems={selectedItems}
empty="Empty"
loading={settings.tableLoading}
loadingText="Loading"
submitEdit={(item, column, newValue) =>
new Promise(resolve =>
resolve(setEditedValues(prev => ({ ...prev, [`${column.id}:${item}`]: newValue as string })))
)
}
expandableRows={expandableRows}
getLoadingStatus={item => itemLoading.get(item) ?? 'finished'}
renderLoaderPending={() => <Button>Load more</Button>}
renderLoaderLoading={() => <StatusIndicator type="loading">Loading more</StatusIndicator>}
renderLoaderError={() => <StatusIndicator type="error">Error when loading more</StatusIndicator>}
/>
);
}

function usePageSettings() {
const { urlParams, setUrlParams } = useContext(AppContext as PageContext);
const settings = {
isExpandable: urlParams.isExpandable ?? false,
isExpanded: urlParams.isExpanded ?? false,
isEditable: urlParams.isEditable ?? false,
sortingDisabled: urlParams.sortingDisabled ?? false,
resizableColumns: urlParams.resizableColumns ?? true,
wrapLines: urlParams.wrapLines ?? false,
stripedRows: urlParams.stripedRows ?? false,
hasSelection: urlParams.hasSelection ?? true,
multiSelection: urlParams.multiSelection ?? true,
hasFooter: urlParams.hasFooter ?? false,
verticalAlign: (urlParams.verticalAlignTop ? 'top' : 'middle') as 'top' | 'middle',
stickyColumnsFirst: parseInt(urlParams.stickyColumnsFirst ?? '') || 0,
stickyColumnsLast: parseInt(urlParams.stickyColumnsLast ?? '') || 0,
tableEmpty: urlParams.tableEmpty ?? false,
tableLoading: urlParams.tableLoading ?? false,
};
return { settings, setUrlParams };
}
1 change: 1 addition & 0 deletions src/table/__tests__/header-cell.test.tsx
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ function TestComponent(props: Partial<TableHeaderCellProps<typeof testItem>>) {
columnId="id"
cellRef={() => {}}
tableRole={tableRole}
variant="container"
{...props}
/>
);
70 changes: 38 additions & 32 deletions src/table/__tests__/progressive-loading.test.tsx
Original file line number Diff line number Diff line change
@@ -25,7 +25,10 @@ interface Instance {
children?: Instance[];
}

const columnDefinitions: TableProps.ColumnDefinition<Instance>[] = [{ header: 'name', cell: item => item.name }];
const columnDefinitions: TableProps.ColumnDefinition<Instance>[] = [
{ header: 'name', cell: item => item.name },
{ header: 'version', cell: () => 'v0' },
];

const nestedItems: Instance[] = [
{
@@ -98,7 +101,7 @@ describe('Progressive loading', () => {
test('renders loaders in correct order for normal table', () => {
const { table } = renderTable({ getLoadingStatus: () => 'pending' });

expect(table.findRows().map(getTextContent)).toEqual(['Root-1', 'Root-2', '[pending] Loader for TABLE ROOT']);
expect(table.findRows().map(getTextContent)).toEqual(['Root-1v0', 'Root-2v0', '[pending] Loader for TABLE ROOT']);
});

test('renders loaders in correct order for expandable table', () => {
@@ -117,21 +120,21 @@ describe('Progressive loading', () => {
});

expect(table.findRows().map(getTextContent)).toEqual([
'Root-1',
'Nested-1.1',
'Nested-1.2',
'Nested-1.2.1',
'Nested-1.2.2',
'Root-1v0',
'Nested-1.1v0',
'Nested-1.2v0',
'Nested-1.2.1v0',
'Nested-1.2.2v0',
'[pending] Loader for Nested-1.2',
'[pending] Loader for Root-1',
'Root-2',
'Nested-2.1',
'Nested-2.1.1',
'Nested-2.1.2',
'Root-2v0',
'Nested-2.1v0',
'Nested-2.1.1v0',
'Nested-2.1.2v0',
'[pending] Loader for Nested-2.1',
'Nested-2.2',
'Nested-2.2.1',
'Nested-2.2.2',
'Nested-2.2v0',
'Nested-2.2.1v0',
'Nested-2.2.2v0',
'[pending] Loader for Nested-2.2',
'[pending] Loader for Root-2',
'[pending] Loader for TABLE ROOT',
@@ -220,25 +223,28 @@ describe('Progressive loading', () => {
}
);

test.each(['single', 'multi'] as const)('selection control is not rendered for loader rows', selectionType => {
const { table } = renderTable({
expandableRows: {
...defaultExpandableRows,
expandedItems: [{ name: 'Root-1' }],
},
getLoadingStatus: () => 'pending',
selectionType,
});
test.each(['single', 'multi'] as const)(
'selection control is not rendered for loader rows, selectionType=%s',
selectionType => {
const { table } = renderTable({
expandableRows: {
...defaultExpandableRows,
expandedItems: [{ name: 'Root-1' }],
},
getLoadingStatus: () => 'pending',
selectionType,
});

expect(table.findRows().map(w => [!!w.find('input'), getTextContent(w)])).toEqual([
[true, 'Root-1'],
[true, 'Nested-1.1'],
[true, 'Nested-1.2'],
[false, '[pending] Loader for Root-1'],
[true, 'Root-2'],
[false, '[pending] Loader for TABLE ROOT'],
]);
});
expect(table.findRows().map(w => [!!w.find('input'), getTextContent(w)])).toEqual([
[true, 'Root-1v0'],
[true, 'Nested-1.1v0'],
[true, 'Nested-1.2v0'],
[false, '[pending] Loader for Root-1'],
[true, 'Root-2v0'],
[false, '[pending] Loader for TABLE ROOT'],
]);
}
);

test.each(['loading', 'error'] as const)('loader row with status="%s" is added after empty expanded item', status => {
const { table } = renderTable({
12 changes: 2 additions & 10 deletions src/table/body-cell/disabled-inline-editor.tsx
Original file line number Diff line number Diff line change
@@ -23,16 +23,13 @@ interface DisabledInlineEditorProps<ItemType> extends TableBodyCellProps<ItemTyp
}

export function DisabledInlineEditor<ItemType>({
className,
item,
column,
ariaLabels,
isEditing,
onEditStart,
onEditEnd,
editDisabledReason,
isVisualRefresh,
resizableColumns = false,
...rest
}: DisabledInlineEditorProps<ItemType>) {
const clickAwayRef = useClickAway(() => {
@@ -72,13 +69,8 @@ export function DisabledInlineEditor<ItemType>({
nativeAttributes={
{ 'data-inline-editing-active': isEditing.toString() } as TableTdElementProps['nativeAttributes']
}
className={clsx(
className,
styles['body-cell-editable'],
resizableColumns && styles['resizable-columns'],
isEditing && styles['body-cell-edit-disabled-popover'],
isVisualRefresh && styles['is-visual-refresh']
)}
isEditing={isEditing}
isEditingDisabled={true}
onClick={!isEditing ? onClick : undefined}
onMouseEnter={() => setHasHover(true)}
onMouseLeave={() => setHasHover(false)}
35 changes: 10 additions & 25 deletions src/table/body-cell/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useRef, useState } from 'react';
import clsx from 'clsx';

import { useInternalI18n } from '../../i18n/context';
import Icon from '../../icon/internal';
@@ -22,8 +21,6 @@ const submitHandlerFallback = () => {
export interface TableBodyCellProps<ItemType> extends TableTdElementProps {
column: TableProps.ColumnDefinition<ItemType>;
item: ItemType;
isEditing: boolean;
resizableColumns?: boolean;
successfulEdit?: boolean;
onEditStart: () => void;
onEditEnd: (cancelled: boolean) => void;
@@ -32,16 +29,13 @@ export interface TableBodyCellProps<ItemType> extends TableTdElementProps {
}

function TableCellEditable<ItemType>({
className,
item,
column,
isEditing,
onEditStart,
onEditEnd,
submitEdit,
ariaLabels,
isVisualRefresh,
resizableColumns = false,
successfulEdit = false,
...rest
}: TableBodyCellProps<ItemType>) {
@@ -85,14 +79,8 @@ function TableCellEditable<ItemType>({
<TableTdElement
{...rest}
nativeAttributes={tdNativeAttributes as TableTdElementProps['nativeAttributes']}
className={clsx(
className,
styles['body-cell-editable'],
resizableColumns && styles['resizable-columns'],
isEditing && styles['body-cell-edit-active'],
showSuccessIcon && showIcon && styles['body-cell-has-success'],
isVisualRefresh && styles['is-visual-refresh']
)}
isEditing={isEditing}
hasSuccessIcon={showSuccessIcon && showIcon}
onClick={!isEditing ? onEditStart : undefined}
onMouseEnter={() => setHasHover(true)}
onMouseLeave={() => setHasHover(false)}
@@ -151,22 +139,19 @@ function TableCellEditable<ItemType>({
);
}

export function TableBodyCell<ItemType>({
isEditable,
...rest
}: TableBodyCellProps<ItemType> & { isEditable: boolean }) {
const isExpandableColumnCell = rest.level !== undefined;
const editDisabledReason = rest.column.editConfig?.disabledReason?.(rest.item);
export function TableBodyCell<ItemType>(props: TableBodyCellProps<ItemType>) {
const isExpandableColumnCell = props.level !== undefined;
const editDisabledReason = props.column.editConfig?.disabledReason?.(props.item);

// Inline editing is deactivated for expandable column because editable cells are interactive
// and cannot include interactive content such as expand toggles.
if (editDisabledReason && !isExpandableColumnCell) {
return <DisabledInlineEditor editDisabledReason={editDisabledReason} {...rest} />;
return <DisabledInlineEditor editDisabledReason={editDisabledReason} {...props} />;
}
if ((isEditable || rest.isEditing) && !isExpandableColumnCell) {
return <TableCellEditable {...rest} />;
if ((props.isEditable || props.isEditing) && !isExpandableColumnCell) {
return <TableCellEditable {...props} />;
}

const { column, item } = rest;
return <TableTdElement {...rest}>{column.cell(item)}</TableTdElement>;
const { column, item } = props;
return <TableTdElement {...props}>{column.cell(item)}</TableTdElement>;
}
26 changes: 21 additions & 5 deletions src/table/body-cell/td-element.tsx
Original file line number Diff line number Diff line change
@@ -7,16 +7,17 @@ import { copyAnalyticsMetadataAttribute } from '@cloudscape-design/component-too

import { useSingleTabStopNavigation } from '../../internal/context/single-tab-stop-navigation-context';
import { useMergeRefs } from '../../internal/hooks/use-merge-refs';
import { useVisualRefresh } from '../../internal/hooks/use-visual-mode';
import { ExpandToggleButton } from '../expandable-rows/expand-toggle-button';
import { TableProps } from '../interfaces.js';
import { StickyColumnsModel, useStickyCellStyles } from '../sticky-columns';
import { getTableCellRoleProps, TableRole } from '../table-role';
import { getStickyClassNames } from '../utils';

import tableStyles from '../styles.css.js';
import styles from './styles.css.js';

export interface TableTdElementProps {
className?: string;
style?: React.CSSProperties;
wrapLines: boolean | undefined;
isRowHeader?: boolean;
@@ -35,12 +36,12 @@ export interface TableTdElementProps {
children?: React.ReactNode;
isEvenRow?: boolean;
stripedRows?: boolean;
isSelection?: boolean;
hasSelection?: boolean;
hasFooter?: boolean;
columnId: PropertyKey;
colIndex: number;
stickyState: StickyColumnsModel;
isVisualRefresh?: boolean;
tableRole: TableRole;
level?: number;
isExpandable?: boolean;
@@ -49,12 +50,16 @@ export interface TableTdElementProps {
expandButtonLabel?: string;
collapseButtonLabel?: string;
verticalAlign?: TableProps.VerticalAlign;
resizableColumns?: boolean;
isEditable: boolean;
isEditing: boolean;
isEditingDisabled?: boolean;
hasSuccessIcon?: boolean;
}

export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElementProps>(
(
{
className,
style,
children,
wrapLines,
@@ -70,7 +75,7 @@ export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElem
onMouseLeave,
isEvenRow,
stripedRows,
isVisualRefresh,
isSelection,
hasSelection,
hasFooter,
columnId,
@@ -84,11 +89,17 @@ export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElem
expandButtonLabel,
collapseButtonLabel,
verticalAlign,
resizableColumns,
isEditable,
isEditing,
isEditingDisabled,
hasSuccessIcon,
...rest
},
ref
) => {
const Element = isRowHeader ? 'th' : 'td';
const isVisualRefresh = useVisualRefresh();

nativeAttributes = { ...nativeAttributes, ...getTableCellRoleProps({ tableRole, isRowHeader, colIndex }) };

@@ -106,7 +117,6 @@ export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElem
<Element
style={{ ...style, ...stickyStyles.style }}
className={clsx(
className,
styles['body-cell'],
wrapLines && styles['body-cell-wrap'],
isFirstRow && styles['body-cell-first-row'],
@@ -117,8 +127,14 @@ export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElem
!isEvenRow && stripedRows && styles['body-cell-shaded'],
stripedRows && styles['has-striped-rows'],
isVisualRefresh && styles['is-visual-refresh'],
isSelection && tableStyles['selection-control'],
hasSelection && styles['has-selection'],
hasFooter && styles['has-footer'],
resizableColumns && styles['resizable-columns'],
isEditable && styles['body-cell-editable'],
isEditing && !isEditingDisabled && styles['body-cell-edit-active'],
isEditing && isEditingDisabled && styles['body-cell-edit-disabled-popover'],
hasSuccessIcon && styles['body-cell-has-success'],
level !== undefined && styles['body-cell-expandable'],
level !== undefined && styles[`expandable-level-${getLevelClassSuffix(level)}`],
verticalAlign === 'top' && styles['body-cell-align-top'],
15 changes: 12 additions & 3 deletions src/table/header-cell/index.tsx
Original file line number Diff line number Diff line change
@@ -24,15 +24,17 @@ import analyticsSelectors from '../analytics-metadata/styles.css.js';
import styles from './styles.css.js';

export interface TableHeaderCellProps<ItemType> {
className?: string;
style?: React.CSSProperties;
tabIndex: number;
column: TableProps.ColumnDefinition<ItemType>;
activeSortingColumn?: TableProps.SortingColumn<ItemType>;
sortingDescending?: boolean;
sortingDisabled?: boolean;
wrapLines?: boolean;
stuck?: boolean;
sticky?: boolean;
hidden?: boolean;
stripedRows?: boolean;
onClick(detail: TableProps.SortingState<any>): void;
onResizeFinish: () => void;
colIndex: number;
@@ -47,10 +49,10 @@ export interface TableHeaderCellProps<ItemType> {
resizerRoleDescription?: string;
isExpandable?: boolean;
hasDynamicContent?: boolean;
variant: TableProps.Variant;
}

export function TableHeaderCell<ItemType>({
className,
style,
tabIndex,
column,
@@ -59,7 +61,10 @@ export function TableHeaderCell<ItemType>({
sortingDisabled,
wrapLines,
focusedComponent,
stuck,
sticky,
hidden,
stripedRows,
onClick,
colIndex,
updateColumn,
@@ -73,6 +78,7 @@ export function TableHeaderCell<ItemType>({
resizerRoleDescription,
isExpandable,
hasDynamicContent,
variant,
}: TableHeaderCellProps<ItemType>) {
const i18n = useInternalI18n('table');
const sortable = !!column.sortingComparator || !!column.sortingField;
@@ -114,17 +120,20 @@ export function TableHeaderCell<ItemType>({

return (
<TableThElement
className={className}
style={style}
cellRef={cellRefCombined}
sortingStatus={sortingStatus}
sortingDisabled={sortingDisabled}
focusedComponent={focusedComponent}
stuck={stuck}
sticky={sticky}
hidden={hidden}
stripedRows={stripedRows}
colIndex={colIndex}
columnId={columnId}
stickyState={stickyState}
tableRole={tableRole}
variant={variant}
{...(sortingDisabled
? {}
: getAnalyticsMetadataAttribute({
27 changes: 23 additions & 4 deletions src/table/header-cell/th-element.tsx
Original file line number Diff line number Diff line change
@@ -7,43 +7,56 @@ import { copyAnalyticsMetadataAttribute } from '@cloudscape-design/component-too

import { useSingleTabStopNavigation } from '../../internal/context/single-tab-stop-navigation-context';
import { useMergeRefs } from '../../internal/hooks/use-merge-refs';
import { useVisualRefresh } from '../../internal/hooks/use-visual-mode';
import { TableProps } from '../interfaces';
import { StickyColumnsModel, useStickyCellStyles } from '../sticky-columns';
import { getTableColHeaderRoleProps, TableRole } from '../table-role';
import { getStickyClassNames } from '../utils';
import { SortingStatus } from './utils';

import tableStyles from '../styles.css.js';
import styles from './styles.css.js';

interface TableThElementProps {
className?: string;
export interface TableThElementProps {
style?: React.CSSProperties;
sortingStatus?: SortingStatus;
sortingDisabled?: boolean;
focusedComponent?: null | string;
stuck?: boolean;
sticky?: boolean;
hidden?: boolean;
stripedRows?: boolean;
isSelection?: boolean;
colIndex: number;
columnId: PropertyKey;
stickyState: StickyColumnsModel;
cellRef?: React.RefCallback<HTMLElement> | null;
tableRole: TableRole;
children: React.ReactNode;
variant: TableProps.Variant;
}

export function TableThElement({
className,
style,
sortingStatus,
sortingDisabled,
focusedComponent,
stuck,
sticky,
hidden,
stripedRows,
isSelection,
colIndex,
columnId,
stickyState,
cellRef,
tableRole,
children,
variant,
...props
}: TableThElementProps) {
const isVisualRefresh = useVisualRefresh();

const stickyStyles = useStickyCellStyles({
stickyColumns: stickyState,
columnId,
@@ -58,7 +71,13 @@ export function TableThElement({
<th
data-focus-id={`header-${String(columnId)}`}
className={clsx(
className,
styles['header-cell'],
styles[`header-cell-variant-${variant}`],
sticky && styles['header-cell-sticky'],
stuck && styles['header-cell-stuck'],
stripedRows && styles['has-striped-rows'],
isVisualRefresh && styles['is-visual-refresh'],
isSelection && clsx(tableStyles['selection-control'], tableStyles['selection-control-header']),
{
[styles['header-cell-fake-focus']]: focusedComponent === `header-${String(columnId)}`,
[styles['header-cell-sortable']]: sortingStatus,
60 changes: 21 additions & 39 deletions src/table/internal.tsx
Original file line number Diff line number Diff line change
@@ -32,15 +32,15 @@ import { SomeRequired } from '../internal/types';
import InternalLiveRegion from '../live-region/internal';
import { GeneratedAnalyticsMetadataTableComponent } from './analytics-metadata/interfaces';
import { TableBodyCell } from './body-cell';
import { TableTdElement } from './body-cell/td-element';
import { checkColumnWidths } from './column-widths-utils';
import { useExpandableTableProps } from './expandable-rows/expandable-rows-utils';
import { TableForwardRefType, TableProps, TableRow } from './interfaces';
import { NoDataCell } from './no-data-cell';
import { ItemsLoader } from './progressive-loading/items-loader';
import { TableLoaderCell } from './progressive-loading/loader-cell';
import { useProgressiveLoadingProps } from './progressive-loading/progressive-loading-utils';
import { ResizeTracker } from './resizer';
import { focusMarkers, SelectionControl, useSelection, useSelectionFocusMove } from './selection';
import { focusMarkers, useSelection, useSelectionFocusMove } from './selection';
import { TableBodySelectionCell } from './selection/selection-cell';
import { useStickyColumns } from './sticky-columns';
import StickyHeader, { StickyHeaderRef } from './sticky-header';
import { StickyScrollbar } from './sticky-scrollbar';
@@ -550,7 +550,6 @@ const InternalTable = React.forwardRef(
});
const getTableItemKey = (item: T) => getItemKey(trackBy, item, rowIndex);
const sharedCellProps = {
isVisualRefresh,
isFirstRow,
isLastRow,
isSelected: hasSelection && isRowSelected(row),
@@ -584,21 +583,17 @@ const InternalTable = React.forwardRef(
{...rowRoleProps}
>
{getItemSelectionProps && (
<TableTdElement
<TableBodySelectionCell
{...sharedCellProps}
className={styles['selection-control']}
wrapLines={false}
columnId={selectionColumnId}
colIndex={0}
>
<SelectionControl
onFocusDown={moveFocusDown}
onFocusUp={moveFocusUp}
{...getItemSelectionProps(row.item)}
rowIndex={rowIndex}
itemKey={`${getTableItemKey(row.item)}`}
/>
</TableTdElement>
selectionControlProps={{
...getItemSelectionProps(row.item),
onFocusDown: moveFocusDown,
onFocusUp: moveFocusUp,
rowIndex,
itemKey: `${getTableItemKey(row.item)}`,
}}
/>
)}

{visibleColumnDefinitions.map((column, colIndex) => {
@@ -667,37 +662,24 @@ const InternalTable = React.forwardRef(
{...rowRoleProps}
>
{getItemSelectionProps && (
<TableTdElement
{...sharedCellProps}
className={styles['selection-control']}
wrapLines={false}
columnId={selectionColumnId}
colIndex={0}
>
{null}
</TableTdElement>
<TableBodySelectionCell {...sharedCellProps} columnId={selectionColumnId} />
)}
{visibleColumnDefinitions.map((column, colIndex) => (
<TableTdElement
<TableLoaderCell
key={getColumnKey(column, colIndex)}
{...sharedCellProps}
wrapLines={false}
columnId={column.id ?? colIndex}
colIndex={colIndex + colIndexOffset}
isRowHeader={colIndex === 0}
level={row.level}
>
{colIndex === 0 ? (
<ItemsLoader
item={row.item}
loadingStatus={row.status}
renderLoaderPending={renderLoaderPending}
renderLoaderLoading={renderLoaderLoading}
renderLoaderError={renderLoaderError}
trackBy={trackBy}
/>
) : null}
</TableTdElement>
item={row.item}
loadingStatus={row.status}
renderLoaderPending={renderLoaderPending}
renderLoaderLoading={renderLoaderLoading}
renderLoaderError={renderLoaderError}
trackBy={trackBy}
/>
))}
</tr>
);
2 changes: 1 addition & 1 deletion src/table/progressive-loading/items-loader.tsx
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import { applyTrackBy } from '../utils';

import styles from './styles.css.js';

interface ItemsLoaderProps<T> {
export interface ItemsLoaderProps<T> {
item: null | T;
loadingStatus: TableProps.LoadingStatus;
renderLoaderPending?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
36 changes: 36 additions & 0 deletions src/table/progressive-loading/loader-cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React from 'react';

import { TableTdElement, TableTdElementProps } from '../body-cell/td-element';
import { ItemsLoader, ItemsLoaderProps } from './items-loader';

export interface TableLoaderCellProps<ItemType>
extends Omit<TableTdElementProps, 'isEditable' | 'isEditing'>,
ItemsLoaderProps<ItemType> {}

export function TableLoaderCell<ItemType>({
item,
loadingStatus,
renderLoaderPending,
renderLoaderLoading,
renderLoaderError,
trackBy,
...props
}: TableLoaderCellProps<ItemType>) {
return (
<TableTdElement {...props} isEditable={false} isEditing={false}>
{props.isRowHeader ? (
<ItemsLoader
item={item}
loadingStatus={loadingStatus}
renderLoaderPending={renderLoaderPending}
renderLoaderLoading={renderLoaderLoading}
renderLoaderError={renderLoaderError}
trackBy={trackBy}
/>
) : null}
</TableTdElement>
);
}
58 changes: 58 additions & 0 deletions src/table/selection/selection-cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import ScreenreaderOnly from '../../internal/components/screenreader-only';
import { TableTdElement, TableTdElementProps } from '../body-cell/td-element';
import { TableThElement, TableThElementProps } from '../header-cell/th-element';
import { Divider } from '../resizer';
import { SelectionProps } from './interfaces';
import { SelectionControl, SelectionControlProps } from './selection-control';

import styles from '../styles.css.js';

interface TableHeaderSelectionCellProps extends Omit<TableThElementProps, 'children' | 'colIndex'> {
focusedComponent?: null | string;
singleSelectionHeaderAriaLabel?: string;
getSelectAllProps?: () => SelectionProps;
onFocusMove: ((sourceElement: HTMLElement, fromIndex: number, direction: -1 | 1) => void) | undefined;
}

interface TableBodySelectionCellProps
extends Omit<TableTdElementProps, 'children' | 'colIndex' | 'wrapLines' | 'isEditable' | 'isEditing'> {
selectionControlProps?: SelectionControlProps;
}

export function TableHeaderSelectionCell({
focusedComponent,
singleSelectionHeaderAriaLabel,
getSelectAllProps,
onFocusMove,
...props
}: TableHeaderSelectionCellProps) {
return (
<TableThElement {...props} isSelection={true} colIndex={0} focusedComponent={focusedComponent}>
{getSelectAllProps ? (
<SelectionControl
onFocusDown={event => {
onFocusMove!(event.target as HTMLElement, -1, +1);
}}
focusedComponent={focusedComponent}
{...getSelectAllProps()}
{...(props.sticky ? { tabIndex: -1 } : {})}
/>
) : (
<ScreenreaderOnly>{singleSelectionHeaderAriaLabel}</ScreenreaderOnly>
)}
<Divider className={styles['resize-divider']} />
</TableThElement>
);
}

export function TableBodySelectionCell({ selectionControlProps, ...props }: TableBodySelectionCellProps) {
return (
<TableTdElement {...props} isSelection={true} wrapLines={false} isEditable={false} isEditing={false} colIndex={0}>
{selectionControlProps ? <SelectionControl {...selectionControlProps} /> : null}
</TableTdElement>
);
}
66 changes: 19 additions & 47 deletions src/table/thead.tsx
Original file line number Diff line number Diff line change
@@ -6,21 +6,17 @@ import clsx from 'clsx';
import { findUpUntil } from '@cloudscape-design/component-toolkit/dom';
import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';

import ScreenreaderOnly from '../internal/components/screenreader-only';
import { fireNonCancelableEvent, NonCancelableEventHandler } from '../internal/events';
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
import { GeneratedAnalyticsMetadataTableSelectAll } from './analytics-metadata/interfaces';
import { TableHeaderCell } from './header-cell';
import { TableThElement } from './header-cell/th-element';
import { TableProps } from './interfaces';
import { Divider } from './resizer';
import { focusMarkers, SelectionControl, SelectionProps } from './selection';
import { focusMarkers, SelectionProps } from './selection';
import { TableHeaderSelectionCell } from './selection/selection-cell';
import { StickyColumnsModel } from './sticky-columns';
import { getTableHeaderRowRoleProps, TableRole } from './table-role';
import { useColumnWidths } from './use-column-widths';
import { getColumnKey } from './utils';

import headerCellStyles from './header-cell/styles.css.js';
import styles from './styles.css.js';

export interface TheadProps {
@@ -82,25 +78,18 @@ const Thead = React.forwardRef(
}: TheadProps,
outerRef: React.Ref<HTMLTableRowElement>
) => {
const isVisualRefresh = useVisualRefresh();

const headerCellClass = clsx(
headerCellStyles['header-cell'],
headerCellStyles[`header-cell-variant-${variant}`],
sticky && headerCellStyles['header-cell-sticky'],
stuck && headerCellStyles['header-cell-stuck'],
stripedRows && headerCellStyles['has-striped-rows'],
isVisualRefresh && headerCellStyles['is-visual-refresh']
);

const selectionCellClass = clsx(
styles['selection-control'],
styles['selection-control-header'],
isVisualRefresh && styles['is-visual-refresh']
);

const { getColumnStyles, columnWidths, updateColumn, setCell } = useColumnWidths();

const commonCellProps = {
stuck,
sticky,
hidden,
stripedRows,
tableRole,
variant,
stickyState,
};

return (
<thead className={clsx(!hidden && styles['thead-active'])}>
<tr
@@ -116,49 +105,33 @@ const Thead = React.forwardRef(
onBlur={() => onFocusedComponentChange?.(null)}
>
{selectionType ? (
<TableThElement
className={clsx(headerCellClass, selectionCellClass, hidden && headerCellStyles['header-cell-hidden'])}
hidden={hidden}
tableRole={tableRole}
colIndex={0}
<TableHeaderSelectionCell
{...commonCellProps}
focusedComponent={focusedComponent}
columnId={selectionColumnId}
stickyState={stickyState}
getSelectAllProps={getSelectAllProps}
onFocusMove={onFocusMove}
singleSelectionHeaderAriaLabel={singleSelectionHeaderAriaLabel}
{...getAnalyticsMetadataAttribute({
action: 'selectAll',
} as Partial<GeneratedAnalyticsMetadataTableSelectAll>)}
>
{getSelectAllProps ? (
<SelectionControl
onFocusDown={event => {
onFocusMove!(event.target as HTMLElement, -1, +1);
}}
focusedComponent={focusedComponent}
{...getSelectAllProps()}
{...(sticky ? { tabIndex: -1 } : {})}
/>
) : (
<ScreenreaderOnly>{singleSelectionHeaderAriaLabel}</ScreenreaderOnly>
)}
<Divider className={styles['resize-divider']} />
</TableThElement>
/>
) : null}

{columnDefinitions.map((column, colIndex) => {
const columnId = getColumnKey(column, colIndex);
return (
<TableHeaderCell
{...commonCellProps}
key={columnId}
style={getColumnStyles(sticky, columnId)}
className={headerCellClass}
tabIndex={sticky ? -1 : 0}
focusedComponent={focusedComponent}
column={column}
activeSortingColumn={sortingColumn}
sortingDescending={sortingDescending}
sortingDisabled={sortingDisabled}
wrapLines={wrapLines}
hidden={hidden}
colIndex={selectionType ? colIndex + 1 : colIndex}
columnId={columnId}
updateColumn={updateColumn}
@@ -169,7 +142,6 @@ const Thead = React.forwardRef(
fireNonCancelableEvent(onSortingChange, detail);
}}
isEditable={!!column.editConfig}
stickyState={stickyState}
cellRef={node => setCell(sticky, columnId, node)}
tableRole={tableRole}
resizerRoleDescription={resizerRoleDescription}

0 comments on commit 8f977eb

Please sign in to comment.