diff --git a/build-tools/utils/custom-css-properties.js b/build-tools/utils/custom-css-properties.js index 73511df043..6096d0fae0 100644 --- a/build-tools/utils/custom-css-properties.js +++ b/build-tools/utils/custom-css-properties.js @@ -32,6 +32,7 @@ const customCssPropertiesList = [ 'offsetTop', 'overlapHeight', 'navigationWidth', + 'navigationCollapsedWidth', 'splitPanelReportedHeaderSize', 'splitPanelReportedSize', 'splitPanelHeight', diff --git a/pages/side-navigation/collapsed-with-app-layout.page.tsx b/pages/side-navigation/collapsed-with-app-layout.page.tsx new file mode 100644 index 0000000000..86161bfa16 --- /dev/null +++ b/pages/side-navigation/collapsed-with-app-layout.page.tsx @@ -0,0 +1,151 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { AppLayoutToolbar, Icon } from '~components'; +import Box from '~components/box'; +import BreadcrumbGroup from '~components/breadcrumb-group'; +import Button from '~components/button'; +import Header from '~components/header'; +// Consumers will use: import { useMobile } from '@cloudscape-design/component-toolkit/internal/use-mobile'; +// Draft started on https://github.com/cloudscape-design/component-toolkit/pull/174 +import { useMobile } from '~components/internal/hooks/use-mobile'; +import SideNavigation, { SideNavigationProps } from '~components/side-navigation'; +import SpaceBetween from '~components/space-between'; +import Toggle from '~components/toggle'; + +import labels from '../app-layout/utils/labels'; + +const items: SideNavigationProps.Item[] = [ + { type: 'link', text: 'Dashboard', href: '#/dashboard', icon: }, + { type: 'link', text: 'Events', href: '#/events', icon: }, + { + type: 'expandable-link-group', + text: 'Projects', + href: '#/projects', + icon: , + items: [ + { type: 'link', text: 'Project 1', href: '#/projects/1' }, + { type: 'link', text: 'Project 2', href: '#/projects/2' }, + { type: 'link', text: 'Project 3', href: '#/projects/3' }, + ], + }, + { type: 'link', text: 'Team', href: '#/team', icon: }, + { type: 'link', text: 'Settings', href: '#/settings', icon: }, + { type: 'divider' }, + { type: 'link', text: 'Notifications', href: '#/notifications', icon: }, + { type: 'link', text: 'Documentation', href: '#/docs', icon: , external: true }, +]; + +const AwsSvg = ( + + + + + +); + +export default function CollapsedWithAppLayoutPage() { + const [navOpen, setNavOpen] = useState(false); + const [activeHref, setActiveHref] = useState('#/dashboard'); + const [navigationCollapsibleEnabled, setNavigationCollapsibleEnabled] = useState(true); + const [navigationSideBorderHide, setNavigationSideBorderHide] = useState(false); + const [hideToolbar, setHideToolbar] = useState(true); + const [hideTools, setHideTools] = useState(true); + const [showHeader, setShowHeader] = useState(true); + const [headerIcon, setHeaderIcon] = useState(true); + const isMobile = useMobile(); + + const header = { text: 'Project 3', href: '#/projects/3' }; + const headerWithIcon = { ...header, logo: { svg: AwsSvg } }; + + return ( + setNavOpen(detail.open)} + ariaLabels={labels} + breadcrumbs={ + isMobile ? ( + + ) : undefined + } + navigation={ + { + e.preventDefault(); + setActiveHref(e.detail.href); + }} + /> + } + content={ + +
Collapsed Navigation with AppLayout
+ + + + This page demonstrates the navigationCollapsible prop on AppLayoutToolbar. When enabled, + the navigation shows a collapsed icon rail instead of disappearing completely when closed. + + setNavigationCollapsibleEnabled(detail.checked)} + > + Enable navigationCollapsible + + setNavigationSideBorderHide(detail.checked)} + > + Enable navigationSideBorderHide + + setHideToolbar(detail.checked)}> + Set navigationTriggerHide + + setHideTools(detail.checked)}> + Hide tools + + setShowHeader(detail.checked)}> + Show side nav header + + {showHeader && ( + + setHeaderIcon(detail.checked)}> + Include icon + + + )} + + + Current state: navigation is{' '} + {navOpen ? 'open' : navigationCollapsibleEnabled ? 'collapsed (rail)' : 'closed (hidden)'} + + + +
+ } + /> + ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 3a778ff8b2..ba02b765c1 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -1473,6 +1473,21 @@ If set to \`Number.MAX_VALUE\`, the main content panel will occupy the full avai "optional": true, "type": "number", }, + { + "defaultValue": "54", + "description": "Width of the collapsed navigation rail in pixels. +Only applies when \`navigationCollapsed\` is \`true\` and \`navigationOpen\` is \`false\`.", + "name": "navigationCollapsedWidth", + "optional": true, + "type": "number", + }, + { + "description": "If \`true\`, the navigation panel shows a collapsed rail at \`navigationCollapsedWidth\` +when \`navigationOpen\` is \`false\`, instead of being completely hidden.", + "name": "navigationCollapsible", + "optional": true, + "type": "boolean", + }, { "description": "If \`true\`, the navigation drawer is not displayed at all.", "name": "navigationHide", @@ -2022,6 +2037,21 @@ If set to \`Number.MAX_VALUE\`, the main content panel will occupy the full avai "optional": true, "type": "number", }, + { + "defaultValue": "54", + "description": "Width of the collapsed navigation rail in pixels. +Only applies when \`navigationCollapsed\` is \`true\` and \`navigationOpen\` is \`false\`.", + "name": "navigationCollapsedWidth", + "optional": true, + "type": "number", + }, + { + "description": "If \`true\`, the navigation panel shows a collapsed rail at \`navigationCollapsedWidth\` +when \`navigationOpen\` is \`false\`, instead of being completely hidden.", + "name": "navigationCollapsible", + "optional": true, + "type": "boolean", + }, { "description": "If \`true\`, the navigation drawer is not displayed at all.", "name": "navigationHide", diff --git a/src/app-layout-toolbar/index.tsx b/src/app-layout-toolbar/index.tsx index 6697b6f5fc..a40660a8ae 100644 --- a/src/app-layout-toolbar/index.tsx +++ b/src/app-layout-toolbar/index.tsx @@ -29,6 +29,7 @@ const AppLayoutToolbar = React.forwardRef( headerSelector = '#b #h', footerSelector = '#b #f', navigationWidth = 280, + navigationCollapsedWidth = 54, toolsWidth = 290, maxContentWidth, minContentWidth, @@ -101,6 +102,7 @@ const AppLayoutToolbar = React.forwardRef( const props = { contentType, navigationWidth, + navigationCollapsedWidth, toolsWidth, navigationOpen, onNavigationChange, 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 d23fa65725..c44fc9b362 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 @@ -90,6 +90,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -192,6 +194,7 @@ Map { "current": null, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": { "current": null, }, @@ -288,6 +291,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -443,6 +448,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -628,6 +635,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -783,6 +792,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -948,6 +959,8 @@ Map { "navigation":
navigation
, + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -1053,6 +1066,7 @@ Map { }, "hasBreadcrumbsPortal": true, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": { "current": null, }, @@ -1155,6 +1169,8 @@ Map { "navigation":
navigation
, + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -1316,6 +1332,8 @@ Map { "navigation":
navigation
, + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -1479,6 +1497,8 @@ Map { "navigation":
navigation
, + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -1672,6 +1692,8 @@ Map { "navigation":
navigation
, + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -1833,6 +1855,8 @@ Map { "navigation":
navigation
, + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -2008,6 +2032,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -2112,6 +2138,7 @@ Map { "current": null, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": { "current": null, }, @@ -2224,6 +2251,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -2395,6 +2424,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -2596,6 +2627,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -2767,6 +2800,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { 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 4883c5fc36..01157680aa 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 @@ -90,6 +90,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -192,6 +194,7 @@ Map { "current": null, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": { "current": null, }, @@ -288,6 +291,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -443,6 +448,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -635,6 +642,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -790,6 +799,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -949,6 +960,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -1051,6 +1064,7 @@ Map { "current": null, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": { "current": null, }, @@ -1147,6 +1161,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -1302,6 +1318,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -1493,6 +1511,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { @@ -1648,6 +1668,8 @@ Map { "minDrawerSize": 290, "minGlobalDrawersSizes": {}, "navigation": , + "navigationCollapsed": false, + "navigationCollapsedWidth": 54, "navigationFocusControl": { "loseFocus": [Function], "refs": { diff --git a/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel.test.tsx.snap b/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel.test.tsx.snap index f880546fa8..a21ca0d373 100644 --- a/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel.test.tsx.snap +++ b/src/app-layout/__tests__/__snapshots__/widget-contract-split-panel.test.tsx.snap @@ -19,6 +19,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -56,6 +57,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -78,6 +80,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -115,6 +118,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -137,6 +141,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -174,6 +179,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -196,6 +202,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -233,6 +240,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -255,6 +263,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -306,6 +315,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -342,6 +352,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -364,6 +375,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -400,6 +412,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -422,6 +435,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -458,6 +472,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -480,6 +495,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -516,6 +532,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -538,6 +555,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], diff --git a/src/app-layout/__tests__/__snapshots__/widget-contract.test.tsx.snap b/src/app-layout/__tests__/__snapshots__/widget-contract.test.tsx.snap index 701222b3bc..85438b4129 100644 --- a/src/app-layout/__tests__/__snapshots__/widget-contract.test.tsx.snap +++ b/src/app-layout/__tests__/__snapshots__/widget-contract.test.tsx.snap @@ -19,6 +19,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -48,6 +49,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -70,6 +72,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -99,6 +102,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -121,6 +125,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -150,6 +155,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -172,6 +178,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -201,6 +208,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -223,6 +231,7 @@ Map { "contentType": "default", "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onNavigationChange": [Function], @@ -275,6 +284,7 @@ Map { "navigation":
navigation
, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "notifications":
@@ -314,6 +324,7 @@ Map { }, "hasBreadcrumbsPortal": true, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -345,6 +356,7 @@ Map { "navigation":
navigation
, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "notifications":
@@ -384,6 +396,7 @@ Map { }, "hasBreadcrumbsPortal": true, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -415,6 +428,7 @@ Map { "navigation":
navigation
, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "notifications":
@@ -454,6 +468,7 @@ Map { }, "hasBreadcrumbsPortal": true, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -485,6 +500,7 @@ Map { "navigation":
navigation
, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "notifications":
@@ -524,6 +540,7 @@ Map { }, "hasBreadcrumbsPortal": true, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -555,6 +572,7 @@ Map { "navigation":
navigation
, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "notifications":
@@ -625,6 +643,7 @@ Map { ], "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onDrawerChange": [Function], @@ -655,6 +674,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -695,6 +715,7 @@ Map { ], "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onDrawerChange": [Function], @@ -725,6 +746,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -765,6 +787,7 @@ Map { ], "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onDrawerChange": [Function], @@ -795,6 +818,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -835,6 +859,7 @@ Map { ], "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onDrawerChange": [Function], @@ -865,6 +890,7 @@ Map { "toolsToggle": undefined, }, "hasNavigation": true, + "navigationCollapsed": false, "navigationFocusRef": undefined, "navigationOpen": true, "onNavigationToggle": undefined, @@ -905,6 +931,7 @@ Map { ], "maxContentWidth": undefined, "minContentWidth": 280, + "navigationCollapsedWidth": 54, "navigationOpen": true, "navigationWidth": 280, "onDrawerChange": [Function], diff --git a/src/app-layout/__tests__/navigation-collapsed.test.tsx b/src/app-layout/__tests__/navigation-collapsed.test.tsx new file mode 100644 index 0000000000..29702ed9aa --- /dev/null +++ b/src/app-layout/__tests__/navigation-collapsed.test.tsx @@ -0,0 +1,167 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import AppLayout from '../../../lib/components/app-layout'; +import customCssProps from '../../../lib/components/internal/generated/custom-css-properties'; +import { describeEachAppLayout, renderComponent } from './utils'; + +import navStyles from '../../../lib/components/app-layout/visual-refresh-toolbar/navigation/styles.css.js'; +import skeletonStyles from '../../../lib/components/app-layout/visual-refresh-toolbar/skeleton/styles.css.js'; +import iconStyles from '../../../lib/components/icon/styles.css.js'; + +jest.mock('@cloudscape-design/component-toolkit', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit'), + useContainerQuery: () => [1300, () => {}], +})); + +describeEachAppLayout({ themes: ['refresh-toolbar'], sizes: ['desktop'] }, () => { + describe('collapsed rail visibility', () => { + test('navigation is visible when navigationCollapsible=true and navigationOpen=false', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + const navContainer = wrapper.findNavigation().getElement().closest(`.${skeletonStyles.navigation}`); + expect(navContainer).not.toHaveClass(skeletonStyles['panel-hidden']); + expect(navContainer).toHaveClass(skeletonStyles['navigation-collapsed']); + }); + + test('navigation is hidden when navigationCollapsible=false and navigationOpen=false', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + const navContainer = wrapper.findNavigation().getElement().closest(`.${skeletonStyles.navigation}`); + expect(navContainer).toHaveClass(skeletonStyles['panel-hidden']); + expect(navContainer).not.toHaveClass(skeletonStyles['navigation-collapsed']); + }); + }); + + describe('toggle behavior', () => { + test('close button shows angle-right icon in collapsed state', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + const closeButton = wrapper.findNavigationClose().getElement(); + expect(closeButton.querySelector(`.${iconStyles['name-angle-right']}`)).not.toBeNull(); + }); + + test('close button shows angle-left icon in open state on desktop', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + const closeButton = wrapper.findNavigationClose().getElement(); + expect(closeButton.querySelector(`.${iconStyles['name-angle-left']}`)).not.toBeNull(); + }); + }); + + describe('aria-expanded', () => { + test('close button has aria-expanded=false when navigation is closed', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + expect(wrapper.findNavigationClose().getElement()).toHaveAttribute('aria-expanded', 'false'); + }); + + test('close button has aria-expanded=true when navigation is open', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + expect(wrapper.findNavigationClose().getElement()).toHaveAttribute('aria-expanded', 'true'); + }); + }); + + describe('aria-hidden', () => { + test('nav is not aria-hidden when collapsed (visible and interactive)', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + expect(wrapper.findNavigation().getElement().closest('nav')).toHaveAttribute('aria-hidden', 'false'); + }); + + test('nav is aria-hidden when closed without collapsed', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + expect(wrapper.findNavigation().getElement().closest('nav')).toHaveAttribute('aria-hidden', 'true'); + }); + }); + + describe('navigationHide guard', () => { + test('navigation is fully hidden when navigationHide=true regardless of navigationCollapsible', () => { + const { wrapper } = renderComponent( + Nav} /> + ); + expect(wrapper.findNavigation()).toBeFalsy(); + expect(wrapper.findNavigationToggle()).toBeFalsy(); + }); + }); + + describe('layout computation', () => { + test('sets navigationCollapsibleWidth CSS property', () => { + const { wrapper } = renderComponent( + Nav} /> + ); + const rootStyle = wrapper.getElement().style; + expect(rootStyle.getPropertyValue(customCssProps.navigationCollapsedWidth)).toBe('54px'); + }); + + test('uses custom navigationCollapsedWidth when provided', () => { + const { wrapper } = renderComponent( + Nav} + /> + ); + const rootStyle = wrapper.getElement().style; + expect(rootStyle.getPropertyValue(customCssProps.navigationCollapsedWidth)).toBe('80px'); + }); + }); + + describe('default values', () => { + test('navigationCollapsedWidth defaults to 54', () => { + const { wrapper } = renderComponent( + Nav} /> + ); + const rootStyle = wrapper.getElement().style; + expect(rootStyle.getPropertyValue(customCssProps.navigationCollapsedWidth)).toBe('54px'); + }); + }); +}); + +describeEachAppLayout({ themes: ['refresh-toolbar'], sizes: ['mobile'] }, () => { + describe('mobile standard behavior (collapsed is desktop-only)', () => { + test('navigation uses panel-hidden when closed on mobile, even with navigationCollapsible', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + const navContainer = wrapper.findNavigation().getElement().closest(`.${skeletonStyles.navigation}`); + expect(navContainer).toHaveClass(skeletonStyles['panel-hidden']); + }); + + test('navigation does not use navigation-collapsed class on mobile', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + const navContainer = wrapper.findNavigation().getElement().closest(`.${skeletonStyles.navigation}`); + expect(navContainer).not.toHaveClass(skeletonStyles['navigation-collapsed']); + }); + + test('navigation-container does not have is-navigation-collapsed class on mobile', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + const nav = wrapper.findNavigation().getElement().closest(`.${navStyles['navigation-container']}`); + expect(nav).not.toHaveClass(navStyles['is-navigation-collapsed']); + }); + + test('navigation opens full width on mobile with standard behavior', () => { + const { wrapper } = renderComponent( + Nav content} /> + ); + const navContainer = wrapper.findNavigation().getElement().closest(`.${skeletonStyles.navigation}`); + expect(navContainer).not.toHaveClass(skeletonStyles['panel-hidden']); + }); + }); +}); diff --git a/src/app-layout/constants.scss b/src/app-layout/constants.scss index 51ed2a67f2..9dfa46984d 100644 --- a/src/app-layout/constants.scss +++ b/src/app-layout/constants.scss @@ -35,4 +35,7 @@ $toolbar-z-index: 1000; // Shared toolbar drawer component values $toolbar-vertical-panel-icon-offset: 14px; +// Default collapsed navigation rail width +$navigation-collapsed-width-default: 54px; + $ai-drawer-background: #161d26; diff --git a/src/app-layout/index.tsx b/src/app-layout/index.tsx index 5c492b8c85..166fcd24b6 100644 --- a/src/app-layout/index.tsx +++ b/src/app-layout/index.tsx @@ -28,6 +28,7 @@ const AppLayout = React.forwardRef( headerSelector = '#b #h', footerSelector = '#b #f', navigationWidth = 280, + navigationCollapsedWidth = 54, toolsWidth = 290, maxContentWidth, minContentWidth, @@ -96,6 +97,7 @@ const AppLayout = React.forwardRef( const props = { contentType, navigationWidth, + navigationCollapsedWidth, toolsWidth, navigationOpen, onNavigationChange, diff --git a/src/app-layout/interfaces.ts b/src/app-layout/interfaces.ts index 753933908d..380ac13a21 100644 --- a/src/app-layout/interfaces.ts +++ b/src/app-layout/interfaces.ts @@ -74,6 +74,24 @@ export interface BaseLayoutProps extends BaseComponentProps { */ navigationWidth?: number; + /** + * If `true`, the navigation panel shows a collapsed rail at `navigationCollapsedWidth` + * when `navigationOpen` is `false`, instead of being completely hidden. + */ + navigationCollapsible?: boolean; + + /** + * Width of the collapsed navigation rail in pixels. + * Only applies when `navigationCollapsed` is `true` and `navigationOpen` is `false`. + * @default 54 + */ + navigationCollapsedWidth?: number; + + /** + * If `true`, the navigation side border is not displayed. + */ + navigationSideBorderHide?: boolean; + /** * If `true`, the navigation drawer is not displayed at all. */ @@ -361,7 +379,13 @@ export namespace AppLayoutProps { export type AppLayoutPropsWithDefaults = SomeRequired< Omit, - 'contentType' | 'navigationWidth' | 'toolsWidth' | 'minContentWidth' | 'navigationOpen' | 'onNavigationChange' + | 'contentType' + | 'navigationWidth' + | 'navigationCollapsedWidth' + | 'toolsWidth' + | 'minContentWidth' + | 'navigationOpen' + | 'onNavigationChange' > & { placement: { insetBlockStart: number; diff --git a/src/app-layout/visual-refresh-toolbar/compute-layout.ts b/src/app-layout/visual-refresh-toolbar/compute-layout.ts index 3bc00b19ff..8274de349b 100644 --- a/src/app-layout/visual-refresh-toolbar/compute-layout.ts +++ b/src/app-layout/visual-refresh-toolbar/compute-layout.ts @@ -7,6 +7,8 @@ import { shouldSplitPanelBeForcedToBottom } from '../split-panel/split-panel-for interface HorizontalLayoutInput { navigationOpen: boolean; navigationWidth: number; + navigationCollapsed: boolean; + navigationCollapsedWidth: number; placement: AppLayoutPropsWithDefaults['placement']; minContentWidth: number; activeDrawerSize: number; @@ -23,6 +25,8 @@ export const CONTENT_PADDING = 2 * 24; // space-xl export function computeHorizontalLayout({ navigationOpen, navigationWidth, + navigationCollapsed, + navigationCollapsedWidth, placement, minContentWidth, activeDrawerSize, @@ -33,7 +37,7 @@ export function computeHorizontalLayout({ activeGlobalDrawersSizes, activeAiDrawerSize, }: HorizontalLayoutInput) { - const activeNavigationWidth = navigationOpen ? navigationWidth : 0; + const activeNavigationWidth = navigationOpen ? navigationWidth : navigationCollapsed ? navigationCollapsedWidth : 0; let resizableSpaceAvailable = placement.inlineSize - minContentWidth - CONTENT_PADDING - activeNavigationWidth - activeAiDrawerSize; diff --git a/src/app-layout/visual-refresh-toolbar/interfaces.ts b/src/app-layout/visual-refresh-toolbar/interfaces.ts index fae99a33ad..d69db96261 100644 --- a/src/app-layout/visual-refresh-toolbar/interfaces.ts +++ b/src/app-layout/visual-refresh-toolbar/interfaces.ts @@ -39,6 +39,8 @@ export interface AppLayoutInternals { headerVariant: AppLayoutPropsWithDefaults['headerVariant']; placement: AppLayoutPropsWithDefaults['placement']; navigationOpen: AppLayoutPropsWithDefaults['navigationOpen']; + navigationCollapsed: boolean; + navigationCollapsedWidth: number; navigationFocusControl: FocusControlState; navigation: React.ReactNode; splitPanelPosition: AppLayoutProps.SplitPanelPreferences['position']; diff --git a/src/app-layout/visual-refresh-toolbar/navigation/index.tsx b/src/app-layout/visual-refresh-toolbar/navigation/index.tsx index 5241cea55d..445d42d0f7 100644 --- a/src/app-layout/visual-refresh-toolbar/navigation/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/navigation/index.tsx @@ -27,6 +27,7 @@ export function AppLayoutNavigationImplementation({ onNavigationToggle, isMobile, navigationOpen, + navigationCollapsed, navigation, navigationFocusControl, placement, @@ -40,6 +41,8 @@ export function AppLayoutNavigationImplementation({ isMobile ? 0 : (bottomDrawerReportedSize ?? 0) ); + const isCollapsedState = navigationCollapsed && !navigationOpen && !isMobile; + // Close the Navigation drawer on mobile when a user clicks a link inside. const onNavigationClick = (event: React.MouseEvent) => { const hasLink = findUpUntil( @@ -55,6 +58,7 @@ export function AppLayoutNavigationImplementation({
-
+
onNavigationToggle(false)} + ariaLabel={ + isCollapsedState + ? (ariaLabels?.navigationToggle ?? undefined) + : (ariaLabels?.navigationClose ?? undefined) + } + ariaExpanded={navigationCollapsed && !isMobile ? navigationOpen : undefined} + iconName={isCollapsedState ? 'angle-right' : isMobile ? 'close' : 'angle-left'} + onClick={() => onNavigationToggle(!navigationOpen)} variant="icon" formAction="none" className={testutilStyles['navigation-close']} ref={navigationFocusControl.refs.close} - analyticsAction="close" + analyticsAction={isCollapsedState ? 'open' : 'close'} />
{navigation} diff --git a/src/app-layout/visual-refresh-toolbar/navigation/styles.scss b/src/app-layout/visual-refresh-toolbar/navigation/styles.scss index adabab1191..4489e24c42 100644 --- a/src/app-layout/visual-refresh-toolbar/navigation/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/navigation/styles.scss @@ -8,6 +8,12 @@ @use '../../../internal/generated/custom-css-properties/index.scss' as custom-props; @use '../../constants.scss' as constants; +.hide-navigation { + position: absolute; + inset-inline-end: awsui.$space-m; + inset-block-start: constants.$toolbar-vertical-panel-icon-offset; +} + // This wrapper clips the actual navigation content during the enter animation. .navigation-container { position: sticky; @@ -20,13 +26,6 @@ display: flex; flex-direction: column; - // All content is hidden by the overflow-x property - &:not(.is-navigation-open) { - inline-size: 0px; - // We need to hide the closed panel to make containing focusable elements not focusable anymore. - display: none; - } - & > .navigation { flex-grow: 1; block-size: 100%; @@ -39,15 +38,42 @@ position: relative; } + // All content is hidden by the overflow-x property + &:not(.is-navigation-open) { + inline-size: 0px; + // We need to hide the closed panel to make containing focusable elements not focusable anymore. + display: none; + } + + &:not(.is-navigation-open).is-navigation-collapsed { + display: flex; + inline-size: var(#{custom-props.$navigationCollapsedWidth}, constants.$navigation-collapsed-width-default); + align-items: center; + + & > .navigation { + inline-size: var(#{custom-props.$navigationCollapsedWidth}, constants.$navigation-collapsed-width-default); + overflow-x: hidden; + } + + // stylelint-disable-next-line @cloudscape-design/no-implicit-descendant + & > .navigation > .hide-navigation { + display: none; + + &.show-navigation-toggle { + display: flex; + position: static; + align-items: center; + justify-content: center; + padding-block: awsui.$space-s; + inset-inline-end: unset; + inset-block-start: unset; + } + } + } + // The Navigation drawer will take up the entire viewport on mobile @include styles.media-breakpoint-down(styles.$breakpoint-x-small) { #{custom-props.$navigationWidth}: 100vw; z-index: constants.$drawer-z-index-mobile; } } - -.hide-navigation { - position: absolute; - inset-inline-end: awsui.$space-m; - inset-block-start: constants.$toolbar-vertical-panel-icon-offset; -} diff --git a/src/app-layout/visual-refresh-toolbar/skeleton/skeleton-parts.tsx b/src/app-layout/visual-refresh-toolbar/skeleton/skeleton-parts.tsx index d697bab89b..dbb5495537 100644 --- a/src/app-layout/visual-refresh-toolbar/skeleton/skeleton-parts.tsx +++ b/src/app-layout/visual-refresh-toolbar/skeleton/skeleton-parts.tsx @@ -35,14 +35,18 @@ export const BeforeMainSlotSkeleton = React.forwardRef