From d2ec0d7e3e1a40e58cc66f362f17fc32fa22e83d Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 7 Oct 2026 11:02:42 +0200 Subject: [PATCH 1/3] feat: Global breadcrumbs feature flag --- pages/app/index.tsx | 14 ++++-- .../widget-contract-old.test.tsx.snap | 6 +-- ...get-contract-split-panel-old.test.tsx.snap | 4 +- .../runtime-breadcrumbs-widgetized.test.tsx | 46 +++++++++++++++++++ src/app-layout/utils/feature-flags.ts | 3 ++ .../visual-refresh-toolbar/interfaces.ts | 2 +- .../skeleton/toolbar-container.tsx | 8 ++-- .../state/use-app-layout.tsx | 3 +- .../visual-refresh-toolbar/toolbar/index.tsx | 6 +-- .../widget-areas/before-main-slot.tsx | 4 +- 10 files changed, 77 insertions(+), 19 deletions(-) diff --git a/pages/app/index.tsx b/pages/app/index.tsx index b7ac1bfe6c..8acc87f7fd 100644 --- a/pages/app/index.tsx +++ b/pages/app/index.tsx @@ -26,6 +26,7 @@ import styles from './styles.scss'; interface GlobalFlags { appLayoutWidget?: boolean; appLayoutToolbar?: boolean; + appLayoutHideToolbarBreadcrumbs?: boolean; oneTheme?: boolean; } // used for local dev / testing @@ -99,9 +100,15 @@ function App() { } const history = createHashHistory(); -const { direction, visualRefresh, theme, appLayoutWidget, appLayoutToolbar, appLayoutDelayedWidget } = parseQuery( - history.location.search -); +const { + direction, + visualRefresh, + theme, + appLayoutWidget, + appLayoutToolbar, + appLayoutHideToolbarBreadcrumbs, + appLayoutDelayedWidget, +} = parseQuery(history.location.search); const oneTheme = theme === Theme.OneTheme; // core-update is an override on top of visual refresh, so it always requires the visual refresh base. const coreNew = theme === Theme.CoreUpdate; @@ -117,6 +124,7 @@ if (!window[awsuiCustomFlagsSymbol]) { } window[awsuiGlobalFlagsSymbol].appLayoutWidget = appLayoutWidget; window[awsuiGlobalFlagsSymbol].appLayoutToolbar = appLayoutToolbar; +window[awsuiGlobalFlagsSymbol].appLayoutHideToolbarBreadcrumbs = appLayoutHideToolbarBreadcrumbs; window[awsuiCustomFlagsSymbol].appLayoutDelayedWidget = appLayoutDelayedWidget; window[awsuiGlobalFlagsSymbol].oneTheme = oneTheme; 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 11a4fd5e2c..b87fcab779 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 @@ -155,11 +155,11 @@ Map { "toolbar": 0, }, }, - "breadcrumbsExternallyOwned": false, "featureNotificationsProps": { "drawer": null, "renderLatestFeaturePrompt": [Function], }, + "hideBreadcrumbsInToolbar": false, "reportOwnBreadcrumbsProps": [Function], "toolbarProps": { "activeDrawerId": null, @@ -1017,11 +1017,11 @@ Map { "toolbar": 0, }, }, - "breadcrumbsExternallyOwned": false, "featureNotificationsProps": { "drawer": null, "renderLatestFeaturePrompt": [Function], }, + "hideBreadcrumbsInToolbar": false, "reportOwnBreadcrumbsProps": [Function], "toolbarProps": { "activeDrawerId": null, @@ -2081,11 +2081,11 @@ Map { "toolbar": 0, }, }, - "breadcrumbsExternallyOwned": false, "featureNotificationsProps": { "drawer": null, "renderLatestFeaturePrompt": [Function], }, + "hideBreadcrumbsInToolbar": false, "reportOwnBreadcrumbsProps": [Function], "toolbarProps": { "activeDrawerId": "security", 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 bf3089b325..774fc09b24 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 @@ -155,11 +155,11 @@ Map { "toolbar": 0, }, }, - "breadcrumbsExternallyOwned": false, "featureNotificationsProps": { "drawer": null, "renderLatestFeaturePrompt": [Function], }, + "hideBreadcrumbsInToolbar": false, "reportOwnBreadcrumbsProps": [Function], "toolbarProps": { "activeDrawerId": null, @@ -1018,11 +1018,11 @@ Map { "toolbar": 0, }, }, - "breadcrumbsExternallyOwned": false, "featureNotificationsProps": { "drawer": null, "renderLatestFeaturePrompt": [Function], }, + "hideBreadcrumbsInToolbar": false, "reportOwnBreadcrumbsProps": [Function], "toolbarProps": { "activeDrawerId": null, diff --git a/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx b/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx index e7db26aabd..61d13786b6 100644 --- a/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx +++ b/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { act, cleanup, render, waitFor } from '@testing-library/react'; +import { setGlobalFlag } from '@cloudscape-design/component-toolkit/internal/testing'; + import AppLayout from '../../../lib/components/app-layout'; import BreadcrumbGroup, { BreadcrumbGroupProps } from '../../../lib/components/breadcrumb-group'; import { getFunnelNameSelector } from '../../../lib/components/internal/analytics/selectors'; @@ -363,4 +365,48 @@ describeEachAppLayout({ themes: ['refresh-toolbar'], sizes: ['desktop'] }, () => second.unregister(); }); + + describe('with appLayoutHideToolbarBreadcrumbs flag', () => { + const flagName = 'appLayoutHideToolbarBreadcrumbs' as Parameters[0]; + + beforeEach(() => setGlobalFlag(flagName, true)); + afterEach(() => setGlobalFlag(flagName, undefined)); + + test('marks breadcrumbs as externally owned on first render without a consumer', () => { + render(} />); + expectAppLayoutBreadcrumbsToBeExternallyOwned(); + expect(getAppLayoutBreadcrumbGroup()).toBeTruthy(); + }); + + test('publishes breadcrumbs once a consumer registers', async () => { + render(} />); + + const externalContainer = document.createElement('div'); + document.body.appendChild(externalContainer); + act(() => { + registerExternalContainer(externalContainer); + }); + + await waitFor(() => expect(externalContainer).toHaveTextContent('Home / Resource')); + expectAppLayoutBreadcrumbsToBeExternallyOwned(); + externalContainer.remove(); + }); + + test('keeps breadcrumbs externally owned after the consumer unregisters', async () => { + const externalContainer = document.createElement('div'); + const { registration } = registerExternalContainer(externalContainer); + render(} />); + + await waitFor(() => expect(externalContainer).toHaveTextContent('Home / Resource')); + act(() => registration.unregister()); + expectAppLayoutBreadcrumbsToBeExternallyOwned(); + }); + }); + + test('does not mark breadcrumbs as externally owned without the flag or a consumer', () => { + render(} />); + const breadcrumbsSection = getAppLayoutBreadcrumbsSection()?.getElement(); + expect(breadcrumbsSection).not.toHaveAttribute('data-awsui-external-breadcrumbs'); + expect(breadcrumbsSection).not.toHaveClass(toolbarStyles['external-breadcrumbs']); + }); }); diff --git a/src/app-layout/utils/feature-flags.ts b/src/app-layout/utils/feature-flags.ts index 71525f2298..7684c2edbf 100644 --- a/src/app-layout/utils/feature-flags.ts +++ b/src/app-layout/utils/feature-flags.ts @@ -23,3 +23,6 @@ export const useAppLayoutToolbarDesignEnabled = () => { return isToolbarPublic || isToolbarPrivate; }; + +export const isAppLayoutHideToolbarBreadcrumbsEnabled = () => + !!getGlobalFlag('appLayoutHideToolbarBreadcrumbs' as Parameters[0]); diff --git a/src/app-layout/visual-refresh-toolbar/interfaces.ts b/src/app-layout/visual-refresh-toolbar/interfaces.ts index a9bee80e2b..c22b1b928e 100644 --- a/src/app-layout/visual-refresh-toolbar/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/interfaces.ts @@ -91,7 +91,7 @@ export interface AppLayoutInternals { } export interface AppLayoutWidgetizedState extends AppLayoutInternals { - breadcrumbsExternallyOwned?: boolean; + hideBreadcrumbsInToolbar?: boolean; reportOwnBreadcrumbsProps?: BreadcrumbsSlotContextType['reportOwnBreadcrumbsProps']; isNested: boolean; verticalOffsets: VerticalLayoutOutput; diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/toolbar-container.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/toolbar-container.tsx index 6401ac362c..b093f7100d 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/toolbar-container.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/toolbar-container.tsx @@ -28,7 +28,7 @@ export function ToolbarContainer({ children, hasAiDrawer }: ToolbarContainerProp interface ToolbarBreadcrumbsSectionProps { ownBreadcrumbs: React.ReactNode; discoveredBreadcrumbs?: BreadcrumbGroupProps | null; - breadcrumbsExternallyOwned?: boolean; + hideBreadcrumbsInToolbar?: boolean; reportOwnBreadcrumbsProps?: BreadcrumbsSlotContextType['reportOwnBreadcrumbsProps']; includeTestUtils?: boolean; } @@ -36,7 +36,7 @@ interface ToolbarBreadcrumbsSectionProps { export function ToolbarBreadcrumbsSection({ ownBreadcrumbs, discoveredBreadcrumbs, - breadcrumbsExternallyOwned, + hideBreadcrumbsInToolbar, reportOwnBreadcrumbsProps, includeTestUtils = false, }: ToolbarBreadcrumbsSectionProps) { @@ -44,10 +44,10 @@ export function ToolbarBreadcrumbsSection({
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 12ce9de3b6..972c7120a3 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 @@ -54,7 +54,7 @@ export const BeforeMainSlotImplementationInternal = ({ activeAiDrawer, bottomDrawerReportedSize, featureNotificationsProps, - breadcrumbsExternallyOwned, + hideBreadcrumbsInToolbar, reportOwnBreadcrumbsProps, } = appLayoutState.widgetizedState; const drawerExpandedMode = !!expandedDrawerId; @@ -67,7 +67,7 @@ export const BeforeMainSlotImplementationInternal = ({ {!!toolbarProps && !embeddedViewMode && !aiDrawerExpandedMode && ( Date: Wed, 7 Oct 2026 12:47:08 +0200 Subject: [PATCH 2/3] chore: Remove custom type cast --- .../__tests__/runtime-breadcrumbs-widgetized.test.tsx | 2 +- src/app-layout/utils/feature-flags.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx b/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx index 61d13786b6..b1fbac6f88 100644 --- a/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx +++ b/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx @@ -367,7 +367,7 @@ describeEachAppLayout({ themes: ['refresh-toolbar'], sizes: ['desktop'] }, () => }); describe('with appLayoutHideToolbarBreadcrumbs flag', () => { - const flagName = 'appLayoutHideToolbarBreadcrumbs' as Parameters[0]; + const flagName = 'appLayoutHideToolbarBreadcrumbs'; beforeEach(() => setGlobalFlag(flagName, true)); afterEach(() => setGlobalFlag(flagName, undefined)); diff --git a/src/app-layout/utils/feature-flags.ts b/src/app-layout/utils/feature-flags.ts index 7684c2edbf..5b052cd4c6 100644 --- a/src/app-layout/utils/feature-flags.ts +++ b/src/app-layout/utils/feature-flags.ts @@ -24,5 +24,4 @@ export const useAppLayoutToolbarDesignEnabled = () => { return isToolbarPublic || isToolbarPrivate; }; -export const isAppLayoutHideToolbarBreadcrumbsEnabled = () => - !!getGlobalFlag('appLayoutHideToolbarBreadcrumbs' as Parameters[0]); +export const isAppLayoutHideToolbarBreadcrumbsEnabled = () => !!getGlobalFlag('appLayoutHideToolbarBreadcrumbs'); From 8e51f6ac690159930b309c3a360d2f43a53ce905 Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 7 Oct 2026 13:46:04 +0200 Subject: [PATCH 3/3] chore: Remove unnecessary u test --- .../runtime-breadcrumbs-widgetized.test.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx b/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx index b1fbac6f88..3e6d50ba44 100644 --- a/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx +++ b/src/app-layout/__tests__/runtime-breadcrumbs-widgetized.test.tsx @@ -378,20 +378,6 @@ describeEachAppLayout({ themes: ['refresh-toolbar'], sizes: ['desktop'] }, () => expect(getAppLayoutBreadcrumbGroup()).toBeTruthy(); }); - test('publishes breadcrumbs once a consumer registers', async () => { - render(} />); - - const externalContainer = document.createElement('div'); - document.body.appendChild(externalContainer); - act(() => { - registerExternalContainer(externalContainer); - }); - - await waitFor(() => expect(externalContainer).toHaveTextContent('Home / Resource')); - expectAppLayoutBreadcrumbsToBeExternallyOwned(); - externalContainer.remove(); - }); - test('keeps breadcrumbs externally owned after the consumer unregisters', async () => { const externalContainer = document.createElement('div'); const { registration } = registerExternalContainer(externalContainer); @@ -407,6 +393,5 @@ describeEachAppLayout({ themes: ['refresh-toolbar'], sizes: ['desktop'] }, () => render(} />); const breadcrumbsSection = getAppLayoutBreadcrumbsSection()?.getElement(); expect(breadcrumbsSection).not.toHaveAttribute('data-awsui-external-breadcrumbs'); - expect(breadcrumbsSection).not.toHaveClass(toolbarStyles['external-breadcrumbs']); }); });