From 0047299433e9a2da829c69524b2e00a79201e514 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Tue, 6 May 2025 13:10:45 +0200 Subject: [PATCH 01/39] feat: initial commit --- build-tools/utils/pluralize.js | 1 + pages/treeview/basic.page.tsx | 279 ++++++++++++++++++ .../__snapshots__/documenter.test.ts.snap | 90 ++++++ .../components/expand-toggle-button/index.tsx | 42 +++ .../expand-toggle-button/motion.scss | 13 + .../expand-toggle-button/styles.scss | 53 ++++ src/internal/styles/utils/styles-reset.scss | 8 + src/table/body-cell/td-element.tsx | 2 +- src/test-utils/dom/treeview/index.ts | 76 +++++ src/treeview/index.tsx | 38 +++ src/treeview/interfaces.ts | 78 +++++ src/treeview/internal.tsx | 47 +++ src/treeview/styles.scss | 124 ++++++++ src/treeview/treeitem.tsx | 142 +++++++++ src/treeview/utils.ts | 14 + 15 files changed, 1006 insertions(+), 1 deletion(-) create mode 100644 pages/treeview/basic.page.tsx create mode 100644 src/internal/components/expand-toggle-button/index.tsx create mode 100644 src/internal/components/expand-toggle-button/motion.scss create mode 100644 src/internal/components/expand-toggle-button/styles.scss create mode 100644 src/test-utils/dom/treeview/index.ts create mode 100644 src/treeview/index.tsx create mode 100644 src/treeview/interfaces.ts create mode 100644 src/treeview/internal.tsx create mode 100644 src/treeview/styles.scss create mode 100644 src/treeview/treeitem.tsx create mode 100644 src/treeview/utils.ts diff --git a/build-tools/utils/pluralize.js b/build-tools/utils/pluralize.js index b5308c2a3a..f5cc10ee4f 100644 --- a/build-tools/utils/pluralize.js +++ b/build-tools/utils/pluralize.js @@ -80,6 +80,7 @@ const pluralizationMap = { TokenGroup: 'TokenGroups', TopNavigation: 'TopNavigations', TreeView: 'TreeViews', + Treeview: 'Treeviews', TutorialPanel: 'TutorialPanels', Wizard: 'Wizards', }; diff --git a/pages/treeview/basic.page.tsx b/pages/treeview/basic.page.tsx new file mode 100644 index 0000000000..aae79cc1f5 --- /dev/null +++ b/pages/treeview/basic.page.tsx @@ -0,0 +1,279 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import Badge from '~components/badge'; +import Box from '~components/box'; +import ButtonGroup from '~components/button-group'; +import Container from '~components/container'; +import Header from '~components/header'; +import Icon from '~components/icon'; +import SpaceBetween from '~components/space-between'; +import StatusIndicator from '~components/status-indicator'; +import Treeview, { TreeviewProps } from '~components/treeview'; + +const progressiveStepContent = ( +
+ Checked 5 nodes +
+ + 1 + 1 + 2 + +
+); + +const progressiveStepItemsContent = [ + + node-17 (eksclu-node-12345) + , + + node-18 (eksclu-node-09876) + , + + node-19 (eksclu-node-ab123) + , +]; + +const items: TreeviewProps.TreeItem[] = [ + { + id: '1', + content: 'Item 1', + items: [ + { + id: '1.1', + content: 'Item 1.1', + items: [ + { + id: '1.1.1', + content: 'Item 1.1.1', + }, + { + id: '1.1.2', + content: 'Item 1.1.2', + }, + ], + }, + { + id: '1.2', + content: 'Item 1.2', + }, + { + id: '1.3', + content: 'Item 1.3', + details: us-east-1, + actions: , + items: [ + { + id: '1.3.1', + content: 'Item 1.3.1', + }, + { + id: '1.3.2', + content: 'Item 1.3.2', + }, + ], + }, + ], + }, + { + id: '2', + content: 'Item 2', + }, + { + id: '3', + content: 'Item 3', + items: [ + { + id: '3.1', + content: 'Item 3.1', + }, + ], + }, + { + id: '4', + content: , + items: [ + { + id: '4.1', + content: 'Item 4.1', + items: [ + { + id: '4.1.1', + content: 'Item 4.1.1', + }, + { + id: '4.1.2', + content: 'Item 4.1.2', + details: us-east-1, + items: [ + { + id: '4.1.2.1', + content: 'Item 4.1.2.1', + }, + ], + }, + { + id: '4.1.3', + content: 'Item 4.1.3', + items: [ + { + id: '4.1.3.1', + content: 'Item 4.1.3.1', + }, + { + id: '4.1.3.2', + content: 'Item 4.1.3.2', + }, + { + id: '4.1.3.3', + content: 'Item 4.1.3.3', + }, + ], + }, + ], + }, + { + id: '4.2', + content: 'Item 4.2', + }, + ], + }, + { + id: '5', + content: 'Item 5', + }, + { + id: '6', + content: progressiveStepContent, + items: [ + { + id: '6.1', + content: progressiveStepItemsContent[0], + }, + { + id: '6.2', + content: progressiveStepItemsContent[1], + }, + { + id: '6.3', + content: progressiveStepItemsContent[2], + }, + ], + }, + { + id: '7', + content: 'Item 7', + }, +]; + +function Actions() { + const [pressed, setPressed] = useState(false); + + return ( + { + if (detail.id === 'favorite') { + setPressed(!pressed); + } + }} + /> + ); +} + +function RdsAccessRoleTreeItemContent() { + return ( +
+
+
+ + RdsAccessRole + 1 + 1 + 3 +
+ +
+ +
+
+ + + + + + us-east-1 + + + + + + prod-eng-xyz-zip-f + + + +
+ ); +} + +export default function BasicTreeview() { + const [expandedItems, setExpandedItems] = useState>(['1', '4.1']); + + return ( + +
Basic treeview
+ +
+ + { + if (detail.expanded) { + return setExpandedItems(prev => [...prev, detail.id]); + } else { + return setExpandedItems(prev => prev.filter(id => id !== detail.id)); + } + }} + expandedItems={expandedItems} + /> + +
+ +
Expanded items: {expandedItems.map(id => `Item ${id}`).join(', ')}
+
+ ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index c8fda9df20..3428cbc7e3 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20399,6 +20399,96 @@ exports[`Documenter definition for tree-view matches the snapshot: tree-view 1`] } `; +exports[`Documenter definition for treeview matches the snapshot: treeview 1`] = ` +{ + "dashCaseName": "treeview", + "events": [ + { + "cancelable": false, + "description": "Called when an item's expand toggle is clicked.", + "detailInlineType": { + "name": "TreeviewProps.ExpandableItemToggleDetail", + "properties": [ + { + "name": "expanded", + "optional": false, + "type": "boolean", + }, + { + "name": "id", + "optional": false, + "type": "string", + }, + ], + "type": "object", + }, + "detailType": "TreeviewProps.ExpandableItemToggleDetail", + "name": "onExpandableItemToggle", + }, + ], + "functions": [], + "name": "Treeview", + "properties": [ + { + "description": "Provides an \`aria-label\` to the treeview that screen readers can read (for accessibility). +If there's a visible label element that you can reference, use this instead of \`ariaLabel\`. +Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", + "name": "ariaLabel", + "optional": true, + "type": "string", + }, + { + "description": "Sets the \`aria-labelledby\` property on the treeview.", + "name": "ariaLabelledby", + "optional": true, + "type": "string", + }, + { + "deprecatedTag": "Custom CSS is not supported. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes).", + "description": "Adds the specified classes to the root element of the component.", + "name": "className", + "optional": true, + "type": "string", + }, + { + "description": "An array of expanded item IDs. Each item ID must match the \`id\` property of an item in the \`items\` array.", + "name": "expandedItems", + "optional": true, + "type": "ReadonlyArray", + }, + { + "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases, +use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must +use the \`id\` attribute, consider setting it on a parent element instead.", + "description": "Adds the specified ID to the root element of the component.", + "name": "id", + "optional": true, + "type": "string", + }, + { + "description": "An array of treeview items. +Each item has the following properties: +* \`id\` (string) - The unique identifier of the item. +* \`content\` (React.ReactNode) - The content of the item. +* \`details\` (optional, React.ReactNode) - The details of the item, displayed below the content. +* \`actions\` (optional, React.ReactNode) - Actions related to item. We recommend using button group. +* \`items\` (optional, TreeItem[]) - The nested items of the item.", + "name": "items", + "optional": false, + "type": "ReadonlyArray", + }, + { + "description": "If \`true\`, adds guide lines connecting child items to the expanded parent item.", + "name": "showGuideLines", + "optional": true, + "type": "boolean", + }, + ], + "regions": [], + "releaseStatus": "stable", +} +`; + exports[`Documenter definition for tutorial-panel matches the snapshot: tutorial-panel 1`] = ` { "dashCaseName": "tutorial-panel", diff --git a/src/internal/components/expand-toggle-button/index.tsx b/src/internal/components/expand-toggle-button/index.tsx new file mode 100644 index 0000000000..1809785b97 --- /dev/null +++ b/src/internal/components/expand-toggle-button/index.tsx @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useRef } from 'react'; +import clsx from 'clsx'; + +import InternalIcon from '../../../icon/internal'; +import { useSingleTabStopNavigation } from '../../../internal/context/single-tab-stop-navigation-context'; + +import styles from './styles.css.js'; + +export function ExpandToggleButton({ + isExpanded, + onExpandableItemToggle, + expandButtonLabel, + collapseButtonLabel, +}: { + isExpanded?: boolean; + onExpandableItemToggle?: () => void; + expandButtonLabel?: string; + collapseButtonLabel?: string; +}) { + const buttonRef = useRef(null); + const { tabIndex } = useSingleTabStopNavigation(buttonRef); + return ( + + ); +} diff --git a/src/internal/components/expand-toggle-button/motion.scss b/src/internal/components/expand-toggle-button/motion.scss new file mode 100644 index 0000000000..4d460c5850 --- /dev/null +++ b/src/internal/components/expand-toggle-button/motion.scss @@ -0,0 +1,13 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '../../../internal/styles' as styles; +@use '../../../internal/styles/tokens' as tokens; + +.expand-toggle-icon { + @include styles.with-motion { + transition: transform tokens.$motion-duration-rotate-90 tokens.$motion-easing-rotate-90; + } +} diff --git a/src/internal/components/expand-toggle-button/styles.scss b/src/internal/components/expand-toggle-button/styles.scss new file mode 100644 index 0000000000..770c218f73 --- /dev/null +++ b/src/internal/components/expand-toggle-button/styles.scss @@ -0,0 +1,53 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '../../../internal/styles/index' as styles; +@use '../../../internal/styles/tokens' as awsui; +@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; + +@use './motion'; + +.expand-toggle-icon { + transform: rotate(-90deg); + + @include styles.with-direction('rtl') { + transform: rotate(90deg); + } + + &-expanded { + transform: rotate(0deg); + + @include styles.with-direction('rtl') { + transform: rotate(0deg); + } + } +} + +.expand-toggle { + @include styles.styles-reset; + cursor: pointer; + inline-size: awsui.$space-m; + block-size: awsui.$space-m; + border-block: 0; + border-inline: 0; + margin-block: 0; + margin-inline: 0; + padding-block: 0; + padding-inline: 0; + background: none; + outline: 0; + color: awsui.$color-text-interactive-default; + + @include focus-visible.when-visible { + @include styles.focus-highlight(awsui.$space-button-inline-icon-focus-outline-gutter); + } + + &:hover { + color: awsui.$color-text-interactive-hover; + } + &:active { + color: awsui.$color-text-interactive-active; + } +} diff --git a/src/internal/styles/utils/styles-reset.scss b/src/internal/styles/utils/styles-reset.scss index 86f1c19cdf..77040e56b9 100644 --- a/src/internal/styles/utils/styles-reset.scss +++ b/src/internal/styles/utils/styles-reset.scss @@ -37,3 +37,11 @@ margin-inline-start: 0; margin-inline-end: 0; } + +@mixin styles-reset-ul { + margin-block: 0; + margin-inline: 0; + + padding-block: 0; + padding-inline: 0; +} diff --git a/src/table/body-cell/td-element.tsx b/src/table/body-cell/td-element.tsx index 21c35b5490..23ebed74ba 100644 --- a/src/table/body-cell/td-element.tsx +++ b/src/table/body-cell/td-element.tsx @@ -5,11 +5,11 @@ import clsx from 'clsx'; import { copyAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; +import { ExpandToggleButton } from '../../internal/components/expand-toggle-button'; 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 { ColumnWidthStyle } from '../column-widths-utils'; -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'; diff --git a/src/test-utils/dom/treeview/index.ts b/src/test-utils/dom/treeview/index.ts new file mode 100644 index 0000000000..2efd929016 --- /dev/null +++ b/src/test-utils/dom/treeview/index.ts @@ -0,0 +1,76 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ComponentWrapper, ElementWrapper, usesDom } from '@cloudscape-design/test-utils-core/dom'; + +import expandToggleStyles from '../../../internal/components/expand-toggle-button/styles.selectors.js'; +import styles from '../../../treeview/styles.selectors.js'; + +class TreeItemWrapper extends ComponentWrapper { + /** + * Finds the content slot of the tree item + */ + findContentSlot(): ElementWrapper | null { + return this.findByClassName(styles['treeitem-content']); + } + + /** + * Finds the details slot of the tree item + */ + findDetailsSlot(): ElementWrapper | null { + return this.findByClassName(styles['treeitem-details']); + } + + /** + * Finds the actions slot of the tree item + */ + findActionsSlot(): ElementWrapper | null { + return this.findByClassName(styles['treeitem-actions']); + } + + /** + * Finds the child items of the tree item + */ + findItems(): Array { + return this.findAllByClassName(styles['child-treeitem']).map(item => new TreeItemWrapper(item.getElement())); + } + + /** + * Finds the expand toggle of the tree item + */ + findExpandToggle(): ElementWrapper | null { + return this.findByClassName(expandToggleStyles['expand-toggle']); + } + + /** + * Finds the expand toggle wrapper of the tree item. Use this with custom toggles. + */ + // findExpandToggleWrapper(): ElementWrapper | null { + // return this.findByClassName(expandToggleStyles['expand-toggle']); + // } + + /** + * Returns `true` if the item expand toggle is present and expanded. Returns `false` otherwise. + */ + @usesDom + isExpanded(): boolean { + return this.getElement().getAttribute('aria-expanded') === 'true'; + } +} + +export default class TreeviewWrapper extends ComponentWrapper { + static rootSelector: string = styles.root; + + /** + * Finds the root level tree items + */ + findItems(): Array { + return this.findAllByClassName(styles['child-treeitem']).map(item => new TreeItemWrapper(item.getElement())); + } + + findItemById(id: string): TreeItemWrapper | null { + // const itemSelector = `[role="treeitem"][data-testid="${id}"]`; + const itemSelector = `.${styles['child-treeitem']}[data-testid="${id}"]`; + const item = this.find(itemSelector); + return item ? new TreeItemWrapper(item.getElement()) : null; + } +} diff --git a/src/treeview/index.tsx b/src/treeview/index.tsx new file mode 100644 index 0000000000..23ad4939b5 --- /dev/null +++ b/src/treeview/index.tsx @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import { getBaseProps } from '../internal/base-component'; +import useBaseComponent from '../internal/hooks/use-base-component'; +import { applyDisplayName } from '../internal/utils/apply-display-name'; +import { getExternalProps } from '../internal/utils/external-props'; +import { TreeviewProps } from './interfaces'; +import InternalTreeview from './internal'; + +export { TreeviewProps }; + +const Treeview = ({ items, ...rest }: TreeviewProps) => { + const baseProps = getBaseProps(rest); + // TODO: analytics metadata? + const baseComponentProps = useBaseComponent('Treeview', { + props: {}, + metadata: { + itemsCount: items.length, + }, + }); + const externalProps = getExternalProps(rest); + + return ( + + ); +}; + +applyDisplayName(Treeview, 'Treeview'); +export default Treeview; diff --git a/src/treeview/interfaces.ts b/src/treeview/interfaces.ts new file mode 100644 index 0000000000..3b27e495a8 --- /dev/null +++ b/src/treeview/interfaces.ts @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import { BaseComponentProps } from '../internal/base-component'; +import { NonCancelableEventHandler } from '../internal/events'; + +export interface TreeviewProps extends BaseComponentProps { + /** + * An array of treeview items. + * Each item has the following properties: + * * `id` (string) - The unique identifier of the item. + * * `content` (React.ReactNode) - The content of the item. + * * `details` (optional, React.ReactNode) - The details of the item, displayed below the content. + * * `actions` (optional, React.ReactNode) - Actions related to item. We recommend using button group. + * * `items` (optional, TreeItem[]) - The nested items of the item. + */ + items: ReadonlyArray; + + /** + * An array of expanded item IDs. Each item ID must match the `id` property of an item in the `items` array. + */ + expandedItems?: ReadonlyArray; + + /** + * Provides an `aria-label` to the treeview that screen readers can read (for accessibility). + * If there's a visible label element that you can reference, use this instead of `ariaLabel`. + * Don't use `ariaLabel` and `ariaLabelledby` at the same time. + */ + ariaLabel?: string; + + /** + * Sets the `aria-labelledby` property on the treeview. + */ + ariaLabelledby?: string; + + /** + * Called when an item's expand toggle is clicked. + */ + onExpandableItemToggle: NonCancelableEventHandler; + + /** + * Renders the treeview in a loading state. We recommend that you also set a `loadingText`. + * Do we need this? Customers could render a spinner or status indicator themselves, unlike table - there the columns should be there. We'd need to suggest them to have a text in the status indicator and wrap the status indicator with the live region component + * I think the text similar to the table should be specified by the customer instead of depending on the i18n strings, specifically to the content that is being rendered (Loading clusters, etc) + */ + // loading?: boolean; + + /** + * Specifies the text that's displayed when the treeview is in a loading state. + */ + // loadingText?: string; + + /** + * Displayed when the `items` property is an empty array. Use it to render an empty state. + */ + // empty?: React.ReactNode; + + /** + * If `true`, adds guide lines connecting child items to the expanded parent item. + */ + showGuideLines?: boolean; +} + +export namespace TreeviewProps { + export interface TreeItem { + id: string; + content: React.ReactNode; + details?: React.ReactNode; + actions?: React.ReactNode; + items?: ReadonlyArray; + } + + export interface ExpandableItemToggleDetail { + id: string; + expanded: boolean; + } +} diff --git a/src/treeview/internal.tsx b/src/treeview/internal.tsx new file mode 100644 index 0000000000..9ff804316e --- /dev/null +++ b/src/treeview/internal.tsx @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +// import clsx from 'clsx'; +import { getBaseProps } from '../internal/base-component'; +import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; +import { TreeviewProps } from './interfaces'; +import TreeItem from './treeitem'; +import { getItemPosition } from './utils'; + +import styles from './styles.css.js'; + +type InternalTreeviewProps = TreeviewProps & InternalBaseComponentProps; + +const InternalTreeview = ({ + items = [], + expandedItems = [], + // ariaLabel, + onExpandableItemToggle, + __internalRootRef, + ...rest +}: InternalTreeviewProps) => { + const baseProps = getBaseProps(rest); + + return ( +
+
    + {/*
      */} + {items.map((item, index) => ( + 0} + onExpandableItemToggle={onExpandableItemToggle} + /> + ))} +
    +
+ ); +}; + +export default InternalTreeview; diff --git a/src/treeview/styles.scss b/src/treeview/styles.scss new file mode 100644 index 0000000000..c5aed470b5 --- /dev/null +++ b/src/treeview/styles.scss @@ -0,0 +1,124 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +@use '../internal/styles/tokens' as awsui; +@use '../internal/styles' as styles; + +.root { + @include styles.styles-reset; + + font-family: awsui.$font-family-base; + font-size: awsui.$font-size-body-m; + line-height: awsui.$line-height-heading-m; +} + +.tree, +.parent-treeitem, +.child-treeitem { + list-style-type: none; + padding-inline-start: 0; + margin-block: 0; +} + +.child-treeitem { + display: flex; +} + +.tree { + @include styles.styles-reset-ul; +} + +.parent-treeitem { + padding-inline-start: 0; + margin-inline-start: 0; +} + +.treeitem-guideline { + inline-size: 30px; + background-color: yellow; + + display: flex; + align-items: baseline; + justify-content: center; + padding-block-start: 6px; + + position: relative; +} + +.treeitem-guideline-horizontal { + block-size: 1px; + position: absolute; + inset-block-start: 14px; + inline-size: 39px; + background: red; + inset-inline-start: -14px; + + &.expandable { + inline-size: 20px; + } +} + +.treeitem-guideline-vertical { + &-root { + inline-size: 1px; + position: absolute; + inset-block: 0; + inset-inline-start: 50%; + background-color: red; + } + + &-root-end { + block-size: 16px; + inset-block: 0 unset; + } + + &-expandable { + inset-block-start: 24px; + } + + &-middle { + inline-size: 1px; + position: absolute; + inset-block: -10px 0px; + inset-inline-start: -14px; + background-color: red; + } + + &-end { + inline-size: 1px; + position: absolute; + inset-block: 0 10px; + inset-inline-start: -14px; + background-color: red; + } +} + +.treeitem-content { + position: relative; + padding-block-start: 2px; + flex: 1; +} + +.treeitem-group { + display: flex; + flex: 1; + flex-direction: column; +} + +.treeitem-layout { + display: flex; + flex-direction: column; +} + +.treeitem-first-line { + display: flex; +} + +.treeitem-actions { + // test +} + +.treeitem-details { + // test +} diff --git a/src/treeview/treeitem.tsx b/src/treeview/treeitem.tsx new file mode 100644 index 0000000000..eeae5d55cd --- /dev/null +++ b/src/treeview/treeitem.tsx @@ -0,0 +1,142 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import clsx from 'clsx'; + +import { ExpandToggleButton } from '../internal/components/expand-toggle-button'; +import { fireNonCancelableEvent } from '../internal/events'; +import { NonCancelableEventHandler } from '../internal/events'; +import { TreeviewProps } from './interfaces'; +import { getItemPosition } from './utils'; + +import styles from './styles.css.js'; + +type TreeItemProps = TreeviewProps.TreeItem & { + isExpanded: boolean; + isExpandable?: boolean; + onExpandableItemToggle: NonCancelableEventHandler; + expandedItems: ReadonlyArray; + level: number; + position: 'start' | 'middle' | 'end'; + details?: React.ReactNode; + actions?: React.ReactNode; +}; + +const TreeItemLayout = ({ + content, + details, + actions, +}: { + content: React.ReactNode; + details?: React.ReactNode; + actions?: React.ReactNode; +}) => { + return ( +
+
+
{content}
+
{actions}
+
+ +
{details}
+
+ ); +}; + +const GuideLine = ({ + level, + position, + isExpandable, +}: { + level: number; + position: 'start' | 'middle' | 'end'; + isExpandable: boolean; +}) => { + if (level === 0) { + return ( +
+ ); + } + + return ( + <> +
+ + {level > 1 && (position === 'start' || position === 'end') && ( +
+ )} + + {level > 1 && position === 'middle' &&
} + + ); +}; + +const TreeItem = ({ + id, + content, + details, + actions, + isExpanded, + isExpandable, + onExpandableItemToggle, + items = [], + expandedItems = [], + level, + position, +}: TreeItemProps) => { + const isExpandableItemExpanded = isExpandable && isExpanded; + const nextLevel = level + 1; + + return ( +
  • +
    + {isExpandable && ( + fireNonCancelableEvent(onExpandableItemToggle, { id, expanded: !isExpanded })} + // expandButtonLabel={expandButtonLabel} + // collapseButtonLabel={collapseButtonLabel} + /> + )} + + +
    + +
    + + + {isExpandableItemExpanded && ( +
      + {items.map((item, index) => ( + + ))} +
    + )} +
    +
  • + ); +}; + +export default TreeItem; diff --git a/src/treeview/utils.ts b/src/treeview/utils.ts new file mode 100644 index 0000000000..cbddd8e2a4 --- /dev/null +++ b/src/treeview/utils.ts @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export function getItemPosition(index: number, itemsLength: number) { + if (index === 0) { + return 'start'; + } + + if (index === itemsLength - 1) { + return 'end'; + } + + return 'middle'; +} From 5f8ee38f571a0e937c85c4829cc73fcb21d073dc Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Wed, 7 May 2025 12:04:08 +0200 Subject: [PATCH 02/39] fix a11y test findings --- pages/treeview/basic.page.tsx | 57 +++++++++++++++++------------------ src/treeview/internal.tsx | 1 - src/treeview/treeitem.tsx | 31 +++++++++---------- 3 files changed, 41 insertions(+), 48 deletions(-) diff --git a/pages/treeview/basic.page.tsx b/pages/treeview/basic.page.tsx index aae79cc1f5..698b10f913 100644 --- a/pages/treeview/basic.page.tsx +++ b/pages/treeview/basic.page.tsx @@ -6,7 +6,6 @@ import Badge from '~components/badge'; import Box from '~components/box'; import ButtonGroup from '~components/button-group'; import Container from '~components/container'; -import Header from '~components/header'; import Icon from '~components/icon'; import SpaceBetween from '~components/space-between'; import StatusIndicator from '~components/status-indicator'; @@ -232,20 +231,16 @@ function RdsAccessRoleTreeItemContent() {
    - +
    - - - us-east-1 - + + us-east-1 - - - prod-eng-xyz-zip-f - + + prod-eng-xyz-zip-f - +
    ); } @@ -254,26 +249,28 @@ export default function BasicTreeview() { const [expandedItems, setExpandedItems] = useState>(['1', '4.1']); return ( - -
    Basic treeview
    + <> +

    Basic treeview

    -
    - - { - if (detail.expanded) { - return setExpandedItems(prev => [...prev, detail.id]); - } else { - return setExpandedItems(prev => prev.filter(id => id !== detail.id)); - } - }} - expandedItems={expandedItems} - /> - -
    + +
    + + { + if (detail.expanded) { + return setExpandedItems(prev => [...prev, detail.id]); + } else { + return setExpandedItems(prev => prev.filter(id => id !== detail.id)); + } + }} + expandedItems={expandedItems} + /> + +
    -
    Expanded items: {expandedItems.map(id => `Item ${id}`).join(', ')}
    -
    +
    Expanded items: {expandedItems.map(id => `Item ${id}`).join(', ')}
    +
    + ); } diff --git a/src/treeview/internal.tsx b/src/treeview/internal.tsx index 9ff804316e..f74ab96d45 100644 --- a/src/treeview/internal.tsx +++ b/src/treeview/internal.tsx @@ -35,7 +35,6 @@ const InternalTreeview = ({ position={getItemPosition(index, items.length)} expandedItems={expandedItems} isExpanded={expandedItems.includes(item.id)} - isExpandable={item.items && item.items.length > 0} onExpandableItemToggle={onExpandableItemToggle} /> ))} diff --git a/src/treeview/treeitem.tsx b/src/treeview/treeitem.tsx index eeae5d55cd..92678a94f8 100644 --- a/src/treeview/treeitem.tsx +++ b/src/treeview/treeitem.tsx @@ -13,7 +13,6 @@ import styles from './styles.css.js'; type TreeItemProps = TreeviewProps.TreeItem & { isExpanded: boolean; - isExpandable?: boolean; onExpandableItemToggle: NonCancelableEventHandler; expandedItems: ReadonlyArray; level: number; @@ -55,23 +54,24 @@ const GuideLine = ({ if (level === 0) { return (
    ); } return ( <> -
    +
    {level > 1 && (position === 'start' || position === 'end') && ( -
    +
    )} - {level > 1 && position === 'middle' &&
    } + {level > 1 && position === 'middle' &&
    } ); }; @@ -82,13 +82,13 @@ const TreeItem = ({ details, actions, isExpanded, - isExpandable, onExpandableItemToggle, items = [], expandedItems = [], level, position, }: TreeItemProps) => { + const isExpandable = items.length > 0; const isExpandableItemExpanded = isExpandable && isExpanded; const nextLevel = level + 1; @@ -96,11 +96,8 @@ const TreeItem = ({
  • @@ -108,8 +105,8 @@ const TreeItem = ({ fireNonCancelableEvent(onExpandableItemToggle, { id, expanded: !isExpanded })} - // expandButtonLabel={expandButtonLabel} - // collapseButtonLabel={collapseButtonLabel} + expandButtonLabel="Expand" + collapseButtonLabel="Collapse" /> )} @@ -123,8 +120,8 @@ const TreeItem = ({
      {items.map((item, index) => ( Date: Wed, 7 May 2025 12:25:20 +0200 Subject: [PATCH 03/39] update test utils snapshots --- .../test-utils-selectors.test.tsx.snap | 6 ++ .../test-utils-wrappers.test.tsx.snap | 60 +++++++++++++++++++ src/treeview/index.tsx | 2 +- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index c47fe6939d..01cbdbb580 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -359,6 +359,7 @@ exports[`test-utils selectors 1`] = ` "awsui_description_1wepg", "awsui_disabled_15o6u", "awsui_dropdown_qwoo0", + "awsui_expand-toggle_1xe88", "awsui_filter-container_z5mul", "awsui_filtering-match-highlight_1p2cx", "awsui_footer_dgs8z", @@ -660,6 +661,11 @@ exports[`test-utils selectors 1`] = ` "tree-view": [ "awsui_root_1js4f", ], + "treeview": [ + "awsui_child-treeitem_uf1x6", + "awsui_root_uf1x6", + "awsui_treeitem-content_uf1x6", + ], "tutorial-panel": [ "awsui_collapse-button_ig8mp", "awsui_completed_ig8mp", diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap index 9be09c8f8d..21bd067f1b 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap @@ -88,6 +88,7 @@ import ToggleButtonWrapper from './toggle-button'; import TokenGroupWrapper from './token-group'; import TopNavigationWrapper from './top-navigation'; import TreeViewWrapper from './tree-view'; +import TreeviewWrapper from './treeview'; import TutorialPanelWrapper from './tutorial-panel'; import WizardWrapper from './wizard'; @@ -171,6 +172,7 @@ export { ToggleButtonWrapper }; export { TokenGroupWrapper }; export { TopNavigationWrapper }; export { TreeViewWrapper }; +export { TreeviewWrapper }; export { TutorialPanelWrapper }; export { WizardWrapper }; @@ -1678,6 +1680,25 @@ findTreeView(selector?: string): TreeViewWrapper | null; * @returns {Array} */ findAllTreeViews(selector?: string): Array; +/** + * Returns the wrapper of the first Treeview that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first Treeview. + * If no matching Treeview is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {TreeviewWrapper | null} + */ +findTreeview(selector?: string): TreeviewWrapper | null; + +/** + * Returns an array of Treeview wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the Treeviews inside the current wrapper. + * If no matching Treeview is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ +findAllTreeviews(selector?: string): Array; /** * Returns the wrapper of the first TutorialPanel that matches the specified CSS selector. * If no CSS selector is specified, returns the wrapper of the first TutorialPanel. @@ -2510,6 +2531,16 @@ ElementWrapper.prototype.findTreeView = function(selector) { ElementWrapper.prototype.findAllTreeViews = function(selector) { return this.findAllComponents(TreeViewWrapper, selector); }; +ElementWrapper.prototype.findTreeview = function(selector) { + const rootSelector = \`.\${TreeviewWrapper.rootSelector}\`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TreeviewWrapper); +}; + +ElementWrapper.prototype.findAllTreeviews = function(selector) { + return this.findAllComponents(TreeviewWrapper, selector); +}; ElementWrapper.prototype.findTutorialPanel = function(selector) { const rootSelector = \`.\${TutorialPanelWrapper.rootSelector}\`; // casting to 'any' is needed to avoid this issue with generics @@ -2629,6 +2660,7 @@ import ToggleButtonWrapper from './toggle-button'; import TokenGroupWrapper from './token-group'; import TopNavigationWrapper from './top-navigation'; import TreeViewWrapper from './tree-view'; +import TreeviewWrapper from './treeview'; import TutorialPanelWrapper from './tutorial-panel'; import WizardWrapper from './wizard'; @@ -2712,6 +2744,7 @@ export { ToggleButtonWrapper }; export { TokenGroupWrapper }; export { TopNavigationWrapper }; export { TreeViewWrapper }; +export { TreeviewWrapper }; export { TutorialPanelWrapper }; export { WizardWrapper }; @@ -4061,6 +4094,23 @@ findTreeView(selector?: string): TreeViewWrapper; * @returns {MultiElementWrapper} */ findAllTreeViews(selector?: string): MultiElementWrapper; +/** + * Returns a wrapper that matches the Treeviews with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches Treeviews. + * + * @param {string} [selector] CSS Selector + * @returns {TreeviewWrapper} + */ +findTreeview(selector?: string): TreeviewWrapper; + +/** + * Returns a multi-element wrapper that matches Treeviews with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches Treeviews. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ +findAllTreeviews(selector?: string): MultiElementWrapper; /** * Returns a wrapper that matches the TutorialPanels with the specified CSS selector. * If no CSS selector is specified, returns a wrapper that matches TutorialPanels. @@ -4889,6 +4939,16 @@ ElementWrapper.prototype.findTreeView = function(selector) { ElementWrapper.prototype.findAllTreeViews = function(selector) { return this.findAllComponents(TreeViewWrapper, selector); }; +ElementWrapper.prototype.findTreeview = function(selector) { + const rootSelector = \`.\${TreeviewWrapper.rootSelector}\`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TreeviewWrapper); +}; + +ElementWrapper.prototype.findAllTreeviews = function(selector) { + return this.findAllComponents(TreeviewWrapper, selector); +}; ElementWrapper.prototype.findTutorialPanel = function(selector) { const rootSelector = \`.\${TutorialPanelWrapper.rootSelector}\`; // casting to 'any' is needed to avoid this issue with generics diff --git a/src/treeview/index.tsx b/src/treeview/index.tsx index 23ad4939b5..5940f66de0 100644 --- a/src/treeview/index.tsx +++ b/src/treeview/index.tsx @@ -17,7 +17,7 @@ const Treeview = ({ items, ...rest }: TreeviewProps) => { const baseComponentProps = useBaseComponent('Treeview', { props: {}, metadata: { - itemsCount: items.length, + itemsCount: items?.length, }, }); const externalProps = getExternalProps(rest); From a52e05f68b5c12754f6b95680e8b3a2191da6985 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Wed, 7 May 2025 12:47:24 +0200 Subject: [PATCH 04/39] test fixes --- src/table/body-cell/td-element.tsx | 2 +- src/treeview/index.tsx | 19 +++++++++---------- src/treeview/internal.tsx | 4 ++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/table/body-cell/td-element.tsx b/src/table/body-cell/td-element.tsx index 23ebed74ba..21c35b5490 100644 --- a/src/table/body-cell/td-element.tsx +++ b/src/table/body-cell/td-element.tsx @@ -5,11 +5,11 @@ import clsx from 'clsx'; import { copyAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; -import { ExpandToggleButton } from '../../internal/components/expand-toggle-button'; 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 { ColumnWidthStyle } from '../column-widths-utils'; +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'; diff --git a/src/treeview/index.tsx b/src/treeview/index.tsx index 5940f66de0..5a455ea6f6 100644 --- a/src/treeview/index.tsx +++ b/src/treeview/index.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { getBaseProps } from '../internal/base-component'; -import useBaseComponent from '../internal/hooks/use-base-component'; +// import useBaseComponent from '../internal/hooks/use-base-component'; import { applyDisplayName } from '../internal/utils/apply-display-name'; import { getExternalProps } from '../internal/utils/external-props'; import { TreeviewProps } from './interfaces'; @@ -14,22 +14,21 @@ export { TreeviewProps }; const Treeview = ({ items, ...rest }: TreeviewProps) => { const baseProps = getBaseProps(rest); // TODO: analytics metadata? - const baseComponentProps = useBaseComponent('Treeview', { - props: {}, - metadata: { - itemsCount: items?.length, - }, - }); + // const { __internalRootRef } = useBaseComponent('Treeview', { + // props: {}, + // metadata: { + // itemsCount: items?.length, + // }, + // }); const externalProps = getExternalProps(rest); return ( ); }; diff --git a/src/treeview/internal.tsx b/src/treeview/internal.tsx index f74ab96d45..a96408fa0e 100644 --- a/src/treeview/internal.tsx +++ b/src/treeview/internal.tsx @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React from 'react'; +import clsx from 'clsx'; -// import clsx from 'clsx'; import { getBaseProps } from '../internal/base-component'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; import { TreeviewProps } from './interfaces'; @@ -24,7 +24,7 @@ const InternalTreeview = ({ const baseProps = getBaseProps(rest); return ( -
      +
        {/*
          */} {items.map((item, index) => ( From 1863bbe9313270bd4b21bcf9f4c4cfc719512107 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Wed, 7 May 2025 13:31:02 +0200 Subject: [PATCH 05/39] test fix --- src/treeview/index.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/treeview/index.tsx b/src/treeview/index.tsx index 5a455ea6f6..187200e812 100644 --- a/src/treeview/index.tsx +++ b/src/treeview/index.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { getBaseProps } from '../internal/base-component'; -// import useBaseComponent from '../internal/hooks/use-base-component'; +import useBaseComponent from '../internal/hooks/use-base-component'; import { applyDisplayName } from '../internal/utils/apply-display-name'; import { getExternalProps } from '../internal/utils/external-props'; import { TreeviewProps } from './interfaces'; @@ -14,19 +14,20 @@ export { TreeviewProps }; const Treeview = ({ items, ...rest }: TreeviewProps) => { const baseProps = getBaseProps(rest); // TODO: analytics metadata? - // const { __internalRootRef } = useBaseComponent('Treeview', { - // props: {}, - // metadata: { - // itemsCount: items?.length, - // }, - // }); + const baseComponentProps = useBaseComponent('Treeview', { + props: {}, + metadata: { + itemsCount: items?.length, + }, + }); const externalProps = getExternalProps(rest); return ( From 25a97abb1341f175cc9960edbe5456030bca993c Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 15 May 2025 15:44:01 +0200 Subject: [PATCH 06/39] updates --- pages/treeview/basic.page.tsx | 52 +++-- pages/treeview/common.tsx | 61 ++++++ pages/treeview/generate-data.ts | 186 ++++++++++++++++++ pages/treeview/test.page.tsx | 80 ++++++++ .../__snapshots__/documenter.test.ts.snap | 157 +++++++++++++-- src/i18n/messages-types.ts | 4 + src/test-utils/dom/treeview/index.ts | 19 +- src/treeview/connector/index.tsx | 42 ++++ src/treeview/connector/styles.scss | 54 +++++ src/treeview/index.tsx | 3 +- src/treeview/interfaces.ts | 78 +++++--- src/treeview/internal.tsx | 53 +++-- src/treeview/styles.scss | 68 ++----- src/treeview/treeitem.tsx | 132 ++++++------- src/treeview/utils.ts | 31 +++ 15 files changed, 807 insertions(+), 213 deletions(-) create mode 100644 pages/treeview/common.tsx create mode 100644 pages/treeview/generate-data.ts create mode 100644 pages/treeview/test.page.tsx create mode 100644 src/treeview/connector/index.tsx create mode 100644 src/treeview/connector/styles.scss diff --git a/pages/treeview/basic.page.tsx b/pages/treeview/basic.page.tsx index 698b10f913..68c0ad2025 100644 --- a/pages/treeview/basic.page.tsx +++ b/pages/treeview/basic.page.tsx @@ -9,7 +9,7 @@ import Container from '~components/container'; import Icon from '~components/icon'; import SpaceBetween from '~components/space-between'; import StatusIndicator from '~components/status-indicator'; -import Treeview, { TreeviewProps } from '~components/treeview'; +import Treeview from '~components/treeview'; const progressiveStepContent = (
          @@ -35,15 +35,23 @@ const progressiveStepItemsContent = [ , ]; -const items: TreeviewProps.TreeItem[] = [ +interface Item { + id: string; + content: React.ReactNode; + details?: string; + children?: Item[]; + hasActions?: boolean; +} + +const items: Item[] = [ { id: '1', content: 'Item 1', - items: [ + children: [ { id: '1.1', content: 'Item 1.1', - items: [ + children: [ { id: '1.1.1', content: 'Item 1.1.1', @@ -61,9 +69,9 @@ const items: TreeviewProps.TreeItem[] = [ { id: '1.3', content: 'Item 1.3', - details: us-east-1, - actions: , - items: [ + details: 'us-east-1', + hasActions: true, + children: [ { id: '1.3.1', content: 'Item 1.3.1', @@ -83,7 +91,7 @@ const items: TreeviewProps.TreeItem[] = [ { id: '3', content: 'Item 3', - items: [ + children: [ { id: '3.1', content: 'Item 3.1', @@ -93,11 +101,11 @@ const items: TreeviewProps.TreeItem[] = [ { id: '4', content: , - items: [ + children: [ { id: '4.1', content: 'Item 4.1', - items: [ + children: [ { id: '4.1.1', content: 'Item 4.1.1', @@ -105,8 +113,8 @@ const items: TreeviewProps.TreeItem[] = [ { id: '4.1.2', content: 'Item 4.1.2', - details: us-east-1, - items: [ + details: 'us-east-1', + children: [ { id: '4.1.2.1', content: 'Item 4.1.2.1', @@ -116,7 +124,7 @@ const items: TreeviewProps.TreeItem[] = [ { id: '4.1.3', content: 'Item 4.1.3', - items: [ + children: [ { id: '4.1.3.1', content: 'Item 4.1.3.1', @@ -146,7 +154,7 @@ const items: TreeviewProps.TreeItem[] = [ { id: '6', content: progressiveStepContent, - items: [ + children: [ { id: '6.1', content: progressiveStepItemsContent[0], @@ -257,11 +265,21 @@ export default function BasicTreeview() { { + renderItem={item => { + return { + icon: , + content: item.content, + description: {item.details}, + actions: item.hasActions ? : undefined, + }; + }} + getItemId={item => item.id} + getItemChildren={item => item.children} + onItemToggle={({ detail }: any) => { if (detail.expanded) { - return setExpandedItems(prev => [...prev, detail.id]); + return setExpandedItems(prev => [...prev, detail.item.id]); } else { - return setExpandedItems(prev => prev.filter(id => id !== detail.id)); + return setExpandedItems(prev => prev.filter(id => id !== detail.item.id)); } }} expandedItems={expandedItems} diff --git a/pages/treeview/common.tsx b/pages/treeview/common.tsx new file mode 100644 index 0000000000..3ca144402a --- /dev/null +++ b/pages/treeview/common.tsx @@ -0,0 +1,61 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import ButtonGroup from '~components/button-group'; +import StatusIndicator from '~components/status-indicator/internal'; + +import { Item } from './generate-data'; + +export function Content(item: Item) { + if (item.status) { + return {item.name}; + } + + return ( +
          + {item.name} + + {item.errorCount && item.errorCount > 0 && {item.errorCount}} + {item.warningCount && item.warningCount > 0 && ( + {item.warningCount} + )} + {item.successCount && item.successCount > 0 && ( + {item.successCount} + )} +
          + ); +} + +export function Actions() { + return ( + {}} + /> + ); +} diff --git a/pages/treeview/generate-data.ts b/pages/treeview/generate-data.ts new file mode 100644 index 0000000000..abfa1852ca --- /dev/null +++ b/pages/treeview/generate-data.ts @@ -0,0 +1,186 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import padStart from 'lodash/padStart'; +import range from 'lodash/range'; + +import pseudoRandom from '../utils/pseudo-random'; + +const MAX_LEVEL = 5; + +export interface Item { + id: string; + name: string; + level: number; + status?: 'error' | 'success' | 'warning'; + tagName?: string; + errorCount?: number; + warningCount?: number; + successCount?: number; + children?: Item[]; + hasActions?: boolean; + parentId: string; + isExpanded?: boolean; +} + +function id() { + const id = Math.ceil(pseudoRandom() * Math.pow(16, 8)).toString(16); + return padStart(id, 8, '0'); +} + +function database() { + const randomNumber = 1 + Math.floor(pseudoRandom() * 256); + return `database-${randomNumber}`; +} + +function tagName() { + const randomNumber = 1 + Math.floor(pseudoRandom() * 256); + return `tag-${randomNumber}`; +} + +export function subnet() { + const randomNumber = 1 + Math.floor(pseudoRandom() * 256); + return `subnet-abcdh12345${randomNumber}`; +} + +function yesOrNo() { + return Math.random() < 0.5; +} + +function statusCount() { + return yesOrNo() ? Math.floor(pseudoRandom() * 5) : 0; +} + +function getChildren({ + level, + parentId, + count = 0, + errorCount = 0, + warningCount = 0, + successCount = 0, +}: { + level: number; + parentId: string; + count?: number; + errorCount?: number; + warningCount?: number; + successCount?: number; +}): Item[] { + if (level + 1 === MAX_LEVEL) { + return []; + } + + return yesOrNo() + ? getStatusIndicatorItems({ level: level + 1, parentId, errorCount, warningCount, successCount }) + : getSubnetItems({ level: level + 1, parentId, count }); +} + +function getStatusIndicatorItems({ + level, + parentId, + errorCount = 0, + warningCount = 0, + successCount = 0, +}: { + level: number; + parentId: string; + errorCount: number; + warningCount: number; + successCount: number; +}) { + const items: Item[] = []; + + if (errorCount > 0) { + const itemId = `${parentId}-${id()}`; + items.push({ + id: itemId, + parentId, + name: `${errorCount} error`, + level, + status: 'error', + children: getChildren({ parentId: itemId, level, count: errorCount }), + }); + } + + if (warningCount > 0) { + const itemId = `${parentId}-${id()}`; + items.push({ + id: itemId, + parentId, + name: `${warningCount} warning`, + level, + status: 'warning', + children: getChildren({ parentId: itemId, level, count: warningCount }), + }); + } + + if (successCount > 0) { + const itemId = `${parentId}-${id()}`; + items.push({ + id: itemId, + parentId, + name: `${successCount} success`, + level, + status: 'success', + children: getChildren({ parentId: itemId, level, count: successCount }), + }); + } + + return items; +} + +function getSubnetItems({ level, parentId, count }: { level: number; parentId: string; count: number }) { + return range(count).map(() => { + const itemId = `${parentId}-${id()}`; + return { + id: itemId, + parentId, + name: subnet(), + children: yesOrNo() ? getChildren({ parentId: itemId, level, count }) : [], + tagName: yesOrNo() ? tagName() : undefined, + level, + hasActions: yesOrNo(), + }; + }); +} + +export default function getItems(rootItemCount = 20) { + return range(rootItemCount).map(() => { + const errorCount = statusCount(); + const warningCount = statusCount(); + const successCount = statusCount(); + + const itemId = id(); + + return { + id: itemId, + parentId: 'root', + name: database(), + level: 0, + tagName: yesOrNo() ? tagName() : undefined, + errorCount, + warningCount, + successCount, + hasActions: yesOrNo(), + children: yesOrNo() + ? getChildren({ level: 0, parentId: itemId, count: rootItemCount / 2, errorCount, warningCount, successCount }) + : [], + }; + }); +} + +function flattenItems(items: Item[]) { + const allItems: Item[] = []; + + const pushItem = (item: Item) => { + allItems.push(item); + if (item.children) { + item.children.forEach(pushItem); + } + }; + + items.forEach(pushItem); + return allItems; +} + +export const items = getItems(15); +export const allItems = flattenItems(items); diff --git a/pages/treeview/test.page.tsx b/pages/treeview/test.page.tsx new file mode 100644 index 0000000000..b787e9ca22 --- /dev/null +++ b/pages/treeview/test.page.tsx @@ -0,0 +1,80 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { Button, SpaceBetween } from '~components'; +import Box from '~components/box'; +import Icon from '~components/icon'; +import Treeview from '~components/treeview'; + +import { Actions, Content } from './common'; +import { allItems, items } from './generate-data'; + +console.log('items: ', items); +console.log('all items: ', allItems); +const allExpandableItemIds = allItems.filter(item => item.children && item.children.length > 0).map(item => item.id); +console.log('expandable item ids: ', allExpandableItemIds); + +export default function TestPage() { + const [expandedItems, setExpandedItems] = useState>([]); + + return ( + <> +

          Test performance page

          + + + + + + + + + { + const isExpanded = expandedItems.includes(item.id); + return { + icon: , + content: , + secondaryContent: item.hasActions ? : undefined, + description: item.tagName ? ( + + + + {item.tagName} + + + ) : undefined, + }; + }} + getItemId={item => item.id} + getItemChildren={item => item.children} + onItemToggle={({ detail }: any) => { + if (detail.expanded) { + return setExpandedItems(prev => [...prev, detail.item.id]); + } else { + return setExpandedItems(prev => prev.filter(id => id !== detail.item.id)); + } + }} + expandedItems={expandedItems} + /> + + + ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 3428cbc7e3..54322afffb 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20407,7 +20407,7 @@ exports[`Documenter definition for treeview matches the snapshot: treeview 1`] = "cancelable": false, "description": "Called when an item's expand toggle is clicked.", "detailInlineType": { - "name": "TreeviewProps.ExpandableItemToggleDetail", + "name": "TreeviewProps.ItemToggleDetail", "properties": [ { "name": "expanded", @@ -20419,26 +20419,44 @@ exports[`Documenter definition for treeview matches the snapshot: treeview 1`] = "optional": false, "type": "string", }, + { + "name": "item", + "optional": false, + "type": "T", + }, ], "type": "object", }, - "detailType": "TreeviewProps.ExpandableItemToggleDetail", - "name": "onExpandableItemToggle", + "detailType": "TreeviewProps.ItemToggleDetail", + "name": "onItemToggle", }, ], "functions": [], "name": "Treeview", "properties": [ + { + "description": "Sets the \`aria-describedby\` property on the treeview.", + "name": "ariaDescribedby", + "optional": true, + "type": "string", + }, + { + "description": "Sets the \`aria-description\` property on the treeview.", + "name": "ariaDescription", + "optional": true, + "type": "string", + }, { "description": "Provides an \`aria-label\` to the treeview that screen readers can read (for accessibility). -If there's a visible label element that you can reference, use this instead of \`ariaLabel\`. Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", "name": "ariaLabel", "optional": true, "type": "string", }, { - "description": "Sets the \`aria-labelledby\` property on the treeview.", + "description": "Sets the \`aria-labelledby\` property on the treeview. +If there's a visible label element that you can reference, use this instead of \`ariaLabel\`. +Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", "name": "ariaLabelledby", "optional": true, "type": "string", @@ -20451,11 +20469,76 @@ Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", "type": "string", }, { - "description": "An array of expanded item IDs. Each item ID must match the \`id\` property of an item in the \`items\` array.", + "description": "An array of expanded item IDs. Use it to control expand state of tree items.", "name": "expandedItems", "optional": true, "type": "ReadonlyArray", }, + { + "description": "The nested items of the item.", + "inlineType": { + "name": "(item: T, index: number) => ReadonlyArray", + "parameters": [ + { + "name": "item", + "type": "T", + }, + { + "name": "index", + "type": "number", + }, + ], + "returnType": "ReadonlyArray", + "type": "function", + }, + "name": "getItemChildren", + "optional": false, + "type": "(item: T, index: number) => ReadonlyArray", + }, + { + "description": "Provides a unique identifier of each item.", + "inlineType": { + "name": "(item: T, index: number) => string", + "parameters": [ + { + "name": "item", + "type": "T", + }, + { + "name": "index", + "type": "number", + }, + ], + "returnType": "string", + "type": "function", + }, + "name": "getItemId", + "optional": false, + "type": "(item: T, index: number) => string", + }, + { + "description": "An object containing all the necessary localized strings required by the component.", + "i18nTag": true, + "inlineType": { + "name": "TreeviewProps.I18nStrings", + "properties": [ + { + "name": "collapseButtonLabel", + "optional": true, + "type": "((item: T) => string)", + }, + { + "name": "expandButtonLabel", + "optional": true, + "type": "((item: T) => string)", + }, + ], + "type": "object", + }, + "name": "i18nStrings", + "optional": true, + "type": "TreeviewProps.I18nStrings", + }, { "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must @@ -20466,20 +20549,62 @@ use the \`id\` attribute, consider setting it on a parent element instead.", "type": "string", }, { - "description": "An array of treeview items. -Each item has the following properties: -* \`id\` (string) - The unique identifier of the item. -* \`content\` (React.ReactNode) - The content of the item. -* \`details\` (optional, React.ReactNode) - The details of the item, displayed below the content. -* \`actions\` (optional, React.ReactNode) - Actions related to item. We recommend using button group. -* \`items\` (optional, TreeItem[]) - The nested items of the item.", + "description": "An array of treeview items.", "name": "items", "optional": false, - "type": "ReadonlyArray", + "type": "ReadonlyArray", }, { - "description": "If \`true\`, adds guide lines connecting child items to the expanded parent item.", - "name": "showGuideLines", + "description": "Use it to map your data to render the item. +For each item the below properties must be returned: +* \`content\` (React.ReactNode) - The content of the item. +* \`icon\` (optional, React.ReactNode) - The leading icon of the item. +* \`description\` (optional, React.ReactNode) - The details of the item, displayed below the content. +* \`secondaryContent\` (optional, React.ReactNode) - Actions related to item. We recommend using a button group.", + "inlineType": { + "name": "(item: T, index: number) => TreeviewProps.TreeItem", + "parameters": [ + { + "name": "item", + "type": "T", + }, + { + "name": "index", + "type": "number", + }, + ], + "returnType": "TreeviewProps.TreeItem", + "type": "function", + }, + "name": "renderItem", + "optional": false, + "type": "(item: T, index: number) => TreeviewProps.TreeItem", + }, + { + "inlineType": { + "name": "(isExpanded: boolean) => React.ReactNode", + "parameters": [ + { + "name": "isExpanded", + "type": "boolean", + }, + ], + "returnType": "React.ReactNode", + "type": "function", + }, + "name": "renderItemToggleIcon", + "optional": true, + "systemTags": [ + "core +Overrides the default expand toggle. +Use it to have different icons/animations for toggle.", + ], + "type": "((isExpanded: boolean) => React.ReactNode)", + }, + { + "defaultValue": "true", + "description": "If \`true\`, adds connecting lines between child items and their expanded parent items.", + "name": "showConnectorLine", "optional": true, "type": "boolean", }, diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 7f41b602d7..7fb71022b2 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -541,6 +541,10 @@ export interface I18nFormatArgTypes { "i18nStrings.labelsTaskStatus.in-progress": never; "i18nStrings.labelsTaskStatus.success": never; } + "treeview": { + "i18nStrings.expandButtonLabel": never; + "i18nStrings.collapseButtonLabel": never; + } "wizard": { "i18nStrings.stepNumberLabel": { "stepNumber": string | number; diff --git a/src/test-utils/dom/treeview/index.ts b/src/test-utils/dom/treeview/index.ts index 2efd929016..bbd7c78da6 100644 --- a/src/test-utils/dom/treeview/index.ts +++ b/src/test-utils/dom/treeview/index.ts @@ -37,16 +37,25 @@ class TreeItemWrapper extends ComponentWrapper { /** * Finds the expand toggle of the tree item */ - findExpandToggle(): ElementWrapper | null { + findItemToggle(): ElementWrapper | null { return this.findByClassName(expandToggleStyles['expand-toggle']); } /** - * Finds the expand toggle wrapper of the tree item. Use this with custom toggles. + * Finds the children tree items of the tree item */ - // findExpandToggleWrapper(): ElementWrapper | null { - // return this.findByClassName(expandToggleStyles['expand-toggle']); - // } + findChildren(): Array { + return this.findAllByClassName(styles['child-treeitem']).map(item => new TreeItemWrapper(item.getElement())); + } + + /** + * Finds a children tree item by its ID + */ + findChildById(id: string): TreeItemWrapper | null { + const itemSelector = `.${styles['child-treeitem']}[data-testid="${id}"]`; + const item = this.find(itemSelector); + return item ? new TreeItemWrapper(item.getElement()) : null; + } /** * Returns `true` if the item expand toggle is present and expanded. Returns `false` otherwise. diff --git a/src/treeview/connector/index.tsx b/src/treeview/connector/index.tsx new file mode 100644 index 0000000000..dbc52605bb --- /dev/null +++ b/src/treeview/connector/index.tsx @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import clsx from 'clsx'; + +import styles from './styles.css.js'; + +const Connector = ({ + level, + position, + isExpandable, +}: { + level: number; + position: 'start' | 'middle' | 'end'; + isExpandable: boolean; +}) => { + if (level === 0) { + return ( +
          + ); + } + + return ( + <> +
          + + {level > 1 && (position === 'start' || position === 'end') && ( +
          + )} + + {level > 1 && position === 'middle' &&
          } + + ); +}; + +export default Connector; diff --git a/src/treeview/connector/styles.scss b/src/treeview/connector/styles.scss new file mode 100644 index 0000000000..7aa3e89fd4 --- /dev/null +++ b/src/treeview/connector/styles.scss @@ -0,0 +1,54 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +@use '../../internal/styles/tokens' as awsui; +@use '../../internal/styles' as styles; + +.treeitem-connector-horizontal { + block-size: 1px; + position: absolute; + inset-block-start: 14px; + inline-size: 39px; + background: red; + inset-inline-start: -14px; + + &.expandable { + inline-size: 20px; + } +} + +.treeitem-connector-vertical { + &-root { + inline-size: 1px; + position: absolute; + inset-block: 0; + inset-inline-start: 50%; + background-color: red; + } + + &-root-end { + block-size: 16px; + inset-block: 0 unset; + } + + &-expandable { + inset-block-start: 24px; + } + + &-middle { + inline-size: 1px; + position: absolute; + inset-block: -10px 0px; + inset-inline-start: -14px; + background-color: red; + } + + &-end { + inline-size: 1px; + position: absolute; + inset-block: 0 10px; + inset-inline-start: -14px; + background-color: red; + } +} diff --git a/src/treeview/index.tsx b/src/treeview/index.tsx index 187200e812..2226e0c9f1 100644 --- a/src/treeview/index.tsx +++ b/src/treeview/index.tsx @@ -11,7 +11,7 @@ import InternalTreeview from './internal'; export { TreeviewProps }; -const Treeview = ({ items, ...rest }: TreeviewProps) => { +const Treeview = ({ items, showConnectorLine = true, ...rest }: TreeviewProps) => { const baseProps = getBaseProps(rest); // TODO: analytics metadata? const baseComponentProps = useBaseComponent('Treeview', { @@ -29,6 +29,7 @@ const Treeview = ({ items, ...rest }: TreeviewProps) => { {...externalProps} // ref={ref} items={items} + showConnectorLine={showConnectorLine} {...rest} /> ); diff --git a/src/treeview/interfaces.ts b/src/treeview/interfaces.ts index 3b27e495a8..70f738aa76 100644 --- a/src/treeview/interfaces.ts +++ b/src/treeview/interfaces.ts @@ -5,74 +5,100 @@ import React from 'react'; import { BaseComponentProps } from '../internal/base-component'; import { NonCancelableEventHandler } from '../internal/events'; -export interface TreeviewProps extends BaseComponentProps { +export interface TreeviewProps extends BaseComponentProps { /** * An array of treeview items. - * Each item has the following properties: - * * `id` (string) - The unique identifier of the item. + */ + items: ReadonlyArray; + + /** + * Use it to map your data to render the item. + * For each item the below properties must be returned: * * `content` (React.ReactNode) - The content of the item. - * * `details` (optional, React.ReactNode) - The details of the item, displayed below the content. - * * `actions` (optional, React.ReactNode) - Actions related to item. We recommend using button group. - * * `items` (optional, TreeItem[]) - The nested items of the item. + * * `icon` (optional, React.ReactNode) - The leading icon of the item. + * * `description` (optional, React.ReactNode) - The details of the item, displayed below the content. + * * `secondaryContent` (optional, React.ReactNode) - Actions related to item. We recommend using a button group. + */ + renderItem: (item: T, index: number) => TreeviewProps.TreeItem; + + /** + * Provides a unique identifier of each item. */ - items: ReadonlyArray; + getItemId: (item: T, index: number) => string; /** - * An array of expanded item IDs. Each item ID must match the `id` property of an item in the `items` array. + * The nested items of the item. + */ + getItemChildren: (item: T, index: number) => ReadonlyArray; + + /** + * An array of expanded item IDs. Use it to control expand state of tree items. */ expandedItems?: ReadonlyArray; /** * Provides an `aria-label` to the treeview that screen readers can read (for accessibility). - * If there's a visible label element that you can reference, use this instead of `ariaLabel`. * Don't use `ariaLabel` and `ariaLabelledby` at the same time. */ ariaLabel?: string; /** * Sets the `aria-labelledby` property on the treeview. + * If there's a visible label element that you can reference, use this instead of `ariaLabel`. + * Don't use `ariaLabel` and `ariaLabelledby` at the same time. */ ariaLabelledby?: string; /** - * Called when an item's expand toggle is clicked. + * Sets the `aria-description` property on the treeview. + */ + ariaDescription?: string; + + /** + * Sets the `aria-describedby` property on the treeview. */ - onExpandableItemToggle: NonCancelableEventHandler; + ariaDescribedby?: string; /** - * Renders the treeview in a loading state. We recommend that you also set a `loadingText`. - * Do we need this? Customers could render a spinner or status indicator themselves, unlike table - there the columns should be there. We'd need to suggest them to have a text in the status indicator and wrap the status indicator with the live region component - * I think the text similar to the table should be specified by the customer instead of depending on the i18n strings, specifically to the content that is being rendered (Loading clusters, etc) + * Called when an item's expand toggle is clicked. */ - // loading?: boolean; + onItemToggle: NonCancelableEventHandler>; /** - * Specifies the text that's displayed when the treeview is in a loading state. + * An object containing all the necessary localized strings required by the component. + * @i18n */ - // loadingText?: string; + i18nStrings?: TreeviewProps.I18nStrings; /** - * Displayed when the `items` property is an empty array. Use it to render an empty state. + * If `true`, adds connecting lines between child items and their expanded parent items. */ - // empty?: React.ReactNode; + showConnectorLine?: boolean; /** - * If `true`, adds guide lines connecting child items to the expanded parent item. + * @awsuiSystem core + * Overrides the default expand toggle. + * Use it to have different icons/animations for toggle. */ - showGuideLines?: boolean; + renderItemToggleIcon?: (isExpanded: boolean) => React.ReactNode; } export namespace TreeviewProps { export interface TreeItem { - id: string; content: React.ReactNode; - details?: React.ReactNode; - actions?: React.ReactNode; - items?: ReadonlyArray; + icon?: React.ReactNode; + description?: React.ReactNode; + secondaryContent?: React.ReactNode; } - export interface ExpandableItemToggleDetail { + export interface ItemToggleDetail { id: string; + item: T; expanded: boolean; } + + export interface I18nStrings { + collapseButtonLabel?: (item: T) => string; + expandButtonLabel?: (item: T) => string; + } } diff --git a/src/treeview/internal.tsx b/src/treeview/internal.tsx index a96408fa0e..a41a088e74 100644 --- a/src/treeview/internal.tsx +++ b/src/treeview/internal.tsx @@ -6,7 +6,7 @@ import clsx from 'clsx'; import { getBaseProps } from '../internal/base-component'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; import { TreeviewProps } from './interfaces'; -import TreeItem from './treeitem'; +import InternalTreeItem from './treeitem'; import { getItemPosition } from './utils'; import styles from './styles.css.js'; @@ -14,10 +14,17 @@ import styles from './styles.css.js'; type InternalTreeviewProps = TreeviewProps & InternalBaseComponentProps; const InternalTreeview = ({ - items = [], expandedItems = [], - // ariaLabel, - onExpandableItemToggle, + onItemToggle, + items = [], + renderItem, + getItemId, + getItemChildren, + ariaLabel, + ariaLabelledby, + ariaDescription, + ariaDescribedby, + i18nStrings, __internalRootRef, ...rest }: InternalTreeviewProps) => { @@ -25,19 +32,31 @@ const InternalTreeview = ({ return (
          -
            - {/*
              */} - {items.map((item, index) => ( - - ))} +
                + {items.map((item, index) => { + return ( + + ); + })}
          ); diff --git a/src/treeview/styles.scss b/src/treeview/styles.scss index c5aed470b5..602f5951ef 100644 --- a/src/treeview/styles.scss +++ b/src/treeview/styles.scss @@ -21,20 +21,7 @@ margin-block: 0; } -.child-treeitem { - display: flex; -} - -.tree { - @include styles.styles-reset-ul; -} - -.parent-treeitem { - padding-inline-start: 0; - margin-inline-start: 0; -} - -.treeitem-guideline { +.treeitem-toggle-area { inline-size: 30px; background-color: yellow; @@ -46,52 +33,17 @@ position: relative; } -.treeitem-guideline-horizontal { - block-size: 1px; - position: absolute; - inset-block-start: 14px; - inline-size: 39px; - background: red; - inset-inline-start: -14px; - - &.expandable { - inline-size: 20px; - } +.child-treeitem { + display: flex; } -.treeitem-guideline-vertical { - &-root { - inline-size: 1px; - position: absolute; - inset-block: 0; - inset-inline-start: 50%; - background-color: red; - } - - &-root-end { - block-size: 16px; - inset-block: 0 unset; - } - - &-expandable { - inset-block-start: 24px; - } - - &-middle { - inline-size: 1px; - position: absolute; - inset-block: -10px 0px; - inset-inline-start: -14px; - background-color: red; - } +.tree { + @include styles.styles-reset-ul; +} - &-end { - inline-size: 1px; - position: absolute; - inset-block: 0 10px; - inset-inline-start: -14px; - background-color: red; - } +.parent-treeitem { + padding-inline-start: 0; + margin-inline-start: 0; } .treeitem-content { @@ -113,6 +65,8 @@ .treeitem-first-line { display: flex; + align-items: baseline; + gap: 4px; } .treeitem-actions { diff --git a/src/treeview/treeitem.tsx b/src/treeview/treeitem.tsx index 92678a94f8..8d47cb15dc 100644 --- a/src/treeview/treeitem.tsx +++ b/src/treeview/treeitem.tsx @@ -3,29 +3,33 @@ import React from 'react'; import clsx from 'clsx'; +import { useInternalI18n } from '../i18n/context'; import { ExpandToggleButton } from '../internal/components/expand-toggle-button'; import { fireNonCancelableEvent } from '../internal/events'; -import { NonCancelableEventHandler } from '../internal/events'; +import Connector from './connector'; import { TreeviewProps } from './interfaces'; -import { getItemPosition } from './utils'; +import { getItemPosition, transformTreeItemProps } from './utils'; import styles from './styles.css.js'; -type TreeItemProps = TreeviewProps.TreeItem & { - isExpanded: boolean; - onExpandableItemToggle: NonCancelableEventHandler; - expandedItems: ReadonlyArray; +interface InternalTreeItemProps + extends Pick< + TreeviewProps, + 'expandedItems' | 'renderItem' | 'getItemId' | 'getItemChildren' | 'onItemToggle' | 'i18nStrings' + > { + item: any; + index: number; level: number; position: 'start' | 'middle' | 'end'; - details?: React.ReactNode; - actions?: React.ReactNode; -}; +} const TreeItemLayout = ({ + icon, content, details, actions, }: { + icon?: React.ReactNode; content: React.ReactNode; details?: React.ReactNode; actions?: React.ReactNode; @@ -33,6 +37,7 @@ const TreeItemLayout = ({ return (
          +
          {icon}
          {content}
          {actions}
          @@ -42,54 +47,28 @@ const TreeItemLayout = ({ ); }; -const GuideLine = ({ +const InternalTreeItem = ({ + item, + index, level, position, - isExpandable, -}: { - level: number; - position: 'start' | 'middle' | 'end'; - isExpandable: boolean; -}) => { - if (level === 0) { - return ( -
          - ); - } - - return ( - <> -
          - - {level > 1 && (position === 'start' || position === 'end') && ( -
          - )} - - {level > 1 && position === 'middle' &&
          } - - ); -}; - -const TreeItem = ({ - id, - content, - details, - actions, - isExpanded, - onExpandableItemToggle, - items = [], + i18nStrings, expandedItems = [], - level, - position, -}: TreeItemProps) => { - const isExpandable = items.length > 0; - const isExpandableItemExpanded = isExpandable && isExpanded; + renderItem, + getItemId, + getItemChildren, + onItemToggle, +}: InternalTreeItemProps) => { + const i18n = useInternalI18n('treeview'); + const { id, isExpandable, isExpanded, children, icon, content, description, secondaryContent } = + transformTreeItemProps({ + item, + index, + expandedItems, + renderItem, + getItemId, + getItemChildren, + }); const nextLevel = level + 1; return ( @@ -97,38 +76,43 @@ const TreeItem = ({ id={id} role="treeitem" className={clsx(styles['child-treeitem'], isExpandable && [styles.expandable], isExpanded && [styles.expanded])} - aria-expanded={isExpandable ? isExpandableItemExpanded : undefined} + aria-expanded={isExpandable ? isExpanded : undefined} data-testid={id} > -
          +
          {isExpandable && ( fireNonCancelableEvent(onExpandableItemToggle, { id, expanded: !isExpanded })} - expandButtonLabel="Expand" - collapseButtonLabel="Collapse" + onExpandableItemToggle={() => fireNonCancelableEvent(onItemToggle, { id, item, expanded: !isExpanded })} + expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} + collapseButtonLabel={i18n('i18nStrings.collapseButtonLabel', i18nStrings?.collapseButtonLabel?.(item))} /> )} - +
          - + - {isExpandableItemExpanded && ( + {isExpanded && (
            - {items.map((item, index) => ( - - ))} + {children.map((child, index) => { + return ( + + ); + })}
          )}
          @@ -136,4 +120,4 @@ const TreeItem = ({ ); }; -export default TreeItem; +export default InternalTreeItem; diff --git a/src/treeview/utils.ts b/src/treeview/utils.ts index cbddd8e2a4..8d6590c138 100644 --- a/src/treeview/utils.ts +++ b/src/treeview/utils.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { TreeviewProps } from './interfaces'; + export function getItemPosition(index: number, itemsLength: number) { if (index === 0) { return 'start'; @@ -12,3 +14,32 @@ export function getItemPosition(index: number, itemsLength: number) { return 'middle'; } + +interface TransformTreeItemPropsParams + extends Pick { + item: any; + index: number; +} + +export function transformTreeItemProps({ + item, + index, + expandedItems = [], + renderItem, + getItemId, + getItemChildren, +}: TransformTreeItemPropsParams) { + const treeitem = renderItem(item, index); + const itemId = getItemId(item, index); + const children = getItemChildren(item, index) || []; + const isExpandable = children.length > 0; + const isExpanded = isExpandable && expandedItems.includes(itemId); + + return { + id: itemId, + isExpandable, + isExpanded, + children, + ...treeitem, + }; +} From debfb6593d6e1bda7e5dc25cf027138ea31accd7 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 22 May 2025 11:11:23 +0200 Subject: [PATCH 07/39] updates --- pages/treeview/basic.page.tsx | 19 ++- src/test-utils/dom/treeview/index.ts | 17 ++- src/treeview/connector/styles.scss | 6 +- src/treeview/internal.tsx | 1 + src/treeview/styles.scss | 133 ++++++++++++++++- src/treeview/treeitem-layout.tsx | 33 +++++ src/treeview/treeitem.tsx | 213 ++++++++++++++++++++------- src/treeview/utils.ts | 4 + 8 files changed, 359 insertions(+), 67 deletions(-) create mode 100644 src/treeview/treeitem-layout.tsx diff --git a/pages/treeview/basic.page.tsx b/pages/treeview/basic.page.tsx index 68c0ad2025..51e7ecefd1 100644 --- a/pages/treeview/basic.page.tsx +++ b/pages/treeview/basic.page.tsx @@ -41,6 +41,7 @@ interface Item { details?: string; children?: Item[]; hasActions?: boolean; + hideIcon?: boolean; } const items: Item[] = [ @@ -65,6 +66,7 @@ const items: Item[] = [ { id: '1.2', content: 'Item 1.2', + hasActions: true, }, { id: '1.3', @@ -75,6 +77,7 @@ const items: Item[] = [ { id: '1.3.1', content: 'Item 1.3.1', + hasActions: true, }, { id: '1.3.2', @@ -87,6 +90,7 @@ const items: Item[] = [ { id: '2', content: 'Item 2', + hasActions: true, }, { id: '3', @@ -101,6 +105,7 @@ const items: Item[] = [ { id: '4', content: , + hideIcon: true, children: [ { id: '4.1', @@ -124,6 +129,7 @@ const items: Item[] = [ { id: '4.1.3', content: 'Item 4.1.3', + details: 'us-east-1', children: [ { id: '4.1.3.1', @@ -154,18 +160,23 @@ const items: Item[] = [ { id: '6', content: progressiveStepContent, + hideIcon: true, + hasActions: true, children: [ { id: '6.1', content: progressiveStepItemsContent[0], + hideIcon: true, }, { id: '6.2', content: progressiveStepItemsContent[1], + hideIcon: true, }, { id: '6.3', content: progressiveStepItemsContent[2], + hideIcon: true, }, ], }, @@ -264,13 +275,17 @@ export default function BasicTreeview() {
          { return { - icon: , + icon: item.hideIcon ? undefined : ( + + ), content: item.content, description: {item.details}, - actions: item.hasActions ? : undefined, + secondaryContent: item.hasActions ? : undefined, }; }} getItemId={item => item.id} diff --git a/src/test-utils/dom/treeview/index.ts b/src/test-utils/dom/treeview/index.ts index bbd7c78da6..ad025a92f5 100644 --- a/src/test-utils/dom/treeview/index.ts +++ b/src/test-utils/dom/treeview/index.ts @@ -9,22 +9,29 @@ class TreeItemWrapper extends ComponentWrapper { /** * Finds the content slot of the tree item */ - findContentSlot(): ElementWrapper | null { + findContent(): ElementWrapper | null { return this.findByClassName(styles['treeitem-content']); } + /** + * Finds the content slot of the tree item + */ + findIcon(): ElementWrapper | null { + return this.findByClassName(styles['treeitem-icon']); + } + /** * Finds the details slot of the tree item */ - findDetailsSlot(): ElementWrapper | null { - return this.findByClassName(styles['treeitem-details']); + findDetails(): ElementWrapper | null { + return this.findByClassName(styles['treeitem-description']); } /** * Finds the actions slot of the tree item */ - findActionsSlot(): ElementWrapper | null { - return this.findByClassName(styles['treeitem-actions']); + findActions(): ElementWrapper | null { + return this.findByClassName(styles['treeitem-secondary-content']); } /** diff --git a/src/treeview/connector/styles.scss b/src/treeview/connector/styles.scss index 7aa3e89fd4..5b72a88760 100644 --- a/src/treeview/connector/styles.scss +++ b/src/treeview/connector/styles.scss @@ -8,7 +8,7 @@ .treeitem-connector-horizontal { block-size: 1px; position: absolute; - inset-block-start: 14px; + inset-block-start: 12px; inline-size: 39px; background: red; inset-inline-start: -14px; @@ -39,7 +39,7 @@ &-middle { inline-size: 1px; position: absolute; - inset-block: -10px 0px; + inset-block: -11px 0px; inset-inline-start: -14px; background-color: red; } @@ -50,5 +50,7 @@ inset-block: 0 10px; inset-inline-start: -14px; background-color: red; + + block-size: 13px; } } diff --git a/src/treeview/internal.tsx b/src/treeview/internal.tsx index a41a088e74..78efe5016b 100644 --- a/src/treeview/internal.tsx +++ b/src/treeview/internal.tsx @@ -54,6 +54,7 @@ const InternalTreeview = ({ renderItem={renderItem} getItemId={getItemId} getItemChildren={getItemChildren} + // withGrid={true} /> ); })} diff --git a/src/treeview/styles.scss b/src/treeview/styles.scss index 602f5951ef..457fb7dc80 100644 --- a/src/treeview/styles.scss +++ b/src/treeview/styles.scss @@ -21,6 +21,10 @@ margin-block: 0; } +.parent-treeitem { + inline-size: 100%; +} + .treeitem-toggle-area { inline-size: 30px; background-color: yellow; @@ -33,6 +37,24 @@ position: relative; } +.connector-area { + background-color: yellow; + display: flex; + padding-block-start: 3px; + justify-content: flex-end; + + &.level-0 { + justify-content: center; + } +} + +.connector-area2 { + display: flex; + align-items: baseline; + position: relative; + inline-size: 100%; +} + .child-treeitem { display: flex; } @@ -41,10 +63,10 @@ @include styles.styles-reset-ul; } -.parent-treeitem { - padding-inline-start: 0; - margin-inline-start: 0; -} +// .parent-treeitem { +// padding-inline-start: 0; +// margin-inline-start: 0; +// } .treeitem-content { position: relative; @@ -61,6 +83,7 @@ .treeitem-layout { display: flex; flex-direction: column; + flex: 1; } .treeitem-first-line { @@ -69,10 +92,108 @@ gap: 4px; } -.treeitem-actions { +.treeitem-secondary-content { // test } -.treeitem-details { +.treeitem-description { // test } + +.horizontal { + inline-size: 32px; + block-size: 1px; + background: red; + position: absolute; + inset-block-start: 11px; + + &.expandable { + inline-size: 22px; + } +} +.vertical { + inline-size: 1px; + background-color: red; + position: absolute; + inset-block: 0; + inset-inline-start: 8px; + + &.expanded { + inset-block-start: 24px; + } + + &.position-end:not(.expanded) { + block-size: 12px; + } +} + +.level0 { + inline-size: 1px; + background-color: red; + position: absolute; + inset-block: 0; + inset-inline-start: 8px; + + &.expandable { + inset-block: 22px -8px; + } + + &.position-start { + inset-block-start: 20px; + } +} + +// .with-grid { +// align-items: center; +// display: grid; +// grid-template-columns: auto auto 1fr; +// grid-template-rows: repeat(2, 1fr) auto; +// // row-gap: 0.4rem; +// // line-height: 2.8rem; +// overflow: visible; +// position: relative; + +// .parent-treeitem { +// grid-column: 3; +// grid-row: 3; +// // grid-template-columns: subgrid; +// // grid-template-rows: subgrid; +// } + +// .toggle { +// align-items: center; +// display: flex; +// grid-column: 1; +// grid-row: 1 / span 2; +// justify-content: center; +// } + +// .treeitem-layout { +// grid-column: 2; +// // grid-column: 3; +// grid-row: 1 / span 2; +// } + +// .vertical-rule { +// background: red; +// grid-column: 1; +// grid-row: 3; +// height: 100%; +// justify-self: center; +// width: 1px; +// } + +// .horizontal-rule { +// background: red; +// grid-column: 1; +// grid-row: 1 / span 2; +// height: 0.1rem; +// margin-left: -2.5rem; +// width: 2rem; +// } + +// .treeitem-icon { +// // grid-column: 2; +// // grid-row: 1 / span 2; +// } +// } diff --git a/src/treeview/treeitem-layout.tsx b/src/treeview/treeitem-layout.tsx new file mode 100644 index 0000000000..7befdd8f91 --- /dev/null +++ b/src/treeview/treeitem-layout.tsx @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import styles from './styles.css.js'; + +const TreeItemLayout = ({ + icon, + content, + description, + secondaryContent, +}: { + icon?: React.ReactNode; + content: React.ReactNode; + description?: React.ReactNode; + secondaryContent?: React.ReactNode; +}) => { + return ( +
          +
          + {icon &&
          {icon}
          } + +
          {content}
          + + {secondaryContent &&
          {secondaryContent}
          } +
          + + {description &&
          {description}
          } +
          + ); +}; + +export default TreeItemLayout; diff --git a/src/treeview/treeitem.tsx b/src/treeview/treeitem.tsx index 8d47cb15dc..267fbcec66 100644 --- a/src/treeview/treeitem.tsx +++ b/src/treeview/treeitem.tsx @@ -6,8 +6,9 @@ import clsx from 'clsx'; import { useInternalI18n } from '../i18n/context'; import { ExpandToggleButton } from '../internal/components/expand-toggle-button'; import { fireNonCancelableEvent } from '../internal/events'; -import Connector from './connector'; +// import Connector from './connector'; import { TreeviewProps } from './interfaces'; +import TreeItemLayout from './treeitem-layout'; import { getItemPosition, transformTreeItemProps } from './utils'; import styles from './styles.css.js'; @@ -21,32 +22,9 @@ interface InternalTreeItemProps index: number; level: number; position: 'start' | 'middle' | 'end'; + withGrid?: boolean; } -const TreeItemLayout = ({ - icon, - content, - details, - actions, -}: { - icon?: React.ReactNode; - content: React.ReactNode; - details?: React.ReactNode; - actions?: React.ReactNode; -}) => { - return ( -
          -
          -
          {icon}
          -
          {content}
          -
          {actions}
          -
          - -
          {details}
          -
          - ); -}; - const InternalTreeItem = ({ item, index, @@ -54,6 +32,7 @@ const InternalTreeItem = ({ position, i18nStrings, expandedItems = [], + withGrid, renderItem, getItemId, getItemChildren, @@ -71,15 +50,22 @@ const InternalTreeItem = ({ }); const nextLevel = level + 1; + console.log(Array.from(Array(level + 1).keys())); + return (
        • -
          + {/*
          {isExpandable && ( -
          - -
          - - - {isExpanded && ( -
            - {children.map((child, index) => { - return ( - */} + + {!withGrid && ( +
            + {/* Try out horizontally constructured connector lines */} +
            +
            0 && !isExpandable ? '20' : '0'}px)`, + marginBlock: 'auto', + }} + > + {level > 0 && ( +
            - ); - })} -
          - )} -
          + )} + + {level === 0 && ( +
          + )} + + {level > 0 && + Array.from(Array(level + 1).keys()).map(l => { + if (l === 0) { + return
          ; + } + + if (l === level && !isExpanded) { + return
          ; + } + + return ( +
          + ); + })} +
          + +
          + {isExpandable && ( + + fireNonCancelableEvent(onItemToggle, { id, item, expanded: !isExpanded }) + } + expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} + collapseButtonLabel={i18n( + 'i18nStrings.collapseButtonLabel', + i18nStrings?.collapseButtonLabel?.(item) + )} + /> + )} +
          + + +
          + + {isExpanded && children.length && ( +
            + {children.map((child, index) => { + return ( + + ); + })} +
          + )} +
          + )} + + {withGrid && ( + <> + {/* Try out constructing connector lines with display grid */} + {isExpandable && ( + <> +
          + + fireNonCancelableEvent(onItemToggle, { id, item, expanded: !isExpanded }) + } + expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} + collapseButtonLabel={i18n( + 'i18nStrings.collapseButtonLabel', + i18nStrings?.collapseButtonLabel?.(item) + )} + /> +
          + +
          + + )} + + {level > 0 &&
          } + + {/* {icon &&
          {icon}
          } */} + + + + {isExpanded && children.length && ( +
            + {children.map((child, index) => { + return ( + + ); + })} +
          + )} + + )}
        • ); }; diff --git a/src/treeview/utils.ts b/src/treeview/utils.ts index 8d6590c138..714ef735e7 100644 --- a/src/treeview/utils.ts +++ b/src/treeview/utils.ts @@ -4,6 +4,10 @@ import { TreeviewProps } from './interfaces'; export function getItemPosition(index: number, itemsLength: number) { + if (index === 0 && itemsLength === 1) { + return 'end'; + } + if (index === 0) { return 'start'; } From b66fdcda254861048336b5cd4005b6d712682917 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 22 May 2025 11:17:51 +0200 Subject: [PATCH 08/39] update snapshot --- .../__snapshots__/test-utils-wrappers.test.tsx.snap | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap index 09f121f852..5bb63c378f 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap @@ -1659,7 +1659,6 @@ findTopNavigation(selector?: string): TopNavigationWrapper | null; * @returns {Array} */ findAllTopNavigations(selector?: string): Array; -/** /** * Returns the wrapper of the first Treeview that matches the specified CSS selector. * If no CSS selector is specified, returns the wrapper of the first Treeview. @@ -4045,7 +4044,6 @@ findTopNavigation(selector?: string): TopNavigationWrapper; * @returns {MultiElementWrapper} */ findAllTopNavigations(selector?: string): MultiElementWrapper; -/** /** * Returns a wrapper that matches the Treeviews with the specified CSS selector. * If no CSS selector is specified, returns a wrapper that matches Treeviews. From 887fc0135607ffd908c26e40392493c44d782b31 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Fri, 30 May 2025 15:31:32 +0200 Subject: [PATCH 09/39] updates to interface, dev pages and added tests --- pages/treeview/basic.page.tsx | 125 +++++++---- pages/treeview/generate-data.ts | 2 +- pages/treeview/test.page.tsx | 6 +- .../test-utils-selectors.test.tsx.snap | 11 +- src/test-utils/dom/treeview/index.ts | 97 ++++---- src/treeview/__tests__/tree-item.test.tsx | 114 ++++++++++ src/treeview/__tests__/tree-view.test.tsx | 210 ++++++++++++++++++ src/treeview/index.tsx | 2 +- src/treeview/interfaces.ts | 16 +- src/treeview/internal.tsx | 49 +++- src/treeview/styles.scss | 26 +-- src/treeview/test-classes/styles.scss | 15 ++ src/treeview/treeitem-layout.tsx | 16 +- src/treeview/treeitem.tsx | 68 +++--- 14 files changed, 581 insertions(+), 176 deletions(-) create mode 100644 src/treeview/__tests__/tree-item.test.tsx create mode 100644 src/treeview/__tests__/tree-view.test.tsx create mode 100644 src/treeview/test-classes/styles.scss diff --git a/pages/treeview/basic.page.tsx b/pages/treeview/basic.page.tsx index 51e7ecefd1..b8a9611752 100644 --- a/pages/treeview/basic.page.tsx +++ b/pages/treeview/basic.page.tsx @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; +import { ButtonDropdown } from '~components'; import Badge from '~components/badge'; import Box from '~components/box'; import ButtonGroup from '~components/button-group'; import Container from '~components/container'; import Icon from '~components/icon'; +import Popover from '~components/popover'; import SpaceBetween from '~components/space-between'; import StatusIndicator from '~components/status-indicator'; import Treeview from '~components/treeview'; @@ -16,23 +18,34 @@ const progressiveStepContent = ( Checked 5 nodes
          - 1 + + 1 + 1 + 1 2
          ); const progressiveStepItemsContent = [ - + node-17 (eksclu-node-12345) , node-18 (eksclu-node-09876) , - + + + node-18 (eksclu-node-09876) + + , + node-19 (eksclu-node-ab123) , + + node-20 (eksclu-node-ab124) + , ]; interface Item { @@ -178,6 +191,11 @@ const items: Item[] = [ content: progressiveStepItemsContent[2], hideIcon: true, }, + { + id: '6.4', + content: progressiveStepItemsContent[3], + hideIcon: true, + }, ], }, { @@ -186,49 +204,71 @@ const items: Item[] = [ }, ]; -function Actions() { +function Actions( + { actionType }: { actionType?: 'button-group' | 'button-dropdown' } = { actionType: 'button-dropdown' } +) { const [pressed, setPressed] = useState(false); + if (actionType === 'button-group') { + return ( + { + if (detail.id === 'favorite') { + setPressed(!pressed); + } + }} + /> + ); + } + return ( - { - if (detail.id === 'favorite') { - setPressed(!pressed); - } - }} + ariaLabel="Control instance" + variant="icon" /> ); } @@ -246,7 +286,7 @@ function RdsAccessRoleTreeItemContent() {
          - +
          @@ -276,7 +316,6 @@ export default function BasicTreeview() { { return { @@ -284,8 +323,8 @@ export default function BasicTreeview() { ), content: item.content, - description: {item.details}, - secondaryContent: item.hasActions ? : undefined, + secondaryContent: {item.details}, + actions: item.hasActions ? : undefined, }; }} getItemId={item => item.id} diff --git a/pages/treeview/generate-data.ts b/pages/treeview/generate-data.ts index abfa1852ca..243b5f9bec 100644 --- a/pages/treeview/generate-data.ts +++ b/pages/treeview/generate-data.ts @@ -143,7 +143,7 @@ function getSubnetItems({ level, parentId, count }: { level: number; parentId: s }); } -export default function getItems(rootItemCount = 20) { +export default function getItems(rootItemCount = 20): Item[] { return range(rootItemCount).map(() => { const errorCount = statusCount(); const warningCount = statusCount(); diff --git a/pages/treeview/test.page.tsx b/pages/treeview/test.page.tsx index b787e9ca22..713f394a29 100644 --- a/pages/treeview/test.page.tsx +++ b/pages/treeview/test.page.tsx @@ -52,8 +52,8 @@ export default function TestPage() { return { icon: , content: , - secondaryContent: item.hasActions ? : undefined, - description: item.tagName ? ( + actions: item.hasActions ? : undefined, + secondaryContent: item.tagName ? ( @@ -65,7 +65,7 @@ export default function TestPage() { }} getItemId={item => item.id} getItemChildren={item => item.children} - onItemToggle={({ detail }: any) => { + onItemToggle={({ detail }) => { if (detail.expanded) { return setExpandedItems(prev => [...prev, detail.item.id]); } else { diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 52c7975c39..2a6402ba65 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -666,9 +666,14 @@ exports[`test-utils selectors 1`] = ` "awsui_utility-wrapper_k5dlb", ], "treeview": [ - "awsui_child-treeitem_uf1x6", - "awsui_root_uf1x6", - "awsui_treeitem-content_uf1x6", + "awsui_actions_re8ah", + "awsui_content_re8ah", + "awsui_expandable_re8ah", + "awsui_expanded_re8ah", + "awsui_icon_re8ah", + "awsui_root_re8ah", + "awsui_secondary-content_re8ah", + "awsui_treeitem_re8ah", ], "tutorial-panel": [ "awsui_collapse-button_ig8mp", diff --git a/src/test-utils/dom/treeview/index.ts b/src/test-utils/dom/treeview/index.ts index ad025a92f5..9dcdb0f620 100644 --- a/src/test-utils/dom/treeview/index.ts +++ b/src/test-utils/dom/treeview/index.ts @@ -1,92 +1,105 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ComponentWrapper, ElementWrapper, usesDom } from '@cloudscape-design/test-utils-core/dom'; +import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; import expandToggleStyles from '../../../internal/components/expand-toggle-button/styles.selectors.js'; -import styles from '../../../treeview/styles.selectors.js'; +import testUtilStyles from '../../../treeview/test-classes/styles.selectors.js'; class TreeItemWrapper extends ComponentWrapper { /** - * Finds the content slot of the tree item + * Finds the content of the tree item. */ findContent(): ElementWrapper | null { - return this.findByClassName(styles['treeitem-content']); + return this.findByClassName(testUtilStyles.content); } /** - * Finds the content slot of the tree item + * Finds the icon of the tree item. */ findIcon(): ElementWrapper | null { - return this.findByClassName(styles['treeitem-icon']); + return this.findByClassName(testUtilStyles.icon); } /** - * Finds the details slot of the tree item + * Finds the secondary content of the tree item. */ - findDetails(): ElementWrapper | null { - return this.findByClassName(styles['treeitem-description']); + findSecondaryContent(): ElementWrapper | null { + return this.findByClassName(testUtilStyles['secondary-content']); } /** - * Finds the actions slot of the tree item + * Finds the actions of the tree item. */ findActions(): ElementWrapper | null { - return this.findByClassName(styles['treeitem-secondary-content']); + return this.findByClassName(testUtilStyles.actions); } /** - * Finds the child items of the tree item - */ - findItems(): Array { - return this.findAllByClassName(styles['child-treeitem']).map(item => new TreeItemWrapper(item.getElement())); - } - - /** - * Finds the expand toggle of the tree item + * Finds the expand toggle of the tree item. */ findItemToggle(): ElementWrapper | null { return this.findByClassName(expandToggleStyles['expand-toggle']); } /** - * Finds the children tree items of the tree item + * Finds all visible child items of the tree item. + * @param options + * * expanded (boolean) - Flag to find the expanded/collapsed items */ - findChildren(): Array { - return this.findAllByClassName(styles['child-treeitem']).map(item => new TreeItemWrapper(item.getElement())); - } + findChildren(options: { expanded?: boolean } = {}): Array { + const selector = getTreeItemSelector(options); - /** - * Finds a children tree item by its ID - */ - findChildById(id: string): TreeItemWrapper | null { - const itemSelector = `.${styles['child-treeitem']}[data-testid="${id}"]`; - const item = this.find(itemSelector); - return item ? new TreeItemWrapper(item.getElement()) : null; + return this.findAll(selector).map(item => new TreeItemWrapper(item.getElement())); } /** - * Returns `true` if the item expand toggle is present and expanded. Returns `false` otherwise. + * Finds a visible child item by its ID. + * @param id of the item to find + * @param options + * * expanded (boolean) - Flag to find the expanded/collapsed item. Use it to test if item is expanded/collapsed. */ - @usesDom - isExpanded(): boolean { - return this.getElement().getAttribute('aria-expanded') === 'true'; + findChildById(id: string, options: { expanded?: boolean } = {}): TreeItemWrapper | null { + const selector = `${getTreeItemSelector(options)}[data-testid="treeitem-${id}"]`; + const item = this.find(selector); + return item ? new TreeItemWrapper(item.getElement()) : null; } } export default class TreeviewWrapper extends ComponentWrapper { - static rootSelector: string = styles.root; + static rootSelector: string = testUtilStyles.root; /** - * Finds the root level tree items + * Finds all visible tree items. + * @param options + * * expanded (boolean) - Flag to find the expanded/collapsed items */ - findItems(): Array { - return this.findAllByClassName(styles['child-treeitem']).map(item => new TreeItemWrapper(item.getElement())); + findItems(options: { expanded?: boolean } = {}): Array { + const selector = getTreeItemSelector(options); + + return this.findAll(selector).map(item => new TreeItemWrapper(item.getElement())); } - findItemById(id: string): TreeItemWrapper | null { - // const itemSelector = `[role="treeitem"][data-testid="${id}"]`; - const itemSelector = `.${styles['child-treeitem']}[data-testid="${id}"]`; - const item = this.find(itemSelector); + /** + * Finds a visible item by its ID. + * @param id of the item to find + * @param options + * * expanded (boolean) - Flag to find the expanded/collapsed item. Use it to test if item is expanded/collapsed. + */ + findItemById(id: string, options: { expanded?: boolean } = {}): TreeItemWrapper | null { + const selector = `${getTreeItemSelector(options)}[data-testid="treeitem-${id}"]`; + const item = this.find(selector); return item ? new TreeItemWrapper(item.getElement()) : null; } } + +function getTreeItemSelector({ expanded }: { expanded?: boolean }): string { + let selector = `.${testUtilStyles.treeitem}`; + + if (expanded === true) { + selector += `.${testUtilStyles.expanded}`; + } else if (expanded === false) { + selector += `.${testUtilStyles.expandable}:not(.${testUtilStyles.expanded})`; + } + + return selector; +} diff --git a/src/treeview/__tests__/tree-item.test.tsx b/src/treeview/__tests__/tree-item.test.tsx new file mode 100644 index 0000000000..b9de6039eb --- /dev/null +++ b/src/treeview/__tests__/tree-item.test.tsx @@ -0,0 +1,114 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import createWrapper from '../../../lib/components/test-utils/dom'; +import Treeview, { TreeviewProps } from '../../../lib/components/treeview'; + +interface Item { + id: string; + title: string; + items?: Item[]; +} + +const defaultItems: Item[] = [ + { + id: '1', + title: 'Item 1', + items: [ + { + id: '1.1', + title: 'Item 1.1', + }, + { + id: '1.2', + title: 'Item 1.2', + items: [ + { + id: '1.2.1', + title: 'Item 1.2.1', + }, + ], + }, + ], + }, + { + id: '2', + title: 'Item 2', + }, +]; + +function renderTreeView(props: Partial> = {}) { + const { container } = render( + item.id} + getItemChildren={item => item.items} + renderItem={item => ({ + content: item.title, + })} + {...props} + /> + ); + const wrapper = createWrapper(container).findTreeview()!; + return { wrapper }; +} + +test('expand toggle is only added to expandable items', () => { + const { wrapper } = renderTreeView(); + + const expandableItem = wrapper.findItemById('1')!; + const nonExpandableItem = wrapper.findItemById('2')!; + + expect(expandableItem.findItemToggle()!.getElement()).toBeVisible(); + expect(nonExpandableItem.findItemToggle()).toBeNull(); +}); + +test('onItemToggle is fired when toggle is clicked', () => { + const onItemToggle = jest.fn(); + const { wrapper } = renderTreeView({ onItemToggle }); + + const itemToggle = wrapper.findItemById('1')!.findItemToggle()!.getElement(); + + // expand + itemToggle.click(); + expect(onItemToggle).toHaveBeenCalledTimes(1); + expect(onItemToggle).toHaveBeenCalledWith( + expect.objectContaining({ detail: { id: defaultItems[0].id, item: defaultItems[0], expanded: true } }) + ); + + // collapse + itemToggle.click(); + expect(onItemToggle).toHaveBeenCalledTimes(2); + expect(onItemToggle).toHaveBeenCalledWith( + expect.objectContaining({ detail: { id: defaultItems[0].id, item: defaultItems[0], expanded: false } }) + ); +}); + +test('children items are rendered only when item is expanded', () => { + const { wrapper } = renderTreeView(); + + const expandableItem = wrapper.findItemById('1')!; + expect(expandableItem.findChildById('1.1')).toBeNull(); + + // expand + expandableItem.findItemToggle()!.getElement().click(); + expect(wrapper.findItemById('1', { expanded: true })!.getElement()).toBeVisible(); + expect(expandableItem.findChildById('1.1')!.getElement()).toBeVisible(); + + // collapse + expandableItem.findItemToggle()!.getElement().click(); + expect(wrapper.findItemById('1', { expanded: false })!.getElement()).toBeVisible(); + expect(expandableItem.findChildById('1.1')).toBeNull(); +}); + +test("expanding an item shouldn't expand its child item", () => { + const { wrapper } = renderTreeView(); + + const expandableItem = wrapper.findItemById('1')!; + + expandableItem.findItemToggle()!.getElement().click(); + expect(expandableItem.findChildById('1.2', { expanded: false })!.getElement()).toBeVisible(); + expect(expandableItem.findChildById('1.2.1')).toBeNull(); +}); diff --git a/src/treeview/__tests__/tree-view.test.tsx b/src/treeview/__tests__/tree-view.test.tsx new file mode 100644 index 0000000000..7d01595c85 --- /dev/null +++ b/src/treeview/__tests__/tree-view.test.tsx @@ -0,0 +1,210 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; + +import ButtonDropdown from '../../../lib/components/button-dropdown'; +import Icon from '../../../lib/components/icon'; +import Link from '../../../lib/components/link'; +import Popover from '../../../lib/components/popover'; +import createWrapper from '../../../lib/components/test-utils/dom'; +import Treeview, { TreeviewProps } from '../../../lib/components/treeview'; + +jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), + warnOnce: jest.fn(), +})); + +afterEach(() => { + (warnOnce as jest.Mock).mockReset(); +}); + +// [DONE] should render tree-view with only content +// [DONE] should render tree-view with various slots +// [DONE] should expand items in expandedItems +// [DONE] should render tree-view with other Cloudscape components inside +// [DONE] dev warning if expanded items is defined but there is no onItemToggle? +// should render connector lines when showConnectorLine is true + +interface Item { + id: string; + title: React.ReactNode; + description?: React.ReactNode; + hasActions?: boolean; + items?: Item[]; +} + +const defaultActions = ( + +); + +const defaultData: Item[] = [ + { + id: '1', + title: 'Item 1', + description: 'Description 1', + hasActions: true, + items: [ + { + id: '1.1', + title: 'Item 1.1', + description: 'Description 1.1', + hasActions: true, + }, + { + id: '1.2', + title: 'Item 1.2', + description: 'Description 1.2', + items: [ + { + id: '1.2.1', + title: 'Item 1.2.1', + description: 'Description 1.2.1', + hasActions: true, + }, + ], + }, + { + id: '1.3', + title: 'Item 1.3', + description: 'Description 1.3', + items: [ + { + id: '1.3.1', + title: 'Item 1.3.1', + description: 'Description 1.3.1', + }, + ], + }, + ], + }, + { + id: '2', + title: ( + + Item 2 + + ), + hasActions: true, + description: Link in description, + }, + { + id: '3', + title: 'Item 3', + description: 'Description 3', + }, +]; + +const defaultProps: TreeviewProps = { + items: defaultData, + getItemId: item => item.id, + getItemChildren: item => item.items, + renderItem: item => ({ + icon: , + content: item.title, + secondaryContent: item.description, + actions: item.hasActions ? defaultActions : undefined, + children: item.items, + }), +}; + +function renderTreeView(props: Partial> = {}) { + const { container, rerender } = render(); + const wrapper = createWrapper(container).findTreeview()!; + return { wrapper, rerender }; +} + +test('should render with only content', () => { + const { wrapper } = renderTreeView({ renderItem: item => ({ content: item.title }) }); + + const items = wrapper.findItems(); + expect(items).toHaveLength(3); + + items.forEach((item, index) => { + expect(item.findContent()?.getElement()).toHaveTextContent(`Item ${index + 1}`); + expect(item.findIcon()).toBeNull(); + expect(item.findSecondaryContent()).toBeNull(); + expect(item.findActions()).toBeNull(); + }); +}); + +test('should render with various slots', () => { + const { wrapper } = renderTreeView(); + + const items = wrapper.findItems(); + expect(items).toHaveLength(3); + + defaultData.forEach((data, index) => { + const item = items[index]; + + expect(item.findContent()!.getElement()).toHaveTextContent(`Item ${index + 1}`); + expect(item.findIcon()!.getElement()).toBeVisible(); + if (data.description) { + expect(item.findSecondaryContent()!.getElement()).toBeVisible(); + + if (index !== 1) { + // index 1 (item 2)'s description is a link + expect(item.findSecondaryContent()!.getElement()).toHaveTextContent(`Description ${index + 1}`); + } + } + if (data.hasActions) { + expect(item.findActions()!.findButtonDropdown()!.getElement()).toBeVisible(); + } + }); +}); + +test('should render with other CDS components inside', () => { + const { wrapper } = renderTreeView(); + + const item = wrapper.findItemById('2')!; + + const contentPopover = item.findContent()!.findPopover()!; + const descriptionLink = item.findSecondaryContent()!.findLink()!; + const actionsButtonDropdown = item.findActions()!.findButtonDropdown()!; + + expect(contentPopover.getElement()).toBeVisible(); + expect(descriptionLink.getElement()).toBeVisible(); + expect(actionsButtonDropdown.getElement()).toBeVisible(); + + fireEvent.click(contentPopover.findTrigger().getElement()); + expect(contentPopover.findContent()?.getElement()).toHaveTextContent('This is a popover'); + + fireEvent.click(actionsButtonDropdown.findNativeButton().getElement()); + expect(actionsButtonDropdown.findItems()).toHaveLength(3); +}); + +test('expand/collapse state should be controlled by expandedItems', () => { + const expandedItems = ['1', '1.2']; + const { wrapper, rerender } = renderTreeView({ + expandedItems, + onItemToggle: () => {}, + }); + expect(wrapper.findItems({ expanded: true })).toHaveLength(2); + + rerender(); + expect(wrapper.findItems({ expanded: true })).toHaveLength(3); + + rerender(); + expect(wrapper.findItems({ expanded: true })).toHaveLength(1); +}); + +test('should warn when expandedItems is provided without onItemToggle', () => { + renderTreeView({ + expandedItems: [], + }); + + expect(warnOnce).toHaveBeenCalledWith( + 'Tree view', + '`expandedItems` is provided without `onItemToggle`. Make sure to provide `onItemToggle` with `expandedItems` to control expand/collapse state of items.' + ); +}); diff --git a/src/treeview/index.tsx b/src/treeview/index.tsx index 2226e0c9f1..8ad2b48f51 100644 --- a/src/treeview/index.tsx +++ b/src/treeview/index.tsx @@ -11,7 +11,7 @@ import InternalTreeview from './internal'; export { TreeviewProps }; -const Treeview = ({ items, showConnectorLine = true, ...rest }: TreeviewProps) => { +const Treeview = ({ items, showConnectorLine = true, ...rest }: TreeviewProps) => { const baseProps = getBaseProps(rest); // TODO: analytics metadata? const baseComponentProps = useBaseComponent('Treeview', { diff --git a/src/treeview/interfaces.ts b/src/treeview/interfaces.ts index 70f738aa76..ed211a7506 100644 --- a/src/treeview/interfaces.ts +++ b/src/treeview/interfaces.ts @@ -16,8 +16,8 @@ export interface TreeviewProps extends BaseComponentProps { * For each item the below properties must be returned: * * `content` (React.ReactNode) - The content of the item. * * `icon` (optional, React.ReactNode) - The leading icon of the item. - * * `description` (optional, React.ReactNode) - The details of the item, displayed below the content. - * * `secondaryContent` (optional, React.ReactNode) - Actions related to item. We recommend using a button group. + * * `secondaryContent` (optional, React.ReactNode) - Secondary content of the item, displayed below the content. + * * `actions` (optional, React.ReactNode) - Actions related to item. We recommend using a button group. */ renderItem: (item: T, index: number) => TreeviewProps.TreeItem; @@ -29,7 +29,7 @@ export interface TreeviewProps extends BaseComponentProps { /** * The nested items of the item. */ - getItemChildren: (item: T, index: number) => ReadonlyArray; + getItemChildren: (item: T, index: number) => ReadonlyArray | undefined; /** * An array of expanded item IDs. Use it to control expand state of tree items. @@ -49,11 +49,6 @@ export interface TreeviewProps extends BaseComponentProps { */ ariaLabelledby?: string; - /** - * Sets the `aria-description` property on the treeview. - */ - ariaDescription?: string; - /** * Sets the `aria-describedby` property on the treeview. */ @@ -62,7 +57,7 @@ export interface TreeviewProps extends BaseComponentProps { /** * Called when an item's expand toggle is clicked. */ - onItemToggle: NonCancelableEventHandler>; + onItemToggle?: NonCancelableEventHandler>; /** * An object containing all the necessary localized strings required by the component. @@ -71,6 +66,7 @@ export interface TreeviewProps extends BaseComponentProps { i18nStrings?: TreeviewProps.I18nStrings; /** + * @awsuiSystem core * If `true`, adds connecting lines between child items and their expanded parent items. */ showConnectorLine?: boolean; @@ -87,8 +83,8 @@ export namespace TreeviewProps { export interface TreeItem { content: React.ReactNode; icon?: React.ReactNode; - description?: React.ReactNode; secondaryContent?: React.ReactNode; + actions?: React.ReactNode; } export interface ItemToggleDetail { diff --git a/src/treeview/internal.tsx b/src/treeview/internal.tsx index 78efe5016b..177fb54238 100644 --- a/src/treeview/internal.tsx +++ b/src/treeview/internal.tsx @@ -1,43 +1,68 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { useState } from 'react'; import clsx from 'clsx'; +import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; + import { getBaseProps } from '../internal/base-component'; +import { fireNonCancelableEvent } from '../internal/events'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; import { TreeviewProps } from './interfaces'; import InternalTreeItem from './treeitem'; import { getItemPosition } from './utils'; import styles from './styles.css.js'; +import testUtilStyles from './test-classes/styles.css.js'; -type InternalTreeviewProps = TreeviewProps & InternalBaseComponentProps; +type InternalTreeviewProps = TreeviewProps & InternalBaseComponentProps; -const InternalTreeview = ({ - expandedItems = [], - onItemToggle, +const InternalTreeview = ({ + expandedItems, items = [], renderItem, getItemId, getItemChildren, + onItemToggle, ariaLabel, ariaLabelledby, - ariaDescription, ariaDescribedby, i18nStrings, __internalRootRef, ...rest -}: InternalTreeviewProps) => { +}: InternalTreeviewProps) => { const baseProps = getBaseProps(rest); + const isExpandStateControlled = !!expandedItems; + const [internalExpandedItems, setInternalExpandedItems] = useState(expandedItems || []); + + if (isExpandStateControlled && !onItemToggle) { + warnOnce( + 'Tree view', + '`expandedItems` is provided without `onItemToggle`. Make sure to provide `onItemToggle` with `expandedItems` to control expand/collapse state of items.' + ); + } + + const onToggle = ({ id, item, expanded }: TreeviewProps.ItemToggleDetail) => { + if (!isExpandStateControlled) { + if (expanded) { + setInternalExpandedItems([...internalExpandedItems, id]); + } else { + setInternalExpandedItems(internalExpandedItems.filter(expandedId => expandedId !== id)); + } + } + + if (onItemToggle) { + fireNonCancelableEvent(onItemToggle, { id, item, expanded }); + } + }; return ( -
          +
            {items.map((item, index) => { @@ -47,10 +72,12 @@ const InternalTreeview = ({ item={item} level={0} index={index} - expandedItems={expandedItems} + expandedItems={ + expandedItems === undefined || expandedItems === null ? internalExpandedItems : expandedItems + } i18nStrings={i18nStrings} position={getItemPosition(index, items.length)} - onItemToggle={onItemToggle} + onItemToggle={onToggle} renderItem={renderItem} getItemId={getItemId} getItemChildren={getItemChildren} diff --git a/src/treeview/styles.scss b/src/treeview/styles.scss index 457fb7dc80..92ca186e0b 100644 --- a/src/treeview/styles.scss +++ b/src/treeview/styles.scss @@ -14,14 +14,14 @@ } .tree, -.parent-treeitem, -.child-treeitem { +.treeitem-group, +.treeitem { list-style-type: none; padding-inline-start: 0; margin-block: 0; } -.parent-treeitem { +.treeitem-group { inline-size: 100%; } @@ -55,7 +55,7 @@ inline-size: 100%; } -.child-treeitem { +.treeitem { display: flex; } @@ -63,18 +63,18 @@ @include styles.styles-reset-ul; } -// .parent-treeitem { +// .treeitem-group { // padding-inline-start: 0; // margin-inline-start: 0; // } -.treeitem-content { +.content { position: relative; padding-block-start: 2px; flex: 1; } -.treeitem-group { +.treeitem-connector-group { display: flex; flex: 1; flex-direction: column; @@ -92,14 +92,6 @@ gap: 4px; } -.treeitem-secondary-content { - // test -} - -.treeitem-description { - // test -} - .horizontal { inline-size: 32px; block-size: 1px; @@ -153,7 +145,7 @@ // overflow: visible; // position: relative; -// .parent-treeitem { +// .treeitem-group { // grid-column: 3; // grid-row: 3; // // grid-template-columns: subgrid; @@ -192,7 +184,7 @@ // width: 2rem; // } -// .treeitem-icon { +// .icon { // // grid-column: 2; // // grid-row: 1 / span 2; // } diff --git a/src/treeview/test-classes/styles.scss b/src/treeview/test-classes/styles.scss new file mode 100644 index 0000000000..3c2dc25a50 --- /dev/null +++ b/src/treeview/test-classes/styles.scss @@ -0,0 +1,15 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +.root, +.treeitem, +.expandable, +.expanded, +.content, +.icon, +.secondary-content, +.actions, +.expand-toggle { + /* used in test-utils */ +} diff --git a/src/treeview/treeitem-layout.tsx b/src/treeview/treeitem-layout.tsx index 7befdd8f91..10196df359 100644 --- a/src/treeview/treeitem-layout.tsx +++ b/src/treeview/treeitem-layout.tsx @@ -1,31 +1,35 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React from 'react'; +import clsx from 'clsx'; import styles from './styles.css.js'; +import testUtilStyles from './test-classes/styles.css.js'; const TreeItemLayout = ({ icon, content, - description, secondaryContent, + actions, }: { icon?: React.ReactNode; content: React.ReactNode; - description?: React.ReactNode; secondaryContent?: React.ReactNode; + actions?: React.ReactNode; }) => { return (
            - {icon &&
            {icon}
            } + {icon &&
            {icon}
            } -
            {content}
            +
            {content}
            - {secondaryContent &&
            {secondaryContent}
            } + {actions &&
            {actions}
            }
            - {description &&
            {description}
            } + {secondaryContent && ( +
            {secondaryContent}
            + )}
            ); }; diff --git a/src/treeview/treeitem.tsx b/src/treeview/treeitem.tsx index 267fbcec66..5c51fb2a74 100644 --- a/src/treeview/treeitem.tsx +++ b/src/treeview/treeitem.tsx @@ -5,27 +5,25 @@ import clsx from 'clsx'; import { useInternalI18n } from '../i18n/context'; import { ExpandToggleButton } from '../internal/components/expand-toggle-button'; -import { fireNonCancelableEvent } from '../internal/events'; // import Connector from './connector'; import { TreeviewProps } from './interfaces'; import TreeItemLayout from './treeitem-layout'; import { getItemPosition, transformTreeItemProps } from './utils'; import styles from './styles.css.js'; +import testUtilStyles from './test-classes/styles.css.js'; -interface InternalTreeItemProps - extends Pick< - TreeviewProps, - 'expandedItems' | 'renderItem' | 'getItemId' | 'getItemChildren' | 'onItemToggle' | 'i18nStrings' - > { - item: any; +interface InternalTreeItemProps + extends Pick { + item: T; index: number; level: number; position: 'start' | 'middle' | 'end'; withGrid?: boolean; + onItemToggle: (detail: TreeviewProps.ItemToggleDetail) => void; } -const InternalTreeItem = ({ +const InternalTreeItem = ({ item, index, level, @@ -37,33 +35,34 @@ const InternalTreeItem = ({ getItemId, getItemChildren, onItemToggle, -}: InternalTreeItemProps) => { +}: InternalTreeItemProps) => { const i18n = useInternalI18n('treeview'); - const { id, isExpandable, isExpanded, children, icon, content, description, secondaryContent } = - transformTreeItemProps({ - item, - index, - expandedItems, - renderItem, - getItemId, - getItemChildren, - }); + const { id, isExpandable, isExpanded, children, icon, content, secondaryContent, actions } = transformTreeItemProps({ + item, + index, + expandedItems, + renderItem, + getItemId, + getItemChildren, + }); const nextLevel = level + 1; - console.log(Array.from(Array(level + 1).keys())); - return (
          • {/*
            {isExpandable && ( @@ -79,7 +78,7 @@ const InternalTreeItem = ({
            */} {!withGrid && ( -
            +
            {/* Try out horizontally constructured connector lines */}
            - fireNonCancelableEvent(onItemToggle, { id, item, expanded: !isExpanded }) - } + onExpandableItemToggle={() => onItemToggle({ id, item, expanded: !isExpanded })} expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} collapseButtonLabel={i18n( 'i18nStrings.collapseButtonLabel', @@ -142,16 +139,11 @@ const InternalTreeItem = ({ )}
            - +
            {isExpanded && children.length && ( -
              +
                {children.map((child, index) => { return ( - fireNonCancelableEvent(onItemToggle, { id, item, expanded: !isExpanded }) - } + onExpandableItemToggle={() => onItemToggle({ id, item, expanded: !isExpanded })} expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} collapseButtonLabel={i18n( 'i18nStrings.collapseButtonLabel', @@ -198,12 +188,12 @@ const InternalTreeItem = ({ {level > 0 &&
                } - {/* {icon &&
                {icon}
                } */} + {/* {icon &&
                {icon}
                } */} - + {isExpanded && children.length && ( -
                  +
                    {children.map((child, index) => { return ( Date: Tue, 3 Jun 2025 12:16:29 +0200 Subject: [PATCH 10/39] change name from Treeview to TreeView --- build-tools/utils/pluralize.js | 2 +- pages/treeview/basic.page.tsx | 10 +-- pages/treeview/test.page.tsx | 4 +- .../__snapshots__/documenter.test.ts.snap | 43 ++++++------- .../test-utils-selectors.test.tsx.snap | 18 +++--- .../test-utils-wrappers.test.tsx.snap | 64 +++++++++---------- src/i18n/messages-types.ts | 2 +- .../dom/{treeview => tree-view}/index.ts | 4 +- .../__tests__/tree-item.test.tsx | 27 ++++---- .../__tests__/tree-view.test.tsx | 23 +++---- .../connector/index.tsx | 0 .../connector/styles.scss | 0 src/{treeview => tree-view}/index.tsx | 16 ++--- src/{treeview => tree-view}/interfaces.ts | 18 +++--- src/{treeview => tree-view}/internal.tsx | 18 +++--- src/{treeview => tree-view}/styles.scss | 0 .../test-classes/styles.scss | 0 .../tree-item/index.tsx} | 18 +++--- .../tree-item/layout.tsx} | 4 +- .../tree-item}/utils.ts | 4 +- 20 files changed, 132 insertions(+), 143 deletions(-) rename src/test-utils/dom/{treeview => tree-view}/index.ts (95%) rename src/{treeview => tree-view}/__tests__/tree-item.test.tsx (85%) rename src/{treeview => tree-view}/__tests__/tree-view.test.tsx (87%) rename src/{treeview => tree-view}/connector/index.tsx (100%) rename src/{treeview => tree-view}/connector/styles.scss (100%) rename src/{treeview => tree-view}/index.tsx (69%) rename src/{treeview => tree-view}/interfaces.ts (82%) rename src/{treeview => tree-view}/internal.tsx (86%) rename src/{treeview => tree-view}/styles.scss (100%) rename src/{treeview => tree-view}/test-classes/styles.scss (100%) rename src/{treeview/treeitem.tsx => tree-view/tree-item/index.tsx} (93%) rename src/{treeview/treeitem-layout.tsx => tree-view/tree-item/layout.tsx} (90%) rename src/{treeview => tree-view/tree-item}/utils.ts (89%) diff --git a/build-tools/utils/pluralize.js b/build-tools/utils/pluralize.js index c2e47e7317..b5308c2a3a 100644 --- a/build-tools/utils/pluralize.js +++ b/build-tools/utils/pluralize.js @@ -79,7 +79,7 @@ const pluralizationMap = { ToggleButton: 'ToggleButtons', TokenGroup: 'TokenGroups', TopNavigation: 'TopNavigations', - Treeview: 'Treeviews', + TreeView: 'TreeViews', TutorialPanel: 'TutorialPanels', Wizard: 'Wizards', }; diff --git a/pages/treeview/basic.page.tsx b/pages/treeview/basic.page.tsx index b8a9611752..27e15571ab 100644 --- a/pages/treeview/basic.page.tsx +++ b/pages/treeview/basic.page.tsx @@ -11,7 +11,7 @@ import Icon from '~components/icon'; import Popover from '~components/popover'; import SpaceBetween from '~components/space-between'; import StatusIndicator from '~components/status-indicator'; -import Treeview from '~components/treeview'; +import TreeView from '~components/tree-view'; const progressiveStepContent = (
                    @@ -304,18 +304,18 @@ function RdsAccessRoleTreeItemContent() { ); } -export default function BasicTreeview() { +export default function BasicTreeView() { const [expandedItems, setExpandedItems] = useState>(['1', '4.1']); return ( <> -

                    Basic treeview

                    +

                    Basic tree view

                    - { return { diff --git a/pages/treeview/test.page.tsx b/pages/treeview/test.page.tsx index 713f394a29..4f955f69b7 100644 --- a/pages/treeview/test.page.tsx +++ b/pages/treeview/test.page.tsx @@ -5,7 +5,7 @@ import React, { useState } from 'react'; import { Button, SpaceBetween } from '~components'; import Box from '~components/box'; import Icon from '~components/icon'; -import Treeview from '~components/treeview'; +import TreeView from '~components/tree-view'; import { Actions, Content } from './common'; import { allItems, items } from './generate-data'; @@ -45,7 +45,7 @@ export default function TestPage() { - { const isExpanded = expandedItems.includes(item.id); diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 4f4f9cc289..ba59730d10 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20442,15 +20442,15 @@ The following properties are supported across all utility types: } `; -exports[`Documenter definition for treeview matches the snapshot: treeview 1`] = ` +exports[`Documenter definition for tree-view matches the snapshot: tree-view 1`] = ` { - "dashCaseName": "treeview", + "dashCaseName": "tree-view", "events": [ { "cancelable": false, "description": "Called when an item's expand toggle is clicked.", "detailInlineType": { - "name": "TreeviewProps.ItemToggleDetail", + "name": "TreeViewProps.ItemToggleDetail", "properties": [ { "name": "expanded", @@ -20470,34 +20470,28 @@ exports[`Documenter definition for treeview matches the snapshot: treeview 1`] = ], "type": "object", }, - "detailType": "TreeviewProps.ItemToggleDetail", + "detailType": "TreeViewProps.ItemToggleDetail", "name": "onItemToggle", }, ], "functions": [], - "name": "Treeview", + "name": "TreeView", "properties": [ { - "description": "Sets the \`aria-describedby\` property on the treeview.", + "description": "Sets the \`aria-describedby\` property on the tree view.", "name": "ariaDescribedby", "optional": true, "type": "string", }, { - "description": "Sets the \`aria-description\` property on the treeview.", - "name": "ariaDescription", - "optional": true, - "type": "string", - }, - { - "description": "Provides an \`aria-label\` to the treeview that screen readers can read (for accessibility). + "description": "Provides an \`aria-label\` to the tree view that screen readers can read (for accessibility). Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", "name": "ariaLabel", "optional": true, "type": "string", }, { - "description": "Sets the \`aria-labelledby\` property on the treeview. + "description": "Sets the \`aria-labelledby\` property on the tree view. If there's a visible label element that you can reference, use this instead of \`ariaLabel\`. Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", "name": "ariaLabelledby", @@ -20563,7 +20557,7 @@ Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", "description": "An object containing all the necessary localized strings required by the component.", "i18nTag": true, "inlineType": { - "name": "TreeviewProps.I18nStrings", + "name": "TreeViewProps.I18nStrings", "properties": [ { "name": "collapseButtonLabel", @@ -20580,7 +20574,7 @@ Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", }, "name": "i18nStrings", "optional": true, - "type": "TreeviewProps.I18nStrings", + "type": "TreeViewProps.I18nStrings", }, { "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases, @@ -20592,7 +20586,7 @@ use the \`id\` attribute, consider setting it on a parent element instead.", "type": "string", }, { - "description": "An array of treeview items.", + "description": "An array of tree view items.", "name": "items", "optional": false, "type": "ReadonlyArray", @@ -20602,10 +20596,10 @@ use the \`id\` attribute, consider setting it on a parent element instead.", For each item the below properties must be returned: * \`content\` (React.ReactNode) - The content of the item. * \`icon\` (optional, React.ReactNode) - The leading icon of the item. -* \`description\` (optional, React.ReactNode) - The details of the item, displayed below the content. -* \`secondaryContent\` (optional, React.ReactNode) - Actions related to item. We recommend using a button group.", +* \`secondaryContent\` (optional, React.ReactNode) - Secondary content of the item, displayed below the content. +* \`actions\` (optional, React.ReactNode) - Actions related to item. We recommend using a button group.", "inlineType": { - "name": "(item: T, index: number) => TreeviewProps.TreeItem", + "name": "(item: T, index: number) => TreeViewProps.TreeItem", "parameters": [ { "name": "item", @@ -20616,12 +20610,12 @@ For each item the below properties must be returned: "type": "number", }, ], - "returnType": "TreeviewProps.TreeItem", + "returnType": "TreeViewProps.TreeItem", "type": "function", }, "name": "renderItem", "optional": false, - "type": "(item: T, index: number) => TreeviewProps.TreeItem", + "type": "(item: T, index: number) => TreeViewProps.TreeItem", }, { "inlineType": { @@ -20646,9 +20640,12 @@ Use it to have different icons/animations for toggle.", }, { "defaultValue": "true", - "description": "If \`true\`, adds connecting lines between child items and their expanded parent items.", "name": "showConnectorLine", "optional": true, + "systemTags": [ + "core +If \`true\`, adds connecting lines between child items and their expanded parent items.", + ], "type": "boolean", }, ], diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 2a6402ba65..f82472cc15 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -665,15 +665,15 @@ exports[`test-utils selectors 1`] = ` "awsui_utility-wrapper_1ca1i", "awsui_utility-wrapper_k5dlb", ], - "treeview": [ - "awsui_actions_re8ah", - "awsui_content_re8ah", - "awsui_expandable_re8ah", - "awsui_expanded_re8ah", - "awsui_icon_re8ah", - "awsui_root_re8ah", - "awsui_secondary-content_re8ah", - "awsui_treeitem_re8ah", + "tree-view": [ + "awsui_actions_1js4f", + "awsui_content_1js4f", + "awsui_expandable_1js4f", + "awsui_expanded_1js4f", + "awsui_icon_1js4f", + "awsui_root_1js4f", + "awsui_secondary-content_1js4f", + "awsui_treeitem_1js4f", ], "tutorial-panel": [ "awsui_collapse-button_ig8mp", diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap index 5bb63c378f..9be09c8f8d 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap @@ -87,7 +87,7 @@ import ToggleWrapper from './toggle'; import ToggleButtonWrapper from './toggle-button'; import TokenGroupWrapper from './token-group'; import TopNavigationWrapper from './top-navigation'; -import TreeviewWrapper from './treeview'; +import TreeViewWrapper from './tree-view'; import TutorialPanelWrapper from './tutorial-panel'; import WizardWrapper from './wizard'; @@ -170,7 +170,7 @@ export { ToggleWrapper }; export { ToggleButtonWrapper }; export { TokenGroupWrapper }; export { TopNavigationWrapper }; -export { TreeviewWrapper }; +export { TreeViewWrapper }; export { TutorialPanelWrapper }; export { WizardWrapper }; @@ -1660,24 +1660,24 @@ findTopNavigation(selector?: string): TopNavigationWrapper | null; */ findAllTopNavigations(selector?: string): Array; /** - * Returns the wrapper of the first Treeview that matches the specified CSS selector. - * If no CSS selector is specified, returns the wrapper of the first Treeview. - * If no matching Treeview is found, returns \`null\`. + * Returns the wrapper of the first TreeView that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first TreeView. + * If no matching TreeView is found, returns \`null\`. * * @param {string} [selector] CSS Selector - * @returns {TreeviewWrapper | null} + * @returns {TreeViewWrapper | null} */ -findTreeview(selector?: string): TreeviewWrapper | null; +findTreeView(selector?: string): TreeViewWrapper | null; /** - * Returns an array of Treeview wrapper that matches the specified CSS selector. - * If no CSS selector is specified, returns all of the Treeviews inside the current wrapper. - * If no matching Treeview is found, returns an empty array. + * Returns an array of TreeView wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the TreeViews inside the current wrapper. + * If no matching TreeView is found, returns an empty array. * * @param {string} [selector] CSS Selector - * @returns {Array} + * @returns {Array} */ -findAllTreeviews(selector?: string): Array; +findAllTreeViews(selector?: string): Array; /** * Returns the wrapper of the first TutorialPanel that matches the specified CSS selector. * If no CSS selector is specified, returns the wrapper of the first TutorialPanel. @@ -2500,15 +2500,15 @@ ElementWrapper.prototype.findTopNavigation = function(selector) { ElementWrapper.prototype.findAllTopNavigations = function(selector) { return this.findAllComponents(TopNavigationWrapper, selector); }; -ElementWrapper.prototype.findTreeview = function(selector) { - const rootSelector = \`.\${TreeviewWrapper.rootSelector}\`; +ElementWrapper.prototype.findTreeView = function(selector) { + const rootSelector = \`.\${TreeViewWrapper.rootSelector}\`; // casting to 'any' is needed to avoid this issue with generics // https://github.com/microsoft/TypeScript/issues/29132 - return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TreeviewWrapper); + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TreeViewWrapper); }; -ElementWrapper.prototype.findAllTreeviews = function(selector) { - return this.findAllComponents(TreeviewWrapper, selector); +ElementWrapper.prototype.findAllTreeViews = function(selector) { + return this.findAllComponents(TreeViewWrapper, selector); }; ElementWrapper.prototype.findTutorialPanel = function(selector) { const rootSelector = \`.\${TutorialPanelWrapper.rootSelector}\`; @@ -2628,7 +2628,7 @@ import ToggleWrapper from './toggle'; import ToggleButtonWrapper from './toggle-button'; import TokenGroupWrapper from './token-group'; import TopNavigationWrapper from './top-navigation'; -import TreeviewWrapper from './treeview'; +import TreeViewWrapper from './tree-view'; import TutorialPanelWrapper from './tutorial-panel'; import WizardWrapper from './wizard'; @@ -2711,7 +2711,7 @@ export { ToggleWrapper }; export { ToggleButtonWrapper }; export { TokenGroupWrapper }; export { TopNavigationWrapper }; -export { TreeviewWrapper }; +export { TreeViewWrapper }; export { TutorialPanelWrapper }; export { WizardWrapper }; @@ -4045,22 +4045,22 @@ findTopNavigation(selector?: string): TopNavigationWrapper; */ findAllTopNavigations(selector?: string): MultiElementWrapper; /** - * Returns a wrapper that matches the Treeviews with the specified CSS selector. - * If no CSS selector is specified, returns a wrapper that matches Treeviews. + * Returns a wrapper that matches the TreeViews with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches TreeViews. * * @param {string} [selector] CSS Selector - * @returns {TreeviewWrapper} + * @returns {TreeViewWrapper} */ -findTreeview(selector?: string): TreeviewWrapper; +findTreeView(selector?: string): TreeViewWrapper; /** - * Returns a multi-element wrapper that matches Treeviews with the specified CSS selector. - * If no CSS selector is specified, returns a multi-element wrapper that matches Treeviews. + * Returns a multi-element wrapper that matches TreeViews with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches TreeViews. * * @param {string} [selector] CSS Selector - * @returns {MultiElementWrapper} + * @returns {MultiElementWrapper} */ -findAllTreeviews(selector?: string): MultiElementWrapper; +findAllTreeViews(selector?: string): MultiElementWrapper; /** * Returns a wrapper that matches the TutorialPanels with the specified CSS selector. * If no CSS selector is specified, returns a wrapper that matches TutorialPanels. @@ -4879,15 +4879,15 @@ ElementWrapper.prototype.findTopNavigation = function(selector) { ElementWrapper.prototype.findAllTopNavigations = function(selector) { return this.findAllComponents(TopNavigationWrapper, selector); }; -ElementWrapper.prototype.findTreeview = function(selector) { - const rootSelector = \`.\${TreeviewWrapper.rootSelector}\`; +ElementWrapper.prototype.findTreeView = function(selector) { + const rootSelector = \`.\${TreeViewWrapper.rootSelector}\`; // casting to 'any' is needed to avoid this issue with generics // https://github.com/microsoft/TypeScript/issues/29132 - return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TreeviewWrapper); + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TreeViewWrapper); }; -ElementWrapper.prototype.findAllTreeviews = function(selector) { - return this.findAllComponents(TreeviewWrapper, selector); +ElementWrapper.prototype.findAllTreeViews = function(selector) { + return this.findAllComponents(TreeViewWrapper, selector); }; ElementWrapper.prototype.findTutorialPanel = function(selector) { const rootSelector = \`.\${TutorialPanelWrapper.rootSelector}\`; diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 72012ca6cf..891f9392e7 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -542,7 +542,7 @@ export interface I18nFormatArgTypes { "i18nStrings.labelsTaskStatus.in-progress": never; "i18nStrings.labelsTaskStatus.success": never; } - "treeview": { + "tree-view": { "i18nStrings.expandButtonLabel": never; "i18nStrings.collapseButtonLabel": never; } diff --git a/src/test-utils/dom/treeview/index.ts b/src/test-utils/dom/tree-view/index.ts similarity index 95% rename from src/test-utils/dom/treeview/index.ts rename to src/test-utils/dom/tree-view/index.ts index 9dcdb0f620..ca0f317819 100644 --- a/src/test-utils/dom/treeview/index.ts +++ b/src/test-utils/dom/tree-view/index.ts @@ -3,7 +3,7 @@ import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; import expandToggleStyles from '../../../internal/components/expand-toggle-button/styles.selectors.js'; -import testUtilStyles from '../../../treeview/test-classes/styles.selectors.js'; +import testUtilStyles from '../../../tree-view/test-classes/styles.selectors.js'; class TreeItemWrapper extends ComponentWrapper { /** @@ -65,7 +65,7 @@ class TreeItemWrapper extends ComponentWrapper { } } -export default class TreeviewWrapper extends ComponentWrapper { +export default class TreeViewWrapper extends ComponentWrapper { static rootSelector: string = testUtilStyles.root; /** diff --git a/src/treeview/__tests__/tree-item.test.tsx b/src/tree-view/__tests__/tree-item.test.tsx similarity index 85% rename from src/treeview/__tests__/tree-item.test.tsx rename to src/tree-view/__tests__/tree-item.test.tsx index b9de6039eb..2d8d1874d8 100644 --- a/src/treeview/__tests__/tree-item.test.tsx +++ b/src/tree-view/__tests__/tree-item.test.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import createWrapper from '../../../lib/components/test-utils/dom'; -import Treeview, { TreeviewProps } from '../../../lib/components/treeview'; +import TreeView, { TreeViewProps } from '../../../lib/components/tree-view'; interface Item { id: string; @@ -39,19 +39,18 @@ const defaultItems: Item[] = [ }, ]; -function renderTreeView(props: Partial> = {}) { - const { container } = render( - item.id} - getItemChildren={item => item.items} - renderItem={item => ({ - content: item.title, - })} - {...props} - /> - ); - const wrapper = createWrapper(container).findTreeview()!; +const defaultProps: TreeViewProps = { + items: defaultItems, + getItemId: item => item.id, + getItemChildren: item => item.items, + renderItem: item => ({ + content: item.title, + }), +}; + +function renderTreeView(props: Partial> = {}) { + const { container } = render(); + const wrapper = createWrapper(container).findTreeView()!; return { wrapper }; } diff --git a/src/treeview/__tests__/tree-view.test.tsx b/src/tree-view/__tests__/tree-view.test.tsx similarity index 87% rename from src/treeview/__tests__/tree-view.test.tsx rename to src/tree-view/__tests__/tree-view.test.tsx index 7d01595c85..c4f44875de 100644 --- a/src/treeview/__tests__/tree-view.test.tsx +++ b/src/tree-view/__tests__/tree-view.test.tsx @@ -10,7 +10,7 @@ import Icon from '../../../lib/components/icon'; import Link from '../../../lib/components/link'; import Popover from '../../../lib/components/popover'; import createWrapper from '../../../lib/components/test-utils/dom'; -import Treeview, { TreeviewProps } from '../../../lib/components/treeview'; +import TreeView, { TreeViewProps } from '../../../lib/components/tree-view'; jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), @@ -21,13 +21,6 @@ afterEach(() => { (warnOnce as jest.Mock).mockReset(); }); -// [DONE] should render tree-view with only content -// [DONE] should render tree-view with various slots -// [DONE] should expand items in expandedItems -// [DONE] should render tree-view with other Cloudscape components inside -// [DONE] dev warning if expanded items is defined but there is no onItemToggle? -// should render connector lines when showConnectorLine is true - interface Item { id: string; title: React.ReactNode; @@ -105,7 +98,7 @@ const defaultData: Item[] = [ }, ]; -const defaultProps: TreeviewProps = { +const defaultProps: TreeViewProps = { items: defaultData, getItemId: item => item.id, getItemChildren: item => item.items, @@ -118,9 +111,9 @@ const defaultProps: TreeviewProps = { }), }; -function renderTreeView(props: Partial> = {}) { - const { container, rerender } = render(); - const wrapper = createWrapper(container).findTreeview()!; +function renderTreeView(props: Partial> = {}) { + const { container, rerender } = render(); + const wrapper = createWrapper(container).findTreeView()!; return { wrapper, rerender }; } @@ -191,10 +184,10 @@ test('expand/collapse state should be controlled by expandedItems', () => { }); expect(wrapper.findItems({ expanded: true })).toHaveLength(2); - rerender(); + rerender(); expect(wrapper.findItems({ expanded: true })).toHaveLength(3); - rerender(); + rerender(); expect(wrapper.findItems({ expanded: true })).toHaveLength(1); }); @@ -204,7 +197,7 @@ test('should warn when expandedItems is provided without onItemToggle', () => { }); expect(warnOnce).toHaveBeenCalledWith( - 'Tree view', + 'TreeView', '`expandedItems` is provided without `onItemToggle`. Make sure to provide `onItemToggle` with `expandedItems` to control expand/collapse state of items.' ); }); diff --git a/src/treeview/connector/index.tsx b/src/tree-view/connector/index.tsx similarity index 100% rename from src/treeview/connector/index.tsx rename to src/tree-view/connector/index.tsx diff --git a/src/treeview/connector/styles.scss b/src/tree-view/connector/styles.scss similarity index 100% rename from src/treeview/connector/styles.scss rename to src/tree-view/connector/styles.scss diff --git a/src/treeview/index.tsx b/src/tree-view/index.tsx similarity index 69% rename from src/treeview/index.tsx rename to src/tree-view/index.tsx index 8ad2b48f51..793983b1a6 100644 --- a/src/treeview/index.tsx +++ b/src/tree-view/index.tsx @@ -6,15 +6,15 @@ import { getBaseProps } from '../internal/base-component'; import useBaseComponent from '../internal/hooks/use-base-component'; import { applyDisplayName } from '../internal/utils/apply-display-name'; import { getExternalProps } from '../internal/utils/external-props'; -import { TreeviewProps } from './interfaces'; -import InternalTreeview from './internal'; +import { TreeViewProps } from './interfaces'; +import InternalTreeView from './internal'; -export { TreeviewProps }; +export { TreeViewProps }; -const Treeview = ({ items, showConnectorLine = true, ...rest }: TreeviewProps) => { +const TreeView = ({ items, showConnectorLine = true, ...rest }: TreeViewProps) => { const baseProps = getBaseProps(rest); // TODO: analytics metadata? - const baseComponentProps = useBaseComponent('Treeview', { + const baseComponentProps = useBaseComponent('TreeView', { props: {}, metadata: { itemsCount: items?.length, @@ -23,7 +23,7 @@ const Treeview = ({ items, showConnectorLine = true, ...rest }: TreeviewProp const externalProps = getExternalProps(rest); return ( - ({ items, showConnectorLine = true, ...rest }: TreeviewProp ); }; -applyDisplayName(Treeview, 'Treeview'); -export default Treeview; +applyDisplayName(TreeView, 'TreeView'); +export default TreeView; diff --git a/src/treeview/interfaces.ts b/src/tree-view/interfaces.ts similarity index 82% rename from src/treeview/interfaces.ts rename to src/tree-view/interfaces.ts index ed211a7506..0e140c6113 100644 --- a/src/treeview/interfaces.ts +++ b/src/tree-view/interfaces.ts @@ -5,9 +5,9 @@ import React from 'react'; import { BaseComponentProps } from '../internal/base-component'; import { NonCancelableEventHandler } from '../internal/events'; -export interface TreeviewProps extends BaseComponentProps { +export interface TreeViewProps extends BaseComponentProps { /** - * An array of treeview items. + * An array of tree view items. */ items: ReadonlyArray; @@ -19,7 +19,7 @@ export interface TreeviewProps extends BaseComponentProps { * * `secondaryContent` (optional, React.ReactNode) - Secondary content of the item, displayed below the content. * * `actions` (optional, React.ReactNode) - Actions related to item. We recommend using a button group. */ - renderItem: (item: T, index: number) => TreeviewProps.TreeItem; + renderItem: (item: T, index: number) => TreeViewProps.TreeItem; /** * Provides a unique identifier of each item. @@ -37,33 +37,33 @@ export interface TreeviewProps extends BaseComponentProps { expandedItems?: ReadonlyArray; /** - * Provides an `aria-label` to the treeview that screen readers can read (for accessibility). + * Provides an `aria-label` to the tree view that screen readers can read (for accessibility). * Don't use `ariaLabel` and `ariaLabelledby` at the same time. */ ariaLabel?: string; /** - * Sets the `aria-labelledby` property on the treeview. + * Sets the `aria-labelledby` property on the tree view. * If there's a visible label element that you can reference, use this instead of `ariaLabel`. * Don't use `ariaLabel` and `ariaLabelledby` at the same time. */ ariaLabelledby?: string; /** - * Sets the `aria-describedby` property on the treeview. + * Sets the `aria-describedby` property on the tree view. */ ariaDescribedby?: string; /** * Called when an item's expand toggle is clicked. */ - onItemToggle?: NonCancelableEventHandler>; + onItemToggle?: NonCancelableEventHandler>; /** * An object containing all the necessary localized strings required by the component. * @i18n */ - i18nStrings?: TreeviewProps.I18nStrings; + i18nStrings?: TreeViewProps.I18nStrings; /** * @awsuiSystem core @@ -79,7 +79,7 @@ export interface TreeviewProps extends BaseComponentProps { renderItemToggleIcon?: (isExpanded: boolean) => React.ReactNode; } -export namespace TreeviewProps { +export namespace TreeViewProps { export interface TreeItem { content: React.ReactNode; icon?: React.ReactNode; diff --git a/src/treeview/internal.tsx b/src/tree-view/internal.tsx similarity index 86% rename from src/treeview/internal.tsx rename to src/tree-view/internal.tsx index 177fb54238..f689f3b3f3 100644 --- a/src/treeview/internal.tsx +++ b/src/tree-view/internal.tsx @@ -8,16 +8,16 @@ import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; import { getBaseProps } from '../internal/base-component'; import { fireNonCancelableEvent } from '../internal/events'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; -import { TreeviewProps } from './interfaces'; -import InternalTreeItem from './treeitem'; -import { getItemPosition } from './utils'; +import { TreeViewProps } from './interfaces'; +import InternalTreeItem from './tree-item'; +import { getItemPosition } from './tree-item/utils'; import styles from './styles.css.js'; import testUtilStyles from './test-classes/styles.css.js'; -type InternalTreeviewProps = TreeviewProps & InternalBaseComponentProps; +type InternalTreeViewProps = TreeViewProps & InternalBaseComponentProps; -const InternalTreeview = ({ +const InternalTreeView = ({ expandedItems, items = [], renderItem, @@ -30,19 +30,19 @@ const InternalTreeview = ({ i18nStrings, __internalRootRef, ...rest -}: InternalTreeviewProps) => { +}: InternalTreeViewProps) => { const baseProps = getBaseProps(rest); const isExpandStateControlled = !!expandedItems; const [internalExpandedItems, setInternalExpandedItems] = useState(expandedItems || []); if (isExpandStateControlled && !onItemToggle) { warnOnce( - 'Tree view', + 'TreeView', '`expandedItems` is provided without `onItemToggle`. Make sure to provide `onItemToggle` with `expandedItems` to control expand/collapse state of items.' ); } - const onToggle = ({ id, item, expanded }: TreeviewProps.ItemToggleDetail) => { + const onToggle = ({ id, item, expanded }: TreeViewProps.ItemToggleDetail) => { if (!isExpandStateControlled) { if (expanded) { setInternalExpandedItems([...internalExpandedItems, id]); @@ -90,4 +90,4 @@ const InternalTreeview = ({ ); }; -export default InternalTreeview; +export default InternalTreeView; diff --git a/src/treeview/styles.scss b/src/tree-view/styles.scss similarity index 100% rename from src/treeview/styles.scss rename to src/tree-view/styles.scss diff --git a/src/treeview/test-classes/styles.scss b/src/tree-view/test-classes/styles.scss similarity index 100% rename from src/treeview/test-classes/styles.scss rename to src/tree-view/test-classes/styles.scss diff --git a/src/treeview/treeitem.tsx b/src/tree-view/tree-item/index.tsx similarity index 93% rename from src/treeview/treeitem.tsx rename to src/tree-view/tree-item/index.tsx index 5c51fb2a74..4daf62e1ee 100644 --- a/src/treeview/treeitem.tsx +++ b/src/tree-view/tree-item/index.tsx @@ -3,24 +3,24 @@ import React from 'react'; import clsx from 'clsx'; -import { useInternalI18n } from '../i18n/context'; -import { ExpandToggleButton } from '../internal/components/expand-toggle-button'; +import { useInternalI18n } from '../../i18n/context'; +import { ExpandToggleButton } from '../../internal/components/expand-toggle-button'; // import Connector from './connector'; -import { TreeviewProps } from './interfaces'; -import TreeItemLayout from './treeitem-layout'; +import { TreeViewProps } from '../interfaces'; +import TreeItemLayout from './layout'; import { getItemPosition, transformTreeItemProps } from './utils'; -import styles from './styles.css.js'; -import testUtilStyles from './test-classes/styles.css.js'; +import styles from '../styles.css.js'; +import testUtilStyles from '../test-classes/styles.css.js'; interface InternalTreeItemProps - extends Pick { + extends Pick { item: T; index: number; level: number; position: 'start' | 'middle' | 'end'; withGrid?: boolean; - onItemToggle: (detail: TreeviewProps.ItemToggleDetail) => void; + onItemToggle: (detail: TreeViewProps.ItemToggleDetail) => void; } const InternalTreeItem = ({ @@ -36,7 +36,7 @@ const InternalTreeItem = ({ getItemChildren, onItemToggle, }: InternalTreeItemProps) => { - const i18n = useInternalI18n('treeview'); + const i18n = useInternalI18n('tree-view'); const { id, isExpandable, isExpanded, children, icon, content, secondaryContent, actions } = transformTreeItemProps({ item, index, diff --git a/src/treeview/treeitem-layout.tsx b/src/tree-view/tree-item/layout.tsx similarity index 90% rename from src/treeview/treeitem-layout.tsx rename to src/tree-view/tree-item/layout.tsx index 10196df359..7c2ef9b2cf 100644 --- a/src/treeview/treeitem-layout.tsx +++ b/src/tree-view/tree-item/layout.tsx @@ -3,8 +3,8 @@ import React from 'react'; import clsx from 'clsx'; -import styles from './styles.css.js'; -import testUtilStyles from './test-classes/styles.css.js'; +import styles from '../styles.css.js'; +import testUtilStyles from '../test-classes/styles.css.js'; const TreeItemLayout = ({ icon, diff --git a/src/treeview/utils.ts b/src/tree-view/tree-item/utils.ts similarity index 89% rename from src/treeview/utils.ts rename to src/tree-view/tree-item/utils.ts index 714ef735e7..f27bd646b1 100644 --- a/src/treeview/utils.ts +++ b/src/tree-view/tree-item/utils.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { TreeviewProps } from './interfaces'; +import { TreeViewProps } from '../interfaces'; export function getItemPosition(index: number, itemsLength: number) { if (index === 0 && itemsLength === 1) { @@ -20,7 +20,7 @@ export function getItemPosition(index: number, itemsLength: number) { } interface TransformTreeItemPropsParams - extends Pick { + extends Pick { item: any; index: number; } From ea2bb743b1dfc8ce0c2a1630080a1250584e5e4f Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Tue, 3 Jun 2025 14:51:47 +0200 Subject: [PATCH 11/39] change page folder name and add a11y tests --- pages/{treeview => tree-view}/basic.page.tsx | 4 + pages/{treeview => tree-view}/common.tsx | 0 .../{treeview => tree-view}/generate-data.ts | 0 pages/{treeview => tree-view}/test.page.tsx | 4 + src/tree-view/__tests__/a11y.test.tsx | 133 ++++++++++++++++++ src/tree-view/__tests__/tree-item.test.tsx | 2 + src/tree-view/tree-item/index.tsx | 3 +- 7 files changed, 145 insertions(+), 1 deletion(-) rename pages/{treeview => tree-view}/basic.page.tsx (98%) rename pages/{treeview => tree-view}/common.tsx (100%) rename pages/{treeview => tree-view}/generate-data.ts (100%) rename pages/{treeview => tree-view}/test.page.tsx (94%) create mode 100644 src/tree-view/__tests__/a11y.test.tsx diff --git a/pages/treeview/basic.page.tsx b/pages/tree-view/basic.page.tsx similarity index 98% rename from pages/treeview/basic.page.tsx rename to pages/tree-view/basic.page.tsx index 27e15571ab..b7d7068a09 100644 --- a/pages/treeview/basic.page.tsx +++ b/pages/tree-view/basic.page.tsx @@ -337,6 +337,10 @@ export default function BasicTreeView() { } }} expandedItems={expandedItems} + i18nStrings={{ + expandButtonLabel: () => 'Expand item', + collapseButtonLabel: () => 'Collapse item', + }} />
                    diff --git a/pages/treeview/common.tsx b/pages/tree-view/common.tsx similarity index 100% rename from pages/treeview/common.tsx rename to pages/tree-view/common.tsx diff --git a/pages/treeview/generate-data.ts b/pages/tree-view/generate-data.ts similarity index 100% rename from pages/treeview/generate-data.ts rename to pages/tree-view/generate-data.ts diff --git a/pages/treeview/test.page.tsx b/pages/tree-view/test.page.tsx similarity index 94% rename from pages/treeview/test.page.tsx rename to pages/tree-view/test.page.tsx index 4f955f69b7..c4b88613ed 100644 --- a/pages/treeview/test.page.tsx +++ b/pages/tree-view/test.page.tsx @@ -73,6 +73,10 @@ export default function TestPage() { } }} expandedItems={expandedItems} + i18nStrings={{ + expandButtonLabel: () => 'Expand item', + collapseButtonLabel: () => 'Collapse item', + }} />
                    diff --git a/src/tree-view/__tests__/a11y.test.tsx b/src/tree-view/__tests__/a11y.test.tsx new file mode 100644 index 0000000000..b02c7a71e0 --- /dev/null +++ b/src/tree-view/__tests__/a11y.test.tsx @@ -0,0 +1,133 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import TestI18nProvider from '../../../lib/components/i18n/testing'; +import createWrapper from '../../../lib/components/test-utils/dom'; +import TreeView, { TreeViewProps } from '../../../lib/components/tree-view'; + +interface Item { + id: string; + title: string; + items?: Item[]; +} + +const defaultItems: Item[] = [ + { + id: '1', + title: 'Item 1', + items: [ + { + id: '1.1', + title: 'Item 1.1', + }, + { + id: '1.2', + title: 'Item 1.2', + items: [ + { + id: '1.2.1', + title: 'Item 1.2.1', + }, + ], + }, + ], + }, + { + id: '2', + title: 'Item 2', + }, +]; + +const defaultProps: TreeViewProps = { + items: defaultItems, + getItemId: item => item.id, + getItemChildren: item => item.items, + renderItem: item => ({ + content: item.title, + }), +}; + +function renderTreeView(props: Partial> = {}) { + const { container } = render(); + const wrapper = createWrapper(container).findTreeView()!; + return { wrapper }; +} + +function renderTreeViewWithI18NProvider(props: Partial> = {}) { + const { container } = render( + + + + ); + const wrapper = createWrapper(container).findTreeView()!; + return { wrapper }; +} + +test('sets aria-label', () => { + const ariaLabel = 'This is the aria label for this tree view'; + const { wrapper } = renderTreeView({ ariaLabel }); + + expect(wrapper.find('[role=tree]')!.getElement().getAttribute('aria-label')).toEqual(ariaLabel); +}); + +test('sets aria-expanded on expandable items', () => { + const { wrapper } = renderTreeView(); + + expect(wrapper.findItemById('1')!.getElement().getAttribute('aria-expanded')).toEqual('false'); + wrapper.findItemById('1')!.findItemToggle()!.getElement().click(); + expect(wrapper.findItemById('1')!.getElement().getAttribute('aria-expanded')).toEqual('true'); + + expect(wrapper.findItemById('2')!.getElement().getAttribute('aria-expanded')).toBeNull(); +}); + +test('i18n provider adds label to expand/collapse toggle', () => { + const { wrapper } = renderTreeViewWithI18NProvider(); + + const collapsedItem = wrapper.findItemById('1', { expanded: false })!; + expect(collapsedItem.findItemToggle()!.getElement()).toHaveAccessibleName('Expand item'); + + collapsedItem.findItemToggle()!.getElement().click(); + const expandedItem = wrapper.findItemById('1', { expanded: true })!; + expect(expandedItem.findItemToggle()!.getElement()).toHaveAccessibleName('Collapse item'); +}); + +test('i18nStrings adds custom label to expand/collapse toggle', () => { + const { wrapper } = renderTreeViewWithI18NProvider({ + i18nStrings: { + expandButtonLabel: item => `Expand ${item.title}`, + collapseButtonLabel: item => `Collapse ${item.title}`, + }, + }); + + const collapsedItem = wrapper.findItemById('1', { expanded: false })!; + expect(collapsedItem.findItemToggle()!.getElement()).toHaveAccessibleName('Expand Item 1'); + + collapsedItem.findItemToggle()!.getElement().click(); + const expandedItem = wrapper.findItemById('1', { expanded: true })!; + expect(expandedItem.findItemToggle()!.getElement()).toHaveAccessibleName('Collapse Item 1'); +}); + +test('correct aria-level is set', () => { + const { wrapper } = renderTreeView(); + + const rootLevelItem = wrapper.findItemById('1')!; + rootLevelItem.findItemToggle()!.getElement().click(); + + const level1Item = wrapper.findItemById('1.2')!; + level1Item.findItemToggle()!.getElement().click(); + + const level2Item = wrapper.findItemById('1.2.1')!; + + expect(rootLevelItem.getElement().getAttribute('aria-level')).toBeNull(); // root level shouldn't have aria-level + expect(level1Item.getElement().getAttribute('aria-level')).toEqual('1'); + expect(level2Item.getElement().getAttribute('aria-level')).toEqual('2'); +}); diff --git a/src/tree-view/__tests__/tree-item.test.tsx b/src/tree-view/__tests__/tree-item.test.tsx index 2d8d1874d8..58897871ad 100644 --- a/src/tree-view/__tests__/tree-item.test.tsx +++ b/src/tree-view/__tests__/tree-item.test.tsx @@ -93,11 +93,13 @@ test('children items are rendered only when item is expanded', () => { // expand expandableItem.findItemToggle()!.getElement().click(); + expect(expandableItem.findChildren().length).toBe(2); expect(wrapper.findItemById('1', { expanded: true })!.getElement()).toBeVisible(); expect(expandableItem.findChildById('1.1')!.getElement()).toBeVisible(); // collapse expandableItem.findItemToggle()!.getElement().click(); + expect(expandableItem.findChildren().length).toBe(0); expect(wrapper.findItemById('1', { expanded: false })!.getElement()).toBeVisible(); expect(expandableItem.findChildById('1.1')).toBeNull(); }); diff --git a/src/tree-view/tree-item/index.tsx b/src/tree-view/tree-item/index.tsx index 4daf62e1ee..e6761d8406 100644 --- a/src/tree-view/tree-item/index.tsx +++ b/src/tree-view/tree-item/index.tsx @@ -61,7 +61,7 @@ const InternalTreeItem = ({ withGrid && styles['with-grid'] )} aria-expanded={isExpandable ? isExpanded : undefined} - aria-level={level} + aria-level={level > 0 ? level : undefined} data-testid={`treeitem-${id}`} > {/*
                    @@ -157,6 +157,7 @@ const InternalTreeItem = ({ renderItem={renderItem} getItemId={getItemId} getItemChildren={getItemChildren} + i18nStrings={i18nStrings} /> ); })} From 03e7bd16a56752092f03ae83d5c0ad2f16fa89de Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Tue, 10 Jun 2025 16:11:00 +0200 Subject: [PATCH 12/39] display grid and structured item transition --- pages/tree-view/basic.page.tsx | 50 ++++- pages/tree-view/generate-data.ts | 2 +- pages/tree-view/test.page.tsx | 3 - .../__snapshots__/documenter.test.ts.snap | 1 - .../__tests__/structured-item.test.tsx | 13 ++ .../components/structured-item/index.tsx | 32 +++ .../components/structured-item/interfaces.ts | 12 ++ .../components/structured-item/styles.scss | 56 +++++ .../structured-item/test-classes/styles.scss | 12 ++ src/tree-view/connector/index.tsx | 60 ++++-- src/tree-view/connector/styles.scss | 80 ++++--- src/tree-view/index.tsx | 5 +- src/tree-view/internal.tsx | 7 +- src/tree-view/styles.scss | 186 +++------------- src/tree-view/tree-item/index.tsx | 201 ++++-------------- src/tree-view/tree-item/layout.tsx | 37 ---- 16 files changed, 337 insertions(+), 420 deletions(-) create mode 100644 src/internal/components/structured-item/__tests__/structured-item.test.tsx create mode 100644 src/internal/components/structured-item/index.tsx create mode 100644 src/internal/components/structured-item/interfaces.ts create mode 100644 src/internal/components/structured-item/styles.scss create mode 100644 src/internal/components/structured-item/test-classes/styles.scss delete mode 100644 src/tree-view/tree-item/layout.tsx diff --git a/pages/tree-view/basic.page.tsx b/pages/tree-view/basic.page.tsx index b7d7068a09..c2d64dff28 100644 --- a/pages/tree-view/basic.page.tsx +++ b/pages/tree-view/basic.page.tsx @@ -55,6 +55,7 @@ interface Item { children?: Item[]; hasActions?: boolean; hideIcon?: boolean; + actionType?: 'button-group' | 'button-dropdown' | 'inline-button-dropdown'; } const items: Item[] = [ @@ -86,6 +87,7 @@ const items: Item[] = [ content: 'Item 1.3', details: 'us-east-1', hasActions: true, + actionType: 'button-group', children: [ { id: '1.3.1', @@ -108,6 +110,8 @@ const items: Item[] = [ { id: '3', content: 'Item 3', + hasActions: true, + actionType: 'inline-button-dropdown', children: [ { id: '3.1', @@ -169,6 +173,7 @@ const items: Item[] = [ { id: '5', content: 'Item 5', + details: 'This item is on level 0', }, { id: '6', @@ -205,7 +210,9 @@ const items: Item[] = [ ]; function Actions( - { actionType }: { actionType?: 'button-group' | 'button-dropdown' } = { actionType: 'button-dropdown' } + { actionType }: { actionType?: 'button-group' | 'button-dropdown' | 'inline-button-dropdown' } = { + actionType: 'button-dropdown', + } ) { const [pressed, setPressed] = useState(false); @@ -220,6 +227,14 @@ function Actions( type: 'icon-button', text: 'Settings', }, + { + type: 'icon-toggle-button', + id: 'favorite', + text: 'Favorite', + pressed: pressed, + iconName: 'star', + pressedIconName: 'star-filled', + }, { id: 'menu', type: 'menu-dropdown', @@ -236,14 +251,6 @@ function Actions( { id: 'terminate', text: 'Terminate' }, ], }, - { - type: 'icon-toggle-button', - id: 'favorite', - text: 'Favorite', - pressed: pressed, - iconName: 'star', - pressedIconName: 'star-filled', - }, ]} onItemClick={({ detail }) => { if (detail.id === 'favorite') { @@ -254,6 +261,26 @@ function Actions( ); } + if (actionType === 'inline-button-dropdown') { + return ( + + ); + } + return ( ), content: item.content, - secondaryContent: {item.details}, - actions: item.hasActions ? : undefined, + secondaryContent: item.details && {item.details}, + actions: item.hasActions ? : undefined, }; }} getItemId={item => item.id} @@ -341,6 +368,7 @@ export default function BasicTreeView() { expandButtonLabel: () => 'Expand item', collapseButtonLabel: () => 'Collapse item', }} + showConnectorLine={true} />
                    diff --git a/pages/tree-view/generate-data.ts b/pages/tree-view/generate-data.ts index 243b5f9bec..5615f4df6c 100644 --- a/pages/tree-view/generate-data.ts +++ b/pages/tree-view/generate-data.ts @@ -172,7 +172,7 @@ function flattenItems(items: Item[]) { const allItems: Item[] = []; const pushItem = (item: Item) => { - allItems.push(item); + allItems.push({ ...item, children: [] }); if (item.children) { item.children.forEach(pushItem); } diff --git a/pages/tree-view/test.page.tsx b/pages/tree-view/test.page.tsx index c4b88613ed..afc8819ec0 100644 --- a/pages/tree-view/test.page.tsx +++ b/pages/tree-view/test.page.tsx @@ -10,10 +10,7 @@ import TreeView from '~components/tree-view'; import { Actions, Content } from './common'; import { allItems, items } from './generate-data'; -console.log('items: ', items); -console.log('all items: ', allItems); const allExpandableItemIds = allItems.filter(item => item.children && item.children.length > 0).map(item => item.id); -console.log('expandable item ids: ', allExpandableItemIds); export default function TestPage() { const [expandedItems, setExpandedItems] = useState>([]); diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index ba59730d10..af3451694a 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20639,7 +20639,6 @@ Use it to have different icons/animations for toggle.", "type": "((isExpanded: boolean) => React.ReactNode)", }, { - "defaultValue": "true", "name": "showConnectorLine", "optional": true, "systemTags": [ diff --git a/src/internal/components/structured-item/__tests__/structured-item.test.tsx b/src/internal/components/structured-item/__tests__/structured-item.test.tsx new file mode 100644 index 0000000000..c2eb1b1aac --- /dev/null +++ b/src/internal/components/structured-item/__tests__/structured-item.test.tsx @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// import React from 'react'; +// import { render } from '@testing-library/react'; + +// import StructuredItem from '../../../lib/components/structured-item'; +// import createWrapper from '../../../lib/components/test-utils/dom'; + +test('renders items', () => { + // const { container } = render(); + // const wrapper = createWrapper(container).findStructuredItem()!; + // expect(wrapper.findAll('li')).toHaveLength(2); +}); diff --git a/src/internal/components/structured-item/index.tsx b/src/internal/components/structured-item/index.tsx new file mode 100644 index 0000000000..c99bd18a95 --- /dev/null +++ b/src/internal/components/structured-item/index.tsx @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import clsx from 'clsx'; + +import { StructuredItemProps } from './interfaces'; + +import styles from './styles.css.js'; +import testClasses from './test-classes/styles.css.js'; + +export { StructuredItemProps }; + +export default function InternalStructuredItem({ + content, + icon, + actions, + secondaryContent, + disablePaddings, +}: StructuredItemProps) { + return ( +
                    + {icon &&
                    {icon}
                    } +
                    +
                    +
                    {content}
                    + {actions &&
                    {actions}
                    } +
                    + {secondaryContent &&
                    {secondaryContent}
                    } +
                    +
                    + ); +} diff --git a/src/internal/components/structured-item/interfaces.ts b/src/internal/components/structured-item/interfaces.ts new file mode 100644 index 0000000000..fec17a312d --- /dev/null +++ b/src/internal/components/structured-item/interfaces.ts @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ReactNode } from 'react'; + +export interface StructuredItemProps { + content: ReactNode; + icon?: ReactNode; + actions?: ReactNode; + secondaryContent?: ReactNode; + disablePaddings?: boolean; +} diff --git a/src/internal/components/structured-item/styles.scss b/src/internal/components/structured-item/styles.scss new file mode 100644 index 0000000000..3f4c6b777d --- /dev/null +++ b/src/internal/components/structured-item/styles.scss @@ -0,0 +1,56 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +@use '../../styles' as styles; +@use '../../styles/tokens' as awsui; + +.root { + @include styles.styles-reset; + @include styles.text-wrapping; + display: flex; + flex: 1; + flex-direction: row; + align-items: baseline; + column-gap: awsui.$space-xs; + &.disable-paddings { + column-gap: 0; + } +} + +.main { + flex-grow: 1; + display: flex; + flex-direction: column; + &.with-secondary { + row-gap: awsui.$space-scaled-xxs; + } + .disable-paddings > & { + row-gap: 0; + } +} + +.content-wrap { + flex-grow: 1; + display: flex; + align-items: baseline; + flex-direction: row; + flex-wrap: wrap; + column-gap: awsui.$space-xs; + row-gap: awsui.$space-scaled-xxs; + + .disable-paddings > .main > & { + column-gap: 0; + row-gap: 0; + } +} + +.content { + flex-grow: 1; +} + +.actions { + flex-shrink: 0; + margin-inline-start: auto; + margin-top: -5px; +} diff --git a/src/internal/components/structured-item/test-classes/styles.scss b/src/internal/components/structured-item/test-classes/styles.scss new file mode 100644 index 0000000000..e72f938351 --- /dev/null +++ b/src/internal/components/structured-item/test-classes/styles.scss @@ -0,0 +1,12 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +.root, +.content, +.icon, +.actions, +.secondary { + /* used in test-utils */ +} diff --git a/src/tree-view/connector/index.tsx b/src/tree-view/connector/index.tsx index dbc52605bb..6d7fe3c1de 100644 --- a/src/tree-view/connector/index.tsx +++ b/src/tree-view/connector/index.tsx @@ -9,33 +9,65 @@ const Connector = ({ level, position, isExpandable, + isExpanded, }: { level: number; position: 'start' | 'middle' | 'end'; isExpandable: boolean; + isExpanded: boolean; }) => { if (level === 0) { return ( -
                    +
                    +
                    + + {!isExpandable &&
                    } +
                    ); } return ( - <> -
                    +
                    +
                    + + {level > 0 && + Array.from(Array(level + 1).keys()).map(l => { + if (l === 0) { + const offsetLevelDiff = level - 1; + return ( +
                    + ); + } + + // No vertical lines shown if item is not expanded + if (l === level && !isExpanded) { + return
                    ; + } - {level > 1 && (position === 'start' || position === 'end') && ( -
                    - )} + const offsetLevelDiff = level - l - 1; - {level > 1 && position === 'middle' &&
                    } - + return ( +
                    + ); + })} +
                    ); }; diff --git a/src/tree-view/connector/styles.scss b/src/tree-view/connector/styles.scss index 5b72a88760..933962bbe3 100644 --- a/src/tree-view/connector/styles.scss +++ b/src/tree-view/connector/styles.scss @@ -5,52 +5,66 @@ @use '../../internal/styles/tokens' as awsui; @use '../../internal/styles' as styles; -.treeitem-connector-horizontal { - block-size: 1px; - position: absolute; - inset-block-start: 12px; - inline-size: 39px; +$max-nesting-levels: 20; + +.vertical-rule-level-0 { background: red; - inset-inline-start: -14px; + inline-size: 1px; + position: absolute; + inset-block-start: 0; + inset-block-end: -5px; + inset-inline-start: 14px; // 28px / 2 &.expandable { - inline-size: 20px; + inset-block-start: 30px; } -} -.treeitem-connector-vertical { - &-root { - inline-size: 1px; - position: absolute; - inset-block: 0; - inset-inline-start: 50%; - background-color: red; + &.position-end { + block-size: 16px; // 32px / 2 } +} - &-root-end { - block-size: 16px; - inset-block: 0 unset; +@for $level from 0 through $max-nesting-levels { + .with-offset-#{$level} { + inset-inline-start: calc((#{$level} * -32px) - 18px); } +} + +.vertical-rule { + background: red; + inline-size: 1px; + position: absolute; + inset-block-start: 0; + inset-block-end: -5px; - &-expandable { - inset-block-start: 24px; + &.position-end { + block-size: 16px; // 32px / 2 } - &-middle { - inline-size: 1px; - position: absolute; - inset-block: -11px 0px; - inset-inline-start: -14px; - background-color: red; + &.expanded { + inset-block-start: 26px; + inset-inline-start: 14px; // 28px / 2 } +} + +.horizontal-rule-level-0 { + background: red; + inline-size: 12px; + block-size: 1px; + position: absolute; + inset-block-start: 16px; + inset-inline-start: 14px; +} - &-end { - inline-size: 1px; - position: absolute; - inset-block: 0 10px; - inset-inline-start: -14px; - background-color: red; +.horizontal-rule { + background: red; + block-size: 1px; + position: absolute; + inset-block-start: 16px; + inset-inline-start: -18px; // 28 / 2 + 4 -> width / 2 + gap + inline-size: 38px; - block-size: 13px; + &.expandable { + inline-size: 12px; } } diff --git a/src/tree-view/index.tsx b/src/tree-view/index.tsx index 793983b1a6..c3d0e4716b 100644 --- a/src/tree-view/index.tsx +++ b/src/tree-view/index.tsx @@ -11,13 +11,13 @@ import InternalTreeView from './internal'; export { TreeViewProps }; -const TreeView = ({ items, showConnectorLine = true, ...rest }: TreeViewProps) => { +const TreeView = ({ items, showConnectorLine, ...rest }: TreeViewProps) => { const baseProps = getBaseProps(rest); - // TODO: analytics metadata? const baseComponentProps = useBaseComponent('TreeView', { props: {}, metadata: { itemsCount: items?.length, + showConnectorLine, }, }); const externalProps = getExternalProps(rest); @@ -27,7 +27,6 @@ const TreeView = ({ items, showConnectorLine = true, ...rest }: TreeViewProp {...baseProps} {...baseComponentProps} {...externalProps} - // ref={ref} items={items} showConnectorLine={showConnectorLine} {...rest} diff --git a/src/tree-view/internal.tsx b/src/tree-view/internal.tsx index f689f3b3f3..357b47f93c 100644 --- a/src/tree-view/internal.tsx +++ b/src/tree-view/internal.tsx @@ -28,6 +28,7 @@ const InternalTreeView = ({ ariaLabelledby, ariaDescribedby, i18nStrings, + showConnectorLine, __internalRootRef, ...rest }: InternalTreeViewProps) => { @@ -72,16 +73,14 @@ const InternalTreeView = ({ item={item} level={0} index={index} - expandedItems={ - expandedItems === undefined || expandedItems === null ? internalExpandedItems : expandedItems - } + expandedItems={isExpandStateControlled ? expandedItems : internalExpandedItems} i18nStrings={i18nStrings} position={getItemPosition(index, items.length)} onItemToggle={onToggle} renderItem={renderItem} getItemId={getItemId} getItemChildren={getItemChildren} - // withGrid={true} + showConnectorLine={showConnectorLine} /> ); })} diff --git a/src/tree-view/styles.scss b/src/tree-view/styles.scss index 92ca186e0b..3a8b206706 100644 --- a/src/tree-view/styles.scss +++ b/src/tree-view/styles.scss @@ -13,6 +13,10 @@ line-height: awsui.$line-height-heading-m; } +.tree { + @include styles.styles-reset-ul; +} + .tree, .treeitem-group, .treeitem { @@ -21,171 +25,35 @@ margin-block: 0; } -.treeitem-group { - inline-size: 100%; -} - -.treeitem-toggle-area { - inline-size: 30px; - background-color: yellow; - - display: flex; - align-items: baseline; - justify-content: center; - padding-block-start: 6px; - - position: relative; -} - -.connector-area { - background-color: yellow; - display: flex; - padding-block-start: 3px; - justify-content: flex-end; - - &.level-0 { - justify-content: center; - } -} - -.connector-area2 { - display: flex; - align-items: baseline; - position: relative; - inline-size: 100%; -} - .treeitem { - display: flex; -} - -.tree { - @include styles.styles-reset-ul; -} - -// .treeitem-group { -// padding-inline-start: 0; -// margin-inline-start: 0; -// } - -.content { + display: grid; + grid-template-columns: 28px 1fr; + column-gap: 4px; + grid-template-rows: minmax(32px, 1fr) auto auto; + overflow: visible; position: relative; - padding-block-start: 2px; - flex: 1; -} - -.treeitem-connector-group { - display: flex; - flex: 1; - flex-direction: column; -} - -.treeitem-layout { - display: flex; - flex-direction: column; - flex: 1; -} -.treeitem-first-line { - display: flex; - align-items: baseline; - gap: 4px; -} - -.horizontal { - inline-size: 32px; - block-size: 1px; - background: red; - position: absolute; - inset-block-start: 11px; - - &.expandable { - inline-size: 22px; + > .connector-toggle-wrapper { + grid-column: 1; + grid-row: 1; + padding-block-start: 6px; + position: relative; + + > .toggle { + justify-self: center; + position: relative; + inset-block-start: 2px; + } } -} -.vertical { - inline-size: 1px; - background-color: red; - position: absolute; - inset-block: 0; - inset-inline-start: 8px; - &.expanded { - inset-block-start: 24px; + > .structured-item-wrapper { + grid-column: 2; + grid-row: 1 / span 2; + padding-block-start: 6px; } - &.position-end:not(.expanded) { - block-size: 12px; + > .treeitem-group { + grid-column: 2; + grid-row: 3; } } - -.level0 { - inline-size: 1px; - background-color: red; - position: absolute; - inset-block: 0; - inset-inline-start: 8px; - - &.expandable { - inset-block: 22px -8px; - } - - &.position-start { - inset-block-start: 20px; - } -} - -// .with-grid { -// align-items: center; -// display: grid; -// grid-template-columns: auto auto 1fr; -// grid-template-rows: repeat(2, 1fr) auto; -// // row-gap: 0.4rem; -// // line-height: 2.8rem; -// overflow: visible; -// position: relative; - -// .treeitem-group { -// grid-column: 3; -// grid-row: 3; -// // grid-template-columns: subgrid; -// // grid-template-rows: subgrid; -// } - -// .toggle { -// align-items: center; -// display: flex; -// grid-column: 1; -// grid-row: 1 / span 2; -// justify-content: center; -// } - -// .treeitem-layout { -// grid-column: 2; -// // grid-column: 3; -// grid-row: 1 / span 2; -// } - -// .vertical-rule { -// background: red; -// grid-column: 1; -// grid-row: 3; -// height: 100%; -// justify-self: center; -// width: 1px; -// } - -// .horizontal-rule { -// background: red; -// grid-column: 1; -// grid-row: 1 / span 2; -// height: 0.1rem; -// margin-left: -2.5rem; -// width: 2rem; -// } - -// .icon { -// // grid-column: 2; -// // grid-row: 1 / span 2; -// } -// } diff --git a/src/tree-view/tree-item/index.tsx b/src/tree-view/tree-item/index.tsx index e6761d8406..1ac36a7ef9 100644 --- a/src/tree-view/tree-item/index.tsx +++ b/src/tree-view/tree-item/index.tsx @@ -5,21 +5,23 @@ import clsx from 'clsx'; import { useInternalI18n } from '../../i18n/context'; import { ExpandToggleButton } from '../../internal/components/expand-toggle-button'; -// import Connector from './connector'; +import InternalStructuredItem from '../../internal/components/structured-item'; +import Connector from '../connector'; import { TreeViewProps } from '../interfaces'; -import TreeItemLayout from './layout'; import { getItemPosition, transformTreeItemProps } from './utils'; import styles from '../styles.css.js'; import testUtilStyles from '../test-classes/styles.css.js'; interface InternalTreeItemProps - extends Pick { + extends Pick< + TreeViewProps, + 'expandedItems' | 'renderItem' | 'getItemId' | 'getItemChildren' | 'showConnectorLine' | 'i18nStrings' + > { item: T; index: number; level: number; position: 'start' | 'middle' | 'end'; - withGrid?: boolean; onItemToggle: (detail: TreeViewProps.ItemToggleDetail) => void; } @@ -30,7 +32,7 @@ const InternalTreeItem = ({ position, i18nStrings, expandedItems = [], - withGrid, + showConnectorLine, renderItem, getItemId, getItemChildren, @@ -58,163 +60,54 @@ const InternalTreeItem = ({ isExpandable && [testUtilStyles.expandable], isExpanded && [styles.expanded], isExpanded && [testUtilStyles.expanded], - withGrid && styles['with-grid'] + styles[`level-${0}`] )} aria-expanded={isExpandable ? isExpanded : undefined} aria-level={level > 0 ? level : undefined} data-testid={`treeitem-${id}`} > - {/*
                    +
                    {isExpandable && ( - fireNonCancelableEvent(onItemToggle, { id, item, expanded: !isExpanded })} - expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} - collapseButtonLabel={i18n('i18nStrings.collapseButtonLabel', i18nStrings?.collapseButtonLabel?.(item))} - /> - )} - - -
                    */} - - {!withGrid && ( -
                    - {/* Try out horizontally constructured connector lines */} -
                    -
                    0 && !isExpandable ? '20' : '0'}px)`, - marginBlock: 'auto', - }} - > - {level > 0 && ( -
                    - )} - - {level === 0 && ( -
                    - )} - - {level > 0 && - Array.from(Array(level + 1).keys()).map(l => { - if (l === 0) { - return
                    ; - } - - if (l === level && !isExpanded) { - return
                    ; - } - - return ( -
                    - ); - })} -
                    - -
                    - {isExpandable && ( - onItemToggle({ id, item, expanded: !isExpanded })} - expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} - collapseButtonLabel={i18n( - 'i18nStrings.collapseButtonLabel', - i18nStrings?.collapseButtonLabel?.(item) - )} - /> - )} -
                    - - +
                    + onItemToggle({ id, item, expanded: !isExpanded })} + expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} + collapseButtonLabel={i18n('i18nStrings.collapseButtonLabel', i18nStrings?.collapseButtonLabel?.(item))} + />
                    + )} - {isExpanded && children.length && ( -
                      - {children.map((child, index) => { - return ( - - ); - })} -
                    - )} -
                    - )} - - {withGrid && ( - <> - {/* Try out constructing connector lines with display grid */} - {isExpandable && ( - <> -
                    - onItemToggle({ id, item, expanded: !isExpanded })} - expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} - collapseButtonLabel={i18n( - 'i18nStrings.collapseButtonLabel', - i18nStrings?.collapseButtonLabel?.(item) - )} - /> -
                    - -
                    - - )} - - {level > 0 &&
                    } - - {/* {icon &&
                    {icon}
                    } */} - - - - {isExpanded && children.length && ( -
                      - {children.map((child, index) => { - return ( - - ); - })} -
                    - )} - + {showConnectorLine && ( + + )} +
                    + +
                    + +
                    + + {isExpanded && children.length && ( +
                      + {children.map((child, index) => { + return ( + + ); + })} +
                    )} ); diff --git a/src/tree-view/tree-item/layout.tsx b/src/tree-view/tree-item/layout.tsx deleted file mode 100644 index 7c2ef9b2cf..0000000000 --- a/src/tree-view/tree-item/layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React from 'react'; -import clsx from 'clsx'; - -import styles from '../styles.css.js'; -import testUtilStyles from '../test-classes/styles.css.js'; - -const TreeItemLayout = ({ - icon, - content, - secondaryContent, - actions, -}: { - icon?: React.ReactNode; - content: React.ReactNode; - secondaryContent?: React.ReactNode; - actions?: React.ReactNode; -}) => { - return ( -
                    -
                    - {icon &&
                    {icon}
                    } - -
                    {content}
                    - - {actions &&
                    {actions}
                    } -
                    - - {secondaryContent && ( -
                    {secondaryContent}
                    - )} -
                    - ); -}; - -export default TreeItemLayout; From 93bf261ef1fd70e5c4b3fa374a4d21b5371559a4 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Tue, 10 Jun 2025 16:43:19 +0200 Subject: [PATCH 13/39] test utils update --- .../__snapshots__/test-utils-selectors.test.tsx.snap | 8 ++++---- src/test-utils/dom/tree-view/index.ts | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index f82472cc15..57e3a1405f 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -348,6 +348,7 @@ exports[`test-utils selectors 1`] = ` "awsui_root_2rhyz", ], "internal": [ + "awsui_actions_x6dl3", "awsui_application_1fcus", "awsui_axis-label--x_f0fot", "awsui_axis-label--y_f0fot", @@ -355,6 +356,7 @@ exports[`test-utils selectors 1`] = ` "awsui_button-trigger_18eso", "awsui_button_m5h9f", "awsui_chart-filter_1px7g", + "awsui_content_x6dl3", "awsui_control_1wepg", "awsui_description_1p2cx", "awsui_description_1wepg", @@ -373,6 +375,7 @@ exports[`test-utils selectors 1`] = ` "awsui_has-background_15o6u", "awsui_header_dgs8z", "awsui_highlighted_15o6u", + "awsui_icon_x6dl3", "awsui_inner-list-item_10ipo", "awsui_key_10ipo", "awsui_label-tag_1p2cx", @@ -395,6 +398,7 @@ exports[`test-utils selectors 1`] = ` "awsui_root_1t44z", "awsui_root_qwoo0", "awsui_root_vrgzu", + "awsui_secondary_x6dl3", "awsui_select-all_15o6u", "awsui_selectable-item_15o6u", "awsui_selected_15o6u", @@ -666,13 +670,9 @@ exports[`test-utils selectors 1`] = ` "awsui_utility-wrapper_k5dlb", ], "tree-view": [ - "awsui_actions_1js4f", - "awsui_content_1js4f", "awsui_expandable_1js4f", "awsui_expanded_1js4f", - "awsui_icon_1js4f", "awsui_root_1js4f", - "awsui_secondary-content_1js4f", "awsui_treeitem_1js4f", ], "tutorial-panel": [ diff --git a/src/test-utils/dom/tree-view/index.ts b/src/test-utils/dom/tree-view/index.ts index ca0f317819..239d937697 100644 --- a/src/test-utils/dom/tree-view/index.ts +++ b/src/test-utils/dom/tree-view/index.ts @@ -3,6 +3,7 @@ import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; import expandToggleStyles from '../../../internal/components/expand-toggle-button/styles.selectors.js'; +import structuredItemTestUtilStyles from '../../../internal/components/structured-item/test-classes/styles.selectors.js'; import testUtilStyles from '../../../tree-view/test-classes/styles.selectors.js'; class TreeItemWrapper extends ComponentWrapper { @@ -10,28 +11,28 @@ class TreeItemWrapper extends ComponentWrapper { * Finds the content of the tree item. */ findContent(): ElementWrapper | null { - return this.findByClassName(testUtilStyles.content); + return this.findByClassName(structuredItemTestUtilStyles.content); } /** * Finds the icon of the tree item. */ findIcon(): ElementWrapper | null { - return this.findByClassName(testUtilStyles.icon); + return this.findByClassName(structuredItemTestUtilStyles.icon); } /** * Finds the secondary content of the tree item. */ findSecondaryContent(): ElementWrapper | null { - return this.findByClassName(testUtilStyles['secondary-content']); + return this.findByClassName(structuredItemTestUtilStyles.secondary); } /** * Finds the actions of the tree item. */ findActions(): ElementWrapper | null { - return this.findByClassName(testUtilStyles.actions); + return this.findByClassName(structuredItemTestUtilStyles.actions); } /** From 7dad45fbefc57543268db8c8658f1706fa6d6a2b Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 12 Jun 2025 11:03:54 +0200 Subject: [PATCH 14/39] renderItemToggleIcon --- pages/tree-view/basic.page.tsx | 34 ++++++++++++++++++- pages/tree-view/styles.scss | 17 ++++++++++ .../components/expand-toggle-button/index.tsx | 14 +++++--- src/tree-view/connector/styles.scss | 14 ++++---- src/tree-view/internal.tsx | 4 ++- src/tree-view/styles.scss | 2 +- src/tree-view/tree-item/index.tsx | 18 ++++++++-- 7 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 pages/tree-view/styles.scss diff --git a/pages/tree-view/basic.page.tsx b/pages/tree-view/basic.page.tsx index c2d64dff28..e3a55bfb29 100644 --- a/pages/tree-view/basic.page.tsx +++ b/pages/tree-view/basic.page.tsx @@ -1,8 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; +import clsx from 'clsx'; -import { ButtonDropdown } from '~components'; +import { ButtonDropdown, Checkbox } from '~components'; import Badge from '~components/badge'; import Box from '~components/box'; import ButtonGroup from '~components/button-group'; @@ -13,6 +14,8 @@ import SpaceBetween from '~components/space-between'; import StatusIndicator from '~components/status-indicator'; import TreeView from '~components/tree-view'; +import styles from './styles.scss'; + const progressiveStepContent = (
                    Checked 5 nodes @@ -333,11 +336,39 @@ function RdsAccessRoleTreeItemContent() { export default function BasicTreeView() { const [expandedItems, setExpandedItems] = useState>(['1', '4.1']); + const [useDifferentIcon, setUseDifferentIcon] = useState(false); + const [useDifferentIconWithAnimation, setUseDifferentIconWithAnimation] = useState(false); + + const renderItemToggleIcon = (isExpanded: boolean) => { + if (useDifferentIcon) { + return ; + } + + if (useDifferentIconWithAnimation) { + return ( + + ); + } + }; return ( <>

                    Basic tree view

                    + setUseDifferentIcon(detail.checked)}> + Use different CDS icon + + setUseDifferentIconWithAnimation(detail.checked)} + > + Use icon with animation + +
                    @@ -369,6 +400,7 @@ export default function BasicTreeView() { collapseButtonLabel: () => 'Collapse item', }} showConnectorLine={true} + renderItemToggleIcon={renderItemToggleIcon} />
                    diff --git a/pages/tree-view/styles.scss b/pages/tree-view/styles.scss new file mode 100644 index 0000000000..1ed56e4797 --- /dev/null +++ b/pages/tree-view/styles.scss @@ -0,0 +1,17 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +@use '../../src/internal/styles/motion/mixins' as styles; + +.animation { + transform: rotate(-90deg); + + @include styles.with-motion { + transition: transform 1000ms cubic-bezier(0.165, 0.84, 0.44, 1); + } + + &-expanded { + transform: rotate(0deg); + } +} diff --git a/src/internal/components/expand-toggle-button/index.tsx b/src/internal/components/expand-toggle-button/index.tsx index 1809785b97..9fb5a28b79 100644 --- a/src/internal/components/expand-toggle-button/index.tsx +++ b/src/internal/components/expand-toggle-button/index.tsx @@ -14,11 +14,13 @@ export function ExpandToggleButton({ onExpandableItemToggle, expandButtonLabel, collapseButtonLabel, + customIcon, }: { isExpanded?: boolean; onExpandableItemToggle?: () => void; expandButtonLabel?: string; collapseButtonLabel?: string; + customIcon?: React.ReactNode; }) { const buttonRef = useRef(null); const { tabIndex } = useSingleTabStopNavigation(buttonRef); @@ -32,11 +34,13 @@ export function ExpandToggleButton({ className={styles['expand-toggle']} onClick={onExpandableItemToggle} > - + {customIcon ?? ( + + )} ); } diff --git a/src/tree-view/connector/styles.scss b/src/tree-view/connector/styles.scss index 933962bbe3..170115d641 100644 --- a/src/tree-view/connector/styles.scss +++ b/src/tree-view/connector/styles.scss @@ -16,11 +16,11 @@ $max-nesting-levels: 20; inset-inline-start: 14px; // 28px / 2 &.expandable { - inset-block-start: 30px; + inset-block-start: 28px; } &.position-end { - block-size: 16px; // 32px / 2 + block-size: 15px; } } @@ -35,14 +35,14 @@ $max-nesting-levels: 20; inline-size: 1px; position: absolute; inset-block-start: 0; - inset-block-end: -5px; + inset-block-end: -3px; &.position-end { - block-size: 16px; // 32px / 2 + block-size: 15px; } &.expanded { - inset-block-start: 26px; + inset-block-start: 32px; inset-inline-start: 14px; // 28px / 2 } } @@ -52,7 +52,7 @@ $max-nesting-levels: 20; inline-size: 12px; block-size: 1px; position: absolute; - inset-block-start: 16px; + inset-block-start: 14px; inset-inline-start: 14px; } @@ -60,7 +60,7 @@ $max-nesting-levels: 20; background: red; block-size: 1px; position: absolute; - inset-block-start: 16px; + inset-block-start: 14px; inset-inline-start: -18px; // 28 / 2 + 4 -> width / 2 + gap inline-size: 38px; diff --git a/src/tree-view/internal.tsx b/src/tree-view/internal.tsx index 357b47f93c..0a23546de1 100644 --- a/src/tree-view/internal.tsx +++ b/src/tree-view/internal.tsx @@ -24,6 +24,7 @@ const InternalTreeView = ({ getItemId, getItemChildren, onItemToggle, + renderItemToggleIcon, ariaLabel, ariaLabelledby, ariaDescribedby, @@ -61,7 +62,7 @@ const InternalTreeView = ({
                      ({ renderItem={renderItem} getItemId={getItemId} getItemChildren={getItemChildren} + renderItemToggleIcon={renderItemToggleIcon} showConnectorLine={showConnectorLine} /> ); diff --git a/src/tree-view/styles.scss b/src/tree-view/styles.scss index 3a8b206706..af7743930e 100644 --- a/src/tree-view/styles.scss +++ b/src/tree-view/styles.scss @@ -42,7 +42,7 @@ > .toggle { justify-self: center; position: relative; - inset-block-start: 2px; + // inset-block-start: 2px; } } diff --git a/src/tree-view/tree-item/index.tsx b/src/tree-view/tree-item/index.tsx index 1ac36a7ef9..5aa6360a49 100644 --- a/src/tree-view/tree-item/index.tsx +++ b/src/tree-view/tree-item/index.tsx @@ -16,7 +16,13 @@ import testUtilStyles from '../test-classes/styles.css.js'; interface InternalTreeItemProps extends Pick< TreeViewProps, - 'expandedItems' | 'renderItem' | 'getItemId' | 'getItemChildren' | 'showConnectorLine' | 'i18nStrings' + | 'expandedItems' + | 'renderItem' + | 'getItemId' + | 'getItemChildren' + | 'renderItemToggleIcon' + | 'showConnectorLine' + | 'i18nStrings' > { item: T; index: number; @@ -33,6 +39,7 @@ const InternalTreeItem = ({ i18nStrings, expandedItems = [], showConnectorLine, + renderItemToggleIcon, renderItem, getItemId, getItemChildren, @@ -48,11 +55,16 @@ const InternalTreeItem = ({ getItemChildren, }); const nextLevel = level + 1; + let customIcon = null; + + if (renderItemToggleIcon) { + customIcon = renderItemToggleIcon(isExpanded); + } return (
                    • ({ onExpandableItemToggle={() => onItemToggle({ id, item, expanded: !isExpanded })} expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} collapseButtonLabel={i18n('i18nStrings.collapseButtonLabel', i18nStrings?.collapseButtonLabel?.(item))} + customIcon={customIcon} />
                    )} @@ -104,6 +117,7 @@ const InternalTreeItem = ({ getItemChildren={getItemChildren} showConnectorLine={showConnectorLine} i18nStrings={i18nStrings} + renderItemToggleIcon={renderItemToggleIcon} /> ); })} From 5aeac363ebf63101acb8f4015841dcae475a9cbb Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 12 Jun 2025 11:28:30 +0200 Subject: [PATCH 15/39] test util update --- .../__snapshots__/test-utils-selectors.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 4c300a2d46..9233d82f9d 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -398,8 +398,8 @@ exports[`test-utils selectors 1`] = ` "awsui_root_1t44z", "awsui_root_qwoo0", "awsui_root_vrgzu", - "awsui_secondary_x6dl3", "awsui_screenreader-content_15o6u", + "awsui_secondary_x6dl3", "awsui_select-all_15o6u", "awsui_selectable-item_15o6u", "awsui_selected_15o6u", From b721ccab7495916157357b9e909aa3a2b0f1e2c5 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 12 Jun 2025 11:37:46 +0200 Subject: [PATCH 16/39] remove connector lines --- pages/tree-view/basic.page.tsx | 1 - src/tree-view/connector/index.tsx | 74 ----------------------------- src/tree-view/connector/styles.scss | 70 --------------------------- src/tree-view/index.tsx | 14 +----- src/tree-view/interfaces.ts | 6 --- src/tree-view/internal.tsx | 2 - src/tree-view/tree-item/index.tsx | 16 +------ 7 files changed, 3 insertions(+), 180 deletions(-) delete mode 100644 src/tree-view/connector/index.tsx delete mode 100644 src/tree-view/connector/styles.scss diff --git a/pages/tree-view/basic.page.tsx b/pages/tree-view/basic.page.tsx index e3a55bfb29..8c386594eb 100644 --- a/pages/tree-view/basic.page.tsx +++ b/pages/tree-view/basic.page.tsx @@ -399,7 +399,6 @@ export default function BasicTreeView() { expandButtonLabel: () => 'Expand item', collapseButtonLabel: () => 'Collapse item', }} - showConnectorLine={true} renderItemToggleIcon={renderItemToggleIcon} /> diff --git a/src/tree-view/connector/index.tsx b/src/tree-view/connector/index.tsx deleted file mode 100644 index 6d7fe3c1de..0000000000 --- a/src/tree-view/connector/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React from 'react'; -import clsx from 'clsx'; - -import styles from './styles.css.js'; - -const Connector = ({ - level, - position, - isExpandable, - isExpanded, -}: { - level: number; - position: 'start' | 'middle' | 'end'; - isExpandable: boolean; - isExpanded: boolean; -}) => { - if (level === 0) { - return ( -
                    -
                    - - {!isExpandable &&
                    } -
                    - ); - } - - return ( -
                    -
                    - - {level > 0 && - Array.from(Array(level + 1).keys()).map(l => { - if (l === 0) { - const offsetLevelDiff = level - 1; - return ( -
                    - ); - } - - // No vertical lines shown if item is not expanded - if (l === level && !isExpanded) { - return
                    ; - } - - const offsetLevelDiff = level - l - 1; - - return ( -
                    - ); - })} -
                    - ); -}; - -export default Connector; diff --git a/src/tree-view/connector/styles.scss b/src/tree-view/connector/styles.scss deleted file mode 100644 index 170115d641..0000000000 --- a/src/tree-view/connector/styles.scss +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ -@use '../../internal/styles/tokens' as awsui; -@use '../../internal/styles' as styles; - -$max-nesting-levels: 20; - -.vertical-rule-level-0 { - background: red; - inline-size: 1px; - position: absolute; - inset-block-start: 0; - inset-block-end: -5px; - inset-inline-start: 14px; // 28px / 2 - - &.expandable { - inset-block-start: 28px; - } - - &.position-end { - block-size: 15px; - } -} - -@for $level from 0 through $max-nesting-levels { - .with-offset-#{$level} { - inset-inline-start: calc((#{$level} * -32px) - 18px); - } -} - -.vertical-rule { - background: red; - inline-size: 1px; - position: absolute; - inset-block-start: 0; - inset-block-end: -3px; - - &.position-end { - block-size: 15px; - } - - &.expanded { - inset-block-start: 32px; - inset-inline-start: 14px; // 28px / 2 - } -} - -.horizontal-rule-level-0 { - background: red; - inline-size: 12px; - block-size: 1px; - position: absolute; - inset-block-start: 14px; - inset-inline-start: 14px; -} - -.horizontal-rule { - background: red; - block-size: 1px; - position: absolute; - inset-block-start: 14px; - inset-inline-start: -18px; // 28 / 2 + 4 -> width / 2 + gap - inline-size: 38px; - - &.expandable { - inline-size: 12px; - } -} diff --git a/src/tree-view/index.tsx b/src/tree-view/index.tsx index c3d0e4716b..0b98b95bbb 100644 --- a/src/tree-view/index.tsx +++ b/src/tree-view/index.tsx @@ -11,27 +11,17 @@ import InternalTreeView from './internal'; export { TreeViewProps }; -const TreeView = ({ items, showConnectorLine, ...rest }: TreeViewProps) => { +const TreeView = ({ items, ...rest }: TreeViewProps) => { const baseProps = getBaseProps(rest); const baseComponentProps = useBaseComponent('TreeView', { props: {}, metadata: { itemsCount: items?.length, - showConnectorLine, }, }); const externalProps = getExternalProps(rest); - return ( - - ); + return ; }; applyDisplayName(TreeView, 'TreeView'); diff --git a/src/tree-view/interfaces.ts b/src/tree-view/interfaces.ts index 0e140c6113..bb98cf584a 100644 --- a/src/tree-view/interfaces.ts +++ b/src/tree-view/interfaces.ts @@ -65,12 +65,6 @@ export interface TreeViewProps extends BaseComponentProps { */ i18nStrings?: TreeViewProps.I18nStrings; - /** - * @awsuiSystem core - * If `true`, adds connecting lines between child items and their expanded parent items. - */ - showConnectorLine?: boolean; - /** * @awsuiSystem core * Overrides the default expand toggle. diff --git a/src/tree-view/internal.tsx b/src/tree-view/internal.tsx index 0a23546de1..d40503d8c3 100644 --- a/src/tree-view/internal.tsx +++ b/src/tree-view/internal.tsx @@ -29,7 +29,6 @@ const InternalTreeView = ({ ariaLabelledby, ariaDescribedby, i18nStrings, - showConnectorLine, __internalRootRef, ...rest }: InternalTreeViewProps) => { @@ -82,7 +81,6 @@ const InternalTreeView = ({ getItemId={getItemId} getItemChildren={getItemChildren} renderItemToggleIcon={renderItemToggleIcon} - showConnectorLine={showConnectorLine} /> ); })} diff --git a/src/tree-view/tree-item/index.tsx b/src/tree-view/tree-item/index.tsx index 5aa6360a49..620d2a10e5 100644 --- a/src/tree-view/tree-item/index.tsx +++ b/src/tree-view/tree-item/index.tsx @@ -6,7 +6,6 @@ import clsx from 'clsx'; import { useInternalI18n } from '../../i18n/context'; import { ExpandToggleButton } from '../../internal/components/expand-toggle-button'; import InternalStructuredItem from '../../internal/components/structured-item'; -import Connector from '../connector'; import { TreeViewProps } from '../interfaces'; import { getItemPosition, transformTreeItemProps } from './utils'; @@ -16,13 +15,7 @@ import testUtilStyles from '../test-classes/styles.css.js'; interface InternalTreeItemProps extends Pick< TreeViewProps, - | 'expandedItems' - | 'renderItem' - | 'getItemId' - | 'getItemChildren' - | 'renderItemToggleIcon' - | 'showConnectorLine' - | 'i18nStrings' + 'expandedItems' | 'renderItem' | 'getItemId' | 'getItemChildren' | 'renderItemToggleIcon' | 'i18nStrings' > { item: T; index: number; @@ -35,10 +28,8 @@ const InternalTreeItem = ({ item, index, level, - position, i18nStrings, expandedItems = [], - showConnectorLine, renderItemToggleIcon, renderItem, getItemId, @@ -90,10 +81,6 @@ const InternalTreeItem = ({ />
                    )} - - {showConnectorLine && ( - - )}
                    @@ -115,7 +102,6 @@ const InternalTreeItem = ({ renderItem={renderItem} getItemId={getItemId} getItemChildren={getItemChildren} - showConnectorLine={showConnectorLine} i18nStrings={i18nStrings} renderItemToggleIcon={renderItemToggleIcon} /> From d63cc1150cfd28463be33dcde47bc0c0daa05114 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 12 Jun 2025 14:20:20 +0200 Subject: [PATCH 17/39] adjust focus offset of toggle --- pages/tree-view/basic.page.tsx | 2 +- src/internal/components/expand-toggle-button/index.tsx | 4 +++- src/internal/components/expand-toggle-button/styles.scss | 6 ++++++ src/tree-view/__tests__/a11y.test.tsx | 4 +++- src/tree-view/tree-item/index.tsx | 5 +++-- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pages/tree-view/basic.page.tsx b/pages/tree-view/basic.page.tsx index 8c386594eb..1c632b5237 100644 --- a/pages/tree-view/basic.page.tsx +++ b/pages/tree-view/basic.page.tsx @@ -341,7 +341,7 @@ export default function BasicTreeView() { const renderItemToggleIcon = (isExpanded: boolean) => { if (useDifferentIcon) { - return ; + return ; } if (useDifferentIconWithAnimation) { diff --git a/src/internal/components/expand-toggle-button/index.tsx b/src/internal/components/expand-toggle-button/index.tsx index 9fb5a28b79..ed705e924a 100644 --- a/src/internal/components/expand-toggle-button/index.tsx +++ b/src/internal/components/expand-toggle-button/index.tsx @@ -15,12 +15,14 @@ export function ExpandToggleButton({ expandButtonLabel, collapseButtonLabel, customIcon, + hasLargeFocusOffset, }: { isExpanded?: boolean; onExpandableItemToggle?: () => void; expandButtonLabel?: string; collapseButtonLabel?: string; customIcon?: React.ReactNode; + hasLargeFocusOffset?: boolean; }) { const buttonRef = useRef(null); const { tabIndex } = useSingleTabStopNavigation(buttonRef); @@ -31,7 +33,7 @@ export function ExpandToggleButton({ tabIndex={tabIndex} aria-label={isExpanded ? collapseButtonLabel : expandButtonLabel} aria-expanded={isExpanded} - className={styles['expand-toggle']} + className={clsx(styles['expand-toggle'], hasLargeFocusOffset && styles['focus-offset-large'])} onClick={onExpandableItemToggle} > {customIcon ?? ( diff --git a/src/internal/components/expand-toggle-button/styles.scss b/src/internal/components/expand-toggle-button/styles.scss index 770c218f73..e67546dcee 100644 --- a/src/internal/components/expand-toggle-button/styles.scss +++ b/src/internal/components/expand-toggle-button/styles.scss @@ -44,6 +44,12 @@ @include styles.focus-highlight(awsui.$space-button-inline-icon-focus-outline-gutter); } + &.focus-offset-large { + @include focus-visible.when-visible { + @include styles.focus-highlight(awsui.$space-button-focus-outline-gutter); + } + } + &:hover { color: awsui.$color-text-interactive-hover; } diff --git a/src/tree-view/__tests__/a11y.test.tsx b/src/tree-view/__tests__/a11y.test.tsx index b02c7a71e0..783869da90 100644 --- a/src/tree-view/__tests__/a11y.test.tsx +++ b/src/tree-view/__tests__/a11y.test.tsx @@ -7,6 +7,8 @@ import TestI18nProvider from '../../../lib/components/i18n/testing'; import createWrapper from '../../../lib/components/test-utils/dom'; import TreeView, { TreeViewProps } from '../../../lib/components/tree-view'; +import styles from '../../../lib/components/tree-view/styles.css.js'; + interface Item { id: string; title: string; @@ -76,7 +78,7 @@ test('sets aria-label', () => { const ariaLabel = 'This is the aria label for this tree view'; const { wrapper } = renderTreeView({ ariaLabel }); - expect(wrapper.find('[role=tree]')!.getElement().getAttribute('aria-label')).toEqual(ariaLabel); + expect(wrapper.findByClassName(styles.tree)!.getElement().getAttribute('aria-label')).toEqual(ariaLabel); }); test('sets aria-expanded on expandable items', () => { diff --git a/src/tree-view/tree-item/index.tsx b/src/tree-view/tree-item/index.tsx index 620d2a10e5..8a2f65e4d0 100644 --- a/src/tree-view/tree-item/index.tsx +++ b/src/tree-view/tree-item/index.tsx @@ -74,10 +74,11 @@ const InternalTreeItem = ({
                    onItemToggle({ id, item, expanded: !isExpanded })} + customIcon={customIcon} + hasLargeFocusOffset={true} expandButtonLabel={i18n('i18nStrings.expandButtonLabel', i18nStrings?.expandButtonLabel?.(item))} collapseButtonLabel={i18n('i18nStrings.collapseButtonLabel', i18nStrings?.collapseButtonLabel?.(item))} - customIcon={customIcon} + onExpandableItemToggle={() => onItemToggle({ id, item, expanded: !isExpanded })} />
                    )} From 20520ced882bc3bbd6e4be7fe0fee7aa94d664fc Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 12 Jun 2025 14:47:49 +0200 Subject: [PATCH 18/39] use internal ExpandToggle in table and update snapshots --- .../__snapshots__/documenter.test.ts.snap | 9 ---- .../test-utils-selectors.test.tsx.snap | 1 - src/table/body-cell/td-element.tsx | 2 +- .../expandable-rows/expand-toggle-button.tsx | 42 --------------- src/table/expandable-rows/motion.scss | 13 ----- src/table/expandable-rows/styles.scss | 53 ------------------- src/test-utils/dom/table/index.ts | 4 +- src/tree-view/internal.tsx | 2 +- src/tree-view/test-classes/styles.scss | 8 +-- 9 files changed, 6 insertions(+), 128 deletions(-) delete mode 100644 src/table/expandable-rows/expand-toggle-button.tsx delete mode 100644 src/table/expandable-rows/motion.scss delete mode 100644 src/table/expandable-rows/styles.scss diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index d6a191726f..3e3011f2b9 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20635,15 +20635,6 @@ Use it to have different icons/animations for toggle.", ], "type": "((isExpanded: boolean) => React.ReactNode)", }, - { - "name": "showConnectorLine", - "optional": true, - "systemTags": [ - "core -If \`true\`, adds connecting lines between child items and their expanded parent items.", - ], - "type": "boolean", - }, ], "regions": [], "releaseStatus": "stable", diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 9233d82f9d..5669111bfc 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -584,7 +584,6 @@ exports[`test-utils selectors 1`] = ` "awsui_body-cell-editor_c6tup", "awsui_body-cell_c6tup", "awsui_empty_wih1l", - "awsui_expand-toggle_1ss49", "awsui_header-cell-ascending_1spae", "awsui_header-cell-descending_1spae", "awsui_header-controls_wih1l", diff --git a/src/table/body-cell/td-element.tsx b/src/table/body-cell/td-element.tsx index cb5b3f5643..8418e3b85e 100644 --- a/src/table/body-cell/td-element.tsx +++ b/src/table/body-cell/td-element.tsx @@ -6,10 +6,10 @@ import clsx from 'clsx'; import { useMergeRefs } from '@cloudscape-design/component-toolkit/internal'; import { copyAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; +import { ExpandToggleButton } from '../../internal/components/expand-toggle-button'; import { useSingleTabStopNavigation } from '../../internal/context/single-tab-stop-navigation-context'; import { useVisualRefresh } from '../../internal/hooks/use-visual-mode'; import { ColumnWidthStyle } from '../column-widths-utils'; -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'; diff --git a/src/table/expandable-rows/expand-toggle-button.tsx b/src/table/expandable-rows/expand-toggle-button.tsx deleted file mode 100644 index bed2c3c291..0000000000 --- a/src/table/expandable-rows/expand-toggle-button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import React, { useRef } from 'react'; -import clsx from 'clsx'; - -import InternalIcon from '../../icon/internal'; -import { useSingleTabStopNavigation } from '../../internal/context/single-tab-stop-navigation-context'; - -import styles from './styles.css.js'; - -export function ExpandToggleButton({ - isExpanded, - onExpandableItemToggle, - expandButtonLabel, - collapseButtonLabel, -}: { - isExpanded?: boolean; - onExpandableItemToggle?: () => void; - expandButtonLabel?: string; - collapseButtonLabel?: string; -}) { - const buttonRef = useRef(null); - const { tabIndex } = useSingleTabStopNavigation(buttonRef); - return ( - - ); -} diff --git a/src/table/expandable-rows/motion.scss b/src/table/expandable-rows/motion.scss deleted file mode 100644 index 692206fb56..0000000000 --- a/src/table/expandable-rows/motion.scss +++ /dev/null @@ -1,13 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ - -@use '../../internal/styles' as styles; -@use '../../internal/styles/tokens' as tokens; - -.expand-toggle-icon { - @include styles.with-motion { - transition: transform tokens.$motion-duration-rotate-90 tokens.$motion-easing-rotate-90; - } -} diff --git a/src/table/expandable-rows/styles.scss b/src/table/expandable-rows/styles.scss deleted file mode 100644 index d723555fa5..0000000000 --- a/src/table/expandable-rows/styles.scss +++ /dev/null @@ -1,53 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ - -@use '../../internal/styles/index' as styles; -@use '../../internal/styles/tokens' as awsui; -@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; - -@use './motion'; - -.expand-toggle-icon { - transform: rotate(-90deg); - - @include styles.with-direction('rtl') { - transform: rotate(90deg); - } - - &-expanded { - transform: rotate(0deg); - - @include styles.with-direction('rtl') { - transform: rotate(0deg); - } - } -} - -.expand-toggle { - @include styles.styles-reset; - cursor: pointer; - inline-size: awsui.$space-m; - block-size: awsui.$space-m; - border-block: 0; - border-inline: 0; - margin-block: 0; - margin-inline: 0; - padding-block: 0; - padding-inline: 0; - background: none; - outline: 0; - color: awsui.$color-text-interactive-default; - - @include focus-visible.when-visible { - @include styles.focus-highlight(awsui.$space-button-inline-icon-focus-outline-gutter); - } - - &:hover { - color: awsui.$color-text-interactive-hover; - } - &:active { - color: awsui.$color-text-interactive-active; - } -} diff --git a/src/test-utils/dom/table/index.ts b/src/test-utils/dom/table/index.ts index 7c7508aaaa..941c386ac2 100644 --- a/src/test-utils/dom/table/index.ts +++ b/src/test-utils/dom/table/index.ts @@ -8,8 +8,8 @@ import PaginationWrapper from '../pagination'; import PropertyFilterWrapper from '../property-filter'; import TextFilterWrapper from '../text-filter'; +import expandToggleStyles from '../../../internal/components/expand-toggle-button/styles.selectors.js'; import bodyCellStyles from '../../../table/body-cell/styles.selectors.js'; -import expandableRowsStyles from '../../../table/expandable-rows/styles.selectors.js'; import headerCellStyles from '../../../table/header-cell/styles.selectors.js'; import progressiveLoadingStyles from '../../../table/progressive-loading/styles.selectors.js'; import resizerStyles from '../../../table/resizer/styles.selectors.js'; @@ -177,7 +177,7 @@ export default class TableWrapper extends ComponentWrapper { * @param rowIndex 1-based index of the row. */ findExpandToggle(rowIndex: number): ElementWrapper | null { - return this.findNativeTable().find(`tbody tr:nth-child(${rowIndex}) .${expandableRowsStyles['expand-toggle']}`); + return this.findNativeTable().find(`tbody tr:nth-child(${rowIndex}) .${expandToggleStyles['expand-toggle']}`); } /** diff --git a/src/tree-view/internal.tsx b/src/tree-view/internal.tsx index d40503d8c3..ac3d192002 100644 --- a/src/tree-view/internal.tsx +++ b/src/tree-view/internal.tsx @@ -60,7 +60,7 @@ const InternalTreeView = ({ return (
                      Date: Thu, 12 Jun 2025 15:32:55 +0200 Subject: [PATCH 19/39] cleanup --- pages/tree-view/basic.page.tsx | 76 +++++++++++++++---------------- pages/tree-view/generate-data.ts | 2 +- src/tree-view/internal.tsx | 3 -- src/tree-view/styles.scss | 22 +++------ src/tree-view/tree-item/index.tsx | 14 ++---- src/tree-view/tree-item/utils.ts | 16 ------- 6 files changed, 49 insertions(+), 84 deletions(-) diff --git a/pages/tree-view/basic.page.tsx b/pages/tree-view/basic.page.tsx index 1c632b5237..ee9ea25ca2 100644 --- a/pages/tree-view/basic.page.tsx +++ b/pages/tree-view/basic.page.tsx @@ -14,6 +14,8 @@ import SpaceBetween from '~components/space-between'; import StatusIndicator from '~components/status-indicator'; import TreeView from '~components/tree-view'; +import ScreenshotArea from '../utils/screenshot-area'; + import styles from './styles.scss'; const progressiveStepContent = ( @@ -356,7 +358,7 @@ export default function BasicTreeView() { }; return ( - <> +

                      Basic tree view

                      setUseDifferentIcon(detail.checked)}> @@ -369,43 +371,41 @@ export default function BasicTreeView() { Use icon with animation - -
                      - - { - return { - icon: item.hideIcon ? undefined : ( - - ), - content: item.content, - secondaryContent: item.details && {item.details}, - actions: item.hasActions ? : undefined, - }; - }} - getItemId={item => item.id} - getItemChildren={item => item.children} - onItemToggle={({ detail }: any) => { - if (detail.expanded) { - return setExpandedItems(prev => [...prev, detail.item.id]); - } else { - return setExpandedItems(prev => prev.filter(id => id !== detail.item.id)); - } - }} - expandedItems={expandedItems} - i18nStrings={{ - expandButtonLabel: () => 'Expand item', - collapseButtonLabel: () => 'Collapse item', - }} - renderItemToggleIcon={renderItemToggleIcon} - /> - -
                      +
                      + + { + return { + icon: item.hideIcon ? undefined : ( + + ), + content: item.content, + secondaryContent: item.details && {item.details}, + actions: item.hasActions ? : undefined, + }; + }} + getItemId={item => item.id} + getItemChildren={item => item.children} + onItemToggle={({ detail }: any) => { + if (detail.expanded) { + return setExpandedItems(prev => [...prev, detail.item.id]); + } else { + return setExpandedItems(prev => prev.filter(id => id !== detail.item.id)); + } + }} + expandedItems={expandedItems} + i18nStrings={{ + expandButtonLabel: () => 'Expand item', + collapseButtonLabel: () => 'Collapse item', + }} + renderItemToggleIcon={renderItemToggleIcon} + /> + +
                      -
                      Expanded items: {expandedItems.map(id => `Item ${id}`).join(', ')}
                      -
                      - +
                      Expanded items: {expandedItems.map(id => `Item ${id}`).join(', ')}
                      +
                      ); } diff --git a/pages/tree-view/generate-data.ts b/pages/tree-view/generate-data.ts index 5615f4df6c..243b5f9bec 100644 --- a/pages/tree-view/generate-data.ts +++ b/pages/tree-view/generate-data.ts @@ -172,7 +172,7 @@ function flattenItems(items: Item[]) { const allItems: Item[] = []; const pushItem = (item: Item) => { - allItems.push({ ...item, children: [] }); + allItems.push(item); if (item.children) { item.children.forEach(pushItem); } diff --git a/src/tree-view/internal.tsx b/src/tree-view/internal.tsx index ac3d192002..aa9056e570 100644 --- a/src/tree-view/internal.tsx +++ b/src/tree-view/internal.tsx @@ -10,7 +10,6 @@ import { fireNonCancelableEvent } from '../internal/events'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; import { TreeViewProps } from './interfaces'; import InternalTreeItem from './tree-item'; -import { getItemPosition } from './tree-item/utils'; import styles from './styles.css.js'; import testUtilStyles from './test-classes/styles.css.js'; @@ -61,7 +60,6 @@ const InternalTreeView = ({
                        ({ index={index} expandedItems={isExpandStateControlled ? expandedItems : internalExpandedItems} i18nStrings={i18nStrings} - position={getItemPosition(index, items.length)} onItemToggle={onToggle} renderItem={renderItem} getItemId={getItemId} diff --git a/src/tree-view/styles.scss b/src/tree-view/styles.scss index af7743930e..cf897c5750 100644 --- a/src/tree-view/styles.scss +++ b/src/tree-view/styles.scss @@ -13,43 +13,33 @@ line-height: awsui.$line-height-heading-m; } -.tree { - @include styles.styles-reset-ul; -} - .tree, .treeitem-group, .treeitem { - list-style-type: none; - padding-inline-start: 0; - margin-block: 0; + @include styles.styles-reset-ul; } .treeitem { display: grid; grid-template-columns: 28px 1fr; - column-gap: 4px; + column-gap: awsui.$space-scaled-xxs; + // min-height 32px for a unified look grid-template-rows: minmax(32px, 1fr) auto auto; - overflow: visible; - position: relative; - > .connector-toggle-wrapper { + > .expand-toggle-wrapper { grid-column: 1; grid-row: 1; - padding-block-start: 6px; - position: relative; + padding-block-start: 6px; // may be a design token? > .toggle { justify-self: center; - position: relative; - // inset-block-start: 2px; } } > .structured-item-wrapper { grid-column: 2; grid-row: 1 / span 2; - padding-block-start: 6px; + padding-block-start: 6px; // may be a design token? } > .treeitem-group { diff --git a/src/tree-view/tree-item/index.tsx b/src/tree-view/tree-item/index.tsx index 8a2f65e4d0..4ad228f619 100644 --- a/src/tree-view/tree-item/index.tsx +++ b/src/tree-view/tree-item/index.tsx @@ -7,7 +7,7 @@ import { useInternalI18n } from '../../i18n/context'; import { ExpandToggleButton } from '../../internal/components/expand-toggle-button'; import InternalStructuredItem from '../../internal/components/structured-item'; import { TreeViewProps } from '../interfaces'; -import { getItemPosition, transformTreeItemProps } from './utils'; +import { transformTreeItemProps } from './utils'; import styles from '../styles.css.js'; import testUtilStyles from '../test-classes/styles.css.js'; @@ -20,7 +20,6 @@ interface InternalTreeItemProps item: T; index: number; level: number; - position: 'start' | 'middle' | 'end'; onItemToggle: (detail: TreeViewProps.ItemToggleDetail) => void; } @@ -55,21 +54,17 @@ const InternalTreeItem = ({ return (
                      • 0 ? level : undefined} data-testid={`treeitem-${id}`} > -
                        +
                        {isExpandable && (
                        ({ key={`${nextLevel}-${index}`} level={nextLevel} expandedItems={expandedItems} - position={getItemPosition(index, children.length)} + i18nStrings={i18nStrings} onItemToggle={onItemToggle} renderItem={renderItem} getItemId={getItemId} getItemChildren={getItemChildren} - i18nStrings={i18nStrings} renderItemToggleIcon={renderItemToggleIcon} /> ); diff --git a/src/tree-view/tree-item/utils.ts b/src/tree-view/tree-item/utils.ts index f27bd646b1..21974d488f 100644 --- a/src/tree-view/tree-item/utils.ts +++ b/src/tree-view/tree-item/utils.ts @@ -3,22 +3,6 @@ import { TreeViewProps } from '../interfaces'; -export function getItemPosition(index: number, itemsLength: number) { - if (index === 0 && itemsLength === 1) { - return 'end'; - } - - if (index === 0) { - return 'start'; - } - - if (index === itemsLength - 1) { - return 'end'; - } - - return 'middle'; -} - interface TransformTreeItemPropsParams extends Pick { item: any; From 68ce23814af2f698ad207133278a8fea7bcc258f Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 12 Jun 2025 15:48:37 +0200 Subject: [PATCH 20/39] remove role group from ul --- src/tree-view/tree-item/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tree-view/tree-item/index.tsx b/src/tree-view/tree-item/index.tsx index 4ad228f619..1d1810d576 100644 --- a/src/tree-view/tree-item/index.tsx +++ b/src/tree-view/tree-item/index.tsx @@ -84,7 +84,7 @@ const InternalTreeItem = ({
                        {isExpanded && children.length && ( -
                          +
                            {children.map((child, index) => { return ( Date: Thu, 12 Jun 2025 16:22:12 +0200 Subject: [PATCH 21/39] rename dev page --- pages/tree-view/{test.page.tsx => dynamic-items.page.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pages/tree-view/{test.page.tsx => dynamic-items.page.tsx} (100%) diff --git a/pages/tree-view/test.page.tsx b/pages/tree-view/dynamic-items.page.tsx similarity index 100% rename from pages/tree-view/test.page.tsx rename to pages/tree-view/dynamic-items.page.tsx From e21a09d797b9266caef5fc498a74d63b0917eeca Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Mon, 23 Jun 2025 12:20:57 +0200 Subject: [PATCH 22/39] update API and test util descriptions --- .../__snapshots__/documenter.test.ts.snap | 20 ++++----- src/test-utils/dom/tree-view/index.ts | 42 +++++++++---------- src/tree-view/interfaces.ts | 20 ++++----- 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 3e3011f2b9..d52c2d8a29 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20445,7 +20445,7 @@ exports[`Documenter definition for tree-view matches the snapshot: tree-view 1`] "events": [ { "cancelable": false, - "description": "Called when an item's expand toggle is clicked.", + "description": "Called when an item expands or collapses.", "detailInlineType": { "name": "TreeViewProps.ItemToggleDetail", "properties": [ @@ -20503,13 +20503,13 @@ Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", "type": "string", }, { - "description": "An array of expanded item IDs. Use it to control expand state of tree items.", + "description": "Provides the IDs of the expanded tree view items. It controls whether an item is expanded or collapsed.", "name": "expandedItems", "optional": true, "type": "ReadonlyArray", }, { - "description": "The nested items of the item.", + "description": "Specifies the nested items that are displayed when a tree view item gets expanded.", "inlineType": { "name": "(item: T, index: number) => ReadonlyArray", "parameters": [ @@ -20530,7 +20530,7 @@ Don't use \`ariaLabel\` and \`ariaLabelledby\` at the same time.", "type": "(item: T, index: number) => ReadonlyArray", }, { - "description": "Provides a unique identifier of each item.", + "description": "Provides a unique identifier for each tree view item.", "inlineType": { "name": "(item: T, index: number) => string", "parameters": [ @@ -20583,18 +20583,17 @@ use the \`id\` attribute, consider setting it on a parent element instead.", "type": "string", }, { - "description": "An array of tree view items.", + "description": "Specifies the items to display in the tree view.", "name": "items", "optional": false, "type": "ReadonlyArray", }, { - "description": "Use it to map your data to render the item. -For each item the below properties must be returned: + "description": "Use this property to map your data to tree view items. This property must return an object with the following properties: * \`content\` (React.ReactNode) - The content of the item. * \`icon\` (optional, React.ReactNode) - The leading icon of the item. -* \`secondaryContent\` (optional, React.ReactNode) - Secondary content of the item, displayed below the content. -* \`actions\` (optional, React.ReactNode) - Actions related to item. We recommend using a button group.", +* \`secondaryContent\` (optional, React.ReactNode) - Secondary content of the item, displayed below \`content\`. +* \`actions\` (optional, React.ReactNode) - Actions related to the item. Use [button group](/components/button-group/).", "inlineType": { "name": "(item: T, index: number) => TreeViewProps.TreeItem", "parameters": [ @@ -20630,8 +20629,7 @@ For each item the below properties must be returned: "optional": true, "systemTags": [ "core -Overrides the default expand toggle. -Use it to have different icons/animations for toggle.", +Use this property to display a custom icon in the toggle button.", ], "type": "((isExpanded: boolean) => React.ReactNode)", }, diff --git a/src/test-utils/dom/tree-view/index.ts b/src/test-utils/dom/tree-view/index.ts index 239d937697..bb8752e8f5 100644 --- a/src/test-utils/dom/tree-view/index.ts +++ b/src/test-utils/dom/tree-view/index.ts @@ -6,63 +6,63 @@ import expandToggleStyles from '../../../internal/components/expand-toggle-butto import structuredItemTestUtilStyles from '../../../internal/components/structured-item/test-classes/styles.selectors.js'; import testUtilStyles from '../../../tree-view/test-classes/styles.selectors.js'; -class TreeItemWrapper extends ComponentWrapper { +class TreeViewItemWrapper extends ComponentWrapper { /** - * Finds the content of the tree item. + * Finds the content slot of the tree view item. */ findContent(): ElementWrapper | null { return this.findByClassName(structuredItemTestUtilStyles.content); } /** - * Finds the icon of the tree item. + * Finds the icon slot of the tree view item. */ findIcon(): ElementWrapper | null { return this.findByClassName(structuredItemTestUtilStyles.icon); } /** - * Finds the secondary content of the tree item. + * Finds the secondary content slot of the tree view item. */ findSecondaryContent(): ElementWrapper | null { return this.findByClassName(structuredItemTestUtilStyles.secondary); } /** - * Finds the actions of the tree item. + * Finds the actions slot of the tree view item. */ findActions(): ElementWrapper | null { return this.findByClassName(structuredItemTestUtilStyles.actions); } /** - * Finds the expand toggle of the tree item. + * Finds the expand toggle of the tree view item. */ findItemToggle(): ElementWrapper | null { return this.findByClassName(expandToggleStyles['expand-toggle']); } /** - * Finds all visible child items of the tree item. + * Finds all visible child items of the tree view item. * @param options - * * expanded (boolean) - Flag to find the expanded/collapsed items + * * expanded (optional, boolean) - Use it to find all visible expanded or collapsed child items. */ - findChildren(options: { expanded?: boolean } = {}): Array { + findChildren(options: { expanded?: boolean } = {}): Array { const selector = getTreeItemSelector(options); - return this.findAll(selector).map(item => new TreeItemWrapper(item.getElement())); + return this.findAll(selector).map(item => new TreeViewItemWrapper(item.getElement())); } /** * Finds a visible child item by its ID. * @param id of the item to find * @param options - * * expanded (boolean) - Flag to find the expanded/collapsed item. Use it to test if item is expanded/collapsed. + * * expanded (optional, boolean) - Use it to find the visible expanded or collapsed child item. */ - findChildById(id: string, options: { expanded?: boolean } = {}): TreeItemWrapper | null { + findChildById(id: string, options: { expanded?: boolean } = {}): TreeViewItemWrapper | null { const selector = `${getTreeItemSelector(options)}[data-testid="treeitem-${id}"]`; const item = this.find(selector); - return item ? new TreeItemWrapper(item.getElement()) : null; + return item ? new TreeViewItemWrapper(item.getElement()) : null; } } @@ -70,26 +70,26 @@ export default class TreeViewWrapper extends ComponentWrapper { static rootSelector: string = testUtilStyles.root; /** - * Finds all visible tree items. + * Finds all visible tree view items. * @param options - * * expanded (boolean) - Flag to find the expanded/collapsed items + * * expanded (optional, boolean) - Use it to find all visible expanded or collapsed items. */ - findItems(options: { expanded?: boolean } = {}): Array { + findItems(options: { expanded?: boolean } = {}): Array { const selector = getTreeItemSelector(options); - return this.findAll(selector).map(item => new TreeItemWrapper(item.getElement())); + return this.findAll(selector).map(item => new TreeViewItemWrapper(item.getElement())); } /** - * Finds a visible item by its ID. + * Finds a visible tree view item by its ID. * @param id of the item to find * @param options - * * expanded (boolean) - Flag to find the expanded/collapsed item. Use it to test if item is expanded/collapsed. + * * expanded (optiona, boolean) - Use it to find the visible expanded or collapsed item. */ - findItemById(id: string, options: { expanded?: boolean } = {}): TreeItemWrapper | null { + findItemById(id: string, options: { expanded?: boolean } = {}): TreeViewItemWrapper | null { const selector = `${getTreeItemSelector(options)}[data-testid="treeitem-${id}"]`; const item = this.find(selector); - return item ? new TreeItemWrapper(item.getElement()) : null; + return item ? new TreeViewItemWrapper(item.getElement()) : null; } } diff --git a/src/tree-view/interfaces.ts b/src/tree-view/interfaces.ts index bb98cf584a..6c39939126 100644 --- a/src/tree-view/interfaces.ts +++ b/src/tree-view/interfaces.ts @@ -7,32 +7,31 @@ import { NonCancelableEventHandler } from '../internal/events'; export interface TreeViewProps extends BaseComponentProps { /** - * An array of tree view items. + * Specifies the items to display in the tree view. */ items: ReadonlyArray; /** - * Use it to map your data to render the item. - * For each item the below properties must be returned: + * Use this property to map your data to tree view items. This property must return an object with the following properties: * * `content` (React.ReactNode) - The content of the item. * * `icon` (optional, React.ReactNode) - The leading icon of the item. - * * `secondaryContent` (optional, React.ReactNode) - Secondary content of the item, displayed below the content. - * * `actions` (optional, React.ReactNode) - Actions related to item. We recommend using a button group. + * * `secondaryContent` (optional, React.ReactNode) - Secondary content of the item, displayed below `content`. + * * `actions` (optional, React.ReactNode) - Actions related to the item. Use [button group](/components/button-group/). */ renderItem: (item: T, index: number) => TreeViewProps.TreeItem; /** - * Provides a unique identifier of each item. + * Provides a unique identifier for each tree view item. */ getItemId: (item: T, index: number) => string; /** - * The nested items of the item. + * Specifies the nested items that are displayed when a tree view item gets expanded. */ getItemChildren: (item: T, index: number) => ReadonlyArray | undefined; /** - * An array of expanded item IDs. Use it to control expand state of tree items. + * Provides the IDs of the expanded tree view items. It controls whether an item is expanded or collapsed. */ expandedItems?: ReadonlyArray; @@ -55,7 +54,7 @@ export interface TreeViewProps extends BaseComponentProps { ariaDescribedby?: string; /** - * Called when an item's expand toggle is clicked. + * Called when an item expands or collapses. */ onItemToggle?: NonCancelableEventHandler>; @@ -67,8 +66,7 @@ export interface TreeViewProps extends BaseComponentProps { /** * @awsuiSystem core - * Overrides the default expand toggle. - * Use it to have different icons/animations for toggle. + * Use this property to display a custom icon in the toggle button. */ renderItemToggleIcon?: (isExpanded: boolean) => React.ReactNode; } From 45f602516555765bb9a74deca1fa36daac22b909 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Mon, 23 Jun 2025 15:49:45 +0200 Subject: [PATCH 23/39] addressed comments --- pages/tree-view/basic.page.tsx | 128 +++--------------- pages/tree-view/common.tsx | 109 +++++++++++---- pages/tree-view/dynamic-items.page.tsx | 16 ++- .../__snapshots__/documenter.test.ts.snap | 16 +-- src/test-utils/dom/tree-view/index.ts | 8 +- src/tree-view/__tests__/a11y.test.tsx | 53 +------- src/tree-view/__tests__/common.ts | 45 ++++++ src/tree-view/__tests__/tree-item.test.tsx | 61 ++------- src/tree-view/__tests__/tree-view.test.tsx | 10 +- src/tree-view/index.tsx | 7 +- src/tree-view/interfaces.ts | 14 +- src/tree-view/internal.tsx | 41 +++--- src/tree-view/styles.scss | 8 +- src/tree-view/tree-item/index.tsx | 25 ++-- src/tree-view/tree-item/utils.ts | 33 ----- 15 files changed, 234 insertions(+), 340 deletions(-) create mode 100644 src/tree-view/__tests__/common.ts delete mode 100644 src/tree-view/tree-item/utils.ts diff --git a/pages/tree-view/basic.page.tsx b/pages/tree-view/basic.page.tsx index ee9ea25ca2..a912c6c3ab 100644 --- a/pages/tree-view/basic.page.tsx +++ b/pages/tree-view/basic.page.tsx @@ -3,18 +3,19 @@ import React, { useState } from 'react'; import clsx from 'clsx'; -import { ButtonDropdown, Checkbox } from '~components'; +import { Checkbox } from '~components'; import Badge from '~components/badge'; import Box from '~components/box'; -import ButtonGroup from '~components/button-group'; import Container from '~components/container'; +import Grid from '~components/grid'; import Icon from '~components/icon'; import Popover from '~components/popover'; import SpaceBetween from '~components/space-between'; import StatusIndicator from '~components/status-indicator'; -import TreeView from '~components/tree-view'; +import TreeView, { TreeViewProps } from '~components/tree-view'; import ScreenshotArea from '../utils/screenshot-area'; +import { Actions } from './common'; import styles from './styles.scss'; @@ -214,97 +215,6 @@ const items: Item[] = [ }, ]; -function Actions( - { actionType }: { actionType?: 'button-group' | 'button-dropdown' | 'inline-button-dropdown' } = { - actionType: 'button-dropdown', - } -) { - const [pressed, setPressed] = useState(false); - - if (actionType === 'button-group') { - return ( - { - if (detail.id === 'favorite') { - setPressed(!pressed); - } - }} - /> - ); - } - - if (actionType === 'inline-button-dropdown') { - return ( - - ); - } - - return ( - - ); -} - function RdsAccessRoleTreeItemContent() { return (
                            @@ -338,40 +248,40 @@ function RdsAccessRoleTreeItemContent() { export default function BasicTreeView() { const [expandedItems, setExpandedItems] = useState>(['1', '4.1']); - const [useDifferentIcon, setUseDifferentIcon] = useState(false); - const [useDifferentIconWithAnimation, setUseDifferentIconWithAnimation] = useState(false); + const [useCustomIcon, setUseCustomIcon] = useState(false); + const [useCaretIconWithSlowerAnimation, setUseCaretIconWithSlowerAnimation] = useState(false); - const renderItemToggleIcon = (isExpanded: boolean) => { - if (useDifferentIcon) { - return ; + const renderItemToggleIcon = ({ expanded }: TreeViewProps.ItemToggleRenderIconData) => { + if (useCustomIcon) { + return ; } - if (useDifferentIconWithAnimation) { + if (useCaretIconWithSlowerAnimation) { return ( ); } }; return ( - +

                            Basic tree view

                            - setUseDifferentIcon(detail.checked)}> - Use different CDS icon + setUseCustomIcon(detail.checked)}> + Use custom icon setUseDifferentIconWithAnimation(detail.checked)} + checked={useCaretIconWithSlowerAnimation} + onChange={({ detail }) => setUseCaretIconWithSlowerAnimation(detail.checked)} > - Use icon with animation + Use caret icon with slower animation -
                            + -
                            +
                            Expanded items: {expandedItems.map(id => `Item ${id}`).join(', ')}
                            diff --git a/pages/tree-view/common.tsx b/pages/tree-view/common.tsx index 3ca144402a..ffefbe435b 100644 --- a/pages/tree-view/common.tsx +++ b/pages/tree-view/common.tsx @@ -1,7 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { useState } from 'react'; +import ButtonDropdown from '~components/button-dropdown'; import ButtonGroup from '~components/button-group'; import StatusIndicator from '~components/status-indicator/internal'; @@ -27,35 +28,93 @@ export function Content(item: Item) { ); } -export function Actions() { +export function Actions( + { actionType }: { actionType?: 'button-group' | 'button-dropdown' | 'inline-button-dropdown' } = { + actionType: 'button-dropdown', + } +) { + const [pressed, setPressed] = useState(false); + + if (actionType === 'button-group') { + return ( + { + if (detail.id === 'favorite') { + setPressed(!pressed); + } + }} + /> + ); + } + + if (actionType === 'inline-button-dropdown') { + return ( + + ); + } + return ( - {}} + ariaLabel="Control instance" + variant="icon" /> ); } diff --git a/pages/tree-view/dynamic-items.page.tsx b/pages/tree-view/dynamic-items.page.tsx index afc8819ec0..6e9d8c9fbe 100644 --- a/pages/tree-view/dynamic-items.page.tsx +++ b/pages/tree-view/dynamic-items.page.tsx @@ -12,12 +12,12 @@ import { allItems, items } from './generate-data'; const allExpandableItemIds = allItems.filter(item => item.children && item.children.length > 0).map(item => item.id); -export default function TestPage() { +export default function DynamicItemsPage() { const [expandedItems, setExpandedItems] = useState>([]); return ( <> -

                            Test performance page

                            +

                            Dynamic items page