Skip to content

Commit 34d2e2a

Browse files
committed
chore: Internal style v2 for table
1 parent d2e84d8 commit 34d2e2a

31 files changed

Lines changed: 1210 additions & 273 deletions

File tree

‎build-tools/utils/custom-css-properties.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,5 +207,7 @@ const customCssPropertiesList = [
207207
// Resolved Style API values for calc(): postcss-calc 7 fails on deeply nested var() inside calc().
208208
'containerBorderRadius',
209209
'popoverArrowBorderWidth',
210+
// Table style API, read by the container header of the table
211+
'tableStickyHeaderShadow',
210212
];
211213
module.exports = customCssPropertiesList;

‎pages/table/style-api-v2.page.tsx‎

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useContext, useState } from 'react';
4+
import clsx from 'clsx';
5+
6+
import { Checkbox, FormField, Header, Select, SpaceBetween, Table, TableProps } from '~components';
7+
import { StyleClassNames } from '~components/table/style-api';
8+
9+
import AppContext, { AppContextType } from '../app/app-context';
10+
import { SimplePage } from '../app/templates';
11+
import { generateItems, Instance, InstanceState } from './generate-data';
12+
import { columnsConfig } from './shared-configs';
13+
14+
import styles from './style-api-v2.scss';
15+
16+
interface Settings {
17+
selectionType: 'none' | TableProps.SelectionType;
18+
stickyColumnsFirst: string;
19+
stickyColumnsLast: string;
20+
statusHighlight: 'none' | 'cells' | 'rows';
21+
nestedRowsHighlight: boolean;
22+
}
23+
24+
type PageContext = React.Context<AppContextType<Partial<Settings>>>;
25+
26+
const allItems = generateItems(30);
27+
const rootItems = allItems.slice(0, 10);
28+
// Four root items have five children each; the first one is expanded by default.
29+
const parentItems = [0, 1, 3, 5].map(index => rootItems[index]);
30+
const itemChildren = new Map(
31+
parentItems.map((item, index) => [item, allItems.slice(10 + index * 5, 15 + index * 5)] as const)
32+
);
33+
const nestedItems = new Set([...itemChildren.values()].flat());
34+
35+
const columnDefinitions = columnsConfig.map(column => ({ ...column, minWidth: 200 }));
36+
const instanceColumns = ['id', 'type', 'state'];
37+
const networkColumns = ['dnsName', 'imageId'];
38+
const columnGroups = new Map([
39+
...instanceColumns.map(id => [id, 'instance'] as const),
40+
...networkColumns.map(id => [id, 'network'] as const),
41+
]);
42+
const groupDefinitions: TableProps.GroupDefinition<Instance>[] = [
43+
{ id: 'instance', header: 'Instance' },
44+
{ id: 'network', header: 'Network' },
45+
];
46+
const columnDisplay: TableProps.ColumnDisplayProperties[] = [
47+
{
48+
type: 'group',
49+
id: 'instance',
50+
visible: true,
51+
children: instanceColumns.map(id => ({ id, visible: true })),
52+
},
53+
{
54+
type: 'group',
55+
id: 'network',
56+
visible: true,
57+
children: networkColumns.map(id => ({ id, visible: true })),
58+
},
59+
];
60+
61+
const ariaLabels: TableProps.AriaLabels<Instance> = {
62+
selectionGroupLabel: 'Items selection',
63+
allItemsSelectionLabel: () => 'Select all',
64+
itemSelectionLabel: (_, item) => `Select ${item.id}`,
65+
expandButtonLabel: () => 'Expand',
66+
collapseButtonLabel: () => 'Collapse',
67+
};
68+
69+
const selectionTypeOptions = ['none', 'single', 'multi'].map(value => ({ value }));
70+
const stickyColumnsOptions = ['0', '1', '2'].map(value => ({ value }));
71+
const statusHighlightOptions = ['none', 'cells', 'rows'].map(value => ({ value }));
72+
73+
const statuses: Partial<Record<InstanceState, string>> = {
74+
RUNNING: 'running',
75+
STOPPING: 'transitioning',
76+
STOPPED: 'stopped',
77+
TERMINATING: 'transitioning',
78+
TERMINATED: 'terminated',
79+
};
80+
81+
export default function () {
82+
const {
83+
urlParams: {
84+
selectionType = 'multi',
85+
stickyColumnsFirst = '1',
86+
stickyColumnsLast = '0',
87+
statusHighlight = 'cells',
88+
nestedRowsHighlight = true,
89+
},
90+
setUrlParams,
91+
} = useContext(AppContext as PageContext);
92+
93+
const [selectedItems, setSelectedItems] = useState<Instance[]>([rootItems[2], rootItems[4]]);
94+
const [expandedItems, setExpandedItems] = useState<Instance[]>([parentItems[0]]);
95+
const [sortingColumn, setSortingColumn] = useState<TableProps.SortingColumn<Instance>>(columnDefinitions[1]);
96+
const [sortingDescending, setSortingDescending] = useState(false);
97+
98+
const sortingField = sortingColumn.sortingField as keyof Instance;
99+
const items = [...rootItems].sort(
100+
(a, b) => String(a[sortingField]).localeCompare(String(b[sortingField])) * (sortingDescending ? -1 : 1)
101+
);
102+
103+
const booleanSetting = (key: keyof Settings, label: string, checked: boolean) => (
104+
<Checkbox checked={checked} onChange={({ detail }) => setUrlParams({ [key]: detail.checked })}>
105+
{label}
106+
</Checkbox>
107+
);
108+
const selectSetting = (key: keyof Settings, label: string, value: string, options: { value: string }[]) => (
109+
<FormField label={label}>
110+
<Select
111+
selectedOption={{ value }}
112+
options={options}
113+
onChange={({ detail }) => setUrlParams({ [key]: detail.selectedOption.value })}
114+
ariaLabel={label}
115+
/>
116+
</FormField>
117+
);
118+
119+
const statusClassName = (item: Instance, slot: 'row' | 'cell') =>
120+
statuses[item.state] && styles[`status-${statuses[item.state]}-${slot}`];
121+
122+
const isItemDisabled = (item: Instance) => item.state === 'TERMINATING' || item.state === 'STOPPING';
123+
124+
// Selected rows keep the selection color: status and nested highlights skip them.
125+
const styleClassNames: StyleClassNames<Instance> = {
126+
root: styles.table,
127+
headerCell: args => {
128+
if (args.type === 'selection') {
129+
return clsx(styles['header-cell'], selectedItems.length > 0 && styles['select-all-selected']);
130+
}
131+
const groupId = args.type === 'group' ? args.group.id : columnGroups.get(args.column.id ?? '');
132+
return groupId ? styles[`${groupId}-header-cell`] : styles['header-cell'];
133+
},
134+
sortingIndicator: styles['sorting-indicator'],
135+
row: ({ item, selected }) =>
136+
clsx(
137+
styles.row,
138+
isItemDisabled(item) && styles['row-disabled'],
139+
selected
140+
? styles['row-selected']
141+
: (statusHighlight === 'rows' && statusClassName(item, 'row')) ||
142+
(nestedRowsHighlight && nestedItems.has(item) && styles['row-nested'])
143+
),
144+
cell: args =>
145+
clsx(
146+
styles.cell,
147+
statusHighlight === 'cells' &&
148+
args.type === 'data' &&
149+
args.column.id === 'state' &&
150+
!selectedItems.includes(args.item) &&
151+
statusClassName(args.item, 'cell')
152+
),
153+
selectionCheckbox: styles.checkbox,
154+
selectionRadio: styles.radio,
155+
};
156+
157+
return (
158+
<SimplePage
159+
title="Table - Style API v2"
160+
screenshotArea={{}}
161+
settings={
162+
<SpaceBetween size="m">
163+
<SpaceBetween size="m" direction="horizontal" alignItems="end">
164+
{selectSetting('selectionType', 'Selection type', selectionType, selectionTypeOptions)}
165+
{selectSetting('stickyColumnsFirst', 'Sticky columns first', stickyColumnsFirst, stickyColumnsOptions)}
166+
{selectSetting('stickyColumnsLast', 'Sticky columns last', stickyColumnsLast, stickyColumnsOptions)}
167+
{selectSetting('statusHighlight', 'Highlight status', statusHighlight, statusHighlightOptions)}
168+
</SpaceBetween>
169+
<SpaceBetween size="m" direction="horizontal">
170+
{booleanSetting('nestedRowsHighlight', 'Highlight nested rows', nestedRowsHighlight)}
171+
</SpaceBetween>
172+
</SpaceBetween>
173+
}
174+
>
175+
<Table
176+
enableKeyboardNavigation={true}
177+
header={<Header counter={`(${items.length})`}>Instances</Header>}
178+
variant="embedded"
179+
items={items}
180+
columnDefinitions={columnDefinitions}
181+
groupDefinitions={groupDefinitions}
182+
columnDisplay={columnDisplay}
183+
trackBy="id"
184+
ariaLabels={ariaLabels}
185+
resizableColumns={true}
186+
stickyHeader={true}
187+
stickyColumns={{ first: +stickyColumnsFirst, last: +stickyColumnsLast }}
188+
selectionType={selectionType === 'none' ? undefined : selectionType}
189+
selectedItems={selectedItems}
190+
onSelectionChange={({ detail }) => setSelectedItems(detail.selectedItems)}
191+
isItemDisabled={isItemDisabled}
192+
sortingColumn={sortingColumn}
193+
sortingDescending={sortingDescending}
194+
onSortingChange={({ detail }) => {
195+
setSortingColumn(detail.sortingColumn);
196+
setSortingDescending(!!detail.isDescending);
197+
}}
198+
expandableRows={{
199+
getItemChildren: item => itemChildren.get(item) ?? [],
200+
isItemExpandable: item => itemChildren.has(item),
201+
expandedItems,
202+
onExpandableItemToggle: ({ detail }) =>
203+
setExpandedItems(prev =>
204+
detail.expanded ? [...prev, detail.item] : prev.filter(item => item !== detail.item)
205+
),
206+
}}
207+
{...{ styleClassNames }}
208+
/>
209+
</SimplePage>
210+
);
211+
}

‎pages/table/style-api-v2.scss‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
@use '~components/styling/checkbox';
7+
@use '~components/styling/radio-button';
8+
@use '~components/styling/table';
9+
@use '../style-api/palette' as palette;
10+
11+
$selected-surface: light-dark(hsl(265, 100%, 98.5%), hsl(265, 40%, 13%));
12+
$instance-surface: light-dark(hsl(45, 90%, 88%), hsl(45, 60%, 18%));
13+
$instance-text: light-dark(hsl(35, 80%, 20%), hsl(45, 90%, 85%));
14+
$network-surface: light-dark(hsl(200, 60%, 93%), hsl(200, 45%, 18%));
15+
$network-text: light-dark(hsl(200, 80%, 22%), hsl(200, 70%, 88%));
16+
$nested-surface: light-dark(hsl(220, 30%, 96.5%), hsl(220, 20%, 18%));
17+
$transitioning-surface: light-dark(hsl(0, 0%, 96.5%), hsl(0, 0%, 21%));
18+
19+
.table {
20+
@include table.root(
21+
$selected-row-outline-color: palette.$genai,
22+
$selected-row-outline-radius: 4px,
23+
$sticky-header-shadow: (
24+
0 1px 0 0 palette.$text,
25+
0 16px 16px -12px rgba(0, 7, 22, 0.1),
26+
)
27+
);
28+
}
29+
30+
// The color also applies to hovered and sorted cells.
31+
@mixin header-cell($background, $color) {
32+
@include table.headerCell(
33+
$background: $background,
34+
$color: $color,
35+
$font-weight: 800,
36+
$letter-spacing: 0.02em,
37+
$border-color: $color,
38+
$divider-color: $color,
39+
$focus-ring-color: palette.$genai,
40+
$focus-ring-width: 3px,
41+
$focus-ring-radius: 6px
42+
);
43+
}
44+
45+
.cell {
46+
@include table.cell($focus-ring-color: palette.$genai, $focus-ring-width: 3px, $focus-ring-radius: 6px);
47+
}
48+
49+
.header-cell {
50+
@include header-cell(palette.$subtle, $instance-text);
51+
}
52+
53+
.instance-header-cell {
54+
@include header-cell($instance-surface, $instance-text);
55+
}
56+
57+
.network-header-cell {
58+
@include header-cell($network-surface, $network-text);
59+
}
60+
61+
.sorting-indicator {
62+
@include table.sortingIndicator($color: palette.$genai);
63+
}
64+
65+
.row {
66+
@include table.row($divider-color: palette.$border);
67+
}
68+
69+
.row-selected {
70+
@include table.row($background-color: $selected-surface);
71+
}
72+
73+
.row-nested {
74+
@include table.row($background-color: $nested-surface);
75+
}
76+
77+
@each $status,
78+
$surface
79+
in (
80+
running: palette.$success-surface,
81+
transitioning: $transitioning-surface,
82+
stopped: palette.$warning-surface,
83+
terminated: palette.$error-surface
84+
)
85+
{
86+
.status-#{$status}-row {
87+
@include table.row($background-color: $surface);
88+
}
89+
90+
.status-#{$status}-cell {
91+
@include table.cell($background-color: $surface);
92+
}
93+
}
94+
95+
// Control looks come from the row and selection header cell classes, as a consumer would write them.
96+
/* stylelint-disable selector-combinator-disallowed-list */
97+
.checkbox {
98+
@include checkbox.control($border-color: palette.$genai, $focus-ring-color: palette.$genai);
99+
}
100+
101+
.row-selected .checkbox,
102+
.select-all-selected .checkbox {
103+
@include checkbox.control(
104+
$fill-color: palette.$genai,
105+
$border-color: palette.$genai,
106+
$indicator-color: palette.$genai-surface
107+
);
108+
}
109+
110+
.row-disabled .checkbox {
111+
@include checkbox.control($fill-color: palette.$control-bg-disabled, $border-color: palette.$border-disabled);
112+
}
113+
114+
.radio {
115+
@include radio-button.control($border-color: palette.$genai, $focus-ring-color: palette.$genai);
116+
}
117+
118+
.row-selected .radio {
119+
@include radio-button.control(
120+
$fill-color: palette.$genai,
121+
$border-color: palette.$genai,
122+
$indicator-color: palette.$genai-surface
123+
);
124+
}
125+
126+
.row-disabled .radio {
127+
@include radio-button.control($fill-color: palette.$control-bg-disabled, $border-color: palette.$border-disabled);
128+
}

0 commit comments

Comments
 (0)