From 03712a342cc23005c8c496d764c462d075b9494a Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 7 Jan 2026 14:15:59 +0100 Subject: [PATCH 1/5] feat: Feature notifications plugin API --- build-tools/tasks/package-json.js | 3 + build-tools/utils/files.js | 1 + package-lock.json | 1 + package.json | 2 +- .../feature-prompt.page.tsx | 386 +++++- src/__a11y__/a11y-page-object.ts | 1 + .../test-utils-selectors.test.tsx.snap | 1 + src/__tests__/utils.tsx | 1 + .../widget-contract-old.test.tsx.snap | 12 + ...get-contract-split-panel-old.test.tsx.snap | 8 + .../runtime-feature-notifications.test.tsx | 442 ++++++ src/app-layout/utils/use-drawers.ts | 10 +- .../feature-notifications-drawer-content.tsx | 94 ++ .../visual-refresh-toolbar/drawer/styles.scss | 8 + .../visual-refresh-toolbar/interfaces.ts | 2 + .../state/use-app-layout.tsx | 18 + .../state/use-feature-notifications.tsx | 229 +++ .../toolbar/drawer-triggers.tsx | 15 +- .../visual-refresh-toolbar/toolbar/index.tsx | 4 + .../toolbar/trigger-button/index.tsx | 12 +- .../widget-areas/before-main-slot.tsx | 7 +- src/drawer/use-sticky-footer.ts | 4 +- src/i18n/messages-types.ts | 1233 +++++++++-------- src/i18n/messages/all.ar.json | 8 + src/i18n/messages/all.de.json | 8 + src/i18n/messages/all.en-GB.json | 8 + src/i18n/messages/all.en.json | 9 + src/i18n/messages/all.es.json | 8 + src/i18n/messages/all.fr.json | 8 + src/i18n/messages/all.id.json | 8 + src/i18n/messages/all.it.json | 8 + src/i18n/messages/all.ja.json | 8 + src/i18n/messages/all.ko.json | 8 + src/i18n/messages/all.pt-BR.json | 8 + src/i18n/messages/all.tr.json | 8 + src/i18n/messages/all.zh-CN.json | 8 + src/i18n/messages/all.zh-TW.json | 8 + .../components/chart-popover/index.tsx | 4 +- .../__tests__/feature-prompt.test.tsx | 29 +- .../do-not-use/feature-prompt/interfaces.ts | 3 +- .../do-not-use/feature-prompt/internal.tsx | 50 +- .../do-not-use/feature-prompt/styles.scss | 2 +- src/internal/persistence/index.ts | 49 +- src/internal/plugins/widget.ts | 9 +- src/internal/plugins/widget/core.ts | 12 +- src/internal/plugins/widget/index.ts | 42 +- src/internal/plugins/widget/interfaces.ts | 64 +- src/internal/utils/__tests__/promises.test.ts | 54 +- src/internal/utils/promises.ts | 32 + src/plugins/index.ts | 15 + src/popover/body.tsx | 8 +- 51 files changed, 2258 insertions(+), 722 deletions(-) create mode 100644 src/app-layout/__tests__/runtime-feature-notifications.test.tsx create mode 100644 src/app-layout/visual-refresh-toolbar/drawer/feature-notifications-drawer-content.tsx create mode 100644 src/app-layout/visual-refresh-toolbar/state/use-feature-notifications.tsx create mode 100644 src/plugins/index.ts diff --git a/build-tools/tasks/package-json.js b/build-tools/tasks/package-json.js index 22340db17f..a0d076df35 100644 --- a/build-tools/tasks/package-json.js +++ b/build-tools/tasks/package-json.js @@ -24,6 +24,8 @@ function getComponentsExports() { // Plugin api './internal/plugins': './internal/plugins/index.js', './internal/plugins/widget': './internal/plugins/widget.js', + // Public plugin api + './plugins': './plugins/index.js', // Public internal components './internal/tooltip-do-not-use': './internal/tooltip-do-not-use/index.js', './internal/do-not-use/chart-filter': './internal/do-not-use/chart-filter.js', @@ -32,6 +34,7 @@ function getComponentsExports() { './internal/do-not-use/i18n': './internal/do-not-use/i18n.js', './internal/do-not-use/tooltip': './internal/do-not-use/tooltip.js', './internal/do-not-use/drag-handle': './internal/do-not-use/drag-handle.js', + './internal/do-not-use/feature-prompt': './internal/do-not-use/feature-prompt/index.js', './internal/widget-exports': './internal/widget-exports.js', './test-utils/dom/internal/drag-handle': './test-utils/dom/internal/drag-handle.js', './test-utils/selectors/internal/drag-handle': './test-utils/selectors/internal/drag-handle.js', diff --git a/build-tools/utils/files.js b/build-tools/utils/files.js index 4ed5d018ed..5ce4c9f9a6 100644 --- a/build-tools/utils/files.js +++ b/build-tools/utils/files.js @@ -22,6 +22,7 @@ function listPublicItems(baseDir) { elem !== 'test-utils' && elem !== 'i18n' && elem !== 'theming' && + elem !== 'plugins' && elem !== 'contexts' ); } diff --git a/package-lock.json b/package-lock.json index d663110273..66823ea2d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@cloudscape-design/components", "version": "3.0.0", + "hasInstallScript": true, "dependencies": { "@cloudscape-design/collection-hooks": "^1.0.0", "@cloudscape-design/component-toolkit": "^1.0.0-beta", diff --git a/package.json b/package.json index e3890a05dd..5e0c145c9f 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "1150 kB", + "limit": "1250 kB", "ignore": "react-dom" } ], diff --git a/pages/feature-notifications/feature-prompt.page.tsx b/pages/feature-notifications/feature-prompt.page.tsx index e2601d1704..38cdaefb85 100644 --- a/pages/feature-notifications/feature-prompt.page.tsx +++ b/pages/feature-notifications/feature-prompt.page.tsx @@ -1,18 +1,201 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useEffect, useRef } from 'react'; +import React, { useContext, useEffect, useRef } from 'react'; -import { AppLayout, Box, Button, Header, Icon, Link, SpaceBetween } from '~components'; +import { + AppLayoutProps, + AppLayoutToolbar, + Badge, + Box, + Button, + Container, + Header, + Icon, + Link, + SpaceBetween, + Toggle, +} from '~components'; +import { I18nProvider } from '~components/i18n'; +import messages from '~components/i18n/messages/all.all'; import FeaturePrompt, { FeaturePromptProps } from '~components/internal/do-not-use/feature-prompt'; +import awsuiPlugins from '~components/internal/plugins'; +import { + clearFeatureNotifications, + FeatureNotificationsPersistenceConfig, + registerFeatureNotifications, + showFeaturePromptIfPossible, +} from '~components/internal/plugins/widget'; import { mount, unmount } from '~mount'; +import AppContext, { AppContextType } from '../app/app-context'; import { Breadcrumbs, Containers, Navigation, Tools } from '../app-layout/utils/content-blocks'; import labels from '../app-layout/utils/labels'; import * as toolsContent from '../app-layout/utils/tools-content'; +import { IframeWrapper } from '../utils/iframe-wrapper'; import ScreenshotArea from '../utils/screenshot-area'; +awsuiPlugins.appLayout.registerDrawer({ + id: 'security', + + ariaLabels: { + closeButton: 'Security close button', + content: 'Security drawer content', + triggerButton: 'Security trigger button', + resizeHandle: 'Security resize handle', + }, + + trigger: { + iconSvg: ` + + + `, + }, + + onToggle: event => { + console.log('security drawer on toggle', event.detail); + awsuiPlugins.appLayout.updateDrawer({ id: 'security', defaultActive: event.detail.isOpen }); + }, + + resizable: true, + defaultSize: 320, + + onResize: event => { + awsuiPlugins.appLayout.updateDrawer({ id: 'security', defaultSize: event.detail.size }); + }, + + mountContent: container => { + mount(
sldkfjsdkl
, container); + }, + unmountContent: container => unmount(container), + headerActions: [ + { + type: 'icon-button', + id: 'add', + iconName: 'add-plus', + text: 'Add', + }, + ], + onHeaderActionClick: ({ detail }) => { + console.log('onHeaderActionClick: ', detail); + }, +}); + +registerFeatureNotifications({ + id: 'local-feature-notifications', + suppressFeaturePrompt: false, + featuresPageLink: '/#/feature-notifications/feature-prompt?appLayoutToolbar=true', + // by default all features older than 90 days old are filtered out, + // so to keep them stable this function needs to always return true + filterFeatures: () => true, + features: [ + { + id: '1', + header: New feature, events with more resource tags, + content: ( + + You can now enrich CloudTrail events with additional information by adding resources tags and IAM global keys + in CloudTrail lake.{' '} + + Learn more + + + ), + contentCategory: ( + + Event coverage + + ), + releaseDate: new Date('2025-11-01'), + }, + { + id: '2', + header: ( + Enhanced filtering options for CloudTrail events ingested into event data stores + ), + content: ( + <> + + More enhanced filtering options provide tighter control over your AWS activity data, improving the + efficiency and precision of security, compliance, and operational investigations.{' '} + + View user guide + + + + + + + ), + releaseDate: new Date('2025-07-28'), + }, + { + id: '3', + header: Introducing Application Map, + content: ( + <> + + Use application map to automatically discover and organize your services into groups based on your business + needs. Identify root cause faster instead of troubleshooting isolated symptoms with operational signals such + as SLOs, health indicators, and top insights in a contextual drawer.{' '} + + Learn more + + + + ), + contentCategory: Operational investigations, + releaseDate: new Date('2025-08-01'), + }, + ], + mountItem: (container, data) => { + mount(data, container); + + return () => unmount(container); + }, + persistenceConfig: { + uniqueKey: 'feature-notifications', + }, + __persistFeatureNotifications: async function ( + persistenceConfig: FeatureNotificationsPersistenceConfig, + value: Record + ) { + const result = await new Promise(resolve => + setTimeout(() => { + localStorage.setItem(persistenceConfig.uniqueKey, JSON.stringify(value)); + resolve(); + }, 150) + ); + return result; + }, + __retrieveFeatureNotifications: async function (persistenceConfig: FeatureNotificationsPersistenceConfig) { + const result = await new Promise>(resolve => + setTimeout( + () => + resolve( + localStorage.getItem(persistenceConfig.uniqueKey) + ? JSON.parse(localStorage.getItem(persistenceConfig.uniqueKey)!) + : {} + ), + 150 + ) + ); + return result; + }, +}); + +type DemoContext = React.Context< + AppContextType<{ + hasTools: boolean | undefined; + hasDrawers: boolean | undefined; + splitPanelPosition: AppLayoutProps.SplitPanelPreferences['position']; + }> +>; + export default function () { const featurePromptRef = useRef(null); + const triggerRef = useRef(null); + const { urlParams, setUrlParams } = useContext(AppContext as DemoContext); + const hasTools = urlParams.hasTools ?? false; useEffect(() => { const root = document.createElement('div'); @@ -34,48 +217,163 @@ export default function () { return ( - { - // handle focus behavior here - }} - position="bottom" - header={ - - Our AI buddy is smarter than ever - - } - content={ - - It supports filtering with plain language, reports generation with .pdf, and so much more! See{' '} - top 10 things it can do for you. - - } - getTrack={() => document.querySelector('#settings-icon')} - /> - } - navigation={} - tools={{toolsContent.long}} - content={ - <> -
-
- Demo page -
-
- - - - } - /> + + { + triggerRef.current?.focus(); + }} + position="bottom" + header={ + + Our AI buddy is smarter than ever + + } + content={ + + It supports filtering with plain language, reports generation with .pdf, and so much more! See{' '} + top 10 things it can do for you. + + } + getTrack={() => document.querySelector('#settings-icon')} + trackKey="settings-icon" + /> + } + navigation={} + tools={{toolsContent.long}} + toolsHide={!hasTools} + content={ + ( + <> +
+
+ Demo page +
+
+ setUrlParams({ hasTools: detail.checked })}> + Use Tools + + + + + + + + + + + + + + + )} + /> + } + /> +
); } diff --git a/src/__a11y__/a11y-page-object.ts b/src/__a11y__/a11y-page-object.ts index f75251b3ca..48d749e04a 100644 --- a/src/__a11y__/a11y-page-object.ts +++ b/src/__a11y__/a11y-page-object.ts @@ -115,6 +115,7 @@ function ariaLevelViolationsFilter(violation: Axe.Result) { function landmarkViolationFilter(violation: Axe.Result, currentUrl: string) { return ( !currentUrl.includes('app-layout/multi') || + !currentUrl.includes('feature-notifications/feature-prompt') || (violation.id !== 'landmark-main-is-top-level' && violation.id !== 'landmark-unique' && violation.id !== 'landmark-no-duplicate-main') 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 2428671d26..df80540e4b 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 @@ -401,6 +401,7 @@ exports[`test-utils selectors 1`] = ` "awsui_recovery_vrgzu", "awsui_root_11n0s", "awsui_root_15oj2", + "awsui_root_1b522", "awsui_root_1fcus", "awsui_root_1kjc7", "awsui_root_1om0h", diff --git a/src/__tests__/utils.tsx b/src/__tests__/utils.tsx index d4d3053d1c..fa07609bd4 100644 --- a/src/__tests__/utils.tsx +++ b/src/__tests__/utils.tsx @@ -19,6 +19,7 @@ export function getAllComponents(): string[] { name !== 'test-utils' && name !== 'theming' && name !== 'contexts' && + name !== 'plugins' && name !== 'i18n' && !name.includes('.') && !name.includes('LICENSE') && diff --git a/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap b/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap index 4e9a6f63c9..9dac021b6e 100644 --- a/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap +++ b/src/app-layout/__tests__/__snapshots__/widget-contract-old.test.tsx.snap @@ -154,6 +154,10 @@ Map { "toolbar": 0, }, }, + "featureNotificationsProps": { + "drawer": null, + "renderLatestFeaturePrompt": [Function], + }, "toolbarProps": { "activeDrawerId": null, "ariaLabels": { @@ -1003,6 +1007,10 @@ Map { "toolbar": 0, }, }, + "featureNotificationsProps": { + "drawer": null, + "renderLatestFeaturePrompt": [Function], + }, "toolbarProps": { "activeDrawerId": null, "ariaLabels": { @@ -2053,6 +2061,10 @@ Map { "toolbar": 0, }, }, + "featureNotificationsProps": { + "drawer": null, + "renderLatestFeaturePrompt": [Function], + }, "toolbarProps": { "activeDrawerId": "security", "ariaLabels": { diff --git a/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel-old.test.tsx.snap b/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel-old.test.tsx.snap index 93d897d529..1f309a2ee6 100644 --- a/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel-old.test.tsx.snap +++ b/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel-old.test.tsx.snap @@ -154,6 +154,10 @@ Map { "toolbar": 0, }, }, + "featureNotificationsProps": { + "drawer": null, + "renderLatestFeaturePrompt": [Function], + }, "toolbarProps": { "activeDrawerId": null, "ariaLabels": { @@ -1004,6 +1008,10 @@ Map { "toolbar": 0, }, }, + "featureNotificationsProps": { + "drawer": null, + "renderLatestFeaturePrompt": [Function], + }, "toolbarProps": { "activeDrawerId": null, "ariaLabels": { diff --git a/src/app-layout/__tests__/runtime-feature-notifications.test.tsx b/src/app-layout/__tests__/runtime-feature-notifications.test.tsx new file mode 100644 index 0000000000..2ea64e4c60 --- /dev/null +++ b/src/app-layout/__tests__/runtime-feature-notifications.test.tsx @@ -0,0 +1,442 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; + +import AppLayout from '../../../lib/components/app-layout'; +import TestI18nProvider from '../../../lib/components/i18n/testing'; +import { + persistFeatureNotifications, + retrieveFeatureNotifications, +} from '../../../lib/components/internal/persistence'; +import awsuiPlugins from '../../../lib/components/internal/plugins'; +import * as awsuiWidgetInternal from '../../../lib/components/internal/plugins/widget/core'; +import { FeatureNotificationsPayload } from '../../../lib/components/internal/plugins/widget/interfaces'; +import { featureNotifications } from '../../../lib/components/plugins'; +import createWrapper from '../../../lib/components/test-utils/dom'; +import FeaturePromptWrapper from '../../../lib/components/test-utils/dom/internal/feature-prompt'; +import { describeEachAppLayout } from './utils'; + +const i18nMessages = { + 'features-notification-drawer': { + 'i18nStrings.title': 'Latest feature releases', + 'i18nStrings.viewAll': 'View all feature releases', + 'ariaLabels.closeButton': 'Close notifications', + 'ariaLabels.content': 'Feature notifications', + 'ariaLabels.triggerButton': 'Show feature notifications', + 'ariaLabels.resizeHandle': 'Resize feature notifications', + }, +}; + +// Mock dates for consistent testing +const mockDate2025 = new Date('2025-01-15'); +const mockDate2024 = new Date('2024-12-01'); +const mockDateOld = new Date('2024-08-01'); // More than 90 days ago from mock current date + +const mockCurrentDate = new Date('2025-01-20'); + +const featureNotificationsDefaults: FeatureNotificationsPayload = { + id: 'test-feature-notifications', + features: [ + { + id: 'feature-1', + header: 'New Feature 1 Header', + content: 'This is the first new feature content', + contentCategory: 'Category A', + releaseDate: mockDate2025, + }, + { + id: 'feature-2', + header: 'New Feature 2 Header', + content: 'This is the second new feature content', + releaseDate: mockDate2024, + }, + { + id: 'feature-old', + header: 'Old Feature Header', + content: 'This is an old feature that should be filtered out by default', + releaseDate: mockDateOld, + }, + ], + featuresPageLink: '/features-page', + mountItem: (container, data) => { + container.textContent = data; + }, + persistenceConfig: { + uniqueKey: 'feature-notifications', + }, +}; + +jest.mock('@cloudscape-design/component-toolkit', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit'), + useContainerQuery: () => [1300, () => {}], +})); + +jest.mock('../../../lib/components/internal/persistence', () => ({ + retrieveFeatureNotifications: jest.fn(), + persistFeatureNotifications: jest.fn(), +})); + +const mockRetrieveFeatureNotifications = jest.mocked(retrieveFeatureNotifications); +const mockPersistFeatureNotifications = jest.mocked(persistFeatureNotifications); + +beforeEach(() => { + awsuiPlugins.appLayout.clearRegisteredDrawersForTesting(); + awsuiWidgetInternal.clearInitialMessages(); + jest.resetAllMocks(); + mockRetrieveFeatureNotifications.mockResolvedValue({}); + mockPersistFeatureNotifications.mockResolvedValue(); + + // Mock current date for consistent filtering + jest.useFakeTimers(); + jest.setSystemTime(mockCurrentDate); +}); + +afterEach(() => { + jest.useRealTimers(); +}); + +function delay() { + return act(() => { + jest.runAllTimers(); + return Promise.resolve(); + }); +} + +function renderComponent(jsx: React.ReactElement) { + const { container, rerender, ...rest } = render(jsx); + const wrapper = createWrapper(container).findAppLayout()!; + return { + wrapper, + container, + rerender, + ...rest, + }; +} + +describeEachAppLayout(() => { + test('[backward compatibility] registerFeatureNotifications does not crash the page', () => { + featureNotifications.registerFeatureNotifications(featureNotificationsDefaults); + render(); + }); +}); + +describeEachAppLayout({ themes: ['refresh-toolbar'] }, ({ size }) => { + test('registers feature notifications correctly', () => { + featureNotifications.registerFeatureNotifications(featureNotificationsDefaults); + const { wrapper } = renderComponent(); + expect(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)).toBeTruthy(); + + wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.click(); + + const activeDrawerWrapper = wrapper.findActiveDrawer()!; + const featureItems = activeDrawerWrapper.findList()!.findItems()!; + + expect(activeDrawerWrapper.getElement()).toBeTruthy(); + + // We expect 2 out of 3 features to be rendered since the default filter + // excludes features older than 90 days, leaving only 2 valid features + expect(featureItems).toHaveLength(2); + + // Check content + expect(featureItems[0].findContent().getElement()).toHaveTextContent('New Feature 1 Header'); + expect(featureItems[0].findSecondaryContent()!.getElement()).toHaveTextContent( + 'This is the first new feature content' + ); + expect(featureItems[0].findSecondaryContent()!.getElement()).toHaveTextContent('Category A'); + expect(featureItems[0].findSecondaryContent()!.getElement()).toHaveTextContent('2025-01-15'); + + expect(featureItems[1].findContent().getElement()).toHaveTextContent('New Feature 2 Header'); + expect(featureItems[1].findSecondaryContent()!.getElement()).toHaveTextContent( + 'This is the second new feature content' + ); + expect(featureItems[1].findSecondaryContent()!.getElement()).toHaveTextContent('2024-12-01'); + + expect(activeDrawerWrapper!.find('a')!.getElement()).toHaveAttribute('href', '/features-page'); + }); + + test('registers feature notifications correctly with no mountItem', () => { + featureNotifications.registerFeatureNotifications({ ...featureNotificationsDefaults, mountItem: undefined }); + const { wrapper } = renderComponent(); + expect(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)).toBeTruthy(); + + wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.click(); + + const activeDrawerWrapper = wrapper.findActiveDrawer()!; + const featureItems = activeDrawerWrapper.findList()!.findItems()!; + + expect(activeDrawerWrapper.getElement()).toBeTruthy(); + + // We expect 2 out of 3 features to be rendered since the default filter + // excludes features older than 90 days, leaving only 2 valid features + expect(featureItems).toHaveLength(2); + + // Check content + expect(featureItems[0].findContent().getElement()).toHaveTextContent('New Feature 1 Header'); + expect(featureItems[0].findSecondaryContent()!.getElement()).toHaveTextContent( + 'This is the first new feature content' + ); + expect(featureItems[0].findSecondaryContent()!.getElement()).toHaveTextContent('Category A'); + expect(featureItems[0].findSecondaryContent()!.getElement()).toHaveTextContent('2025-01-15'); + + expect(featureItems[1].findContent().getElement()).toHaveTextContent('New Feature 2 Header'); + expect(featureItems[1].findSecondaryContent()!.getElement()).toHaveTextContent( + 'This is the second new feature content' + ); + expect(featureItems[1].findSecondaryContent()!.getElement()).toHaveTextContent('2024-12-01'); + + expect(activeDrawerWrapper!.find('a')!.getElement()).toHaveAttribute('href', '/features-page'); + }); + + test('registerFeatureNotifications should override previous call when called multiple times', async () => { + featureNotifications.registerFeatureNotifications({ ...featureNotificationsDefaults, mountItem: undefined }); + const { wrapper } = renderComponent(); + expect(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)).toBeTruthy(); + + wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.click(); + + const activeDrawerWrapper = wrapper.findActiveDrawer()!; + const featureItems = activeDrawerWrapper.findList()!.findItems()!; + + expect(activeDrawerWrapper.getElement()).toBeTruthy(); + + expect(featureItems).toHaveLength(2); + + // override + featureNotifications.registerFeatureNotifications({ + ...featureNotificationsDefaults, + features: [ + { + id: 'feature-new', + header: 'New new Feature 1 Header', + content: 'This is the first new feature content', + contentCategory: 'Category A', + releaseDate: mockDate2025, + }, + ], + }); + + await waitFor(() => { + const featureItems = activeDrawerWrapper.findList()!.findItems()!; + expect(featureItems).toHaveLength(1); + expect(featureItems[0].findContent().getElement()).toHaveTextContent('New new Feature 1 Header'); + }); + }); + + test('clears feature notifications correctly', () => { + featureNotifications.registerFeatureNotifications({ ...featureNotificationsDefaults, mountItem: undefined }); + const { wrapper } = renderComponent(); + expect(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)).toBeTruthy(); + + featureNotifications.clearFeatureNotifications(); + expect(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)).toBeFalsy(); + }); + + test('supports custom filterFeatures function', () => { + featureNotifications.registerFeatureNotifications({ + ...featureNotificationsDefaults, + filterFeatures: feature => { + const halfYearAgo = new Date(); + halfYearAgo.setDate(halfYearAgo.getDate() - 180); + return feature.releaseDate >= halfYearAgo; + }, + }); + const { wrapper } = renderComponent(); + expect(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)).toBeTruthy(); + + wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.click(); + + const activeDrawerWrapper = wrapper.findActiveDrawer()!; + const featureItems = activeDrawerWrapper.findList()!.findItems()!; + + expect(activeDrawerWrapper.getElement()).toBeTruthy(); + + // We expect all features to be rendered since the default filter has been overridden + expect(featureItems).toHaveLength(3); + }); + + test('shows feature prompt for latest features', async () => { + featureNotifications.registerFeatureNotifications(featureNotificationsDefaults); + const { container } = renderComponent(); + await delay(); + + const featurePromptWrapper = new FeaturePromptWrapper(container); + + await waitFor(() => { + expect(featurePromptWrapper.findContent()!.getElement()).toHaveTextContent( + 'This is the first new feature content' + ); + }); + }); + + test('should focus on the drawer trigger button when feature prompt is dismissed', async () => { + featureNotifications.registerFeatureNotifications(featureNotificationsDefaults); + const { container, wrapper } = await renderComponent( + + + + ); + await delay(); + + const featurePromptWrapper = new FeaturePromptWrapper(container); + expect(featurePromptWrapper.findContent()!.getElement()).toHaveTextContent('This is the first new feature content'); + + featurePromptWrapper.findDismissButton()!.click(); + expect(featurePromptWrapper.findContent()).toBeFalsy(); + + expect(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.getElement()).toHaveFocus(); + // ensure the built-in tooltip on the trigger button (not the feature prompt) doesn't appear after focus changes. + expect(wrapper.findDrawerTriggerTooltip()).toBeFalsy(); + fireEvent.pointerEnter(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.getElement()); + // ensure the built-in tooltip on the trigger button (not the feature prompt) doesn't appear after focus changes. + expect(wrapper.findDrawerTriggerTooltip()).toBeFalsy(); + + // ensure the built-in tooltip is restored + fireEvent.pointerLeave(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.getElement()); + await delay(); + fireEvent.pointerEnter(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.getElement()); + expect(wrapper.findDrawerTriggerTooltip()).not.toBeFalsy(); + }); + + test('renders labels from i18n provider', async () => { + featureNotifications.registerFeatureNotifications(featureNotificationsDefaults); + const { wrapper } = await renderComponent( + + + + ); + await delay(); + + expect(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.getElement()).toHaveAttribute( + 'aria-label', + 'Show feature notifications' + ); + wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.click(); + + const activeDrawerWrapper = wrapper.findActiveDrawer()!; + + expect(activeDrawerWrapper.getElement()).toHaveAttribute('aria-label', 'Feature notifications'); + expect(activeDrawerWrapper.getElement()).toHaveTextContent('Latest feature releases'); + expect(activeDrawerWrapper.getElement()).toHaveTextContent('View all feature releases'); + expect(wrapper.findActiveDrawerCloseButton()!.getElement()).toHaveAttribute('aria-label', 'Close notifications'); + if (size === 'desktop') { + expect(wrapper.findActiveDrawerResizeHandle()!.getElement()).toHaveAttribute( + 'aria-label', + 'Resize feature notifications' + ); + } + }); + + test('shows feature prompt for a latest unseen features', async () => { + mockRetrieveFeatureNotifications.mockResolvedValue({ 'feature-1': mockDate2025.toString() }); + featureNotifications.registerFeatureNotifications(featureNotificationsDefaults); + const { container } = renderComponent(); + await delay(); + + await waitFor(() => { + const featurePromptWrapper = new FeaturePromptWrapper(container); + expect(featurePromptWrapper.findContent()!.getElement()).toHaveTextContent( + 'This is the second new feature content' + ); + }); + }); + + test('should not show feature prompt for unseen features when suppressed', async () => { + featureNotifications.registerFeatureNotifications({ ...featureNotificationsDefaults, suppressFeaturePrompt: true }); + const { container } = renderComponent(); + await delay(); + + const featurePromptWrapper = new FeaturePromptWrapper(container); + expect(featurePromptWrapper.findContent()).toBeFalsy(); + }); + + test('shows feature prompt via triggering showFeaturePromptIfPossible when possible', async () => { + featureNotifications.registerFeatureNotifications({ ...featureNotificationsDefaults, suppressFeaturePrompt: true }); + const { container } = renderComponent(); + await delay(); + + const featurePromptWrapper = new FeaturePromptWrapper(container); + expect(featurePromptWrapper.findContent()).toBeFalsy(); + + featureNotifications.showFeaturePromptIfPossible(); + + await delay(); + expect(featurePromptWrapper.findContent()!.getElement()).toHaveTextContent('This is the first new feature content'); + }); + + test('should not show feature prompt if all feature are seen', () => { + mockRetrieveFeatureNotifications.mockResolvedValue({ + 'feature-1': mockDate2025.toString(), + 'feature-2': mockDate2024.toString(), + 'feature-old': mockDateOld.toString(), + }); + featureNotifications.registerFeatureNotifications({ ...featureNotificationsDefaults, suppressFeaturePrompt: true }); + const { container } = renderComponent(); + + const featurePromptWrapper = new FeaturePromptWrapper(container); + expect(featurePromptWrapper.findContent()).toBeFalsy(); + + featureNotifications.showFeaturePromptIfPossible(); + + expect(featurePromptWrapper.findContent()).toBeFalsy(); + }); + + test('filters outdated seen features when marking all as read', async () => { + const oldSeenFeatureDate = new Date(mockCurrentDate); + oldSeenFeatureDate.setDate(oldSeenFeatureDate.getDate() - 200); // More than 180 days ago + + const recentSeenFeatureDate = new Date(mockCurrentDate); + recentSeenFeatureDate.setDate(recentSeenFeatureDate.getDate() - 100); // Less than 180 days ago + + const seenFeatures = { + 'old-seen-feature': oldSeenFeatureDate.toISOString(), + 'recent-seen-feature': recentSeenFeatureDate.toISOString(), + 'feature-1': mockDate2025.toISOString(), + }; + + mockRetrieveFeatureNotifications.mockResolvedValue(seenFeatures); + + featureNotifications.registerFeatureNotifications(featureNotificationsDefaults); + const { wrapper } = renderComponent(); + await delay(); + + wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)!.click(); + + expect(mockPersistFeatureNotifications).toHaveBeenCalled(); + + const persistedFeaturesMap = mockPersistFeatureNotifications.mock.calls[0][1]; + + expect(persistedFeaturesMap).toHaveProperty('feature-1'); + expect(persistedFeaturesMap).toHaveProperty('feature-2'); + expect(persistedFeaturesMap).toHaveProperty('recent-seen-feature'); + expect(persistedFeaturesMap).not.toHaveProperty('old-seen-feature'); + }); + + test('handles empty features array', () => { + const emptyFeatures: FeatureNotificationsPayload = { + id: 'empty-features', + features: [], + mountItem: (container, data) => { + container.textContent = data; + }, + }; + + featureNotifications.registerFeatureNotifications(emptyFeatures); + const { wrapper } = renderComponent(); + expect(wrapper.findDrawerTriggerById('empty-features')).toBeFalsy(); + }); + + test('renders feature notifications drawer alongside tools', () => { + featureNotifications.registerFeatureNotifications(featureNotificationsDefaults); + const { wrapper } = renderComponent( + + + + ); + + // Both feature notifications and tools should be available + expect(wrapper.findDrawersTriggers()).toHaveLength(2); + expect(wrapper.findDrawerTriggerById(featureNotificationsDefaults.id)).toBeTruthy(); + expect(wrapper.findToolsToggle()).toBeTruthy(); + }); +}); diff --git a/src/app-layout/utils/use-drawers.ts b/src/app-layout/utils/use-drawers.ts index 3bb065599b..2f1eb1b6e7 100644 --- a/src/app-layout/utils/use-drawers.ts +++ b/src/app-layout/utils/use-drawers.ts @@ -197,11 +197,13 @@ type UseDrawersProps = Pick void; expandedDrawerId?: string | null; setExpandedDrawerId?: (value: string | null) => void; + externalLocalRuntimeDrawers?: Array | null; }; export function useDrawers( { drawers, + externalLocalRuntimeDrawers, activeDrawerId: controlledActiveDrawerId, onDrawerChange, onGlobalDrawerFocus, @@ -293,13 +295,19 @@ export function useDrawers( : activeDrawerId !== TOOLS_DRAWER_ID ? activeDrawerId : null; - const runtimeDrawers = useRuntimeDrawers( + let runtimeDrawers = useRuntimeDrawers( disableRuntimeDrawers, activeDrawerIdResolved, onActiveDrawerChange, activeGlobalDrawersIds, onActiveGlobalDrawersChange ); + if (externalLocalRuntimeDrawers) { + runtimeDrawers = { + ...runtimeDrawers, + localBefore: [...externalLocalRuntimeDrawers, ...runtimeDrawers.localBefore], + }; + } const { localBefore, localAfter, global: runtimeGlobalDrawers } = runtimeDrawers; const combinedLocalDrawers = drawers ? [...localBefore, ...drawers, ...localAfter] diff --git a/src/app-layout/visual-refresh-toolbar/drawer/feature-notifications-drawer-content.tsx b/src/app-layout/visual-refresh-toolbar/drawer/feature-notifications-drawer-content.tsx new file mode 100644 index 0000000000..e01d9e3644 --- /dev/null +++ b/src/app-layout/visual-refresh-toolbar/drawer/feature-notifications-drawer-content.tsx @@ -0,0 +1,94 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useEffect, useRef } from 'react'; + +import Box from '../../../box/internal'; +import { InternalDrawer } from '../../../drawer/internal'; +import { useInternalI18n } from '../../../i18n/context'; +import { Feature, MountContentPart } from '../../../internal/plugins/widget/interfaces'; +import { formatDate } from '../../../internal/utils/date-time'; +import Link from '../../../link/internal'; +import List from '../../../list/internal'; +import SpaceBetween from '../../../space-between/internal'; + +import styles from './styles.css.js'; + +interface RuntimeContentPartProps { + mountContent?: MountContentPart; + content: T; +} + +export function RuntimeContentPart({ content, mountContent }: RuntimeContentPartProps) { + const ref = useRef(null); + + useEffect(() => { + if (!(mountContent && ref.current)) { + return; + } + + const container = ref.current; + const destructor = mountContent(container, content); + + return () => { + destructor?.(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return mountContent ?
: <>{content}; +} + +export default function RuntimeFeaturesNotificationDrawer({ + features, + mountItem, + featuresPageLink, +}: { + features: Array>; + mountItem?: MountContentPart; + featuresPageLink?: string; +}) { + const i18n = useInternalI18n('features-notification-drawer'); + + return ( + +
+ + ({ + id: item.id, + content: ( + + + + ), + secondaryContent: ( + + + {formatDate(item.releaseDate)} + + {!!item.contentCategory && ( + + + + )} + + + + + ), + })} + /> + + {!!featuresPageLink && ( +
+ + {i18n('i18nStrings.viewAll', undefined)} + +
+ )} +
+
+
+ ); +} diff --git a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss index d1879fa43d..fe2e323083 100644 --- a/src/app-layout/visual-refresh-toolbar/drawer/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/drawer/styles.scss @@ -488,3 +488,11 @@ $ai-drawer-heider-height: 42px; } } } + +.runtime-feature-notifications-drawer-content { + border-block-start: 1px solid awsui.$color-border-divider-default; +} + +.runtime-feature-notifications-footer { + border-block-start: awsui.$border-divider-list-width solid awsui.$color-border-divider-secondary; +} diff --git a/src/app-layout/visual-refresh-toolbar/interfaces.ts b/src/app-layout/visual-refresh-toolbar/interfaces.ts index 5d91283079..8fb7779d47 100644 --- a/src/app-layout/visual-refresh-toolbar/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/interfaces.ts @@ -14,6 +14,7 @@ import { OnChangeParams } from '../utils/use-drawers'; import { FocusControlMultipleStates, FocusControlState } from '../utils/use-focus-control'; import { SplitPanelFocusControlState } from '../utils/use-split-panel-focus-control'; import { VerticalLayoutOutput } from './compute-layout'; +import { FeatureNotificationsProps } from './state/use-feature-notifications'; export interface AppLayoutInternalProps extends AppLayoutPropsWithDefaults { navigationTriggerHide?: boolean; @@ -106,6 +107,7 @@ export interface AppLayoutWidgetizedState extends AppLayoutInternals { bottomDrawersFocusControl: FocusControlState; onActiveBottomDrawerResize: ({ id, size }: { id: string; size: number }) => void; bottomDrawers: ReadonlyArray; + featureNotificationsProps?: FeatureNotificationsProps; } // New widget interface diff --git a/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx b/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx index 48a5e7f184..d58868eb1e 100644 --- a/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx +++ b/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx @@ -30,6 +30,7 @@ import { AppLayoutState } from '../interfaces'; import { AppLayoutInternalProps, AppLayoutInternals } from '../interfaces'; import { useAiDrawer } from './use-ai-drawer'; import { useBottomDrawers } from './use-bottom-drawers'; +import { useFeatureNotifications } from './use-feature-notifications'; import { useWidgetMessages } from './use-widget-messages'; export const useAppLayout = ( @@ -136,6 +137,9 @@ export const useAppLayout = ( bottomDrawersFocusControl.setFocus(); }; + const { featureNotificationsProps, onOpenFeatureNotificationsDrawer, featureNotificationsMessageHandler } = + useFeatureNotifications(); + const { drawers, activeDrawer, @@ -155,6 +159,7 @@ export const useAppLayout = ( } = useDrawers( { ...rest, + externalLocalRuntimeDrawers: featureNotificationsProps?.drawer && [featureNotificationsProps?.drawer], onGlobalDrawerFocus, onAddNewActiveDrawer, expandedDrawerId, @@ -240,6 +245,15 @@ export const useAppLayout = ( return; } + if ( + ['registerFeatureNotifications', 'showFeaturePromptIfPossible', 'clearFeatureNotifications'].includes( + message.type + ) + ) { + featureNotificationsMessageHandler(message); + return; + } + if (!('payload' in message && 'id' in message.payload)) { metrics.sendOpsMetricObject('awsui-widget-drawer-incorrect-payload', { type: message.type, @@ -275,6 +289,9 @@ export const useAppLayout = ( ) => { onActiveDrawerChange(drawerId, params); drawersFocusControl.setFocus(); + if (featureNotificationsProps?.drawer?.id && featureNotificationsProps?.drawer?.id === drawerId) { + onOpenFeatureNotificationsDrawer(); + } }; const [splitPanelOpen = false, setSplitPanelOpen] = useControllable( @@ -619,6 +636,7 @@ export const useAppLayout = ( onActiveBottomDrawerResize, bottomDrawers, bottomDrawersFocusControl, + featureNotificationsProps, }, }; }; diff --git a/src/app-layout/visual-refresh-toolbar/state/use-feature-notifications.tsx b/src/app-layout/visual-refresh-toolbar/state/use-feature-notifications.tsx new file mode 100644 index 0000000000..96547209bf --- /dev/null +++ b/src/app-layout/visual-refresh-toolbar/state/use-feature-notifications.tsx @@ -0,0 +1,229 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { RefObject, useMemo, useRef, useState } from 'react'; + +import { useMergeRefs } from '@cloudscape-design/component-toolkit/internal'; + +import { useInternalI18n } from '../../../i18n/context'; +import FeaturePrompt, { FeaturePromptProps } from '../../../internal/do-not-use/feature-prompt'; +import { persistFeatureNotifications, retrieveFeatureNotifications } from '../../../internal/persistence'; +import { + Feature, + FeatureNotificationsPayload, + PersistedFeaturesDict, + WidgetMessage, +} from '../../../internal/plugins/widget/interfaces'; +import { useMountRefPromise } from '../../../internal/utils/promises'; +import { AppLayoutProps } from '../../interfaces'; +import RuntimeFeaturesNotificationDrawer, { RuntimeContentPart } from '../drawer/feature-notifications-drawer-content'; + +const DEFAULT_PERSISTENCE_CONFIG = { + uniqueKey: 'awsui-feature-notifications', +}; + +interface RenderLatestFeaturePromptProps { + triggerRef: RefObject; +} +export interface FeatureNotificationsProps { + renderLatestFeaturePrompt: RenderLatestFeaturePrompt; + drawer?: AppLayoutProps.Drawer | null; +} +export type RenderLatestFeaturePrompt = (props: RenderLatestFeaturePromptProps) => JSX.Element | null; +interface FeatureNotifications extends FeatureNotificationsPayload { + badge?: boolean; +} + +const FEATURE_NOTIFICATIONS_RETENTION_DAYS = 180; +const DEFAULT_FEATURE_FILTER_DAYS = 90; + +function subtractDaysFromDate(currentDate: Date, daysToSubtract: number) { + daysToSubtract = daysToSubtract || 0; + const pastDate = new Date(currentDate); + pastDate.setDate(pastDate.getDate() - daysToSubtract); + + return pastDate; +} + +function filterOutdatedFeatures(features: PersistedFeaturesDict): PersistedFeaturesDict { + const cutoffDate = subtractDaysFromDate(new Date(), FEATURE_NOTIFICATIONS_RETENTION_DAYS); + + return Object.keys(features).reduce((acc, key) => { + const featureDate = new Date(features[key]); + + if (featureDate && featureDate >= cutoffDate) { + return { + ...acc, + [key]: features[key], + }; + } + + return acc; + }, {}); +} + +export function useFeatureNotifications() { + const i18n = useInternalI18n('features-notification-drawer'); + const [featurePromptDismissed, setFeaturePromptDismissed] = useState(false); + const [featureNotificationsData, setFeatureNotificationsData] = useState(null); + const [seenFeatures, setSeenFeatures] = useState({}); + const featurePromptRef = useRef(null); + const { ref: featurePromptMountRef, promise: featurePromptMountPromise } = useMountRefPromise(); + const featurePromptMergedRef = useMergeRefs(featurePromptRef, featurePromptMountRef); + const allNotificationsSeen = useMemo(() => { + return featureNotificationsData?.features.every(feature => !!seenFeatures[feature.id]); + }, [featureNotificationsData, seenFeatures]); + + const defaultFeaturesFilter = (feature: Feature) => { + return feature.releaseDate >= subtractDaysFromDate(new Date(), DEFAULT_FEATURE_FILTER_DAYS); + }; + + const getFeaturesToDisplay = (payload: FeatureNotificationsPayload): Array> => { + return payload.features + .slice() + .filter(payload.filterFeatures ? payload.filterFeatures : defaultFeaturesFilter) + .sort((a, b) => b.releaseDate.getTime() - a.releaseDate.getTime()); + }; + + const mapPayloadToDrawer = (payload: FeatureNotifications): AppLayoutProps.Drawer => { + return { + id: payload.id, + content: ( + + ), + trigger: { + iconName: 'suggestions', + }, + ariaLabels: { + closeButton: i18n('ariaLabels.closeButton', undefined), + drawerName: i18n('ariaLabels.content', undefined) ?? '', + triggerButton: i18n('ariaLabels.triggerButton', undefined), + resizeHandle: i18n('ariaLabels.resizeHandle', undefined), + }, + resizable: true, + defaultSize: 320, + badge: payload.badge, + }; + }; + + function featureNotificationsMessageHandler(event: WidgetMessage) { + if (event.type === 'registerFeatureNotifications') { + const { payload } = event; + const features = getFeaturesToDisplay(payload); + if (features.length === 0) { + return; + } + + setFeatureNotificationsData({ ...payload, features }); + + const persistenceConfig = payload.persistenceConfig ?? DEFAULT_PERSISTENCE_CONFIG; + // Retrieve previously seen feature notifications from persistence to determine + // which features the user has already viewed + (payload?.__retrieveFeatureNotifications || retrieveFeatureNotifications)(persistenceConfig).then( + seenFeatureNotifications => { + setSeenFeatures(seenFeatureNotifications); + const hasUnseenFeatures = features.some(feature => !seenFeatureNotifications[feature.id]); + if (hasUnseenFeatures) { + if (!payload.suppressFeaturePrompt && !featurePromptDismissed) { + featurePromptMountPromise.then(() => { + featurePromptRef.current?.show(); + }); + } + setFeatureNotificationsData(data => (data ? { ...data, badge: true } : data)); + } + } + ); + return; + } + + if (event.type === 'showFeaturePromptIfPossible' && !allNotificationsSeen) { + featurePromptRef.current?.show(); + return; + } + + if (event.type === 'clearFeatureNotifications') { + setFeatureNotificationsData(null); + return; + } + } + + function getLatestUnseenFeature() { + return featureNotificationsData?.features.find(feature => !seenFeatures[feature.id]) ?? null; + } + + function renderLatestFeaturePrompt({ triggerRef }: RenderLatestFeaturePromptProps) { + const latestFeature = getLatestUnseenFeature(); + if (!triggerRef.current || !latestFeature) { + return null; + } + return ( + { + triggerRef.current!.dataset!.awsuiSuppressTooltip = 'true'; + }} + onDismiss={event => { + if (event.detail?.method !== 'blur') { + triggerRef?.current!.focus(); + } + setFeaturePromptDismissed(true); + Promise.resolve().then(() => { + if (triggerRef.current?.dataset) { + triggerRef.current!.dataset!.awsuiSuppressTooltip = 'false'; + } + }); + }} + header={ + + } + content={ + + } + trackKey={latestFeature.id} + position="left" + getTrack={() => triggerRef.current} + /> + ); + } + + const setFeaturesToSeenAndPersist = () => { + const persistenceConfig = featureNotificationsData?.persistenceConfig ?? DEFAULT_PERSISTENCE_CONFIG; + const featuresMap = featureNotificationsData?.features.reduce((acc, feature) => { + return { + ...acc, + [feature.id]: feature.releaseDate, + }; + }, {}); + const filteredSeenFeaturesMap = filterOutdatedFeatures(seenFeatures); + const allFeaturesMap = { ...featuresMap, ...filteredSeenFeaturesMap }; + (featureNotificationsData?.__persistFeatureNotifications ?? persistFeatureNotifications)( + persistenceConfig, + allFeaturesMap + ).then(() => { + setSeenFeatures(allFeaturesMap); + setFeatureNotificationsData(data => (data ? { ...data, badge: false } : data)); + }); + }; + + const onOpenFeatureNotificationsDrawer = () => { + if (!featureNotificationsData || allNotificationsSeen) { + return; + } + + setFeaturesToSeenAndPersist(); + }; + + const featureNotificationsProps: FeatureNotificationsProps = { + renderLatestFeaturePrompt, + drawer: featureNotificationsData && mapPayloadToDrawer(featureNotificationsData), + }; + + return { + featureNotificationsProps, + onOpenFeatureNotificationsDrawer, + featureNotificationsMessageHandler, + }; +} diff --git a/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx b/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx index 4df4cf7aab..d0ad20a7ba 100644 --- a/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx +++ b/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx @@ -12,6 +12,7 @@ import { AppLayoutProps, AppLayoutPropsWithDefaults } from '../../interfaces'; import { OnChangeParams, TOOLS_DRAWER_ID } from '../../utils/use-drawers'; import { Focusable, FocusControlMultipleStates } from '../../utils/use-focus-control'; import { InternalDrawer } from '../interfaces'; +import { FeatureNotificationsProps } from '../state/use-feature-notifications'; import TriggerButton from './trigger-button'; import splitPanelTestUtilStyles from '../../../split-panel/test-classes/styles.css.js'; @@ -50,6 +51,8 @@ interface DrawerTriggersProps { splitPanelFocusRef: React.Ref | undefined; onSplitPanelToggle: (() => void) | undefined; disabled: boolean; + + featureNotificationsProps?: FeatureNotificationsProps; } export function DrawerTriggers({ @@ -74,6 +77,7 @@ export function DrawerTriggers({ onActiveGlobalBottomDrawerChange, bottomDrawersFocusRef, bottomDrawers, + featureNotificationsProps, }: DrawerTriggersProps) { const isMobile = useMobile(); const hasMultipleTriggers = drawers.length > 1; @@ -81,6 +85,7 @@ export function DrawerTriggers({ const previousActiveGlobalBottomDrawerId = useRef(activeGlobalBottomDrawerId); const previousActiveGlobalDrawersIds = useRef(activeGlobalDrawersIds); const [containerWidth, triggersContainerRef] = useContainerQuery(rect => rect.contentBoxWidth); + const featureNotificationTriggerRef = useRef(null); if (!drawers.length && !globalDrawers.length && !bottomDrawers?.length && !splitPanelToggleProps) { return null; } @@ -142,6 +147,7 @@ export function DrawerTriggers({ ref={triggersContainerRef} role="region" > + {featureNotificationsProps?.renderLatestFeaturePrompt?.({ triggerRef: featureNotificationTriggerRef })}
{ const isForPreviousActiveDrawer = previousActiveLocalDrawerId?.current === item.id; const selected = !expandedDrawerId && item.id === activeDrawerId; + const isFeatureNotificationsDrawer = featureNotificationsProps?.drawer?.id === item.id; return (
)} diff --git a/src/app-layout/visual-refresh-toolbar/toolbar/trigger-button/index.tsx b/src/app-layout/visual-refresh-toolbar/toolbar/trigger-button/index.tsx index 99bd139789..cdeea862ae 100644 --- a/src/app-layout/visual-refresh-toolbar/toolbar/trigger-button/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/toolbar/trigger-button/index.tsx @@ -110,7 +110,11 @@ function TriggerButton( setShowTooltip(false); }; - const handlePointerEnter = () => { + const handlePointerEnter = (event: React.MouseEvent) => { + const suppressedTooltip = event.currentTarget.querySelector('button')?.dataset?.awsuiSuppressTooltip === 'true'; + if (suppressedTooltip) { + return; + } setSupressTooltip(false); setShowTooltip(true); }; @@ -125,6 +129,7 @@ function TriggerButton( let shouldShowTooltip = false; const eventWithRelatedTarget = event as any; const relatedTarget = eventWithRelatedTarget?.relatedTarget; + const isSuppressedOnTarget = eventWithRelatedTarget?.target?.dataset?.awsuiSuppressTooltip === 'true'; const isFromAnotherTrigger = relatedTarget?.dataset?.shiftFocus === 'awsui-layout-drawer-trigger'; if ( (isForSplitPanel && !!relatedTarget) || // relatedTarget is null when split panel is closed @@ -134,6 +139,9 @@ function TriggerButton( ) { shouldShowTooltip = true; } + if (isSuppressedOnTarget) { + shouldShowTooltip = false; + } setSupressTooltip(!shouldShowTooltip); setShowTooltip(true); }, @@ -205,7 +213,7 @@ function TriggerButton(
handlePointerEnter(), + onPointerEnter: event => handlePointerEnter(event), onPointerLeave: () => handleBlur(true), onFocus: e => handleOnFocus(e as any), onBlur: () => handleBlur(true), diff --git a/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx b/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx index b8325b7fcf..1edc091271 100644 --- a/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx +++ b/src/app-layout/visual-refresh-toolbar/widget-areas/before-main-slot.tsx @@ -47,6 +47,7 @@ export const BeforeMainSlotImplementation = ({ toolbarProps, appLayoutState, app onActiveAiDrawerChange, activeAiDrawer, bottomDrawerReportedSize, + featureNotificationsProps, } = appLayoutState.widgetizedState; const drawerExpandedMode = !!expandedDrawerId; const toolsOpen = !!activeDrawer; @@ -56,7 +57,11 @@ export const BeforeMainSlotImplementation = ({ toolbarProps, appLayoutState, app return ( <> {!!toolbarProps && !embeddedViewMode && !aiDrawerExpandedMode && ( - + )} {aiDrawer && (
{ - if (!drawerRef.current || !footerRef.current) { + if (!drawerRef?.current || !footerRef?.current) { return; } @@ -43,7 +43,7 @@ export function useStickyFooter({ STICKY_STATE_CHECK_THROTTLE_DELAY ); - useResizeObserver(() => drawerRef.current?.parentElement ?? null, checkStickyState); + useResizeObserver(() => drawerRef?.current?.parentElement ?? null, checkStickyState); return { isSticky }; } diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index bd7f1ba79e..523849e5f2 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -1,621 +1,628 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -/* eslint-disable prettier/prettier */ /** Auto-generated argument types based on "en" i18n strings. Do not edit manually. */ export interface I18nFormatArgTypes { - "[charts]": { - "loadingText": never; - "errorText": never; - "recoveryText": never; - "i18nStrings.filterLabel": never; - "i18nStrings.filterPlaceholder": never; - "i18nStrings.legendAriaLabel": never; - "i18nStrings.chartAriaRoleDescription": never; - "i18nStrings.xAxisAriaRoleDescription": never; - "i18nStrings.yAxisAriaRoleDescription": never; - } - "alert": { - "dismissAriaLabel": never; - "i18nStrings.successIconAriaLabel": never; - "i18nStrings.errorIconAriaLabel": never; - "i18nStrings.warningIconAriaLabel": never; - "i18nStrings.infoIconAriaLabel": never; - "i18nStrings.dismissAriaLabel": never; - } - "annotation-context": { - "i18nStrings.nextButtonText": never; - "i18nStrings.previousButtonText": never; - "i18nStrings.finishButtonText": never; - "i18nStrings.labelDismissAnnotation": never; - "i18nStrings.stepCounterText": { - "stepNumber": string | number; - "totalStepCount": string | number; - } - "i18nStrings.taskTitle": { - "taskNumber": string | number; - "taskTitle": string | number; - } - "i18nStrings.labelHotspot": { - "openState": string; - "stepNumber": string | number; - "totalStepCount": string | number; - } - } - "app-layout": { - "ariaLabels.drawers": never; - "ariaLabels.drawersOverflow": never; - "ariaLabels.drawersOverflowWithBadge": never; - "ariaLabels.navigation": never; - "ariaLabels.navigationClose": never; - "ariaLabels.navigationToggle": never; - "ariaLabels.notifications": never; - "ariaLabels.tools": never; - "ariaLabels.toolsClose": never; - "ariaLabels.toolsToggle": never; - } - "area-chart": { - "i18nStrings.detailTotalLabel": never; - } - "attribute-editor": { - "removeButtonText": never; - "i18nStrings.itemRemovedAriaLive": never; - } - "autosuggest": { - "errorIconAriaLabel": never; - "selectedAriaLabel": never; - "enteredTextLabel": { - "value": string | number; - } - "recoveryText": never; - } - "breadcrumb-group": { - "expandAriaLabel": never; - } - "button": { - "i18nStrings.externalIconAriaLabel": never; - } - "calendar": { - "nextMonthAriaLabel": never; - "previousMonthAriaLabel": never; - "todayAriaLabel": never; - "i18nStrings.nextYearAriaLabel": never; - "i18nStrings.previousYearAriaLabel": never; - "i18nStrings.currentMonthAriaLabel": never; - } - "cards": { - "ariaLabels.selectionGroupLabel": never; - } - "code-editor": { - "i18nStrings.loadingState": never; - "i18nStrings.errorState": never; - "i18nStrings.errorStateRecovery": never; - "i18nStrings.editorGroupAriaLabel": never; - "i18nStrings.statusBarGroupAriaLabel": never; - "i18nStrings.cursorPosition": { - "row": string | number; - "column": string | number; - } - "i18nStrings.errorsTab": never; - "i18nStrings.warningsTab": never; - "i18nStrings.preferencesButtonAriaLabel": never; - "i18nStrings.paneCloseButtonAriaLabel": never; - "i18nStrings.preferencesModalHeader": never; - "i18nStrings.preferencesModalCancel": never; - "i18nStrings.preferencesModalConfirm": never; - "i18nStrings.preferencesModalWrapLines": never; - "i18nStrings.preferencesModalTheme": never; - "i18nStrings.preferencesModalThemeFilteringAriaLabel": never; - "i18nStrings.preferencesModalThemeFilteringPlaceholder": never; - "i18nStrings.preferencesModalLightThemes": never; - "i18nStrings.preferencesModalDarkThemes": never; - "i18nStrings.cursorPositionAriaLabel": { - "row": string | number; - } - "i18nStrings.resizeHandleAriaLabel": never; - "i18nStrings.resizeHandleTooltipText": never; - } - "collection-preferences": { - "title": never; - "confirmLabel": never; - "cancelLabel": never; - "pageSizePreference.title": never; - "wrapLinesPreference.label": never; - "wrapLinesPreference.description": never; - "stripedRowsPreference.label": never; - "stripedRowsPreference.description": never; - "contentDensityPreference.label": never; - "contentDensityPreference.description": never; - "stickyColumnsPreference.firstColumns.title": never; - "stickyColumnsPreference.firstColumns.description": never; - "stickyColumnsPreference.firstColumns.options[0].label": never; - "stickyColumnsPreference.firstColumns.options[1].label": never; - "stickyColumnsPreference.firstColumns.options[2].label": never; - "stickyColumnsPreference.lastColumns.title": never; - "stickyColumnsPreference.lastColumns.description": never; - "stickyColumnsPreference.lastColumns.options[0].label": never; - "stickyColumnsPreference.lastColumns.options[1].label": never; - "contentDisplayPreference.title": never; - "contentDisplayPreference.description": never; - "contentDisplayPreference.dragHandleAriaLabel": never; - "contentDisplayPreference.dragHandleAriaDescription": never; - "contentDisplayPreference.liveAnnouncementDndStarted": { - "position": string | number; - "total": string | number; - } - "contentDisplayPreference.liveAnnouncementDndDiscarded": never; - "contentDisplayPreference.i18nStrings.columnFilteringPlaceholder": never; - "contentDisplayPreference.i18nStrings.columnFilteringAriaLabel": never; - "contentDisplayPreference.i18nStrings.columnFilteringNoMatchText": never; - "contentDisplayPreference.i18nStrings.columnFilteringClearFilterText": never; - "contentDisplayPreference.liveAnnouncementDndItemReordered": { - "isInitialPosition": string; - "currentPosition": string | number; - "total": string | number; - } - "contentDisplayPreference.liveAnnouncementDndItemCommitted": { - "isInitialPosition": string; - "initialPosition": string | number; - "total": string | number; - "finalPosition": string | number; - } - "contentDisplayPreference.i18nStrings.columnFilteringCountText": { - "count": number; - } - } - "copy-to-clipboard": { - "i18nStrings.copyButtonText": never; - } - "date-picker": { - "i18nStrings.openCalendarAriaLabel": { - "selectedDate": string; - } - } - "date-range-picker": { - "i18nStrings.relativeModeTitle": never; - "i18nStrings.absoluteModeTitle": never; - "i18nStrings.relativeRangeSelectionHeading": never; - "i18nStrings.relativeRangeSelectionMonthlyDescription": never; - "i18nStrings.cancelButtonLabel": never; - "i18nStrings.clearButtonLabel": never; - "i18nStrings.applyButtonLabel": never; - "i18nStrings.customRelativeRangeOptionLabel": never; - "i18nStrings.customRelativeRangeOptionDescription": never; - "i18nStrings.customRelativeRangeUnitLabel": never; - "i18nStrings.customRelativeRangeDurationLabel": never; - "i18nStrings.customRelativeRangeDurationPlaceholder": never; - "i18nStrings.previousMonthAriaLabel": never; - "i18nStrings.nextMonthAriaLabel": never; - "i18nStrings.previousYearAriaLabel": never; - "i18nStrings.nextYearAriaLabel": never; - "i18nStrings.currentMonthAriaLabel": never; - "i18nStrings.todayAriaLabel": never; - "i18nStrings.startMonthLabel": never; - "i18nStrings.startDateLabel": never; - "i18nStrings.startTimeLabel": never; - "i18nStrings.endMonthLabel": never; - "i18nStrings.endDateLabel": never; - "i18nStrings.endTimeLabel": never; - "i18nStrings.isoDatePlaceholder": never; - "i18nStrings.slashedDatePlaceholder": never; - "i18nStrings.timePlaceholder": never; - "i18nStrings.dateTimeConstraintText": never; - "i18nStrings.dateConstraintText": never; - "i18nStrings.slashedDateTimeConstraintText": never; - "i18nStrings.isoDateTimeConstraintText": never; - "i18nStrings.slashedDateConstraintText": never; - "i18nStrings.isoDateConstraintText": never; - "i18nStrings.slashedMonthConstraintText": never; - "i18nStrings.isoMonthConstraintText": never; - "i18nStrings.monthConstraintText": never; - "i18nStrings.errorIconAriaLabel": never; - "i18nStrings.renderSelectedAbsoluteRangeAriaLive": { - "startDate": string | number; - "endDate": string | number; - } - "i18nStrings.formatRelativeRange": { - "unit": string; - "amount": number; - } - "i18nStrings.formatUnit": { - "unit": string; - "amount": number; - } - } - "drawer": { - "i18nStrings.loadingText": never; - } - "error-boundary": { - "i18nStrings.headerText"?: never; - "i18nStrings.descriptionText"?: { + '[charts]': { + loadingText: never; + errorText: never; + recoveryText: never; + 'i18nStrings.filterLabel': never; + 'i18nStrings.filterPlaceholder': never; + 'i18nStrings.legendAriaLabel': never; + 'i18nStrings.chartAriaRoleDescription': never; + 'i18nStrings.xAxisAriaRoleDescription': never; + 'i18nStrings.yAxisAriaRoleDescription': never; + }; + alert: { + dismissAriaLabel: never; + 'i18nStrings.successIconAriaLabel': never; + 'i18nStrings.errorIconAriaLabel': never; + 'i18nStrings.warningIconAriaLabel': never; + 'i18nStrings.infoIconAriaLabel': never; + 'i18nStrings.dismissAriaLabel': never; + }; + 'annotation-context': { + 'i18nStrings.nextButtonText': never; + 'i18nStrings.previousButtonText': never; + 'i18nStrings.finishButtonText': never; + 'i18nStrings.labelDismissAnnotation': never; + 'i18nStrings.stepCounterText': { + stepNumber: string | number; + totalStepCount: string | number; + }; + 'i18nStrings.taskTitle': { + taskNumber: string | number; + taskTitle: string | number; + }; + 'i18nStrings.labelHotspot': { + openState: string; + stepNumber: string | number; + totalStepCount: string | number; + }; + }; + 'app-layout': { + 'ariaLabels.drawers': never; + 'ariaLabels.drawersOverflow': never; + 'ariaLabels.drawersOverflowWithBadge': never; + 'ariaLabels.navigation': never; + 'ariaLabels.navigationClose': never; + 'ariaLabels.navigationToggle': never; + 'ariaLabels.notifications': never; + 'ariaLabels.tools': never; + 'ariaLabels.toolsClose': never; + 'ariaLabels.toolsToggle': never; + }; + 'area-chart': { + 'i18nStrings.detailTotalLabel': never; + }; + 'attribute-editor': { + removeButtonText: never; + 'i18nStrings.itemRemovedAriaLive': never; + }; + autosuggest: { + errorIconAriaLabel: never; + selectedAriaLabel: never; + enteredTextLabel: { + value: string | number; + }; + recoveryText: never; + }; + 'breadcrumb-group': { + expandAriaLabel: never; + }; + button: { + 'i18nStrings.externalIconAriaLabel': never; + }; + calendar: { + nextMonthAriaLabel: never; + previousMonthAriaLabel: never; + todayAriaLabel: never; + 'i18nStrings.nextYearAriaLabel': never; + 'i18nStrings.previousYearAriaLabel': never; + 'i18nStrings.currentMonthAriaLabel': never; + }; + cards: { + 'ariaLabels.selectionGroupLabel': never; + }; + 'code-editor': { + 'i18nStrings.loadingState': never; + 'i18nStrings.errorState': never; + 'i18nStrings.errorStateRecovery': never; + 'i18nStrings.editorGroupAriaLabel': never; + 'i18nStrings.statusBarGroupAriaLabel': never; + 'i18nStrings.cursorPosition': { + row: string | number; + column: string | number; + }; + 'i18nStrings.errorsTab': never; + 'i18nStrings.warningsTab': never; + 'i18nStrings.preferencesButtonAriaLabel': never; + 'i18nStrings.paneCloseButtonAriaLabel': never; + 'i18nStrings.preferencesModalHeader': never; + 'i18nStrings.preferencesModalCancel': never; + 'i18nStrings.preferencesModalConfirm': never; + 'i18nStrings.preferencesModalWrapLines': never; + 'i18nStrings.preferencesModalTheme': never; + 'i18nStrings.preferencesModalThemeFilteringAriaLabel': never; + 'i18nStrings.preferencesModalThemeFilteringPlaceholder': never; + 'i18nStrings.preferencesModalLightThemes': never; + 'i18nStrings.preferencesModalDarkThemes': never; + 'i18nStrings.cursorPositionAriaLabel': { + row: string | number; + }; + 'i18nStrings.resizeHandleAriaLabel': never; + 'i18nStrings.resizeHandleTooltipText': never; + }; + 'collection-preferences': { + title: never; + confirmLabel: never; + cancelLabel: never; + 'pageSizePreference.title': never; + 'wrapLinesPreference.label': never; + 'wrapLinesPreference.description': never; + 'stripedRowsPreference.label': never; + 'stripedRowsPreference.description': never; + 'contentDensityPreference.label': never; + 'contentDensityPreference.description': never; + 'stickyColumnsPreference.firstColumns.title': never; + 'stickyColumnsPreference.firstColumns.description': never; + 'stickyColumnsPreference.firstColumns.options[0].label': never; + 'stickyColumnsPreference.firstColumns.options[1].label': never; + 'stickyColumnsPreference.firstColumns.options[2].label': never; + 'stickyColumnsPreference.lastColumns.title': never; + 'stickyColumnsPreference.lastColumns.description': never; + 'stickyColumnsPreference.lastColumns.options[0].label': never; + 'stickyColumnsPreference.lastColumns.options[1].label': never; + 'contentDisplayPreference.title': never; + 'contentDisplayPreference.description': never; + 'contentDisplayPreference.dragHandleAriaLabel': never; + 'contentDisplayPreference.dragHandleAriaDescription': never; + 'contentDisplayPreference.liveAnnouncementDndStarted': { + position: string | number; + total: string | number; + }; + 'contentDisplayPreference.liveAnnouncementDndDiscarded': never; + 'contentDisplayPreference.i18nStrings.columnFilteringPlaceholder': never; + 'contentDisplayPreference.i18nStrings.columnFilteringAriaLabel': never; + 'contentDisplayPreference.i18nStrings.columnFilteringNoMatchText': never; + 'contentDisplayPreference.i18nStrings.columnFilteringClearFilterText': never; + 'contentDisplayPreference.liveAnnouncementDndItemReordered': { + isInitialPosition: string; + currentPosition: string | number; + total: string | number; + }; + 'contentDisplayPreference.liveAnnouncementDndItemCommitted': { + isInitialPosition: string; + initialPosition: string | number; + total: string | number; + finalPosition: string | number; + }; + 'contentDisplayPreference.i18nStrings.columnFilteringCountText': { + count: number; + }; + }; + 'copy-to-clipboard': { + 'i18nStrings.copyButtonText': never; + }; + 'date-picker': { + 'i18nStrings.openCalendarAriaLabel': { + selectedDate: string; + }; + }; + 'date-range-picker': { + 'i18nStrings.relativeModeTitle': never; + 'i18nStrings.absoluteModeTitle': never; + 'i18nStrings.relativeRangeSelectionHeading': never; + 'i18nStrings.relativeRangeSelectionMonthlyDescription': never; + 'i18nStrings.cancelButtonLabel': never; + 'i18nStrings.clearButtonLabel': never; + 'i18nStrings.applyButtonLabel': never; + 'i18nStrings.customRelativeRangeOptionLabel': never; + 'i18nStrings.customRelativeRangeOptionDescription': never; + 'i18nStrings.customRelativeRangeUnitLabel': never; + 'i18nStrings.customRelativeRangeDurationLabel': never; + 'i18nStrings.customRelativeRangeDurationPlaceholder': never; + 'i18nStrings.previousMonthAriaLabel': never; + 'i18nStrings.nextMonthAriaLabel': never; + 'i18nStrings.previousYearAriaLabel': never; + 'i18nStrings.nextYearAriaLabel': never; + 'i18nStrings.currentMonthAriaLabel': never; + 'i18nStrings.todayAriaLabel': never; + 'i18nStrings.startMonthLabel': never; + 'i18nStrings.startDateLabel': never; + 'i18nStrings.startTimeLabel': never; + 'i18nStrings.endMonthLabel': never; + 'i18nStrings.endDateLabel': never; + 'i18nStrings.endTimeLabel': never; + 'i18nStrings.isoDatePlaceholder': never; + 'i18nStrings.slashedDatePlaceholder': never; + 'i18nStrings.timePlaceholder': never; + 'i18nStrings.dateTimeConstraintText': never; + 'i18nStrings.dateConstraintText': never; + 'i18nStrings.slashedDateTimeConstraintText': never; + 'i18nStrings.isoDateTimeConstraintText': never; + 'i18nStrings.slashedDateConstraintText': never; + 'i18nStrings.isoDateConstraintText': never; + 'i18nStrings.slashedMonthConstraintText': never; + 'i18nStrings.isoMonthConstraintText': never; + 'i18nStrings.monthConstraintText': never; + 'i18nStrings.errorIconAriaLabel': never; + 'i18nStrings.renderSelectedAbsoluteRangeAriaLive': { + startDate: string | number; + endDate: string | number; + }; + 'i18nStrings.formatRelativeRange': { + unit: string; + amount: number; + }; + 'i18nStrings.formatUnit': { + unit: string; + amount: number; + }; + }; + drawer: { + 'i18nStrings.loadingText': never; + }; + 'error-boundary': { + 'i18nStrings.headerText'?: never; + 'i18nStrings.descriptionText'?: { hasFeedback: boolean; - Feedback: (chunks: React.ReactNode[]) => React.ReactNode; - }; - "i18nStrings.refreshActionText"?: never; - } - "file-token-group": { - "i18nStrings.limitShowFewer": never; - "i18nStrings.limitShowMore": never; - "i18nStrings.removeFileAriaLabel": { - "fileIndex": string | number; - "fileName": string; - } - "i18nStrings.errorIconAriaLabel": never; - "i18nStrings.warningIconAriaLabel": never; - } - "file-upload": { - "i18nStrings.limitShowFewer": never; - "i18nStrings.limitShowMore": never; - "i18nStrings.removeFileAriaLabel": { - "fileIndex": string | number; - "fileName": string; - } - "i18nStrings.errorIconAriaLabel": never; - "i18nStrings.warningIconAriaLabel": never; - "i18nStrings.uploadButtonText": { - "multiple": string; - } - "i18nStrings.dropzoneText": { - "multiple": string; - } - } - "flashbar": { - "i18nStrings.ariaLabel": never; - "i18nStrings.errorIconAriaLabel": never; - "i18nStrings.inProgressIconAriaLabel": never; - "i18nStrings.infoIconAriaLabel": never; - "i18nStrings.notificationBarAriaLabel": never; - "i18nStrings.notificationBarText": never; - "i18nStrings.successIconAriaLabel": never; - "i18nStrings.warningIconAriaLabel": never; - } - "form-field": { - "i18nStrings.errorIconAriaLabel": never; - "i18nStrings.warningIconAriaLabel": never; - } - "form": { - "errorIconAriaLabel": never; - } - "help-panel": { - "loadingText": never; - } - "input": { - "clearAriaLabel": never; - } - "link": { - "externalIconAriaLabel": never; - } - "list": { - "dragHandleAriaLabel": never; - "dragHandleAriaDescription": never; - "liveAnnouncementDndStarted": { - "position": string | number; - "total": string | number; - } - "liveAnnouncementDndDiscarded": never; - "liveAnnouncementDndItemReordered": { - "isInitialPosition": string; - "currentPosition": string | number; - "total": string | number; - } - "liveAnnouncementDndItemCommitted": { - "isInitialPosition": string; - "initialPosition": string | number; - "total": string | number; - "finalPosition": string | number; - } - } - "modal": { - "closeAriaLabel": never; - } - "multiselect": { - "deselectAriaLabel": { - "option__label": string | number; - } - "i18nStrings.selectAllText": never; - } - "pagination": { - "ariaLabels.nextPageLabel": never; - "ariaLabels.pageLabel": { - "pageNumber": string | number; - } - "ariaLabels.previousPageLabel": never; - "ariaLabels.jumpToPageButtonLabel": never; - "i18nStrings.jumpToPageInputLabel": never; - "i18nStrings.jumpToPageError": never; + Feedback: (chunks: React.ReactNode[]) => React.ReactNode; + }; + 'i18nStrings.refreshActionText'?: never; + }; + 'features-notification-drawer': { + 'i18nStrings.title': never; + 'i18nStrings.viewAll': never; + 'ariaLabels.closeButton': never; + 'ariaLabels.content': never; + 'ariaLabels.triggerButton': never; + 'ariaLabels.resizeHandle': never; + }; + 'file-token-group': { + 'i18nStrings.limitShowFewer': never; + 'i18nStrings.limitShowMore': never; + 'i18nStrings.removeFileAriaLabel': { + fileIndex: string | number; + fileName: string; + }; + 'i18nStrings.errorIconAriaLabel': never; + 'i18nStrings.warningIconAriaLabel': never; + }; + 'file-upload': { + 'i18nStrings.limitShowFewer': never; + 'i18nStrings.limitShowMore': never; + 'i18nStrings.removeFileAriaLabel': { + fileIndex: string | number; + fileName: string; + }; + 'i18nStrings.errorIconAriaLabel': never; + 'i18nStrings.warningIconAriaLabel': never; + 'i18nStrings.uploadButtonText': { + multiple: string; + }; + 'i18nStrings.dropzoneText': { + multiple: string; + }; + }; + flashbar: { + 'i18nStrings.ariaLabel': never; + 'i18nStrings.errorIconAriaLabel': never; + 'i18nStrings.inProgressIconAriaLabel': never; + 'i18nStrings.infoIconAriaLabel': never; + 'i18nStrings.notificationBarAriaLabel': never; + 'i18nStrings.notificationBarText': never; + 'i18nStrings.successIconAriaLabel': never; + 'i18nStrings.warningIconAriaLabel': never; + }; + 'form-field': { + 'i18nStrings.errorIconAriaLabel': never; + 'i18nStrings.warningIconAriaLabel': never; + }; + form: { + errorIconAriaLabel: never; + }; + 'help-panel': { + loadingText: never; + }; + input: { + clearAriaLabel: never; + }; + link: { + externalIconAriaLabel: never; + }; + list: { + dragHandleAriaLabel: never; + dragHandleAriaDescription: never; + liveAnnouncementDndStarted: { + position: string | number; + total: string | number; + }; + liveAnnouncementDndDiscarded: never; + liveAnnouncementDndItemReordered: { + isInitialPosition: string; + currentPosition: string | number; + total: string | number; + }; + liveAnnouncementDndItemCommitted: { + isInitialPosition: string; + initialPosition: string | number; + total: string | number; + finalPosition: string | number; + }; + }; + modal: { + closeAriaLabel: never; + }; + multiselect: { + deselectAriaLabel: { + option__label: string | number; + }; + 'i18nStrings.selectAllText': never; + }; + pagination: { + 'ariaLabels.nextPageLabel': never; + 'ariaLabels.pageLabel': { + pageNumber: string | number; + }; + 'ariaLabels.previousPageLabel': never; + 'ariaLabels.jumpToPageButtonLabel': never; + 'i18nStrings.jumpToPageInputLabel': never; + 'i18nStrings.jumpToPageError': never; "i18nStrings.jumpToPageLoadingText": never; - } - "panel-resize-handle": { - "i18nStrings.resizeHandleAriaLabel": never; - "i18nStrings.resizeHandleTooltipText": never; - } - "pie-chart": { - "i18nStrings.detailsValue": never; - "i18nStrings.detailsPercentage": never; - "i18nStrings.chartAriaRoleDescription": never; - "i18nStrings.segmentAriaRoleDescription": never; - } - "popover": { - "dismissAriaLabel": never; - } - "property-filter": { - "i18nStrings.allPropertiesLabel": never; - "i18nStrings.applyActionText": never; - "i18nStrings.cancelActionText": never; - "i18nStrings.clearFiltersText": never; - "i18nStrings.editTokenHeader": never; - "i18nStrings.groupPropertiesText": never; - "i18nStrings.groupValuesText": never; - "i18nStrings.operationAndText": never; - "i18nStrings.operationOrText": never; - "i18nStrings.operatorContainsText": never; - "i18nStrings.operatorDoesNotContainText": never; - "i18nStrings.operatorDoesNotEqualText": never; - "i18nStrings.operatorEqualsText": never; - "i18nStrings.operatorGreaterOrEqualText": never; - "i18nStrings.operatorGreaterText": never; - "i18nStrings.operatorLessOrEqualText": never; - "i18nStrings.operatorLessText": never; - "i18nStrings.operatorStartsWithText": never; - "i18nStrings.operatorDoesNotStartWithText": never; - "i18nStrings.operatorText": never; - "i18nStrings.operatorsText": never; - "i18nStrings.propertyText": never; - "i18nStrings.removeTokenButtonAriaLabel": { - "token__formattedText": string | number; - } - "i18nStrings.tokenEditorTokenActionsAriaLabel": { - "token__formattedText": string | number; - } - "i18nStrings.tokenEditorTokenRemoveAriaLabel": { - "token__formattedText": string | number; - } - "i18nStrings.tokenEditorTokenRemoveLabel": never; - "i18nStrings.tokenEditorTokenRemoveFromGroupLabel": never; - "i18nStrings.tokenEditorAddTokenActionsAriaLabel": never; - "i18nStrings.tokenEditorAddNewTokenLabel": never; - "i18nStrings.tokenEditorAddExistingTokenAriaLabel": { - "token__formattedText": string | number; - } - "i18nStrings.tokenEditorAddExistingTokenLabel": { - "token__propertyLabel": string | number; - "token__operator": string | number; - "token__value": string | number; - } - "i18nStrings.tokenLimitShowFewer": never; - "i18nStrings.tokenLimitShowMore": never; - "i18nStrings.valueText": never; - "i18nStrings.formatToken": { - "token__operator": string; - "token__propertyLabel": string | number; - "token__value": string | number; - } - "i18nStrings.groupEditAriaLabel": { - "group__formattedTokens__length": string; - "group__formattedTokens0__formattedText": string | number; - "group__operationLabel": string | number; - "group__formattedTokens1__formattedText": string | number; - "group__formattedTokens2__formattedText": string | number; - "group__formattedTokens3__formattedText": string | number; - } - } - "s3-resource-selector": { - "i18nStrings.inContextSelectPlaceholder": never; - "i18nStrings.inContextBrowseButton": never; - "i18nStrings.inContextViewButton": never; - "i18nStrings.inContextViewButtonAriaLabel": never; - "i18nStrings.inContextLoadingText": never; - "i18nStrings.inContextUriLabel": never; - "i18nStrings.inContextVersionSelectLabel": never; - "i18nStrings.modalTitle": never; - "i18nStrings.modalCancelButton": never; - "i18nStrings.modalSubmitButton": never; - "i18nStrings.modalBreadcrumbRootItem": never; - "i18nStrings.modalLastUpdatedText": never; - "i18nStrings.selectionBuckets": never; - "i18nStrings.selectionObjects": never; - "i18nStrings.selectionVersions": never; - "i18nStrings.selectionBucketsSearchPlaceholder": never; - "i18nStrings.selectionObjectsSearchPlaceholder": never; - "i18nStrings.selectionVersionsSearchPlaceholder": never; - "i18nStrings.selectionBucketsLoading": never; - "i18nStrings.selectionBucketsNoItems": never; - "i18nStrings.selectionObjectsLoading": never; - "i18nStrings.selectionObjectsNoItems": never; - "i18nStrings.selectionVersionsLoading": never; - "i18nStrings.selectionVersionsNoItems": never; - "i18nStrings.filteringNoMatches": never; - "i18nStrings.filteringCantFindMatch": never; - "i18nStrings.clearFilterButtonText": never; - "i18nStrings.columnBucketID": never; - "i18nStrings.columnBucketName": never; - "i18nStrings.columnBucketCreationDate": never; - "i18nStrings.columnBucketRegion": never; - "i18nStrings.columnObjectKey": never; - "i18nStrings.columnObjectLastModified": never; - "i18nStrings.columnObjectSize": never; - "i18nStrings.columnVersionID": never; - "i18nStrings.columnVersionLastModified": never; - "i18nStrings.columnVersionSize": never; - "i18nStrings.validationPathMustBegin": never; - "i18nStrings.validationBucketLowerCase": never; - "i18nStrings.validationBucketMustNotContain": never; - "i18nStrings.validationBucketLength": never; - "i18nStrings.validationBucketMustComplyDns": never; - "i18nStrings.labelSortedDescending": { - "columnName": string | number; - } - "i18nStrings.labelSortedAscending": { - "columnName": string | number; - } - "i18nStrings.labelNotSorted": { - "columnName": string | number; - } - "i18nStrings.labelsBucketsSelection.selectionGroupLabel": never; - "i18nStrings.labelsBucketsSelection.itemSelectionLabel": { - "item__Name": string | number; - } - "i18nStrings.labelsObjectsSelection.selectionGroupLabel": never; - "i18nStrings.labelsObjectsSelection.itemSelectionLabel": { - "item__Key": string | number; - } - "i18nStrings.labelsVersionsSelection.selectionGroupLabel": never; - "i18nStrings.labelsVersionsSelection.itemSelectionLabel": { - "item__VersionId": string | number; - } - "i18nStrings.labelFiltering": { - "itemsType": string | number; - } - "i18nStrings.labelRefresh": never; - "i18nStrings.labelBreadcrumbs": never; - "i18nStrings.labelIconFolder": never; - "i18nStrings.labelIconObject": never; - "i18nStrings.filteringCounterText": { - "count": number; - } - } - "select": { - "errorIconAriaLabel": never; - "selectedAriaLabel": never; - "recoveryText": never; - } - "slider": { - "i18nStrings.valueTextRange": { - "value": string | number; - "previousValue": string | number; - "nextValue": string | number; - } - } - "split-panel": { - "i18nStrings.closeButtonAriaLabel": never; - "i18nStrings.openButtonAriaLabel": never; - "i18nStrings.preferencesTitle": never; - "i18nStrings.preferencesPositionLabel": never; - "i18nStrings.preferencesPositionDescription": never; - "i18nStrings.preferencesPositionSide": never; - "i18nStrings.preferencesPositionBottom": never; - "i18nStrings.preferencesConfirm": never; - "i18nStrings.preferencesCancel": never; - "i18nStrings.resizeHandleAriaLabel": never; - "i18nStrings.resizeHandleTooltipText": never; - } - "table": { - "ariaLabels.resizerRoleDescription": never; - "ariaLabels.submittingEditText": never; - "ariaLabels.successfulEditLabel": never; - "ariaLabels.expandButtonLabel": never; - "ariaLabels.collapseButtonLabel": never; - "columnDefinitions.editConfig.errorIconAriaLabel": never; - "columnDefinitions.editConfig.editIconAriaLabel": never; - } - "tabs": { - "i18nStrings.scrollLeftAriaLabel": never; - "i18nStrings.scrollRightAriaLabel": never; - "i18nStrings.tabsWithActionsAriaRoleDescription": never; - } - "tag-editor": { - "i18nStrings.keyPlaceholder": never; - "i18nStrings.valuePlaceholder": never; - "i18nStrings.addButton": never; - "i18nStrings.removeButton": never; - "i18nStrings.removeButtonAriaLabel": { - "tag__key": string | number; - } - "i18nStrings.undoButton": never; - "i18nStrings.undoPrompt": never; - "i18nStrings.loading": never; - "i18nStrings.keyHeader": never; - "i18nStrings.valueHeader": never; - "i18nStrings.optional": never; - "i18nStrings.keySuggestion": never; - "i18nStrings.valueSuggestion": never; - "i18nStrings.emptyTags": never; - "i18nStrings.tooManyKeysSuggestion": never; - "i18nStrings.tooManyValuesSuggestion": never; - "i18nStrings.keysSuggestionLoading": never; - "i18nStrings.keysSuggestionError": never; - "i18nStrings.valuesSuggestionLoading": never; - "i18nStrings.valuesSuggestionError": never; - "i18nStrings.emptyKeyError": never; - "i18nStrings.maxKeyCharLengthError": never; - "i18nStrings.maxValueCharLengthError": never; - "i18nStrings.duplicateKeyError": never; - "i18nStrings.invalidKeyError": never; - "i18nStrings.invalidValueError": never; - "i18nStrings.awsPrefixError": never; - "i18nStrings.tagLimitReached": { - "tagLimit": number; - } - "i18nStrings.tagLimitExceeded": { - "tagLimit": number; - } - "i18nStrings.tagLimit": { - "tagLimitAvailable": string; - "availableTags": number; - "tagLimit": string | number; - } - } - "token-group": { - "i18nStrings.limitShowFewer": never; - "i18nStrings.limitShowMore": never; - } - "top-navigation": { - "i18nStrings.searchIconAriaLabel": never; - "i18nStrings.searchDismissIconAriaLabel": never; - "i18nStrings.overflowMenuTriggerText": never; - "i18nStrings.overflowMenuDismissIconAriaLabel": never; - "i18nStrings.overflowMenuBackIconAriaLabel": never; - "i18nStrings.overflowMenuTitleText": never; - } - "tree-view": { - "i18nStrings.expandButtonLabel": never; - "i18nStrings.collapseButtonLabel": never; - } - "tutorial-panel": { - "i18nStrings.loadingText": never; - "i18nStrings.tutorialListTitle": never; - "i18nStrings.tutorialListDownloadLinkText": never; - "i18nStrings.labelTutorialListDownloadLink": never; - "i18nStrings.tutorialCompletedText": never; - "i18nStrings.learnMoreLinkText": never; - "i18nStrings.startTutorialButtonText": never; - "i18nStrings.restartTutorialButtonText": never; - "i18nStrings.completionScreenTitle": never; - "i18nStrings.feedbackLinkText": never; - "i18nStrings.dismissTutorialButtonText": never; - "i18nStrings.taskTitle": { - "taskNumber": string | number; - "taskTitle": string | number; - } - "i18nStrings.stepTitle": { - "stepNumber": string | number; - "stepTitle": string | number; - } - "i18nStrings.labelExitTutorial": never; - "i18nStrings.labelTotalSteps": { - "totalStepCount": string | number; - } - "i18nStrings.labelsTaskStatus.pending": never; - "i18nStrings.labelsTaskStatus.in-progress": never; - "i18nStrings.labelsTaskStatus.success": never; - } - "wizard": { - "i18nStrings.stepNumberLabel": { - "stepNumber": string | number; - } - "i18nStrings.collapsedStepsLabel": { - "stepNumber": string | number; - "stepsCount": string | number; - } - "i18nStrings.skipToButtonLabel": { - "task__title": string | number; - } - "i18nStrings.navigationAriaLabel": never; - "i18nStrings.cancelButton": never; - "i18nStrings.previousButton": never; - "i18nStrings.nextButton": never; - "i18nStrings.optional": never; - "i18nStrings.nextButtonLoadingAnnouncement": never; - "i18nStrings.submitButtonLoadingAnnouncement": never; - } + }; + 'panel-resize-handle': { + 'i18nStrings.resizeHandleAriaLabel': never; + 'i18nStrings.resizeHandleTooltipText': never; + }; + 'pie-chart': { + 'i18nStrings.detailsValue': never; + 'i18nStrings.detailsPercentage': never; + 'i18nStrings.chartAriaRoleDescription': never; + 'i18nStrings.segmentAriaRoleDescription': never; + }; + popover: { + dismissAriaLabel: never; + }; + 'property-filter': { + 'i18nStrings.allPropertiesLabel': never; + 'i18nStrings.applyActionText': never; + 'i18nStrings.cancelActionText': never; + 'i18nStrings.clearFiltersText': never; + 'i18nStrings.editTokenHeader': never; + 'i18nStrings.groupPropertiesText': never; + 'i18nStrings.groupValuesText': never; + 'i18nStrings.operationAndText': never; + 'i18nStrings.operationOrText': never; + 'i18nStrings.operatorContainsText': never; + 'i18nStrings.operatorDoesNotContainText': never; + 'i18nStrings.operatorDoesNotEqualText': never; + 'i18nStrings.operatorEqualsText': never; + 'i18nStrings.operatorGreaterOrEqualText': never; + 'i18nStrings.operatorGreaterText': never; + 'i18nStrings.operatorLessOrEqualText': never; + 'i18nStrings.operatorLessText': never; + 'i18nStrings.operatorStartsWithText': never; + 'i18nStrings.operatorDoesNotStartWithText': never; + 'i18nStrings.operatorText': never; + 'i18nStrings.operatorsText': never; + 'i18nStrings.propertyText': never; + 'i18nStrings.removeTokenButtonAriaLabel': { + token__formattedText: string | number; + }; + 'i18nStrings.tokenEditorTokenActionsAriaLabel': { + token__formattedText: string | number; + }; + 'i18nStrings.tokenEditorTokenRemoveAriaLabel': { + token__formattedText: string | number; + }; + 'i18nStrings.tokenEditorTokenRemoveLabel': never; + 'i18nStrings.tokenEditorTokenRemoveFromGroupLabel': never; + 'i18nStrings.tokenEditorAddTokenActionsAriaLabel': never; + 'i18nStrings.tokenEditorAddNewTokenLabel': never; + 'i18nStrings.tokenEditorAddExistingTokenAriaLabel': { + token__formattedText: string | number; + }; + 'i18nStrings.tokenEditorAddExistingTokenLabel': { + token__propertyLabel: string | number; + token__operator: string | number; + token__value: string | number; + }; + 'i18nStrings.tokenLimitShowFewer': never; + 'i18nStrings.tokenLimitShowMore': never; + 'i18nStrings.valueText': never; + 'i18nStrings.formatToken': { + token__operator: string; + token__propertyLabel: string | number; + token__value: string | number; + }; + 'i18nStrings.groupEditAriaLabel': { + group__formattedTokens__length: string; + group__formattedTokens0__formattedText: string | number; + group__operationLabel: string | number; + group__formattedTokens1__formattedText: string | number; + group__formattedTokens2__formattedText: string | number; + group__formattedTokens3__formattedText: string | number; + }; + }; + 's3-resource-selector': { + 'i18nStrings.inContextSelectPlaceholder': never; + 'i18nStrings.inContextBrowseButton': never; + 'i18nStrings.inContextViewButton': never; + 'i18nStrings.inContextViewButtonAriaLabel': never; + 'i18nStrings.inContextLoadingText': never; + 'i18nStrings.inContextUriLabel': never; + 'i18nStrings.inContextVersionSelectLabel': never; + 'i18nStrings.modalTitle': never; + 'i18nStrings.modalCancelButton': never; + 'i18nStrings.modalSubmitButton': never; + 'i18nStrings.modalBreadcrumbRootItem': never; + 'i18nStrings.modalLastUpdatedText': never; + 'i18nStrings.selectionBuckets': never; + 'i18nStrings.selectionObjects': never; + 'i18nStrings.selectionVersions': never; + 'i18nStrings.selectionBucketsSearchPlaceholder': never; + 'i18nStrings.selectionObjectsSearchPlaceholder': never; + 'i18nStrings.selectionVersionsSearchPlaceholder': never; + 'i18nStrings.selectionBucketsLoading': never; + 'i18nStrings.selectionBucketsNoItems': never; + 'i18nStrings.selectionObjectsLoading': never; + 'i18nStrings.selectionObjectsNoItems': never; + 'i18nStrings.selectionVersionsLoading': never; + 'i18nStrings.selectionVersionsNoItems': never; + 'i18nStrings.filteringNoMatches': never; + 'i18nStrings.filteringCantFindMatch': never; + 'i18nStrings.clearFilterButtonText': never; + 'i18nStrings.columnBucketID': never; + 'i18nStrings.columnBucketName': never; + 'i18nStrings.columnBucketCreationDate': never; + 'i18nStrings.columnBucketRegion': never; + 'i18nStrings.columnObjectKey': never; + 'i18nStrings.columnObjectLastModified': never; + 'i18nStrings.columnObjectSize': never; + 'i18nStrings.columnVersionID': never; + 'i18nStrings.columnVersionLastModified': never; + 'i18nStrings.columnVersionSize': never; + 'i18nStrings.validationPathMustBegin': never; + 'i18nStrings.validationBucketLowerCase': never; + 'i18nStrings.validationBucketMustNotContain': never; + 'i18nStrings.validationBucketLength': never; + 'i18nStrings.validationBucketMustComplyDns': never; + 'i18nStrings.labelSortedDescending': { + columnName: string | number; + }; + 'i18nStrings.labelSortedAscending': { + columnName: string | number; + }; + 'i18nStrings.labelNotSorted': { + columnName: string | number; + }; + 'i18nStrings.labelsBucketsSelection.selectionGroupLabel': never; + 'i18nStrings.labelsBucketsSelection.itemSelectionLabel': { + item__Name: string | number; + }; + 'i18nStrings.labelsObjectsSelection.selectionGroupLabel': never; + 'i18nStrings.labelsObjectsSelection.itemSelectionLabel': { + item__Key: string | number; + }; + 'i18nStrings.labelsVersionsSelection.selectionGroupLabel': never; + 'i18nStrings.labelsVersionsSelection.itemSelectionLabel': { + item__VersionId: string | number; + }; + 'i18nStrings.labelFiltering': { + itemsType: string | number; + }; + 'i18nStrings.labelRefresh': never; + 'i18nStrings.labelBreadcrumbs': never; + 'i18nStrings.labelIconFolder': never; + 'i18nStrings.labelIconObject': never; + 'i18nStrings.filteringCounterText': { + count: number; + }; + }; + select: { + errorIconAriaLabel: never; + selectedAriaLabel: never; + recoveryText: never; + }; + slider: { + 'i18nStrings.valueTextRange': { + value: string | number; + previousValue: string | number; + nextValue: string | number; + }; + }; + 'split-panel': { + 'i18nStrings.closeButtonAriaLabel': never; + 'i18nStrings.openButtonAriaLabel': never; + 'i18nStrings.preferencesTitle': never; + 'i18nStrings.preferencesPositionLabel': never; + 'i18nStrings.preferencesPositionDescription': never; + 'i18nStrings.preferencesPositionSide': never; + 'i18nStrings.preferencesPositionBottom': never; + 'i18nStrings.preferencesConfirm': never; + 'i18nStrings.preferencesCancel': never; + 'i18nStrings.resizeHandleAriaLabel': never; + 'i18nStrings.resizeHandleTooltipText': never; + }; + table: { + 'ariaLabels.resizerRoleDescription': never; + 'ariaLabels.submittingEditText': never; + 'ariaLabels.successfulEditLabel': never; + 'ariaLabels.expandButtonLabel': never; + 'ariaLabels.collapseButtonLabel': never; + 'columnDefinitions.editConfig.errorIconAriaLabel': never; + 'columnDefinitions.editConfig.editIconAriaLabel': never; + }; + tabs: { + 'i18nStrings.scrollLeftAriaLabel': never; + 'i18nStrings.scrollRightAriaLabel': never; + 'i18nStrings.tabsWithActionsAriaRoleDescription': never; + }; + 'tag-editor': { + 'i18nStrings.keyPlaceholder': never; + 'i18nStrings.valuePlaceholder': never; + 'i18nStrings.addButton': never; + 'i18nStrings.removeButton': never; + 'i18nStrings.removeButtonAriaLabel': { + tag__key: string | number; + }; + 'i18nStrings.undoButton': never; + 'i18nStrings.undoPrompt': never; + 'i18nStrings.loading': never; + 'i18nStrings.keyHeader': never; + 'i18nStrings.valueHeader': never; + 'i18nStrings.optional': never; + 'i18nStrings.keySuggestion': never; + 'i18nStrings.valueSuggestion': never; + 'i18nStrings.emptyTags': never; + 'i18nStrings.tooManyKeysSuggestion': never; + 'i18nStrings.tooManyValuesSuggestion': never; + 'i18nStrings.keysSuggestionLoading': never; + 'i18nStrings.keysSuggestionError': never; + 'i18nStrings.valuesSuggestionLoading': never; + 'i18nStrings.valuesSuggestionError': never; + 'i18nStrings.emptyKeyError': never; + 'i18nStrings.maxKeyCharLengthError': never; + 'i18nStrings.maxValueCharLengthError': never; + 'i18nStrings.duplicateKeyError': never; + 'i18nStrings.invalidKeyError': never; + 'i18nStrings.invalidValueError': never; + 'i18nStrings.awsPrefixError': never; + 'i18nStrings.tagLimitReached': { + tagLimit: number; + }; + 'i18nStrings.tagLimitExceeded': { + tagLimit: number; + }; + 'i18nStrings.tagLimit': { + tagLimitAvailable: string; + availableTags: number; + tagLimit: string | number; + }; + }; + 'token-group': { + 'i18nStrings.limitShowFewer': never; + 'i18nStrings.limitShowMore': never; + }; + 'top-navigation': { + 'i18nStrings.searchIconAriaLabel': never; + 'i18nStrings.searchDismissIconAriaLabel': never; + 'i18nStrings.overflowMenuTriggerText': never; + 'i18nStrings.overflowMenuDismissIconAriaLabel': never; + 'i18nStrings.overflowMenuBackIconAriaLabel': never; + 'i18nStrings.overflowMenuTitleText': never; + }; + 'tree-view': { + 'i18nStrings.expandButtonLabel': never; + 'i18nStrings.collapseButtonLabel': never; + }; + 'tutorial-panel': { + 'i18nStrings.loadingText': never; + 'i18nStrings.tutorialListTitle': never; + 'i18nStrings.tutorialListDownloadLinkText': never; + 'i18nStrings.labelTutorialListDownloadLink': never; + 'i18nStrings.tutorialCompletedText': never; + 'i18nStrings.learnMoreLinkText': never; + 'i18nStrings.startTutorialButtonText': never; + 'i18nStrings.restartTutorialButtonText': never; + 'i18nStrings.completionScreenTitle': never; + 'i18nStrings.feedbackLinkText': never; + 'i18nStrings.dismissTutorialButtonText': never; + 'i18nStrings.taskTitle': { + taskNumber: string | number; + taskTitle: string | number; + }; + 'i18nStrings.stepTitle': { + stepNumber: string | number; + stepTitle: string | number; + }; + 'i18nStrings.labelExitTutorial': never; + 'i18nStrings.labelTotalSteps': { + totalStepCount: string | number; + }; + 'i18nStrings.labelsTaskStatus.pending': never; + 'i18nStrings.labelsTaskStatus.in-progress': never; + 'i18nStrings.labelsTaskStatus.success': never; + }; + wizard: { + 'i18nStrings.stepNumberLabel': { + stepNumber: string | number; + }; + 'i18nStrings.collapsedStepsLabel': { + stepNumber: string | number; + stepsCount: string | number; + }; + 'i18nStrings.skipToButtonLabel': { + task__title: string | number; + }; + 'i18nStrings.navigationAriaLabel': never; + 'i18nStrings.cancelButton': never; + 'i18nStrings.previousButton': never; + 'i18nStrings.nextButton': never; + 'i18nStrings.optional': never; + 'i18nStrings.nextButtonLoadingAnnouncement': never; + 'i18nStrings.submitButtonLoadingAnnouncement': never; + }; } diff --git a/src/i18n/messages/all.ar.json b/src/i18n/messages/all.ar.json index 3b4da99de6..a5f8e80f0e 100644 --- a/src/i18n/messages/all.ar.json +++ b/src/i18n/messages/all.ar.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {حدِّث الصفحة للمحاولة مرة أخرى. نحن نتتبع هذه المشكلة، ولكن يمكنك مشاركة المزيد من المعلومات هنا.} other {حدِّث الصفحة للمحاولة مرة أخرى.}}", "i18nStrings.refreshActionText": "تحديث الصفحة" }, + "features-notification-drawer": { + "i18nStrings.title": "أحدث إصدارات الميزات", + "i18nStrings.viewAll": "عرض جميع إصدارات الميزات", + "ariaLabels.closeButton": "إغلاق الإشعارات", + "ariaLabels.content": "إشعارات الميزات", + "ariaLabels.triggerButton": "عرض إشعارات الميزات", + "ariaLabels.resizeHandle": "إعادة ضبط حجم إشعارات الميزات" + }, "file-token-group": { "i18nStrings.limitShowFewer": "إظهار عدد أقل من عوامل التصفية", "i18nStrings.limitShowMore": "عرض المزيد من عوامل التصفية", diff --git a/src/i18n/messages/all.de.json b/src/i18n/messages/all.de.json index c479df5f1d..67e1f87187 100644 --- a/src/i18n/messages/all.de.json +++ b/src/i18n/messages/all.de.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Aktualisieren Sie die Seite, um es erneut zu versuchen. Wir verfolgen dieses Problem nach, Sie können aber hier zusätzliche Informationen mitteilen.} other {Aktualisieren Sie die Seite, um es erneut zu versuchen.}}", "i18nStrings.refreshActionText": "Seite aktualisieren" }, + "features-notification-drawer": { + "i18nStrings.title": "Neueste Feature-Versionen", + "i18nStrings.viewAll": "Alle Feature-Versionen anzeigen", + "ariaLabels.closeButton": "Benachrichtigungen schließen", + "ariaLabels.content": "Benachrichtigungen über Funktionen", + "ariaLabels.triggerButton": "Funktionsbenachrichtigungen anzeigen", + "ariaLabels.resizeHandle": "Größe von Funktionsbenachrichtigungen ändern" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Weniger anzeigen", "i18nStrings.limitShowMore": "Mehr anzeigen", diff --git a/src/i18n/messages/all.en-GB.json b/src/i18n/messages/all.en-GB.json index 9985e4891b..f23c65ab3b 100644 --- a/src/i18n/messages/all.en-GB.json +++ b/src/i18n/messages/all.en-GB.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Refresh to try again. We are tracking this issue, but you can share more information here.} other {Refresh to try again.}}", "i18nStrings.refreshActionText": "Refresh page" }, + "features-notification-drawer": { + "i18nStrings.title": "Latest feature releases", + "i18nStrings.viewAll": "View all feature releases", + "ariaLabels.closeButton": "Close notifications", + "ariaLabels.content": "Feature notifications", + "ariaLabels.triggerButton": "Show feature notifications", + "ariaLabels.resizeHandle": "Resize feature notifications" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Show fewer", "i18nStrings.limitShowMore": "Show more", diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 6dd837856a..36a1352ef6 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Refresh to try again. We are tracking this issue, but you can share more information here.} other {Refresh to try again.}}", "i18nStrings.refreshActionText": "Refresh page" }, + "features-notification-drawer": { + "i18nStrings.title": "Latest feature releases", + "i18nStrings.viewAll": "View all feature releases", + "ariaLabels.closeButton": "Close notifications", + "ariaLabels.content": "Feature notifications", + "ariaLabels.triggerButton": "Show feature notifications", + "ariaLabels.resizeHandle": "Resize feature notifications" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Show fewer", "i18nStrings.limitShowMore": "Show more", @@ -476,3 +484,4 @@ "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } } + diff --git a/src/i18n/messages/all.es.json b/src/i18n/messages/all.es.json index b4cbc94404..43c12e5842 100644 --- a/src/i18n/messages/all.es.json +++ b/src/i18n/messages/all.es.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Actualice para intentarlo de nuevo. Estamos siguiendo este problema, pero puede compartir más información aquí.} other {Actualice para intentarlo de nuevo.}}", "i18nStrings.refreshActionText": "Actualizar página" }, + "features-notification-drawer": { + "i18nStrings.title": "Últimos lanzamientos de características", + "i18nStrings.viewAll": "Ver todos los lanzamientos de características", + "ariaLabels.closeButton": "Cerrar notificaciones", + "ariaLabels.content": "Notificaciones de características", + "ariaLabels.triggerButton": "Mostrar notificaciones de características", + "ariaLabels.resizeHandle": "Redimensionar notificaciones de características" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Mostrar menos", "i18nStrings.limitShowMore": "Mostrar más", diff --git a/src/i18n/messages/all.fr.json b/src/i18n/messages/all.fr.json index 5fd8337e11..f420434c39 100644 --- a/src/i18n/messages/all.fr.json +++ b/src/i18n/messages/all.fr.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Actualisez pour réessayer. Nous suivons ce problème mais vous pouvez fournir plus d'informations ici.} other {Actualisez pour réessayer.}}", "i18nStrings.refreshActionText": "Actualiser la page" }, + "features-notification-drawer": { + "i18nStrings.title": "Dernières fonctionnalités publiées", + "i18nStrings.viewAll": "Voir toutes les fonctionnalités publiées", + "ariaLabels.closeButton": "Fermer les notifications", + "ariaLabels.content": "Notifications de fonctionnalités", + "ariaLabels.triggerButton": "Afficher les notifications de fonctionnalités", + "ariaLabels.resizeHandle": "Redimensionner les notifications de fonctionnalités" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Afficher moins", "i18nStrings.limitShowMore": "Afficher plus", diff --git a/src/i18n/messages/all.id.json b/src/i18n/messages/all.id.json index 0f83619ab7..1b9efc8652 100644 --- a/src/i18n/messages/all.id.json +++ b/src/i18n/messages/all.id.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Refresh untuk mencoba lagi. Kami sedang melacak masalah ini, tetapi Anda dapat membagikan informasi selengkapnya di sini.} other {Refresh untuk mencoba lagi.}}", "i18nStrings.refreshActionText": "Segarkan halaman" }, + "features-notification-drawer": { + "i18nStrings.title": "Rilis fitur terbaru", + "i18nStrings.viewAll": "Lihat semua rilis fitur", + "ariaLabels.closeButton": "Tutup notifikasi", + "ariaLabels.content": "Notifikasi fitur", + "ariaLabels.triggerButton": "Tampilkan notifikasi fitur", + "ariaLabels.resizeHandle": "Ubah ukuran notifikasi fitur" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Tampilkan lebih sedikit", "i18nStrings.limitShowMore": "Tampilkan selengkapnya", diff --git a/src/i18n/messages/all.it.json b/src/i18n/messages/all.it.json index 6f3d4040aa..d9da27dfa2 100644 --- a/src/i18n/messages/all.it.json +++ b/src/i18n/messages/all.it.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Aggiorna la pagina per riprovare. Stiamo monitorando il problema, ma puoi condividere maggiori informazioni qui.} other {Aggiorna la pagina per riprovare.}}", "i18nStrings.refreshActionText": "Aggiorna pagina" }, + "features-notification-drawer": { + "i18nStrings.title": "Ultime versioni delle funzionalità", + "i18nStrings.viewAll": "Visualizza tutte le versioni delle funzionalità", + "ariaLabels.closeButton": "Chiudi notifiche", + "ariaLabels.content": "Notifiche delle funzionalità", + "ariaLabels.triggerButton": "Mostra le notifiche delle funzionalità", + "ariaLabels.resizeHandle": "Ridimensiona le notifiche delle funzionalità" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Mostra meno", "i18nStrings.limitShowMore": "Mostra altro", diff --git a/src/i18n/messages/all.ja.json b/src/i18n/messages/all.ja.json index 4b58af9cc6..3fdbc5b7a9 100644 --- a/src/i18n/messages/all.ja.json +++ b/src/i18n/messages/all.ja.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {更新してもう一度お試しください。この問題は現在追跡中ですが、詳しい情報はこちらで共有できます。} other {更新してもう一度お試しください。}}", "i18nStrings.refreshActionText": "ページを更新" }, + "features-notification-drawer": { + "i18nStrings.title": "最新機能リリース", + "i18nStrings.viewAll": "すべての機能リリースを表示", + "ariaLabels.closeButton": "通知を閉じる", + "ariaLabels.content": "機能の通知", + "ariaLabels.triggerButton": "機能の通知を表示", + "ariaLabels.resizeHandle": "機能の通知をリサイズ" + }, "file-token-group": { "i18nStrings.limitShowFewer": "少なく表示", "i18nStrings.limitShowMore": "さらに表示", diff --git a/src/i18n/messages/all.ko.json b/src/i18n/messages/all.ko.json index 16cfbda6f4..170e290cea 100644 --- a/src/i18n/messages/all.ko.json +++ b/src/i18n/messages/all.ko.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {새로 고침하여 다시 시도하세요. 현재 이 문제를 추적하고 있지만 여기에서 더 자세한 정보를 공유할 수 있습니다.} other {새로 고침하여 다시 시도하세요.}}", "i18nStrings.refreshActionText": "페이지 새로고침" }, + "features-notification-drawer": { + "i18nStrings.title": "최신 기능 릴리스", + "i18nStrings.viewAll": "모든 기능 릴리스 보기", + "ariaLabels.closeButton": "알림 닫기", + "ariaLabels.content": "기능 알림", + "ariaLabels.triggerButton": "기능 알림 보기", + "ariaLabels.resizeHandle": "크기 조정 기능 알림" + }, "file-token-group": { "i18nStrings.limitShowFewer": "간단히 표시", "i18nStrings.limitShowMore": "자세히 표시", diff --git a/src/i18n/messages/all.pt-BR.json b/src/i18n/messages/all.pt-BR.json index 1c57151b22..4065565c2c 100644 --- a/src/i18n/messages/all.pt-BR.json +++ b/src/i18n/messages/all.pt-BR.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Atualize para tentar novamente. Estamos rastreando esse problema, mas você pode compartilhar mais informações aqui.} other {Atualize para tentar novamente.}}", "i18nStrings.refreshActionText": "Atualizar página" }, + "features-notification-drawer": { + "i18nStrings.title": "Últimos lançamentos de atributos", + "i18nStrings.viewAll": "Visualizar todos os lançamentos de atributos", + "ariaLabels.closeButton": "Fechar notificações", + "ariaLabels.content": "Notificações de recursos", + "ariaLabels.triggerButton": "Mostrar notificações de recursos", + "ariaLabels.resizeHandle": "Redimensionar notificações de recursos" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Mostrar menos", "i18nStrings.limitShowMore": "Mostrar mais", diff --git a/src/i18n/messages/all.tr.json b/src/i18n/messages/all.tr.json index 864159b0a4..435f4b57a1 100644 --- a/src/i18n/messages/all.tr.json +++ b/src/i18n/messages/all.tr.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Yeniden denemek için yenileyin. Bu sorunu takip ediyoruz ancak buradan daha fazla bilgi paylaşabilirsiniz.} other {Yeniden denemek için yenileyin.}}", "i18nStrings.refreshActionText": "Sayfayı yenile" }, + "features-notification-drawer": { + "i18nStrings.title": "En son özellik sürümleri", + "i18nStrings.viewAll": "Tüm özellik sürümlerini görüntüleyin", + "ariaLabels.closeButton": "Bildirimleri kapat", + "ariaLabels.content": "Özellik bildirimleri", + "ariaLabels.triggerButton": "Özellik bildirimlerini göster", + "ariaLabels.resizeHandle": "Özellik bildirimlerini yeniden boyutlandır" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Daha az göster", "i18nStrings.limitShowMore": "Daha fazla göster", diff --git a/src/i18n/messages/all.zh-CN.json b/src/i18n/messages/all.zh-CN.json index 91f4e1aa2c..236b92e571 100644 --- a/src/i18n/messages/all.zh-CN.json +++ b/src/i18n/messages/all.zh-CN.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {刷新再试一次。我们正在跟踪此问题,但您可以在此处共享更多信息。} other {刷新再试一次。}}", "i18nStrings.refreshActionText": "刷新页面" }, + "features-notification-drawer": { + "i18nStrings.title": "最新功能发布", + "i18nStrings.viewAll": "查看所有功能发布", + "ariaLabels.closeButton": "关闭通知", + "ariaLabels.content": "功能通知", + "ariaLabels.triggerButton": "显示功能通知", + "ariaLabels.resizeHandle": "调整功能通知的大小" + }, "file-token-group": { "i18nStrings.limitShowFewer": "显示更少", "i18nStrings.limitShowMore": "显示更多", diff --git a/src/i18n/messages/all.zh-TW.json b/src/i18n/messages/all.zh-TW.json index 90d6cdb317..d5c8489302 100644 --- a/src/i18n/messages/all.zh-TW.json +++ b/src/i18n/messages/all.zh-TW.json @@ -184,6 +184,14 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {重新整理以再試一次。我們正在追蹤此問題,但您可以在此處分享更多資訊。} other {重新整理以再試一次。}}", "i18nStrings.refreshActionText": "重新整理頁面" }, + "features-notification-drawer": { + "i18nStrings.title": "最新功能發布", + "i18nStrings.viewAll": "檢視所有功能發布", + "ariaLabels.closeButton": "關閉通知", + "ariaLabels.content": "功能通知", + "ariaLabels.triggerButton": "顯示功能通知", + "ariaLabels.resizeHandle": "調整功能通知的大小" + }, "file-token-group": { "i18nStrings.limitShowFewer": "顯示較少", "i18nStrings.limitShowMore": "顯示更多", diff --git a/src/internal/components/chart-popover/index.tsx b/src/internal/components/chart-popover/index.tsx index 88d9f2c2bf..d71f364a8c 100644 --- a/src/internal/components/chart-popover/index.tsx +++ b/src/internal/components/chart-popover/index.tsx @@ -24,7 +24,7 @@ export interface ChartPopoverProps extends PopoverProps { getTrack?: () => null | HTMLElement | SVGElement; /** Used to update the container position in case track or track position changes: - + const trackRef = useRef(null) return (<> @@ -153,7 +153,7 @@ function ChartPopover( dismissButton={dismissButton} dismissAriaLabel={dismissAriaLabel} header={{title}} - onDismiss={onDismiss} + onDismiss={() => onDismiss()} overflowVisible="content" className={styles['popover-body']} variant="chart" diff --git a/src/internal/do-not-use/feature-prompt/__tests__/feature-prompt.test.tsx b/src/internal/do-not-use/feature-prompt/__tests__/feature-prompt.test.tsx index ee2c5a78eb..bfae0069ab 100644 --- a/src/internal/do-not-use/feature-prompt/__tests__/feature-prompt.test.tsx +++ b/src/internal/do-not-use/feature-prompt/__tests__/feature-prompt.test.tsx @@ -11,7 +11,7 @@ function renderComponent(jsx: React.ReactElement) { const { container, ...rest } = render(jsx); const wrapper = new FeaturePromptWrapper(container); - return { wrapper, ...rest }; + return { wrapper, container, ...rest }; } const TestComponent = ({ onDismiss }: { onDismiss?: FeaturePromptProps['onDismiss'] }) => { @@ -46,6 +46,7 @@ const TestComponent = ({ onDismiss }: { onDismiss?: FeaturePromptProps['onDismis getTrack={() => trackRef.current} trackKey="track-element" /> +
); }; @@ -72,7 +73,9 @@ describe('FeaturePrompt', () => { expect(wrapper.findHeader()!.getElement()).toHaveTextContent('header'); expect(wrapper.findContent()!.getElement()).toHaveTextContent('content'); - fireEvent.blur(wrapper.findContent()!.getElement()); + fireEvent.blur(wrapper.findContent()!.getElement(), { + relatedTarget: getByTestId('trigger-button'), + }); expect(wrapper.findContent()).toBeFalsy(); }); @@ -85,7 +88,7 @@ describe('FeaturePrompt', () => { wrapper.findDismissButton()!.click(); - expect(onDismissMock).toHaveBeenCalledTimes(1); + expect(onDismissMock).toHaveBeenCalled(); expect(wrapper.findContent()).toBeFalsy(); }); @@ -98,7 +101,7 @@ describe('FeaturePrompt', () => { getByTestId('dismiss-button').click(); - expect(onDismissMock).toHaveBeenCalledTimes(1); + expect(onDismissMock).toHaveBeenCalled(); expect(wrapper.findContent()).toBeFalsy(); }); @@ -109,7 +112,9 @@ describe('FeaturePrompt', () => { getByTestId('trigger-button').click(); expect(wrapper.findContent()).toBeTruthy(); - fireEvent.blur(wrapper.findContent()!.getElement()); + fireEvent.blur(wrapper.findContent()!.getElement(), { + relatedTarget: getByTestId('trigger-button'), + }); expect(onDismissMock).toHaveBeenCalledTimes(1); expect(wrapper.findContent()).toBeFalsy(); @@ -150,4 +155,18 @@ describe('FeaturePrompt', () => { expect(onDismissMock).toHaveBeenCalledTimes(1); expect(wrapper.findContent()).toBeFalsy(); }); + + test('should dismiss when clicking inside an iframe', () => { + const onDismissMock = jest.fn(); + const { getByTestId, wrapper } = renderComponent(); + + getByTestId('trigger-button').click(); + expect(wrapper.findContent()).toBeTruthy(); + + getByTestId('test-iframe').click(); + + expect(onDismissMock).toHaveBeenCalledTimes(1); + expect(onDismissMock).toHaveBeenCalledWith(expect.objectContaining({ detail: { method: 'click-outside' } })); + expect(wrapper.findContent()).toBeFalsy(); + }); }); diff --git a/src/internal/do-not-use/feature-prompt/interfaces.ts b/src/internal/do-not-use/feature-prompt/interfaces.ts index 3845b81268..7e97a9ef98 100644 --- a/src/internal/do-not-use/feature-prompt/interfaces.ts +++ b/src/internal/do-not-use/feature-prompt/interfaces.ts @@ -14,7 +14,7 @@ export interface FeaturePromptProps { * Called when a user closes the prompt by using the close icon button, * clicking outside the prompt, shifting focus out of the prompt or pressing ESC. */ - onDismiss?: NonCancelableEventHandler; + onDismiss?: NonCancelableEventHandler<{ method?: FeaturePromptProps.DismissMethod }>; /** * Determines where the feature prompt is displayed when opened, relative to the trigger. @@ -59,6 +59,7 @@ export interface FeaturePromptProps { } export namespace FeaturePromptProps { + export type DismissMethod = 'click-outside' | 'blur' | 'escape' | 'close-button'; export type Position = PopoverProps.Position; export type Size = PopoverProps.Size; export interface I18nStrings { diff --git a/src/internal/do-not-use/feature-prompt/internal.tsx b/src/internal/do-not-use/feature-prompt/internal.tsx index 9d2d294ba9..5122b07c6e 100644 --- a/src/internal/do-not-use/feature-prompt/internal.tsx +++ b/src/internal/do-not-use/feature-prompt/internal.tsx @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useImperativeHandle, useRef, useState } from 'react'; +import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import { Portal } from '@cloudscape-design/component-toolkit/internal'; @@ -13,6 +13,7 @@ import ResetContextsForModal from '../../context/reset-contexts-for-modal'; import { fireNonCancelableEvent } from '../../events'; import { InternalBaseComponentProps } from '../../hooks/use-base-component'; import { SomeRequired } from '../../types'; +import { nodeBelongs } from '../../utils/node-belongs'; import { FeaturePromptProps } from './interfaces'; import styles from './styles.css.js'; @@ -53,6 +54,37 @@ function InternalFeaturePrompt( }, })); + useEffect(() => { + if (!show) { + return; + } + const clickListener = (event: MouseEvent) => { + // Since the listener is registered on the window, `event.target` will incorrectly point at the + // shadow root if the component is rendered inside shadow DOM. + const target = event.composedPath ? event.composedPath()[0] : event.target; + if (!nodeBelongs(popoverBodyRef.current, target)) { + setShow(false); + fireNonCancelableEvent(onDismiss, { method: 'click-outside' }); + } + }; + + // Listen for blur events on the window to detect clicks in iframes + const blurListener = () => { + if (document.activeElement?.tagName === 'IFRAME') { + setShow(false); + fireNonCancelableEvent(onDismiss, { method: 'click-outside' }); + } + }; + + const controller = new AbortController(); + window.addEventListener('click', clickListener, { signal: controller.signal, capture: true }); + window.addEventListener('blur', blurListener, { signal: controller.signal, capture: true }); + + return () => { + controller.abort(); + }; + }, [show, onDismiss]); + return ( {show && ( @@ -74,20 +106,18 @@ function InternalFeaturePrompt( dismissButton={true} dismissAriaLabel={i18nStrings?.dismissAriaLabel} header={header} - onDismiss={() => { + onDismiss={method => { setShow(false); - fireNonCancelableEvent(onDismiss); + fireNonCancelableEvent(onDismiss, { method }); }} - variant="annotation" - overflowVisible="content" onBlur={event => { - const relatedTarget = event.relatedTarget; - if (relatedTarget && popoverBodyRef.current?.contains(relatedTarget)) { - return; + if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) { + setShow(false); + fireNonCancelableEvent(onDismiss, { method: 'blur' }); } - setShow(false); - fireNonCancelableEvent(onDismiss); }} + variant="feature-prompt" + overflowVisible="content" > {content} diff --git a/src/internal/do-not-use/feature-prompt/styles.scss b/src/internal/do-not-use/feature-prompt/styles.scss index e7853904c7..cb992c6e1c 100644 --- a/src/internal/do-not-use/feature-prompt/styles.scss +++ b/src/internal/do-not-use/feature-prompt/styles.scss @@ -3,5 +3,5 @@ SPDX-License-Identifier: Apache-2.0 */ .root { - // used in test utils + /* used in test-utils */ } diff --git a/src/internal/persistence/index.ts b/src/internal/persistence/index.ts index d3088fb61c..4fe9716a3d 100644 --- a/src/internal/persistence/index.ts +++ b/src/internal/persistence/index.ts @@ -1,15 +1,24 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 /* istanbul ignore file */ +/* eslint-disable require-await, @typescript-eslint/no-unused-vars */ import { AlertProps } from '../../alert/interfaces'; import { FlashbarProps } from '../../flashbar/interfaces'; +import { FeatureNotificationsPersistenceConfig } from '../plugins/widget/interfaces'; interface PersistenceFunction { persistFlashbarDismiss?: (persistenceConfig: FlashbarProps.PersistenceConfig) => Promise; retrieveFlashbarDismiss?: (persistenceConfig: FlashbarProps.PersistenceConfig) => Promise; persistAlertDismiss?: (persistenceConfig: AlertProps.PersistenceConfig) => Promise; retrieveAlertDismiss?: (persistenceConfig: AlertProps.PersistenceConfig) => Promise; + persistFeatureNotifications?: ( + persistenceConfig: FeatureNotificationsPersistenceConfig, + value: Record + ) => Promise; + retrieveFeatureNotifications?: ( + persistenceConfig: FeatureNotificationsPersistenceConfig + ) => Promise>; } export function setPersistenceFunctionsForTesting(functions: PersistenceFunction) { @@ -25,36 +34,42 @@ export function setPersistenceFunctionsForTesting(functions: PersistenceFunction if (functions.retrieveAlertDismiss) { retrieveAlertDismiss = functions.retrieveAlertDismiss; } + if (functions.persistFeatureNotifications) { + persistFeatureNotifications = functions.persistFeatureNotifications; + } + if (functions.retrieveFeatureNotifications) { + retrieveFeatureNotifications = functions.retrieveFeatureNotifications; + } } -// eslint-disable-next-line require-await -export let persistFlashbarDismiss = async function ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - persistenceConfig: FlashbarProps.PersistenceConfig -): Promise { +export let persistFlashbarDismiss = async function (persistenceConfig: FlashbarProps.PersistenceConfig): Promise { return Promise.resolve(); }; -// eslint-disable-next-line require-await export let retrieveFlashbarDismiss = async function ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars persistenceConfig: FlashbarProps.PersistenceConfig ): Promise { return Promise.resolve(false); }; -// eslint-disable-next-line require-await -export let persistAlertDismiss = async function ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - persistenceConfig: AlertProps.PersistenceConfig -): Promise { +export let persistAlertDismiss = async function (persistenceConfig: AlertProps.PersistenceConfig): Promise { return Promise.resolve(); }; -// eslint-disable-next-line require-await -export let retrieveAlertDismiss = async function ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - persistenceConfig: AlertProps.PersistenceConfig -): Promise { +export let retrieveAlertDismiss = async function (persistenceConfig: AlertProps.PersistenceConfig): Promise { return Promise.resolve(false); }; + +export let persistFeatureNotifications = async function ( + persistenceConfig: FeatureNotificationsPersistenceConfig, + + value: Record +): Promise { + return Promise.resolve(); +}; + +export let retrieveFeatureNotifications = async function ( + persistenceConfig: FeatureNotificationsPersistenceConfig +): Promise> { + return Promise.resolve({}); +}; diff --git a/src/internal/plugins/widget.ts b/src/internal/plugins/widget.ts index df14403dc4..fff48163f1 100644 --- a/src/internal/plugins/widget.ts +++ b/src/internal/plugins/widget.ts @@ -2,4 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 export * from './widget/interfaces'; export { isAppLayoutReady, whenAppLayoutReady } from './widget/core'; -export { registerLeftDrawer, registerBottomDrawer, updateDrawer } from './widget/index'; +export { + registerLeftDrawer, + registerBottomDrawer, + registerFeatureNotifications, + updateDrawer, + showFeaturePromptIfPossible, + clearFeatureNotifications, +} from './widget/index'; diff --git a/src/internal/plugins/widget/core.ts b/src/internal/plugins/widget/core.ts index 34a0a75805..f0e46228ae 100644 --- a/src/internal/plugins/widget/core.ts +++ b/src/internal/plugins/widget/core.ts @@ -10,13 +10,13 @@ const storageKeyReadyDeferCallbacks = Symbol.for('awsui-widget-api-ready-defer') interface WindowWithApi extends Window { [storageKeyMessageHandler]: MessageHandler | undefined; - [storageKeyInitialMessages]: Array | undefined; + [storageKeyInitialMessages]: Array> | undefined; [storageKeyReadyDeferCallbacks]: Array<(value?: unknown) => void> | undefined; } const oneTimeMessageTypes = ['emit-notification']; -type MessageHandler = (event: WidgetMessage) => void; +type MessageHandler = (event: WidgetMessage) => void; function getWindow() { return window as Window as WindowWithApi; @@ -27,18 +27,18 @@ export function getAppLayoutMessageHandler() { return win[storageKeyMessageHandler]; } -export function getAppLayoutInitialMessages(): Array { +export function getAppLayoutInitialMessages(): Array> { const initialMessages = getWindow()[storageKeyInitialMessages] ?? []; getWindow()[storageKeyInitialMessages] = initialMessages.filter( message => !oneTimeMessageTypes.includes(message.type) ); - return initialMessages; + return initialMessages as Array>; } -export function pushInitialMessage(message: InitialMessage) { +export function pushInitialMessage(message: InitialMessage) { const win = getWindow(); win[storageKeyInitialMessages] = win[storageKeyInitialMessages] ?? []; - win[storageKeyInitialMessages].push(message); + win[storageKeyInitialMessages].push(message as InitialMessage); } export function registerAppLayoutHandler(handler: MessageHandler) { diff --git a/src/internal/plugins/widget/index.ts b/src/internal/plugins/widget/index.ts index fb776aaa55..bd88633460 100644 --- a/src/internal/plugins/widget/index.ts +++ b/src/internal/plugins/widget/index.ts @@ -2,7 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 import { getAppLayoutInitialMessages, getAppLayoutMessageHandler, pushInitialMessage } from './core'; -import { AppLayoutUpdateMessage, DrawerPayload, RegisterDrawerMessage } from './interfaces'; +import { + AppLayoutUpdateMessage, + DrawerPayload, + FeatureNotificationsPayload, + FeatureNotificationsPayloadPublic, + RegisterDrawerMessage, + RegisterFeatureNotificationsMessage, + WidgetMessage, +} from './interfaces'; /** * Registers a new left runtime drawer to app layout @@ -11,7 +19,7 @@ import { AppLayoutUpdateMessage, DrawerPayload, RegisterDrawerMessage } from './ export function registerLeftDrawer(drawer: DrawerPayload) { const message: RegisterDrawerMessage = { type: 'registerLeftDrawer', payload: drawer }; pushInitialMessage(message); - getAppLayoutMessageHandler()?.(message); + getAppLayoutMessageHandler()?.(message as WidgetMessage); } /** @@ -21,14 +29,38 @@ export function registerLeftDrawer(drawer: DrawerPayload) { export function registerBottomDrawer(drawer: DrawerPayload) { const message: RegisterDrawerMessage = { type: 'registerBottomDrawer', payload: { ...drawer, position: 'bottom' } }; pushInitialMessage(message); - getAppLayoutMessageHandler()?.(message); + getAppLayoutMessageHandler()?.(message as WidgetMessage); +} + +/** + * Registers a new feature notifications runtime drawer to app layout + * @param payload + */ +export function registerFeatureNotifications(payload: FeatureNotificationsPayload) { + const message: RegisterFeatureNotificationsMessage = { + type: 'registerFeatureNotifications', + payload, + }; + pushInitialMessage(message); + getAppLayoutMessageHandler()?.(message as WidgetMessage); +} +export function registerFeatureNotificationsPublic(payload: FeatureNotificationsPayloadPublic) { + registerFeatureNotifications(payload); +} + +export function showFeaturePromptIfPossible() { + updateDrawer({ type: 'showFeaturePromptIfPossible' }); +} + +export function clearFeatureNotifications() { + updateDrawer({ type: 'clearFeatureNotifications' }); } /** * Interact with already registered app layout drawers * @param message */ -export function updateDrawer(message: AppLayoutUpdateMessage) { +export function updateDrawer(message: AppLayoutUpdateMessage) { if (message.type === 'updateDrawerConfig') { getAppLayoutInitialMessages().forEach(initialMessage => { if (initialMessage.payload.id === message.payload.id) { @@ -36,5 +68,5 @@ export function updateDrawer(message: AppLayoutUpdateMessage) { } }); } - getAppLayoutMessageHandler()?.(message); + getAppLayoutMessageHandler()?.(message as WidgetMessage); } diff --git a/src/internal/plugins/widget/interfaces.ts b/src/internal/plugins/widget/interfaces.ts index 26b3147198..1f3a84252f 100644 --- a/src/internal/plugins/widget/interfaces.ts +++ b/src/internal/plugins/widget/interfaces.ts @@ -54,7 +54,54 @@ export interface DrawerPayload { position?: 'side' | 'bottom'; } +type Destructor = () => void; +export type MountContentPart = (container: HTMLElement, data: T) => Destructor | void; + +export interface Feature { + id: string; + header: T; + content: T; + contentCategory?: T; + releaseDate: Date; +} + +export interface FeatureNotificationsPersistenceConfig { + uniqueKey: string; +} + +type FeatureId = string; +type FeatureReleaseDateString = string; +export type PersistedFeaturesDict = Record; +export type PersistFeatureNotifications = ( + persistenceConfig: FeatureNotificationsPersistenceConfig, + value: PersistedFeaturesDict +) => Promise; +export type RetrieveFeatureNotifications = ( + persistenceConfig: FeatureNotificationsPersistenceConfig +) => Promise; + +export interface FeatureNotificationsPayload { + id: string; + suppressFeaturePrompt?: boolean; + features: Array>; + mountItem?: MountContentPart; + featuresPageLink?: string; + filterFeatures?: (item: Feature) => boolean; + persistenceConfig?: FeatureNotificationsPersistenceConfig; + __persistFeatureNotifications?: PersistFeatureNotifications; + __retrieveFeatureNotifications?: RetrieveFeatureNotifications; +} + +export type FeatureNotificationsPayloadPublic = Omit< + FeatureNotificationsPayload, + '__persistFeatureNotifications' | '__retrieveFeatureNotifications' +>; + export type RegisterDrawerMessage = Message<'registerLeftDrawer' | 'registerBottomDrawer', DrawerPayload>; +export type RegisterFeatureNotificationsMessage = Message< + 'registerFeatureNotifications', + FeatureNotificationsPayload +>; export type UpdateDrawerConfigMessage = Message< 'updateDrawerConfig', Pick & @@ -67,15 +114,24 @@ export type ExpandDrawerMessage = Message<'expandDrawer', { id: string }>; export interface ExitExpandedModeMessage { type: 'exitExpandedMode'; } +export interface ShowFeaturePromptIfPossible { + type: 'showFeaturePromptIfPossible'; +} +export interface ClearFeatureNotifications { + type: 'clearFeatureNotifications'; +} -export type AppLayoutUpdateMessage = +export type AppLayoutUpdateMessage = | UpdateDrawerConfigMessage | OpenDrawerMessage | CloseDrawerMessage | ResizeDrawerMessage | ExpandDrawerMessage - | ExitExpandedModeMessage; + | ExitExpandedModeMessage + | RegisterFeatureNotificationsMessage + | ShowFeaturePromptIfPossible + | ClearFeatureNotifications; -export type InitialMessage = RegisterDrawerMessage; +export type InitialMessage = RegisterDrawerMessage | RegisterFeatureNotificationsMessage; -export type WidgetMessage = InitialMessage | AppLayoutUpdateMessage; +export type WidgetMessage = InitialMessage | AppLayoutUpdateMessage; diff --git a/src/internal/utils/__tests__/promises.test.ts b/src/internal/utils/__tests__/promises.test.ts index 668a474491..04fd4f5dcf 100644 --- a/src/internal/utils/__tests__/promises.test.ts +++ b/src/internal/utils/__tests__/promises.test.ts @@ -1,7 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { makeCancellable, PromiseCancelledSignal } from '../promises'; +import { act } from '@testing-library/react'; +import { renderHook } from '../../../__tests__/render-hook'; +import { makeCancellable, PromiseCancelledSignal, useMountRefPromise } from '../promises'; const waitForPromises = () => new Promise(resolve => setTimeout(resolve, 0)); describe('makeCancellable', () => { @@ -58,3 +60,53 @@ describe('makeCancellable', () => { expect(onCancel).toHaveBeenCalledWith(expect.any(PromiseCancelledSignal)); }); }); + +describe('useMountRefPromise', () => { + test('resolves promise when element mounts', async () => { + const onResolve = jest.fn(); + const { result } = renderHook(() => useMountRefPromise()); + + result.current.promise.then(onResolve); + + const element = document.createElement('div'); + act(() => { + result.current.ref(element); + }); + + await waitForPromises(); + expect(onResolve).toHaveBeenCalledWith(element); + }); + + test('does not resolve when element is null', async () => { + const onResolve = jest.fn(); + const { result } = renderHook(() => useMountRefPromise()); + + result.current.promise.then(onResolve); + + act(() => { + result.current.ref(null); + }); + + await waitForPromises(); + expect(onResolve).not.toHaveBeenCalled(); + }); + + test('resolves only once for multiple ref calls', async () => { + const onResolve = jest.fn(); + const { result } = renderHook(() => useMountRefPromise()); + + result.current.promise.then(onResolve); + + const element1 = document.createElement('div'); + const element2 = document.createElement('div'); + + act(() => { + result.current.ref(element1); + result.current.ref(element2); + }); + + await waitForPromises(); + expect(onResolve).toHaveBeenCalledTimes(1); + expect(onResolve).toHaveBeenCalledWith(element1); + }); +}); diff --git a/src/internal/utils/promises.ts b/src/internal/utils/promises.ts index 6b2a7e9b53..244de03ab6 100644 --- a/src/internal/utils/promises.ts +++ b/src/internal/utils/promises.ts @@ -1,5 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { useCallback, useRef } from 'react'; + export class PromiseCancelledSignal {} /** @@ -37,3 +39,33 @@ export function makeCancellable(promise: Promise): { isCancelled: () => cancelled, }; } + +/** + * Returns a ref callback and promise that resolves when the ref is attached to a mounted element. + * Useful for coordinating async operations that depend on DOM element availability, such as + * setting focus or measuring dimensions after mount. + * + * @returns An object containing a ref callback and a promise that resolves with the mounted element + */ +export function useMountRefPromise() { + const promiseRef = useRef<{ + promise: Promise; + resolve: (element: T) => void; + }>(); + + if (!promiseRef.current) { + let resolve: (element: T) => void; + const promise = new Promise(res => { + resolve = res; + }); + promiseRef.current = { promise, resolve: resolve! }; + } + + const ref = useCallback(element => { + if (element) { + promiseRef.current!.resolve(element); + } + }, []); + + return { ref, promise: promiseRef.current.promise }; +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 0000000000..7b1809a98b --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +'use client'; +export * from '../internal/plugins/widget/interfaces'; +import { + clearFeatureNotifications, + registerFeatureNotificationsPublic, + showFeaturePromptIfPossible, +} from '../internal/plugins/widget/index'; + +export const featureNotifications = { + registerFeatureNotifications: registerFeatureNotificationsPublic, + showFeaturePromptIfPossible, + clearFeatureNotifications, +}; diff --git a/src/popover/body.tsx b/src/popover/body.tsx index 4742e42bc2..283a4236b1 100644 --- a/src/popover/body.tsx +++ b/src/popover/body.tsx @@ -18,12 +18,12 @@ import styles from './styles.css.js'; export interface PopoverBodyProps { dismissButton: boolean; dismissAriaLabel: string | undefined; - onDismiss: (() => void) | undefined; + onDismiss: ((method?: 'escape' | 'close-button') => void) | undefined; onBlur?: (event: React.FocusEvent) => void; header: React.ReactNode | undefined; children: React.ReactNode; - variant?: 'annotation' | 'chart'; + variant?: 'annotation' | 'chart' | 'feature-prompt'; overflowVisible?: 'content' | 'both'; className?: string; @@ -58,7 +58,7 @@ const PopoverBody = React.forwardRef( (event: React.KeyboardEvent) => { if (event.keyCode === KeyCode.escape) { event.stopPropagation(); - onDismiss?.(); + onDismiss?.('escape'); } }, [onDismiss] @@ -85,7 +85,7 @@ const PopoverBody = React.forwardRef( iconName="close" className={styles['dismiss-control']} ariaLabel={i18n('dismissAriaLabel', dismissAriaLabel)} - onClick={() => onDismiss?.()} + onClick={() => onDismiss?.('close-button')} ref={dismissButtonRef} />
From 63d4bd47e54854e97a60a66e8775e0ff15ac5b0f Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 24 Feb 2026 16:17:24 +0100 Subject: [PATCH 2/5] chore: small fix --- .../feature-prompt.page.tsx | 238 +++++++++--------- src/__a11y__/a11y-page-object.ts | 1 - 2 files changed, 116 insertions(+), 123 deletions(-) diff --git a/pages/feature-notifications/feature-prompt.page.tsx b/pages/feature-notifications/feature-prompt.page.tsx index 38cdaefb85..2c6d04562d 100644 --- a/pages/feature-notifications/feature-prompt.page.tsx +++ b/pages/feature-notifications/feature-prompt.page.tsx @@ -31,7 +31,6 @@ import AppContext, { AppContextType } from '../app/app-context'; import { Breadcrumbs, Containers, Navigation, Tools } from '../app-layout/utils/content-blocks'; import labels from '../app-layout/utils/labels'; import * as toolsContent from '../app-layout/utils/tools-content'; -import { IframeWrapper } from '../utils/iframe-wrapper'; import ScreenshotArea from '../utils/screenshot-area'; awsuiPlugins.appLayout.registerDrawer({ @@ -249,128 +248,123 @@ export default function () { tools={{toolsContent.long}} toolsHide={!hasTools} content={ - ( - <> -
-
- Demo page -
-
- setUrlParams({ hasTools: detail.checked })}> - Use Tools - - - - - - - - - + + + + - - - - - - )} - /> + return () => unmount(container); + }, + persistenceConfig: { + uniqueKey: 'feature-notifications', + }, + // DON'T USE + ...{ + __persistFeatureNotifications: async function ( + persistenceConfig: FeatureNotificationsPersistenceConfig, + value: Record + ) { + const result = await new Promise(resolve => + setTimeout(() => { + localStorage.setItem(persistenceConfig.uniqueKey, JSON.stringify(value)); + resolve(); + }, 150) + ); + return result; + }, + __retrieveFeatureNotifications: async function ( + persistenceConfig: FeatureNotificationsPersistenceConfig + ) { + const result = await new Promise>(resolve => + setTimeout( + () => + resolve( + localStorage.getItem(persistenceConfig.uniqueKey) + ? JSON.parse(localStorage.getItem(persistenceConfig.uniqueKey)!) + : {} + ), + 150 + ) + ); + return result; + }, + }, + }); + }} + > + Override + + + + + + } /> diff --git a/src/__a11y__/a11y-page-object.ts b/src/__a11y__/a11y-page-object.ts index 48d749e04a..f75251b3ca 100644 --- a/src/__a11y__/a11y-page-object.ts +++ b/src/__a11y__/a11y-page-object.ts @@ -115,7 +115,6 @@ function ariaLevelViolationsFilter(violation: Axe.Result) { function landmarkViolationFilter(violation: Axe.Result, currentUrl: string) { return ( !currentUrl.includes('app-layout/multi') || - !currentUrl.includes('feature-notifications/feature-prompt') || (violation.id !== 'landmark-main-is-top-level' && violation.id !== 'landmark-unique' && violation.id !== 'landmark-no-duplicate-main') From 492d92d2ddcdfc8f1f42b2aed683d8590aa17657 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 24 Feb 2026 16:43:42 +0100 Subject: [PATCH 3/5] chore: Exclude feature notifications from a11y tests, use getExternalProps for registerFeatureNotifications --- src/__a11y__/run-a11y-tests.ts | 2 +- src/internal/plugins/widget/index.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/__a11y__/run-a11y-tests.ts b/src/__a11y__/run-a11y-tests.ts index 5e1ca838ae..eb38f0a14e 100644 --- a/src/__a11y__/run-a11y-tests.ts +++ b/src/__a11y__/run-a11y-tests.ts @@ -22,7 +22,7 @@ function urlFormatter(inputUrl: string, theme: Theme, mode: Mode) { return `#/${mode}/${inputUrl}?visualRefresh=${theme === 'visual-refresh' ? 'true' : 'false'}`; } -const vrOnlyComponents = ['app-layout-toolbar', 'list']; +const vrOnlyComponents = ['app-layout-toolbar', 'list', 'feature-notifications']; export default function runA11yTests(theme: Theme, mode: Mode, skip: string[] = []) { describe(`A11y checks for ${mode} ${theme}`, () => { diff --git a/src/internal/plugins/widget/index.ts b/src/internal/plugins/widget/index.ts index bd88633460..83dea3e289 100644 --- a/src/internal/plugins/widget/index.ts +++ b/src/internal/plugins/widget/index.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { getExternalProps } from '../../utils/external-props'; import { getAppLayoutInitialMessages, getAppLayoutMessageHandler, pushInitialMessage } from './core'; import { AppLayoutUpdateMessage, @@ -45,7 +46,7 @@ export function registerFeatureNotifications(payload: FeatureNotificationsPay getAppLayoutMessageHandler()?.(message as WidgetMessage); } export function registerFeatureNotificationsPublic(payload: FeatureNotificationsPayloadPublic) { - registerFeatureNotifications(payload); + registerFeatureNotifications(getExternalProps(payload)); } export function showFeaturePromptIfPossible() { From 0f194cfbedf03b0021f0ce109a6e8e2fa6c4edf1 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Tue, 24 Feb 2026 19:38:57 +0100 Subject: [PATCH 4/5] chore: Small refactoring --- .../state/use-feature-notifications.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app-layout/visual-refresh-toolbar/state/use-feature-notifications.tsx b/src/app-layout/visual-refresh-toolbar/state/use-feature-notifications.tsx index 96547209bf..00bab5a0e8 100644 --- a/src/app-layout/visual-refresh-toolbar/state/use-feature-notifications.tsx +++ b/src/app-layout/visual-refresh-toolbar/state/use-feature-notifications.tsx @@ -36,11 +36,9 @@ interface FeatureNotifications extends FeatureNotificationsPayload { const FEATURE_NOTIFICATIONS_RETENTION_DAYS = 180; const DEFAULT_FEATURE_FILTER_DAYS = 90; -function subtractDaysFromDate(currentDate: Date, daysToSubtract: number) { - daysToSubtract = daysToSubtract || 0; +function subtractDaysFromDate(currentDate: Date, daysToSubtract: number = 0): Date { const pastDate = new Date(currentDate); pastDate.setDate(pastDate.getDate() - daysToSubtract); - return pastDate; } From c73929adfcf643d3b6a518730f96654f2af99410 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Thu, 26 Feb 2026 10:14:39 +0100 Subject: [PATCH 5/5] fix: eslint --- src/i18n/messages-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 523849e5f2..f706a66441 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -330,7 +330,7 @@ export interface I18nFormatArgTypes { 'ariaLabels.jumpToPageButtonLabel': never; 'i18nStrings.jumpToPageInputLabel': never; 'i18nStrings.jumpToPageError': never; - "i18nStrings.jumpToPageLoadingText": never; + 'i18nStrings.jumpToPageLoadingText': never; }; 'panel-resize-handle': { 'i18nStrings.resizeHandleAriaLabel': never;