From e793e79b885b9237fc6ae264d40f91c55deaa891 Mon Sep 17 00:00:00 2001 From: at-susie Date: Mon, 8 Sep 2025 12:07:12 +0200 Subject: [PATCH 01/32] chore: Add new inline-code variant in Box --- pages/box/inline-code-example.page.tsx | 73 +++++++++++++++++++ .../__snapshots__/documenter.test.ts.snap | 1 + src/box/interfaces.ts | 3 +- src/box/internal.tsx | 4 + src/box/text.scss | 11 +++ style-dictionary/utils/token-names.ts | 1 + style-dictionary/visual-refresh/colors.ts | 1 + 7 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 pages/box/inline-code-example.page.tsx diff --git a/pages/box/inline-code-example.page.tsx b/pages/box/inline-code-example.page.tsx new file mode 100644 index 0000000000..b94510e677 --- /dev/null +++ b/pages/box/inline-code-example.page.tsx @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import Alert from '~components/alert'; +import Box from '~components/box'; +import Flashbar from '~components/flashbar'; +import SpaceBetween from '~components/space-between'; + +export default function InlineCodeExample() { + return ( + + +
+

Example usage

+

+ When writing documentation, you can use inline code elements to highlight variables like{' '} + const myVariable = 42; or function names like{' '} + calculateTotal(). +

+
+ +
+

Alert component with inline code

+ + To configure your application, set the API_ENDPOINT environment + variable to your API URL. For example:{' '} + export API_ENDPOINT="https://api.example.com" + + + + The function getUserData() is deprecated. Please use{' '} + fetchUserProfile() instead. + +
+ +
+

Flashbar component with inline code

+ + Your application has been deployed successfully. The new version{' '} + v2.1.0 is now live at{' '} + arn:service23G2::123:distribution/23E1. + + ), + dismissible: true, + id: 'success-message', + }, + { + type: 'error', + header: 'Build failed', + content: ( + <> + The build process failed with exit code 1. Check the{' '} + package.json file for missing dependencies or run{' '} + npm install to resolve issues. + + ), + dismissible: true, + id: 'error-message', + }, + ]} + /> +
+
+
+ ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index fbc36e36b9..ffe824f8a5 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -4482,6 +4482,7 @@ Override the HTML tag by using property \`tagOverride\`.", "awsui-value-large", "awsui-key-label", "awsui-gen-ai-label", + "awsui-inline-code", ], }, "name": "variant", diff --git a/src/box/interfaces.ts b/src/box/interfaces.ts index 7d8d7a16f8..d288186a27 100644 --- a/src/box/interfaces.ts +++ b/src/box/interfaces.ts @@ -151,7 +151,8 @@ export namespace BoxProps { | 'samp' | 'awsui-key-label' | 'awsui-gen-ai-label' - | 'awsui-value-large'; + | 'awsui-value-large' + | 'awsui-inline-code'; export type Display = 'block' | 'inline' | 'inline-block' | 'none'; export type TextAlign = 'left' | 'center' | 'right'; diff --git a/src/box/internal.tsx b/src/box/internal.tsx index a084e9f15e..f9d371877a 100644 --- a/src/box/internal.tsx +++ b/src/box/internal.tsx @@ -82,5 +82,9 @@ const getTag = (variant: BoxProps.Variant, tagOverride: BoxProps['tagOverride']) return 'div'; } + if (variant === 'awsui-inline-code') { + return 'code'; + } + return variant; }; diff --git a/src/box/text.scss b/src/box/text.scss index 5df964f5f9..448068784c 100644 --- a/src/box/text.scss +++ b/src/box/text.scss @@ -53,6 +53,17 @@ font-weight: awsui.$font-box-value-large-weight; color: inherit; } + &.inline-code-variant { + @include base-styles.code-extra-defaults; + @include styles.font-body-s; + border-start-start-radius: awsui.$space-static-xxs; + border-start-end-radius: awsui.$space-static-xxs; + border-end-start-radius: awsui.$space-static-xxs; + border-end-end-radius: awsui.$space-static-xxs; + background: awsui.$color-background-inline-code; + padding-block: awsui.$space-static-xxxs; + padding-inline: awsui.$space-static-xxs; + } &.h1-variant.font-weight-default, &.h2-variant.font-weight-default, &.h3-variant.font-weight-default, diff --git a/style-dictionary/utils/token-names.ts b/style-dictionary/utils/token-names.ts index d6b7660b46..5b05fccde5 100644 --- a/style-dictionary/utils/token-names.ts +++ b/style-dictionary/utils/token-names.ts @@ -456,6 +456,7 @@ export type ColorsTokenName = | 'colorBackgroundDropdownItemHover' | 'colorBackgroundDropdownItemSelected' | 'colorBackgroundHomeHeader' + | 'colorBackgroundInlineCode' | 'colorBackgroundInputDefault' | 'colorBackgroundInputDisabled' | 'colorBackgroundItemSelected' diff --git a/style-dictionary/visual-refresh/colors.ts b/style-dictionary/visual-refresh/colors.ts index 8a44604d2d..9a54d56300 100644 --- a/style-dictionary/visual-refresh/colors.ts +++ b/style-dictionary/visual-refresh/colors.ts @@ -51,6 +51,7 @@ const tokens: StyleDictionary.ColorsDictionary = { colorBackgroundDropdownItemHover: { light: '{colorGrey200}', dark: '{colorGrey900}' }, colorBackgroundDropdownItemSelected: '{colorBackgroundItemSelected}', colorBackgroundHomeHeader: '{colorGrey950}', + colorBackgroundInlineCode: { light: 'rgba(27, 35, 45, 0.1)', dark: 'rgba(255, 255, 255, 0.15)' }, colorBackgroundInputDefault: { light: '{colorWhite}', dark: '{colorGrey850}' }, colorBackgroundInputDisabled: { light: '{colorGrey250}', dark: '{colorGrey800}' }, colorBackgroundItemSelected: { light: '{colorBlue50}', dark: '{colorBlue1000}' }, From 67c2b99e536b3c1f45d5691401d32d39abc0c45e Mon Sep 17 00:00:00 2001 From: at-susie Date: Mon, 8 Sep 2025 12:18:22 +0200 Subject: [PATCH 02/32] chore: Increase size limit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f491041e74..cb5834742f 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "869 kB", + "limit": "870 kB", "ignore": "react-dom" } ], From 91f243e6b3acb49c1ff3705f6dcbd52cf1edf6ad Mon Sep 17 00:00:00 2001 From: at-susie Date: Mon, 8 Sep 2025 17:07:05 +0200 Subject: [PATCH 03/32] chore: Updated example page --- pages/box/inline-code-example.page.tsx | 141 +++++++++++++++++++++---- 1 file changed, 121 insertions(+), 20 deletions(-) diff --git a/pages/box/inline-code-example.page.tsx b/pages/box/inline-code-example.page.tsx index b94510e677..56948ae2af 100644 --- a/pages/box/inline-code-example.page.tsx +++ b/pages/box/inline-code-example.page.tsx @@ -2,40 +2,119 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import Alert from '~components/alert'; -import Box from '~components/box'; -import Flashbar from '~components/flashbar'; -import SpaceBetween from '~components/space-between'; +import { Alert, Box, Flashbar, Header, Link, SpaceBetween, Table } from '~components'; export default function InlineCodeExample() { return (
-

Example usage

+

Example usage in paragraph

When writing documentation, you can use inline code elements to highlight variables like{' '} const myVariable = 42; or function names like{' '} - calculateTotal(). + calculateTotal(). For example:{' '} + export API_ENDPOINT="https://api.example.com"

-

Alert component with inline code

- - To configure your application, set the API_ENDPOINT environment - variable to your API URL. For example:{' '} - export API_ENDPOINT="https://api.example.com" - + + `Displaying items ${firstIndex} to ${lastIndex} of ${totalItemsCount}` + } + columnDefinitions={[ + { + id: 'variable', + header: 'Variable name', + cell: item => {item.name || '-'}, + sortingField: 'name', + isRowHeader: true, + }, + { + id: 'alt', + header: 'Text value', + cell: item => item.alt || '-', + sortingField: 'alt', + }, + { + id: 'description', + header: 'Description', + cell: item => item.description || '-', + }, + ]} + enableKeyboardNavigation={true} + items={[ + { + name: 'Item 1', + alt: 'First', + description: ( + <> + This is the first db.t2.large item + + ), + type: '1A', + size: 'Small', + }, + { + name: 'Item 2', + alt: 'Second', + description: ( + <> + This is the second S3-aws-phoenix.example.com item + + ), + type: '1B', + size: 'Large', + }, + { + name: 'Item 3', + alt: 'Third', + description: '-', + type: '1A', + size: 'Large', + }, + { + name: 'Item 4', + alt: 'Fourth', + description: 'This is the fourth item', + type: '2A', + size: 'Small', + }, + ]} + loadingText="Loading resources" + sortingDisabled={true} + header={
In table
} + /> + + +
+

In Alert component

+ + + To configure your application, set the API_ENDPOINT environment + variable to your API URL. + + + + The function getUserData() is deprecated. Please use{' '} + fetchUserProfile() instead. + - - The function getUserData() is deprecated. Please use{' '} - fetchUserProfile() instead. - + + The function getUserData() is deprecated. Please use{' '} + fetchUserProfile() instead. + + + + The function getUserData() is deprecated. Please use{' '} + fetchUserProfile() instead. + +
-

Flashbar component with inline code

+

In Flashbar component

arn:service23G2::123:distribution/23E1. ), - dismissible: true, id: 'success-message', }, { @@ -56,14 +134,37 @@ export default function InlineCodeExample() { header: 'Build failed', content: ( <> - The build process failed with exit code 1. Check the{' '} + The build process failed with exit code 1. Check the{' '} package.json file for missing dependencies or run{' '} npm install to resolve issues. ), - dismissible: true, id: 'error-message', }, + { + type: 'info', + header: 'Build information', + content: ( + <> + The build process failed with exit code 1. Check the{' '} + package.json file for missing dependencies or run{' '} + npm install to resolve issues. + + ), + id: 'info-message', + }, + { + type: 'warning', + header: 'Build failed', + content: ( + <> + The build process failed with exit code 1. Check the{' '} + package.json file for missing dependencies or run{' '} + npm install to resolve issues. + + ), + id: 'warning-message', + }, ]} />
From e52b266147a78a50f0aba02ce93810622256cd8a Mon Sep 17 00:00:00 2001 From: at-susie Date: Mon, 8 Sep 2025 17:49:36 +0200 Subject: [PATCH 04/32] chore: Update dark mode background color and example page --- pages/box/inline-code-example.page.tsx | 319 +++++++++++----------- style-dictionary/visual-refresh/colors.ts | 2 +- 2 files changed, 162 insertions(+), 159 deletions(-) diff --git a/pages/box/inline-code-example.page.tsx b/pages/box/inline-code-example.page.tsx index 56948ae2af..bbc986a5e1 100644 --- a/pages/box/inline-code-example.page.tsx +++ b/pages/box/inline-code-example.page.tsx @@ -6,169 +6,172 @@ import { Alert, Box, Flashbar, Header, Link, SpaceBetween, Table } from '~compon export default function InlineCodeExample() { return ( - - -
-

Example usage in paragraph

-

- When writing documentation, you can use inline code elements to highlight variables like{' '} - const myVariable = 42; or function names like{' '} - calculateTotal(). For example:{' '} - export API_ENDPOINT="https://api.example.com" -

-
+ <> +

Inline-code examples

+ + +
+

Example usage in paragraph

+

+ When writing documentation, you can use inline code elements to highlight variables like{' '} + const myVariable = 42; or function names like{' '} + calculateTotal(). For example:{' '} + export API_ENDPOINT="https://api.example.com" +

+
-
-
- `Displaying items ${firstIndex} to ${lastIndex} of ${totalItemsCount}` - } - columnDefinitions={[ - { - id: 'variable', - header: 'Variable name', - cell: item => {item.name || '-'}, - sortingField: 'name', - isRowHeader: true, - }, - { - id: 'alt', - header: 'Text value', - cell: item => item.alt || '-', - sortingField: 'alt', - }, - { - id: 'description', - header: 'Description', - cell: item => item.description || '-', - }, - ]} - enableKeyboardNavigation={true} - items={[ - { - name: 'Item 1', - alt: 'First', - description: ( - <> - This is the first db.t2.large item - - ), - type: '1A', - size: 'Small', - }, - { - name: 'Item 2', - alt: 'Second', - description: ( - <> - This is the second S3-aws-phoenix.example.com item - - ), - type: '1B', - size: 'Large', - }, - { - name: 'Item 3', - alt: 'Third', - description: '-', - type: '1A', - size: 'Large', - }, - { - name: 'Item 4', - alt: 'Fourth', - description: 'This is the fourth item', - type: '2A', - size: 'Small', - }, - ]} - loadingText="Loading resources" - sortingDisabled={true} - header={
In table
} - /> - +
+
+ `Displaying items ${firstIndex} to ${lastIndex} of ${totalItemsCount}` + } + columnDefinitions={[ + { + id: 'variable', + header: 'Variable name', + cell: item => {item.name || '-'}, + sortingField: 'name', + isRowHeader: true, + }, + { + id: 'alt', + header: 'Text value', + cell: item => item.alt || '-', + sortingField: 'alt', + }, + { + id: 'description', + header: 'Description', + cell: item => item.description || '-', + }, + ]} + enableKeyboardNavigation={true} + items={[ + { + name: 'Item 1', + alt: 'First', + description: ( + <> + This is the first db.t2.large item + + ), + type: '1A', + size: 'Small', + }, + { + name: 'Item 2', + alt: 'Second', + description: ( + <> + This is the second S3-aws-phoenix.example.com item + + ), + type: '1B', + size: 'Large', + }, + { + name: 'Item 3', + alt: 'Third', + description: '-', + type: '1A', + size: 'Large', + }, + { + name: 'Item 4', + alt: 'Fourth', + description: 'This is the fourth item', + type: '2A', + size: 'Small', + }, + ]} + loadingText="Loading resources" + sortingDisabled={true} + header={
In table
} + /> + -
-

In Alert component

- - - To configure your application, set the API_ENDPOINT environment - variable to your API URL. - +
+

In Alert component

+ + + To configure your application, set the API_ENDPOINT environment + variable to your API URL. + - - The function getUserData() is deprecated. Please use{' '} - fetchUserProfile() instead. - + + The function getUserData() is deprecated. Please use{' '} + fetchUserProfile() instead. + - - The function getUserData() is deprecated. Please use{' '} - fetchUserProfile() instead. - + + The function getUserData() is deprecated. Please use{' '} + fetchUserProfile() instead. + - - The function getUserData() is deprecated. Please use{' '} - fetchUserProfile() instead. - - -
+ + The function getUserData() is deprecated. Please use{' '} + fetchUserProfile() instead. + +
+
-
-

In Flashbar component

- - Your application has been deployed successfully. The new version{' '} - v2.1.0 is now live at{' '} - arn:service23G2::123:distribution/23E1. - - ), - id: 'success-message', - }, - { - type: 'error', - header: 'Build failed', - content: ( - <> - The build process failed with exit code 1. Check the{' '} - package.json file for missing dependencies or run{' '} - npm install to resolve issues. - - ), - id: 'error-message', - }, - { - type: 'info', - header: 'Build information', - content: ( - <> - The build process failed with exit code 1. Check the{' '} - package.json file for missing dependencies or run{' '} - npm install to resolve issues. - - ), - id: 'info-message', - }, - { - type: 'warning', - header: 'Build failed', - content: ( - <> - The build process failed with exit code 1. Check the{' '} - package.json file for missing dependencies or run{' '} - npm install to resolve issues. - - ), - id: 'warning-message', - }, - ]} - /> -
- - +
+

In Flashbar component

+ + Your application has been deployed successfully. The new version{' '} + v2.1.0 is now live at{' '} + arn:service23G2::123:distribution/23E1. + + ), + id: 'success-message', + }, + { + type: 'error', + header: 'Build failed', + content: ( + <> + The build process failed with exit code 1. Check the{' '} + package.json file for missing dependencies or run{' '} + npm install to resolve issues. + + ), + id: 'error-message', + }, + { + type: 'info', + header: 'Build information', + content: ( + <> + The build process failed with exit code 1. Check the{' '} + package.json file for missing dependencies or run{' '} + npm install to resolve issues. + + ), + id: 'info-message', + }, + { + type: 'warning', + header: 'Build failed', + content: ( + <> + The build process failed with exit code 1. Check the{' '} + package.json file for missing dependencies or run{' '} + npm install to resolve issues. + + ), + id: 'warning-message', + }, + ]} + /> +
+ + + ); } diff --git a/style-dictionary/visual-refresh/colors.ts b/style-dictionary/visual-refresh/colors.ts index 9a54d56300..2411f859cf 100644 --- a/style-dictionary/visual-refresh/colors.ts +++ b/style-dictionary/visual-refresh/colors.ts @@ -51,7 +51,7 @@ const tokens: StyleDictionary.ColorsDictionary = { colorBackgroundDropdownItemHover: { light: '{colorGrey200}', dark: '{colorGrey900}' }, colorBackgroundDropdownItemSelected: '{colorBackgroundItemSelected}', colorBackgroundHomeHeader: '{colorGrey950}', - colorBackgroundInlineCode: { light: 'rgba(27, 35, 45, 0.1)', dark: 'rgba(255, 255, 255, 0.15)' }, + colorBackgroundInlineCode: { light: 'rgba(0, 0, 0, 0.1)', dark: 'rgba(255, 255, 255, 0.1)' }, colorBackgroundInputDefault: { light: '{colorWhite}', dark: '{colorGrey850}' }, colorBackgroundInputDisabled: { light: '{colorGrey250}', dark: '{colorGrey800}' }, colorBackgroundItemSelected: { light: '{colorBlue50}', dark: '{colorBlue1000}' }, From 8e5d46878c0a6250f58d701b54ee20f9e6c8299a Mon Sep 17 00:00:00 2001 From: at-susie Date: Mon, 8 Sep 2025 18:26:02 +0200 Subject: [PATCH 05/32] Add specific background opacity value for flashbar context for a11y color contrast --- style-dictionary/visual-refresh/contexts/flashbar.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/style-dictionary/visual-refresh/contexts/flashbar.ts b/style-dictionary/visual-refresh/contexts/flashbar.ts index 092077c745..0cbda207ae 100644 --- a/style-dictionary/visual-refresh/contexts/flashbar.ts +++ b/style-dictionary/visual-refresh/contexts/flashbar.ts @@ -24,6 +24,7 @@ export const baseTokens: StyleDictionary.ColorsDictionary = { colorBorderDividerDefault: '{colorGrey100}', colorTextTutorialHotspotDefault: '{colorGrey300}', colorTextTutorialHotspotHover: '{colorGrey100}', + colorBackgroundInlineCode: 'rgba(0, 0, 0, 0.15)', }; const expandedTokens: StyleDictionary.ExpandedColorScopeDictionary = expandColorDictionary( From b6f297573f59b923e93677823545350318db589e Mon Sep 17 00:00:00 2001 From: at-susie Date: Mon, 8 Sep 2025 18:34:43 +0200 Subject: [PATCH 06/32] Adjsut background opacity for warning flashbar --- style-dictionary/visual-refresh/contexts/flashbar-warning.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/style-dictionary/visual-refresh/contexts/flashbar-warning.ts b/style-dictionary/visual-refresh/contexts/flashbar-warning.ts index 3f11e758ae..9ef63247b0 100644 --- a/style-dictionary/visual-refresh/contexts/flashbar-warning.ts +++ b/style-dictionary/visual-refresh/contexts/flashbar-warning.ts @@ -39,6 +39,9 @@ export const sharedTokens: StyleDictionary.ColorsDictionary = { // Tutorial hotspot colorTextTutorialHotspotDefault: '{colorGrey600}', colorTextTutorialHotspotHover: '{colorGrey900}', + + // Inline-code variant background in Box + colorBackgroundInlineCode: 'rgba(0, 0, 0, 0.1)', }; const tokens: StyleDictionary.ColorsDictionary = { From b0f7d1cdf59822bb0b8dd7c773198d8f57ff7048 Mon Sep 17 00:00:00 2001 From: at-susie Date: Mon, 8 Sep 2025 21:36:37 +0200 Subject: [PATCH 07/32] Update example page --- pages/box/inline-code-example.page.tsx | 28 ++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/pages/box/inline-code-example.page.tsx b/pages/box/inline-code-example.page.tsx index bbc986a5e1..ae8a623f7a 100644 --- a/pages/box/inline-code-example.page.tsx +++ b/pages/box/inline-code-example.page.tsx @@ -2,7 +2,22 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { Alert, Box, Flashbar, Header, Link, SpaceBetween, Table } from '~components'; +import Alert from '~components/alert'; +import Box from '~components/box'; +import Flashbar from '~components/flashbar'; +import Header from '~components/header'; +import Link from '~components/link'; +import SpaceBetween from '~components/space-between'; +import Table from '~components/table'; +import { TableProps } from '~components/table'; + +interface TableItem { + name: string; + alt: string; + description: React.ReactNode; + type: string; + size: string; +} export default function InlineCodeExample() { return ( @@ -21,28 +36,29 @@ export default function InlineCodeExample() {
-
+ + totalItemsCount={4} + renderAriaLive={({ firstIndex, lastIndex, totalItemsCount }: TableProps.LiveAnnouncement) => `Displaying items ${firstIndex} to ${lastIndex} of ${totalItemsCount}` } columnDefinitions={[ { id: 'variable', header: 'Variable name', - cell: item => {item.name || '-'}, + cell: (item: TableItem) => {item.name || '-'}, sortingField: 'name', isRowHeader: true, }, { id: 'alt', header: 'Text value', - cell: item => item.alt || '-', + cell: (item: TableItem) => item.alt || '-', sortingField: 'alt', }, { id: 'description', header: 'Description', - cell: item => item.description || '-', + cell: (item: TableItem) => item.description || '-', }, ]} enableKeyboardNavigation={true} From dc6fd451f6099d05aa96d8105a593a70142b3a3a Mon Sep 17 00:00:00 2001 From: Georgii Lobko <47106899+georgylobko@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:27:24 +0200 Subject: [PATCH 08/32] feat: AI drawer header actions (#3824) --- package.json | 2 +- .../external-global-left-panel-widget.tsx | 47 ++++++---- pages/app-layout/utils/external-widget.tsx | 31 +++++-- .../__integ__/runtime-drawers.test.ts | 9 +- .../runtime-drawers-widgetized.test.tsx | 2 +- .../__tests__/runtime-drawers.test.tsx | 57 ++++++++---- src/app-layout/__tests__/utils.tsx | 29 +++++-- src/app-layout/runtime-drawer/index.tsx | 24 +++++ .../drawer/global-ai-drawer.tsx | 80 ++++++++++------- .../drawer/global-drawer.tsx | 87 ++++++++++++------- .../visual-refresh-toolbar/interfaces.ts | 4 + src/button-group/icon-button-item.tsx | 5 +- src/button-group/interfaces.ts | 12 +++ src/internal/plugins/controllers/drawers.ts | 3 + src/internal/plugins/widget/interfaces.ts | 3 + 15 files changed, 281 insertions(+), 114 deletions(-) diff --git a/package.json b/package.json index cb5834742f..cbff2ebddf 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "870 kB", + "limit": "890 kB", "ignore": "react-dom" } ], diff --git a/pages/app-layout/utils/external-global-left-panel-widget.tsx b/pages/app-layout/utils/external-global-left-panel-widget.tsx index 445905672d..007fd959f0 100644 --- a/pages/app-layout/utils/external-global-left-panel-widget.tsx +++ b/pages/app-layout/utils/external-global-left-panel-widget.tsx @@ -4,8 +4,6 @@ import React from 'react'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; import { Box } from '~components'; -import Button from '~components/button'; -import ButtonDropdown from '~components/button-dropdown'; import { registerLeftDrawer } from '~components/internal/plugins/widget'; import styles from '../styles.scss'; @@ -69,20 +67,37 @@ registerLeftDrawer({ unmountContent: container => unmountComponentAtNode(container), mountHeader: container => { - ReactDOM.render( -
-
AI Panel
-
- -
-
, - container - ); + ReactDOM.render(
AI Panel
, container); }, unmountHeader: container => unmountComponentAtNode(container), + + headerActions: [ + { + type: 'menu-dropdown', + id: 'more-actions', + text: 'More actions', + items: [ + { + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + { + id: 'remove', + iconName: 'remove', + text: 'Remove', + }, + ], + }, + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, }); diff --git a/pages/app-layout/utils/external-widget.tsx b/pages/app-layout/utils/external-widget.tsx index 1fd2d3fa2b..d010b011e2 100644 --- a/pages/app-layout/utils/external-widget.tsx +++ b/pages/app-layout/utils/external-widget.tsx @@ -4,7 +4,6 @@ import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; import Box from '~components/box'; -import ButtonDropdown from '~components/button-dropdown'; import Drawer from '~components/drawer'; import awsuiPlugins from '~components/internal/plugins'; @@ -67,6 +66,17 @@ awsuiPlugins.appLayout.registerDrawer({ ReactDOM.render(, container); }, unmountContent: container => unmountComponentAtNode(container), + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, }); awsuiPlugins.appLayout.registerDrawer({ @@ -140,6 +150,7 @@ awsuiPlugins.appLayout.registerDrawer({ content: 'Content', triggerButton: 'Trigger button', resizeHandle: 'Resize handle', + expandedModeButton: 'Expanded mode button', }, onToggle: event => { console.log('circle-global drawer on toggle', event.detail); @@ -158,12 +169,7 @@ awsuiPlugins.appLayout.registerDrawer({ mountContent: (container, mountContext) => { ReactDOM.render( - Global drawer} - headerActions={ - - } - > + Global drawer}> global widget content circle 1 {new Array(100).fill(null).map((_, index) => ( @@ -176,6 +182,17 @@ awsuiPlugins.appLayout.registerDrawer({ ); }, unmountContent: container => unmountComponentAtNode(container), + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, }); awsuiPlugins.appLayout.registerDrawer({ diff --git a/src/app-layout/__integ__/runtime-drawers.test.ts b/src/app-layout/__integ__/runtime-drawers.test.ts index e8becfdabc..f2597a21dc 100644 --- a/src/app-layout/__integ__/runtime-drawers.test.ts +++ b/src/app-layout/__integ__/runtime-drawers.test.ts @@ -3,12 +3,11 @@ import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; -import createWrapper, { AppLayoutWrapper } from '../../../lib/components/test-utils/selectors'; +import createWrapper, { AppLayoutWrapper, ButtonGroupWrapper } from '../../../lib/components/test-utils/selectors'; import { Theme } from '../../__integ__/utils.js'; import { viewports } from './constants'; import { getUrlParams } from './utils'; -import testUtilsStyles from '../../../lib/components/app-layout/test-classes/styles.selectors.js'; import vrDrawerStyles from '../../../lib/components/app-layout/visual-refresh/styles.selectors.js'; import vrToolbarDrawerStyles from '../../../lib/components/app-layout/visual-refresh-toolbar/drawer/styles.selectors.js'; @@ -21,9 +20,9 @@ const findDrawerContentById = (wrapper: AppLayoutWrapper, id: string) => { }; const findExpandedModeButtonByActiveDrawerId = (wrapper: AppLayoutWrapper, id: string) => { - return wrapper.find( - `[data-testid="awsui-app-layout-drawer-${id}"] .${testUtilsStyles['active-drawer-expanded-mode-button']}` - ); + return wrapper + .findComponent(`[data-testid="awsui-app-layout-drawer-${id}"]`, ButtonGroupWrapper)! + .findButtonById('expand'); }; describe.each(['classic', 'refresh', 'refresh-toolbar'] as Theme[])('%s', theme => { diff --git a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx index 1bcd908e42..dcc32b9095 100644 --- a/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers-widgetized.test.tsx @@ -168,7 +168,7 @@ describeEachAppLayout({ themes: ['refresh-toolbar'] }, ({ size }) => { if (size === 'mobile') { expect(globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerDefaults.id)).toBeFalsy(); } else { - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerDefaults.id)!.click(); + createWrapper().findButtonGroup()!.findButtonById('expand')!.click(); expect(globalDrawersWrapper.findDrawerById(drawerDefaults.id)!.isDrawerInExpandedMode()).toBe(true); expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(true); globalDrawersWrapper.findLeaveExpandedModeButtonInAIDrawer()!.click(); diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index 55112c3250..45ddfdbb06 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -50,6 +50,7 @@ async function renderComponent(jsx: React.ReactElement) { globalDrawersWrapper, rerender, getByTestId, + container, ...rest, }; } @@ -946,6 +947,9 @@ describe('toolbar mode only features', () => { awsuiWidgetPlugins.registerLeftDrawer(payload as WidgetDrawerPayload); } }; + const findDrawerHeaderActionById = (id: string, renderProps: Awaited>) => { + return createWrapper(renderProps.container).findButtonGroup()!.findButtonById(id); + }; test('renders resize handle for a global drawer when config is enabled', async () => { registerDrawer({ @@ -990,7 +994,7 @@ describe('toolbar mode only features', () => { findDrawerTriggerById('global-drawer', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer')!.getElement()).toBeInTheDocument(); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer')).toBeNull(); }); @@ -1074,7 +1078,7 @@ describe('toolbar mode only features', () => { findDrawerTriggerById('global-drawer-1', renderProps)!.focus(); findDrawerTriggerById('global-drawer-1', renderProps)!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')).toBeNull(); await waitFor(() => { expect(findDrawerTriggerById('global-drawer-1', renderProps)!.getElement()).toHaveFocus(); @@ -1099,7 +1103,7 @@ describe('toolbar mode only features', () => { await delay(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.getElement()).toBeInTheDocument(); expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(false); @@ -1129,7 +1133,7 @@ describe('toolbar mode only features', () => { expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true); expect(onVisibilityChangeMock).toHaveBeenCalledWith(true); - globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer-1')!.click(); expect(onVisibilityChangeMock).toHaveBeenCalledWith(false); }); @@ -1186,6 +1190,28 @@ describe('toolbar mode only features', () => { renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId('global-drawer')!.click(); expect(onToggle).toHaveBeenCalledWith({ isOpen: false, initiatedByUserAction: true }); }); + + test(`calls onHeaderActionClick handler by clicking on drawers header action button in left runtime drawer)`, async () => { + const onHeaderActionClick = jest.fn(); + registerDrawer({ + ...drawerDefaults, + id: 'global-drawer', + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + onHeaderActionClick: event => onHeaderActionClick(event.detail), + }); + const renderProps = await renderComponent(); + findDrawerTriggerById('global-drawer', renderProps)!.click(); + + findDrawerHeaderActionById('add', renderProps)!.click(); + expect(onHeaderActionClick).toHaveBeenCalledWith({ id: 'add' }); + }); }); test('the order of the opened global drawers should match the positions of their corresponding toggle buttons on the toolbar', async () => { @@ -1442,36 +1468,37 @@ describe('toolbar mode only features', () => { findDrawerTriggerById(drawerId, renderProps)!.click(); expect( - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() ).toBeInTheDocument(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); + expect( getGeneratedAnalyticsMetadata( - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() ) ).toEqual( expect.objectContaining({ action: 'expand', - detail: { + detail: expect.objectContaining({ label: 'Expanded mode button', - }, + }), }) ); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(false); expect( getGeneratedAnalyticsMetadata( - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.getElement() ) ).toEqual( expect.objectContaining({ action: 'collapse', - detail: { + detail: expect.objectContaining({ label: 'Expanded mode button', - }, + }), }) ); }); @@ -1545,10 +1572,10 @@ describe('toolbar mode only features', () => { await delay(); findDrawerTriggerById(drawerId, renderProps)!.click(); - globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); + renderProps.globalDrawersWrapper.findExpandedModeButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.findDrawerById(drawerId)!.isDrawerInExpandedMode()).toBe(true); expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(true); - globalDrawersWrapper.findCloseButtonByActiveDrawerId(drawerId)!.click(); + renderProps.globalDrawersWrapper.findCloseButtonByActiveDrawerId(drawerId)!.click(); expect(globalDrawersWrapper.isLayoutInDrawerExpandedMode()).toBe(false); }); }); diff --git a/src/app-layout/__tests__/utils.tsx b/src/app-layout/__tests__/utils.tsx index a52b6a6da7..9e8eb95fb2 100644 --- a/src/app-layout/__tests__/utils.tsx +++ b/src/app-layout/__tests__/utils.tsx @@ -12,7 +12,12 @@ import AppLayout, { AppLayoutProps } from '../../../lib/components/app-layout'; import customCssProps from '../../../lib/components/internal/generated/custom-css-properties'; import { forceMobileModeSymbol } from '../../../lib/components/internal/hooks/use-mobile'; import { SplitPanelProps } from '../../../lib/components/split-panel'; -import createWrapper, { AppLayoutWrapper, ElementWrapper } from '../../../lib/components/test-utils/dom'; +import createWrapper, { + AppLayoutWrapper, + ButtonGroupWrapper, + ButtonWrapper, + ElementWrapper, +} from '../../../lib/components/test-utils/dom'; import testutilStyles from '../../../lib/components/app-layout/test-classes/styles.css.js'; import visualRefreshStyles from '../../../lib/components/app-layout/visual-refresh/styles.css.js'; @@ -206,16 +211,22 @@ export const getGlobalDrawersTestUtils = (wrapper: AppLayoutWrapper) => { ); }, - findCloseButtonByActiveDrawerId(id: string): ElementWrapper | null { - return wrapper.find( - `.${testutilStyles['active-drawer']}[data-testid="awsui-app-layout-drawer-${id}"] .${testutilStyles['active-drawer-close-button']}` - ); + findCloseButtonByActiveDrawerId(id: string): ButtonWrapper | null { + return wrapper + .findComponent( + `.${testutilStyles['active-drawer']}[data-testid="awsui-app-layout-drawer-${id}"]`, + ButtonGroupWrapper + )! + .findButtonById('close'); }, - findExpandedModeButtonByActiveDrawerId(id: string): ElementWrapper | null { - return wrapper.find( - `.${testutilStyles['active-drawer']}[data-testid="awsui-app-layout-drawer-${id}"] .${testutilStyles['active-drawer-expanded-mode-button']}` - ); + findExpandedModeButtonByActiveDrawerId(id: string): ButtonWrapper | null { + return wrapper + .findComponent( + `.${testutilStyles['active-drawer']}[data-testid="awsui-app-layout-drawer-${id}"]`, + ButtonGroupWrapper + )! + .findButtonById('expand'); }, findLeaveExpandedModeButtonInAIDrawer(): ElementWrapper | null { diff --git a/src/app-layout/runtime-drawer/index.tsx b/src/app-layout/runtime-drawer/index.tsx index 2a40354c1c..0336991328 100644 --- a/src/app-layout/runtime-drawer/index.tsx +++ b/src/app-layout/runtime-drawer/index.tsx @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useContext, useEffect, useRef } from 'react'; +import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; + +import { ButtonGroupProps } from '../../button-group/interfaces'; import { fireNonCancelableEvent, NonCancelableEventHandler } from '../../internal/events'; import { DrawerConfig as RuntimeDrawerConfig, @@ -79,11 +82,29 @@ function RuntimeDrawerHeader({ mountHeader, unmountHeader }: RuntimeContentHeade return
; } +function checkForUnsupportedProps(headerActions: ReadonlyArray) { + const unsupportedProps = new Set([ + 'iconSvg', + 'popoverFeedback', + 'pressedIconSvg', + 'popoverFeedback', + 'pressedPopoverFeedback', + ]); + for (const item of headerActions) { + const unsupported = Object.keys(item).filter(key => unsupportedProps.has(key)); + if (unsupported.length > 0) { + warnOnce('AppLayout', `The headerActions properties are not supported for runtime api: ${unsupported.join(' ')}`); + } + } + return headerActions; +} + export const mapRuntimeConfigToDrawer = ( runtimeConfig: RuntimeDrawerConfig ): AppLayoutProps.Drawer & { orderPriority?: number; onToggle?: NonCancelableEventHandler; + headerActions?: ReadonlyArray; } => { const { mountContent, unmountContent, trigger, ...runtimeDrawer } = runtimeConfig; @@ -111,6 +132,7 @@ export const mapRuntimeConfigToDrawer = ( onResize: event => { fireNonCancelableEvent(runtimeDrawer.onResize, { size: event.detail.size, id: runtimeDrawer.id }); }, + headerActions: runtimeDrawer.headerActions ? checkForUnsupportedProps(runtimeDrawer.headerActions) : undefined, }; }; @@ -119,6 +141,7 @@ export const mapRuntimeConfigToAiDrawer = ( ): AppLayoutProps.Drawer & { orderPriority?: number; onToggle?: NonCancelableEventHandler; + headerActions?: ReadonlyArray; } => { const { mountContent, unmountContent, trigger, ...runtimeDrawer } = runtimeConfig; @@ -153,6 +176,7 @@ export const mapRuntimeConfigToAiDrawer = ( onResize: event => { fireNonCancelableEvent(runtimeDrawer.onResize, { size: event.detail.size, id: runtimeDrawer.id }); }, + headerActions: runtimeDrawer.headerActions ? checkForUnsupportedProps(runtimeDrawer.headerActions) : undefined, }; }; diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx index 7c37fa33bf..fc4ff30fba 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-ai-drawer.tsx @@ -4,7 +4,8 @@ import React, { useRef } from 'react'; import { Transition } from 'react-transition-group'; import clsx from 'clsx'; -import { InternalButton } from '../../../button/internal'; +import { InternalItemOrGroup } from '../../../button-group/interfaces'; +import ButtonGroup from '../../../button-group/internal'; import PanelResizeHandle from '../../../internal/components/panel-resize-handle'; import customCssProps from '../../../internal/generated/custom-css-properties'; import { usePrevious } from '../../../internal/hooks/use-previous'; @@ -90,6 +91,37 @@ export function AppLayoutGlobalAiDrawerImplementation({ // (window is between mobile and desktop sizes). At this point, the drawer can't be // resized in either direction, so we disable the resize handler const isResizingDisabled = maxAiDrawerSize < activeAiDrawerSize; + let drawerActions: ReadonlyArray = [ + { + type: 'icon-button', + id: 'close', + iconName: isMobile ? 'close' : 'angle-left', + text: computedAriaLabels.closeButton, + analyticsAction: 'close', + }, + ]; + if (!isMobile && activeAiDrawer?.isExpandable) { + drawerActions = [ + { + type: 'icon-button', + id: 'expand', + iconName: isExpanded ? 'shrink' : 'expand', + text: activeAiDrawer?.ariaLabels?.expandedModeButton ?? '', + analyticsAction: isExpanded ? 'expand' : 'collapse', + }, + ...drawerActions, + ]; + } + if (activeAiDrawer?.headerActions) { + drawerActions = [ + { + type: 'group', + text: 'Actions', + items: activeAiDrawer.headerActions!, + }, + ...drawerActions, + ]; + } return ( @@ -152,34 +184,24 @@ export function AppLayoutGlobalAiDrawerImplementation({
{activeAiDrawer?.header ??
}
- {!isMobile && activeAiDrawer?.isExpandable && ( -
- setExpandedDrawerId(isExpanded ? null : activeDrawerId!)} - variant="icon" - analyticsAction={isExpanded ? 'expand' : 'collapse'} - /> -
- )} -
- onActiveAiDrawerChange?.(null, { initiatedByUserAction: true })} - ref={aiDrawerFocusControl?.refs.close} - variant="icon" - analyticsAction="close" - /> -
+ { + switch (event.detail.id) { + case 'close': + onActiveAiDrawerChange?.(null, { initiatedByUserAction: true }); + break; + case 'expand': + setExpandedDrawerId(isExpanded ? null : activeDrawerId!); + break; + default: + activeAiDrawer?.onHeaderActionClick?.(event); + } + }} + ariaLabel="Left panel actions" + items={drawerActions} + />
{!isMobile && isExpanded && activeAiDrawer?.ariaLabels?.exitExpandedModeButton && ( diff --git a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx index 3acfa57ae4..27ed111208 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx +++ b/src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx @@ -4,11 +4,13 @@ import React, { useRef } from 'react'; import { Transition } from 'react-transition-group'; import clsx from 'clsx'; -import { InternalButton } from '../../../button/internal'; +import { InternalItemOrGroup } from '../../../button-group/interfaces'; +import ButtonGroup from '../../../button-group/internal'; import PanelResizeHandle from '../../../internal/components/panel-resize-handle'; import customCssProps from '../../../internal/generated/custom-css-properties'; import { usePrevious } from '../../../internal/hooks/use-previous'; import { getLimitedValue } from '../../../split-panel/utils/size-utils'; +import { Focusable } from '../../utils/use-focus-control'; import { getDrawerStyles } from '../compute-layout'; import { AppLayoutInternals, InternalDrawer } from '../interfaces'; import { useResize } from './use-resize'; @@ -74,6 +76,37 @@ function AppLayoutGlobalDrawerImplementation({ const animationDisabled = (activeGlobalDrawer?.defaultActive && !drawersOpenQueue.includes(activeGlobalDrawer.id)) || (wasExpanded && !isExpanded); + let drawerActions: ReadonlyArray = [ + { + type: 'icon-button', + id: 'close', + iconName: isMobile ? 'close' : 'angle-right', + text: computedAriaLabels.closeButton ?? '', + analyticsAction: 'close', + }, + ]; + if (!isMobile && activeGlobalDrawer?.isExpandable) { + drawerActions = [ + { + type: 'icon-button', + id: 'expand', + iconName: isExpanded ? 'shrink' : 'expand', + text: activeGlobalDrawer?.ariaLabels?.expandedModeButton ?? '', + analyticsAction: isExpanded ? 'expand' : 'collapse', + }, + ...drawerActions, + ]; + } + if (activeGlobalDrawer?.headerActions) { + drawerActions = [ + { + type: 'group', + text: 'Actions', + items: activeGlobalDrawer.headerActions!, + }, + ...drawerActions, + ]; + } return ( @@ -146,34 +179,30 @@ function AppLayoutGlobalDrawerImplementation({ data-testid={`awsui-app-layout-drawer-content-${activeDrawerId}`} >
- {!isMobile && activeGlobalDrawer?.isExpandable && ( -
- setExpandedDrawerId(isExpanded ? null : activeDrawerId)} - variant="icon" - analyticsAction={isExpanded ? 'expand' : 'collapse'} - /> -
- )} -
- onActiveGlobalDrawersChange(activeDrawerId, { initiatedByUserAction: true })} - ref={refs?.close} - variant="icon" - analyticsAction="close" - /> -
+ { + switch (event.detail.id) { + case 'close': + onActiveGlobalDrawersChange(activeDrawerId, { initiatedByUserAction: true }); + break; + case 'expand': + setExpandedDrawerId(isExpanded ? null : activeDrawerId); + break; + default: + activeGlobalDrawer?.onHeaderActionClick?.(event); + } + }} + ariaLabel="Global panel actions" + items={drawerActions} + __internalRootRef={(root: HTMLElement) => { + if (!root) { + return; + } + refs.close = { current: root.querySelector('[data-itemid="close"]') as unknown as Focusable }; + }} + />
{activeGlobalDrawer?.content} diff --git a/src/app-layout/visual-refresh-toolbar/interfaces.ts b/src/app-layout/visual-refresh-toolbar/interfaces.ts index 509ba31cfa..968a690e95 100644 --- a/src/app-layout/visual-refresh-toolbar/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/interfaces.ts @@ -4,7 +4,9 @@ import React from 'react'; import { BreadcrumbGroupProps } from '../../breadcrumb-group/interfaces'; +import { ButtonGroupProps } from '../../button-group/interfaces'; import { SplitPanelSideToggleProps } from '../../internal/context/split-panel-context'; +import { NonCancelableEventHandler } from '../../internal/events'; import { SomeOptional } from '../../internal/types'; import { AppLayoutProps, AppLayoutPropsWithDefaults } from '../interfaces'; import { SplitPanelProviderProps } from '../split-panel'; @@ -22,6 +24,8 @@ export type InternalDrawer = AppLayoutProps.Drawer & { isExpandable?: boolean; ariaLabels: AppLayoutProps.Drawer['ariaLabels'] & { expandedModeButton?: string; exitExpandedModeButton?: string }; header?: React.ReactNode; + headerActions?: ReadonlyArray; + onHeaderActionClick?: NonCancelableEventHandler; }; // Widgetization notice: structures in this file are shared multiple app layout instances, possibly different minor versions. diff --git a/src/button-group/icon-button-item.tsx b/src/button-group/icon-button-item.tsx index 9b7040b5da..4a2861f141 100644 --- a/src/button-group/icon-button-item.tsx +++ b/src/button-group/icon-button-item.tsx @@ -10,12 +10,12 @@ import { InternalButton } from '../button/internal.js'; import Tooltip from '../internal/components/tooltip/index.js'; import { CancelableEventHandler, fireCancelableEvent } from '../internal/events/index.js'; import InternalLiveRegion from '../live-region/internal.js'; -import { ButtonGroupProps } from './interfaces.js'; +import { ButtonGroupProps, InternalIconButton } from './interfaces.js'; import testUtilStyles from './test-classes/styles.css.js'; interface IconButtonItemProps { - item: ButtonGroupProps.IconButton; + item: InternalIconButton; showTooltip: boolean; showFeedback: boolean; onTooltipDismiss: () => void; @@ -55,6 +55,7 @@ const IconButtonItem = forwardRef( data-testid={item.id} data-itemid={item.id} className={clsx(testUtilStyles.item, testUtilStyles['button-group-item'])} + analyticsAction={item.analyticsAction} __title="" > {item.text} diff --git a/src/button-group/interfaces.ts b/src/button-group/interfaces.ts index 02c327a689..00d434c8eb 100644 --- a/src/button-group/interfaces.ts +++ b/src/button-group/interfaces.ts @@ -105,10 +105,22 @@ export interface ButtonGroupProps extends BaseComponentProps { style?: ButtonGroupProps.Style; } +export interface InternalIconButton extends ButtonGroupProps.IconButton { + analyticsAction?: string; +} + +export type InternalItemOrGroup = InternalItem | ButtonGroupProps.Group; +export type InternalItem = + | InternalIconButton + | ButtonGroupProps.IconToggleButton + | ButtonGroupProps.IconFileInput + | ButtonGroupProps.MenuDropdown; + export interface InternalButtonGroupProps extends SomeRequired, InternalBaseComponentProps { style?: ButtonGroupProps.Style; + items: ReadonlyArray; } export namespace ButtonGroupProps { diff --git a/src/internal/plugins/controllers/drawers.ts b/src/internal/plugins/controllers/drawers.ts index ecc9d3058f..87d4c20473 100644 --- a/src/internal/plugins/controllers/drawers.ts +++ b/src/internal/plugins/controllers/drawers.ts @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { ButtonGroupProps } from '../../../button-group/interfaces'; import debounce from '../../debounce'; import { NonCancelableEventHandler } from '../../events'; import { reportRuntimeApiWarning } from '../helpers/metrics'; @@ -40,6 +41,8 @@ export interface DrawerConfig { unmountContent: (container: HTMLElement) => void; preserveInactiveContent?: boolean; onToggle?: NonCancelableEventHandler; + headerActions?: ReadonlyArray; + onHeaderActionClick?: NonCancelableEventHandler; } const updatableProperties = [ diff --git a/src/internal/plugins/widget/interfaces.ts b/src/internal/plugins/widget/interfaces.ts index 3c3f00fc1b..24a4c870a8 100644 --- a/src/internal/plugins/widget/interfaces.ts +++ b/src/internal/plugins/widget/interfaces.ts @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { ButtonGroupProps } from '../../../button-group/interfaces'; import { NonCancelableEventHandler } from '../../events'; interface Message { @@ -44,6 +45,8 @@ export interface DrawerPayload { onToggle?: NonCancelableEventHandler; mountHeader?: (container: HTMLElement) => void; unmountHeader?: (container: HTMLElement) => void; + headerActions?: ReadonlyArray; + onHeaderActionClick?: NonCancelableEventHandler; } export type RegisterDrawerMessage = Message<'registerLeftDrawer', DrawerPayload>; From d5b41ab430a246181ceb9379c0b28c898d75e3c2 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Tue, 9 Sep 2025 18:49:36 +0200 Subject: [PATCH 09/32] chore: Uses updated stsn testing utils (#3850) --- src/button/__tests__/button.test.tsx | 27 ++++++++++++------- src/checkbox/__tests__/checkbox.test.tsx | 27 ++++++++++++------- src/link/__tests__/index.test.tsx | 25 ++++++++++++----- src/popover/__tests__/popover.test.tsx | 27 ++++++++++++------- .../__tests__/radio-group.test.tsx | 21 +++++++++------ src/table/__tests__/body-cell.test.tsx | 15 ++++++++--- src/table/__tests__/header-cell.test.tsx | 17 +++++++----- 7 files changed, 104 insertions(+), 55 deletions(-) diff --git a/src/button/__tests__/button.test.tsx b/src/button/__tests__/button.test.tsx index c3124a79cb..4d3f62edfe 100644 --- a/src/button/__tests__/button.test.tsx +++ b/src/button/__tests__/button.test.tsx @@ -4,7 +4,10 @@ import React from 'react'; import { act, fireEvent, render } from '@testing-library/react'; import { clearMessageCache } from '@cloudscape-design/component-toolkit/internal'; -import { renderWithSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal/testing'; +import { + setTestSingleTabStopNavigationTarget, + TestSingleTabStopNavigationProvider, +} from '@cloudscape-design/component-toolkit/internal/testing'; import Button, { ButtonProps } from '../../../lib/components/button'; import InternalButton from '../../../lib/components/button/internal'; @@ -811,30 +814,34 @@ describe('table grid navigation support', () => { } test('does not override tab index when keyboard navigation is not active', () => { - renderWithSingleTabStopNavigation(
+ ); - setCurrentTarget(getButton('#button1')); + setTestSingleTabStopNavigationTarget(getButton('#button1')); expect(getButton('#button1')).toHaveAttribute('tabIndex', '0'); expect(getButton('#button2')).toHaveAttribute('tabIndex', '-1'); }); test('does not override explicit tab index with 0', () => { - const { setCurrentTarget } = renderWithSingleTabStopNavigation( -
+ render( + -
+ ); - setCurrentTarget(getButton('#button1')); + setTestSingleTabStopNavigationTarget(getButton('#button1')); expect(getButton('#button1')).toHaveAttribute('tabIndex', '-2'); expect(getButton('#button2')).toHaveAttribute('tabIndex', '-2'); }); diff --git a/src/checkbox/__tests__/checkbox.test.tsx b/src/checkbox/__tests__/checkbox.test.tsx index 7912f158cc..6020cb5d68 100644 --- a/src/checkbox/__tests__/checkbox.test.tsx +++ b/src/checkbox/__tests__/checkbox.test.tsx @@ -3,7 +3,10 @@ import React, { useState } from 'react'; import { render } from '@testing-library/react'; -import { renderWithSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal/testing'; +import { + setTestSingleTabStopNavigationTarget, + TestSingleTabStopNavigationProvider, +} from '@cloudscape-design/component-toolkit/internal/testing'; import Checkbox, { CheckboxProps } from '../../../lib/components/checkbox'; import InternalCheckbox from '../../../lib/components/checkbox/internal'; @@ -243,30 +246,34 @@ describe('table grid navigation support', () => { } test('does not override tab index when keyboard navigation is not active', () => { - renderWithSingleTabStopNavigation(, { navigationActive: false }); + render( + + + + ); expect(getCheckboxInput('#checkbox')).not.toHaveAttribute('tabIndex'); }); test('overrides tab index when keyboard navigation is active', () => { - const { setCurrentTarget } = renderWithSingleTabStopNavigation( -
+ render( + -
+ ); - setCurrentTarget(getCheckboxInput('#checkbox1')); + setTestSingleTabStopNavigationTarget(getCheckboxInput('#checkbox1')); expect(getCheckboxInput('#checkbox1')).toHaveAttribute('tabIndex', '0'); expect(getCheckboxInput('#checkbox2')).toHaveAttribute('tabIndex', '-1'); }); test('does not override explicit tab index with 0', () => { - const { setCurrentTarget } = renderWithSingleTabStopNavigation( -
+ render( + -
+ ); - setCurrentTarget(getCheckboxInput('#checkbox1')); + setTestSingleTabStopNavigationTarget(getCheckboxInput('#checkbox1')); expect(getCheckboxInput('#checkbox1')).toHaveAttribute('tabIndex', '-1'); expect(getCheckboxInput('#checkbox2')).toHaveAttribute('tabIndex', '-1'); }); diff --git a/src/link/__tests__/index.test.tsx b/src/link/__tests__/index.test.tsx index 996234d020..4b92151b9b 100644 --- a/src/link/__tests__/index.test.tsx +++ b/src/link/__tests__/index.test.tsx @@ -3,7 +3,10 @@ import React from 'react'; import { act, render } from '@testing-library/react'; -import { renderWithSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal/testing'; +import { + setTestSingleTabStopNavigationTarget, + TestSingleTabStopNavigationProvider, +} from '@cloudscape-design/component-toolkit/internal/testing'; import { KeyCode } from '@cloudscape-design/test-utils-core/utils'; import FormField from '../../../lib/components/form-field'; @@ -285,23 +288,31 @@ describe('table grid navigation support', () => { } test('does not override tab index for button link when keyboard navigation is not active', () => { - renderWithSingleTabStopNavigation(, { navigationActive: false }); + render( + + + + ); expect(getLink('#link')).toHaveAttribute('tabIndex', '0'); }); test('does not override tab index for anchor link when keyboard navigation is not active', () => { - renderWithSingleTabStopNavigation(, { navigationActive: false }); + render( + + + + ); expect(getLink('#link')).not.toHaveAttribute('tabIndex'); }); test.each([undefined, '#'])('overrides tab index when keyboard navigation is active href=%s', href => { - const { setCurrentTarget } = renderWithSingleTabStopNavigation( -
+ render( + -
+ ); - setCurrentTarget(getLink('#link1')); + setTestSingleTabStopNavigationTarget(getLink('#link1')); expect(getLink('#link1')).toHaveAttribute('tabIndex', '0'); expect(getLink('#link2')).toHaveAttribute('tabIndex', '-1'); }); diff --git a/src/popover/__tests__/popover.test.tsx b/src/popover/__tests__/popover.test.tsx index e129bbfd79..daf786f83f 100644 --- a/src/popover/__tests__/popover.test.tsx +++ b/src/popover/__tests__/popover.test.tsx @@ -3,7 +3,10 @@ import React from 'react'; import { act, render } from '@testing-library/react'; -import { renderWithSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal/testing'; +import { + setTestSingleTabStopNavigationTarget, + TestSingleTabStopNavigationProvider, +} from '@cloudscape-design/component-toolkit/internal/testing'; import { KeyCode } from '@cloudscape-design/test-utils-core/utils'; import '../../__a11y__/to-validate-a11y'; @@ -366,29 +369,33 @@ describe('table grid navigation support', () => { } test('does not override tab index when keyboard navigation is not active', () => { - renderWithSingleTabStopNavigation(Trigger, { navigationActive: false }); + render( + + Trigger + + ); expect(getTrigger()).not.toHaveAttribute('tabIndex'); }); test('overrides tab index when keyboard navigation is active', () => { - const { setCurrentTarget } = renderWithSingleTabStopNavigation( -
+ render( + Trigger Trigger -
+ ); - setCurrentTarget(getTrigger('#popover1')); + setTestSingleTabStopNavigationTarget(getTrigger('#popover1')); expect(getTrigger('#popover1')).toHaveAttribute('tabIndex', '0'); expect(getTrigger('#popover2')).toHaveAttribute('tabIndex', '-1'); }); test('does not override tab index for custom trigger', () => { - const { setCurrentTarget } = renderWithSingleTabStopNavigation( -
+ render( + Trigger -
+ ); - setCurrentTarget(getTrigger()); + setTestSingleTabStopNavigationTarget(getTrigger()); expect(getTrigger()).not.toHaveAttribute('tabIndex'); }); }); diff --git a/src/radio-group/__tests__/radio-group.test.tsx b/src/radio-group/__tests__/radio-group.test.tsx index 662c82b8ec..14e707c30b 100644 --- a/src/radio-group/__tests__/radio-group.test.tsx +++ b/src/radio-group/__tests__/radio-group.test.tsx @@ -3,7 +3,10 @@ import React, { useState } from 'react'; import { act, render } from '@testing-library/react'; -import { renderWithSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal/testing'; +import { + setTestSingleTabStopNavigationTarget, + TestSingleTabStopNavigationProvider, +} from '@cloudscape-design/component-toolkit/internal/testing'; import '../../__a11y__/to-validate-a11y'; import RadioGroup, { RadioGroupProps } from '../../../lib/components/radio-group'; @@ -369,20 +372,22 @@ describe('table grid navigation support', () => { } test('does not override tab index when keyboard navigation is not active', () => { - renderWithSingleTabStopNavigation(, { - navigationActive: false, - }); + render( + + + + ); expect(getRadioInput('#radio')).not.toHaveAttribute('tabIndex'); }); test('overrides tab index when keyboard navigation is active', () => { - const { setCurrentTarget } = renderWithSingleTabStopNavigation( -
+ render( + -
+ ); - setCurrentTarget(getRadioInput('#radio1')); + setTestSingleTabStopNavigationTarget(getRadioInput('#radio1')); expect(getRadioInput('#radio1')).toHaveAttribute('tabIndex', '0'); expect(getRadioInput('#radio2')).toHaveAttribute('tabIndex', '-1'); }); diff --git a/src/table/__tests__/body-cell.test.tsx b/src/table/__tests__/body-cell.test.tsx index 9b5e62c88c..5101175c37 100644 --- a/src/table/__tests__/body-cell.test.tsx +++ b/src/table/__tests__/body-cell.test.tsx @@ -3,7 +3,10 @@ import * as React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { renderWithSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal/testing'; +import { + setTestSingleTabStopNavigationTarget, + TestSingleTabStopNavigationProvider, +} from '@cloudscape-design/component-toolkit/internal/testing'; import { LiveRegionController } from '../../../lib/components/live-region/controller.js'; import { TableBodyCell, TableBodyCellProps } from '../../../lib/components/table/body-cell'; @@ -336,13 +339,17 @@ describe('TableBodyCell', () => { }); test('does not set tab index when negative', () => { - const { setCurrentTarget } = renderWithSingleTabStopNavigation(, { navigationActive: true }); + render( + + + + ); const tableCell = wrapper().find('td')!.getElement(); expect(tableCell).not.toHaveAttribute('tabIndex'); - setCurrentTarget(tableCell); + setTestSingleTabStopNavigationTarget(tableCell); expect(tableCell).toHaveAttribute('tabIndex', '0'); - setCurrentTarget(null); + setTestSingleTabStopNavigationTarget(null); expect(tableCell).not.toHaveAttribute('tabIndex'); }); diff --git a/src/table/__tests__/header-cell.test.tsx b/src/table/__tests__/header-cell.test.tsx index f22c1c47b9..a525db2467 100644 --- a/src/table/__tests__/header-cell.test.tsx +++ b/src/table/__tests__/header-cell.test.tsx @@ -4,7 +4,10 @@ import * as React from 'react'; import { fireEvent, render } from '@testing-library/react'; import { ContainerQueryEntry } from '@cloudscape-design/component-toolkit'; -import { renderWithSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal/testing'; +import { + setTestSingleTabStopNavigationTarget, + TestSingleTabStopNavigationProvider, +} from '@cloudscape-design/component-toolkit/internal/testing'; import TestI18nProvider from '../../../lib/components/i18n/testing'; import { TableHeaderCell, TableHeaderCellProps } from '../../../lib/components/table/header-cell'; @@ -124,15 +127,17 @@ describe('i18n', () => { }); test('does not set tab index when negative', () => { - const { setCurrentTarget } = renderWithSingleTabStopNavigation(, { - navigationActive: true, - }); + render( + + + + ); const headerCell = document.querySelector('th')!; expect(headerCell).not.toHaveAttribute('tabIndex'); - setCurrentTarget(headerCell); + setTestSingleTabStopNavigationTarget(headerCell); expect(headerCell).toHaveAttribute('tabIndex', '0'); - setCurrentTarget(null); + setTestSingleTabStopNavigationTarget(null); expect(headerCell).not.toHaveAttribute('tabIndex'); }); }); From 562af68da7f01b2794da599044c26e1f8d3f5efa Mon Sep 17 00:00:00 2001 From: Avinash Dwarapu Date: Wed, 10 Sep 2025 12:00:39 +0200 Subject: [PATCH 10/32] fix: Allow interactive elements to be placed inside key of a key-value pair (#3845) --- src/key-value-pairs/internal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/key-value-pairs/internal.tsx b/src/key-value-pairs/internal.tsx index efe5e276b8..318a8ff02b 100644 --- a/src/key-value-pairs/internal.tsx +++ b/src/key-value-pairs/internal.tsx @@ -19,9 +19,9 @@ const InternalKeyValuePair = ({ label, info, value, id }: KeyValuePairsProps.Pai return ( <>
-