Skip to content
Draft
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
148 changes: 148 additions & 0 deletions pages/table/expandable-rows-filter-highlight.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useMemo, useState } from 'react';

import { Box, Checkbox, Header, Input, SpaceBetween, Table, TableProps } from '~components';
import I18nProvider from '~components/i18n';
import messages from '~components/i18n/messages/all.en';

interface Node {
id: string;
name: string;
type: string;
size: string;
children?: Node[];
}

// A small static hierarchy so the page does not depend on collection-hooks tree filtering.
const data: Node[] = [
{
id: 'eu',
name: 'eu-west-1',
type: 'region',
size: '—',
children: [
{
id: 'eu-prod',
name: 'production',
type: 'cluster',
size: '—',
children: [
{ id: 'eu-prod-db1', name: 'orders-db', type: 'instance', size: 'r5.large' },
{ id: 'eu-prod-db2', name: 'payments-db', type: 'instance', size: 'r5.xlarge' },
],
},
{
id: 'eu-staging',
name: 'staging',
type: 'cluster',
size: '—',
children: [{ id: 'eu-staging-db1', name: 'orders-db-staging', type: 'instance', size: 't3.medium' }],
},
],
},
{
id: 'us',
name: 'us-east-1',
type: 'region',
size: '—',
children: [
{
id: 'us-prod',
name: 'production',
type: 'cluster',
size: '—',
children: [
{ id: 'us-prod-db1', name: 'analytics-db', type: 'instance', size: 'r5.2xlarge' },
{ id: 'us-prod-db2', name: 'orders-db', type: 'instance', size: 'r5.large' },
],
},
],
},
];

const columnDefinitions: TableProps.ColumnDefinition<Node>[] = [
{ id: 'name', header: 'Name', cell: item => item.name, isRowHeader: true },
{ id: 'type', header: 'Type', cell: item => item.type },
{ id: 'size', header: 'Size', cell: item => item.size },
];

const getItemChildren = (item: Node) => item.children ?? [];

function subtreeHasMatch(item: Node, isMatched: (item: Node) => boolean): boolean {
return isMatched(item) || getItemChildren(item).some(child => subtreeHasMatch(child, isMatched));
}

export default function ExpandableFilterHighlightPage() {
const [filteringText, setFilteringText] = useState('');
const [expandedItems, setExpandedItems] = useState<ReadonlyArray<Node>>([]);
const [highlightMatched, setHighlightMatched] = useState(true);

const isItemMatched = useMemo(() => {
const query = filteringText.trim().toLowerCase();
return (item: Node) => query.length > 0 && item.name.toLowerCase().includes(query);
}, [filteringText]);

// Keep parent rows of matching descendants visible: only surface roots whose subtree contains a match.
const items = useMemo(() => {
if (filteringText.trim().length === 0) {
return data;
}
return data.filter(root => subtreeHasMatch(root, isItemMatched));
}, [filteringText, isItemMatched]);

return (
<I18nProvider messages={[messages]} locale="en">
<Box padding="l">
<SpaceBetween size="l">
<Header
variant="h1"
description="AWSUI-61158: keep and auto-expand ancestor rows when filtering expandable tables"
>
Expandable table filter highlight
</Header>

<SpaceBetween size="s" direction="horizontal">
<div style={{ width: 320 }}>
<Input
value={filteringText}
onChange={e => setFilteringText(e.detail.value)}
placeholder="Filter by name (e.g. orders-db)"
ariaLabel="Filter databases"
type="search"
/>
</div>
<Checkbox checked={highlightMatched} onChange={e => setHighlightMatched(e.detail.checked)}>
Highlight & auto-expand matches
</Checkbox>
</SpaceBetween>

<Table
columnDefinitions={columnDefinitions}
items={items}
trackBy="id"
variant="container"
header={<Header>Databases</Header>}
ariaLabels={{
tableLabel: 'Databases',
expandButtonLabel: () => 'Expand row',
collapseButtonLabel: () => 'Collapse row',
}}
expandableRows={{
getItemChildren,
isItemExpandable: item => getItemChildren(item).length > 0,
expandedItems,
onExpandableItemToggle: ({ detail }) =>
setExpandedItems(prev =>
detail.expanded ? [...prev, detail.item] : prev.filter(i => i.id !== detail.item.id)
),
highlightMatched,
isItemMatched,
}}
empty={<Box textAlign="center">No matches</Box>}
/>
</SpaceBetween>
</Box>
</I18nProvider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28587,7 +28587,12 @@ The value is passed as \`itemsCount\` property to the \`ariaLabels.allItemsSelec
* \`getSelectedItemsCount\` (optional, (Item) => number) - Use it to indicate the number of selected resources nested under the given item.
The value is passed as \`selectedItemsCount\` property to the \`columnDefinitions[index].counter\` and \`ariaLabels.itemSelectionLabel\` functions.
* \`totalSelectedItemsCount\` (optional, number) - Use it to indicate the total number of selected resources in the table.
The value is passed as \`selectedItemsCount\` property to the \`ariaLabels.allItemsSelectionLabel\`.",
The value is passed as \`selectedItemsCount\` property to the \`ariaLabels.allItemsSelectionLabel\`.
* \`highlightMatched\` (optional, boolean) - When set to \`true\` (together with \`isItemMatched\`), the table keeps ancestor
rows of matching descendants visible, automatically expands those ancestors so the matches are reachable, and visually
highlights the matched rows. This is opt-in and backward compatible; when omitted the expandable rows behave as before.
* \`isItemMatched\` (optional, (Item) => boolean) - A predicate returning \`true\` for items that match the currently applied
filter. Used together with \`highlightMatched\` to determine which ancestors to auto-expand and which rows to highlight.",
"inlineType": {
"name": "TableProps.ExpandableRows<T>",
"properties": [
Expand Down Expand Up @@ -28665,6 +28670,11 @@ The value is passed as \`selectedItemsCount\` property to the \`ariaLabels.allIt
"optional": true,
"type": "TableProps.GroupSelectionState<T>",
},
{
"name": "highlightMatched",
"optional": true,
"type": "boolean",
},
{
"inlineType": {
"name": "(item: T) => boolean",
Expand All @@ -28681,6 +28691,22 @@ The value is passed as \`selectedItemsCount\` property to the \`ariaLabels.allIt
"optional": false,
"type": "(item: T) => boolean",
},
{
"inlineType": {
"name": "(item: T) => boolean",
"parameters": [
{
"name": "item",
"type": "T",
},
],
"returnType": "boolean",
"type": "function",
},
"name": "isItemMatched",
"optional": true,
"type": "((item: T) => boolean)",
},
{
"inlineType": {
"name": "TableProps.OnExpandableItemToggle<T>",
Expand Down Expand Up @@ -44334,6 +44360,21 @@ Note: when used with collection-hooks the \`trackBy\` is set automatically from
],
},
},
{
"description": "Returns rows highlighted as matching the current filter. Only relevant for expandable tables
using \`expandableRows.highlightMatched\`.",
"name": "findMatchedRows",
"parameters": [],
"returnType": {
"isNullable": false,
"name": "Array",
"typeArguments": [
{
"name": "ElementWrapper<HTMLElement>",
},
],
},
},
{
"name": "findPagination",
"parameters": [],
Expand Down Expand Up @@ -53754,6 +53795,21 @@ Note: when used with collection-hooks the \`trackBy\` is set automatically from
"name": "ElementWrapper",
},
},
{
"description": "Returns rows highlighted as matching the current filter. Only relevant for expandable tables
using \`expandableRows.highlightMatched\`.",
"name": "findMatchedRows",
"parameters": [],
"returnType": {
"isNullable": false,
"name": "MultiElementWrapper",
"typeArguments": [
{
"name": "ElementWrapper",
},
],
},
},
{
"name": "findPagination",
"parameters": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ exports[`test-utils selectors 1`] = `
"awsui_resizer_x7peu",
"awsui_root_1s55x",
"awsui_root_wih1l",
"awsui_row-match-highlight_wih1l",
"awsui_row-selected_wih1l",
"awsui_row_wih1l",
"awsui_table_wih1l",
Expand Down
Loading
Loading