diff --git a/pages/side-navigation/collapsed.page.tsx b/pages/side-navigation/collapsed.page.tsx index c118076565..f97c1866cd 100644 --- a/pages/side-navigation/collapsed.page.tsx +++ b/pages/side-navigation/collapsed.page.tsx @@ -1,13 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useEffect, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import { useReducedMotion } from '@cloudscape-design/component-toolkit/internal'; -import { Box, Button, Icon, SpaceBetween, Toggle } from '~components'; +import { Box, Button, Icon, SpaceBetween } from '~components'; import SideNavigation, { SideNavigationProps } from '~components/side-navigation'; -import { applyTheme } from '~components/theming'; import { colorBorderDividerDefault } from '~design-tokens'; const items: SideNavigationProps.Item[] = [ @@ -24,7 +23,6 @@ const items: SideNavigationProps.Item[] = [ ], }, { type: 'link', text: 'Settings', href: '#/settings', icon: }, - { type: 'divider' }, { type: 'section', text: 'Resources', @@ -34,7 +32,6 @@ const items: SideNavigationProps.Item[] = [ { type: 'link', text: 'Networking', href: '#/networking', icon: }, ], }, - { type: 'divider' }, { type: 'link', text: 'Documentation', href: '#/docs', external: true }, ]; @@ -44,36 +41,11 @@ const EXPANDED_WIDTH = 220; export default function SideNavigationCollapsedPage() { const [activeHref, setActiveHref] = useState('#/calendar'); const [collapsed, setCollapsed] = useState(false); - const [highlighted, setHighlighted] = useState(true); - - useEffect(() => { - handleHighlightedChange(true); - }, []); const [panelWidth, setPanelWidth] = useState(EXPANDED_WIDTH); const navRef = useRef(null); const toggleRef = useRef(null); const reducedMotion = useReducedMotion(navRef); - const resetThemeRef = useRef<(() => void) | null>(null); - - function handleHighlightedChange(value: boolean) { - setHighlighted(value); - if (value) { - const { reset } = applyTheme({ - theme: { - tokens: { - colorBackgroundSideNavigationItemActive: { light: '{colorPrimary50}', dark: '#0099FF20' }, - colorTextSideNavigationItemActive: { light: '{colorPrimary600}', dark: '{colorPrimary300}' }, - }, - }, - }); - resetThemeRef.current = reset; - } else { - resetThemeRef.current?.(); - resetThemeRef.current = null; - } - } - function handleToggle() { const willCollapse = !collapsed; // Focus management: move focus to toggle if focused item will be hidden @@ -107,7 +79,7 @@ export default function SideNavigationCollapsedPage() { {/* Toggle button at top */}
Collapsed state demo Active: {activeHref} - handleHighlightedChange(detail.checked)}> - Background highlight - Toggle the navigation panel using the button. Items without icons are hidden in collapsed mode. Sections - show their icon-bearing children as a flat list. + show their icon-bearing children as a flat list, and the section title becomes a divider in its place.
diff --git a/src/__integ__/__snapshots__/themes.test.ts.snap b/src/__integ__/__snapshots__/themes.test.ts.snap index ca1310c70a..268451d92c 100644 --- a/src/__integ__/__snapshots__/themes.test.ts.snap +++ b/src/__integ__/__snapshots__/themes.test.ts.snap @@ -167,6 +167,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "compact" 1`] = "color-background-segment-hover": "#fafafa", "color-background-segment-wrapper": "#ffffff", "color-background-side-navigation-item-active": "transparent", + "color-background-side-navigation-item-active-collapsed": "transparent", "color-background-skeleton": "#eaeded", "color-background-skeleton-wave": "#f6f6f9", "color-background-slider-handle-active": "#0a4a74", @@ -639,6 +640,8 @@ exports[`CSS Custom Properties match previous snapshot for mode "compact" 1`] = "color-text-segment-default": "#545b64", "color-text-segment-hover": "#16191f", "color-text-side-navigation-item-active": "#0073bb", + "color-text-side-navigation-item-active-collapsed": "#0073bb", + "color-text-side-navigation-item-default": "#545b64", "color-text-small": "#687078", "color-text-status-error": "#d13212", "color-text-status-inactive": "#687078", @@ -1075,6 +1078,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "dark" 1`] = ` "color-background-segment-hover": "#21252c", "color-background-segment-wrapper": "#2a2e33", "color-background-side-navigation-item-active": "transparent", + "color-background-side-navigation-item-active-collapsed": "transparent", "color-background-skeleton": "#2a2e33", "color-background-skeleton-wave": "#414750", "color-background-slider-handle-active": "#44b9d6", @@ -1547,6 +1551,8 @@ exports[`CSS Custom Properties match previous snapshot for mode "dark" 1`] = ` "color-text-segment-default": "#d5dbdb", "color-text-segment-hover": "#fafafa", "color-text-side-navigation-item-active": "#44b9d6", + "color-text-side-navigation-item-active-collapsed": "#44b9d6", + "color-text-side-navigation-item-default": "#d5dbdb", "color-text-small": "#95a5a6", "color-text-status-error": "#ff5d64", "color-text-status-inactive": "#95a5a6", @@ -1983,6 +1989,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "light" 1`] = ` "color-background-segment-hover": "#fafafa", "color-background-segment-wrapper": "#ffffff", "color-background-side-navigation-item-active": "transparent", + "color-background-side-navigation-item-active-collapsed": "transparent", "color-background-skeleton": "#eaeded", "color-background-skeleton-wave": "#f6f6f9", "color-background-slider-handle-active": "#0a4a74", @@ -2455,6 +2462,8 @@ exports[`CSS Custom Properties match previous snapshot for mode "light" 1`] = ` "color-text-segment-default": "#545b64", "color-text-segment-hover": "#16191f", "color-text-side-navigation-item-active": "#0073bb", + "color-text-side-navigation-item-active-collapsed": "#0073bb", + "color-text-side-navigation-item-default": "#545b64", "color-text-small": "#687078", "color-text-status-error": "#d13212", "color-text-status-inactive": "#687078", @@ -2891,6 +2900,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "reduced-motion" "color-background-segment-hover": "#fafafa", "color-background-segment-wrapper": "#ffffff", "color-background-side-navigation-item-active": "transparent", + "color-background-side-navigation-item-active-collapsed": "transparent", "color-background-skeleton": "#eaeded", "color-background-skeleton-wave": "#f6f6f9", "color-background-slider-handle-active": "#0a4a74", @@ -3363,6 +3373,8 @@ exports[`CSS Custom Properties match previous snapshot for mode "reduced-motion" "color-text-segment-default": "#545b64", "color-text-segment-hover": "#16191f", "color-text-side-navigation-item-active": "#0073bb", + "color-text-side-navigation-item-active-collapsed": "#0073bb", + "color-text-side-navigation-item-default": "#545b64", "color-text-small": "#687078", "color-text-status-error": "#d13212", "color-text-status-inactive": "#687078", @@ -3799,6 +3811,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh" "color-background-segment-hover": "#f0fbff", "color-background-segment-wrapper": "#ffffff", "color-background-side-navigation-item-active": "transparent", + "color-background-side-navigation-item-active-collapsed": "transparent", "color-background-skeleton": "#ebebf0", "color-background-skeleton-wave": "#f6f6f9", "color-background-slider-handle-active": "#004a9e", @@ -4271,6 +4284,8 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh" "color-text-segment-default": "#424650", "color-text-segment-hover": "#002b66", "color-text-side-navigation-item-active": "#006ce0", + "color-text-side-navigation-item-active-collapsed": "#006ce0", + "color-text-side-navigation-item-default": "#424650", "color-text-small": "#656871", "color-text-status-error": "#db0000", "color-text-status-inactive": "#656871", @@ -4707,6 +4722,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh- "color-background-segment-hover": "#f0fbff", "color-background-segment-wrapper": "#ffffff", "color-background-side-navigation-item-active": "transparent", + "color-background-side-navigation-item-active-collapsed": "transparent", "color-background-skeleton": "#ebebf0", "color-background-skeleton-wave": "#f6f6f9", "color-background-slider-handle-active": "#004a9e", @@ -5179,6 +5195,8 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh- "color-text-segment-default": "#424650", "color-text-segment-hover": "#002b66", "color-text-side-navigation-item-active": "#006ce0", + "color-text-side-navigation-item-active-collapsed": "#006ce0", + "color-text-side-navigation-item-default": "#424650", "color-text-small": "#656871", "color-text-status-error": "#db0000", "color-text-status-inactive": "#656871", @@ -5615,6 +5633,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh- "color-background-segment-hover": "#1b232d", "color-background-segment-wrapper": "#0f141a", "color-background-side-navigation-item-active": "transparent", + "color-background-side-navigation-item-active-collapsed": "transparent", "color-background-skeleton": "#232b37", "color-background-skeleton-wave": "#333843", "color-background-slider-handle-active": "#75cfff", @@ -6087,6 +6106,8 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh- "color-text-segment-default": "#dedee3", "color-text-segment-hover": "#75cfff", "color-text-side-navigation-item-active": "#42b4ff", + "color-text-side-navigation-item-active-collapsed": "#42b4ff", + "color-text-side-navigation-item-default": "#c6c6cd", "color-text-small": "#a4a4ad", "color-text-status-error": "#ff7a7a", "color-text-status-inactive": "#a4a4ad", @@ -6523,6 +6544,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh- "color-background-segment-hover": "#1b232d", "color-background-segment-wrapper": "#161d26", "color-background-side-navigation-item-active": "transparent", + "color-background-side-navigation-item-active-collapsed": "transparent", "color-background-skeleton": "#232b37", "color-background-skeleton-wave": "#333843", "color-background-slider-handle-active": "#75cfff", @@ -6995,6 +7017,8 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh- "color-text-segment-default": "#dedee3", "color-text-segment-hover": "#75cfff", "color-text-side-navigation-item-active": "#42b4ff", + "color-text-side-navigation-item-active-collapsed": "#42b4ff", + "color-text-side-navigation-item-default": "#c6c6cd", "color-text-small": "#a4a4ad", "color-text-status-error": "#ff7a7a", "color-text-status-inactive": "#a4a4ad", diff --git a/src/side-navigation/__tests__/collapsed-side-navigation.test.tsx b/src/side-navigation/__tests__/collapsed-side-navigation.test.tsx index 60149bfdb6..20b0570f3d 100644 --- a/src/side-navigation/__tests__/collapsed-side-navigation.test.tsx +++ b/src/side-navigation/__tests__/collapsed-side-navigation.test.tsx @@ -137,6 +137,30 @@ describe('SideNavigation collapsed mode', () => { const wrapper = renderSideNavigation({ collapsed: true, items }); expect(wrapper.findAll('a')).toHaveLength(0); }); + + it('renders a divider in place of the section title when collapsed', () => { + const wrapper = renderSideNavigation({ + collapsed: true, + items: [ + iconLink('Top', '#/top'), + { + type: 'section', + text: 'Resources', + items: [iconLink('Compute', '#/compute'), iconLink('Storage', '#/storage')], + }, + ], + }); + expect(wrapper.findAll('hr')).toHaveLength(1); + expect(wrapper.find('ul[aria-label="Resources"]')).not.toBeNull(); + }); + + it('does not render a section divider when the section has no icon-bearing children', () => { + const wrapper = renderSideNavigation({ + collapsed: true, + items: [iconLink('Top', '#/top'), { type: 'section', text: 'Empty', items: [plainLink('A', '#/a')] }], + }); + expect(wrapper.findAll('hr')).toHaveLength(0); + }); }); describe('expandable link groups', () => { diff --git a/src/side-navigation/parts.tsx b/src/side-navigation/parts.tsx index a851da0f24..54eb95c776 100644 --- a/src/side-navigation/parts.tsx +++ b/src/side-navigation/parts.tsx @@ -135,14 +135,39 @@ export function NavigationItemsList({ const itemid = index + 1; const itemPosition = `${position ? `${position},` : ''}${itemid}`; + // Emits a divider as its own list segment (dividers break the
    grouping). + function pushDivider() { + lists[lists.length] = { + listVariant: variant, + element: ( +
    + +
    + ), + }; + currentListIndex = lists.length; + lists[currentListIndex] = { + listVariant: variant, + items: [], + }; + } + // Renders icon-bearing children of a container item as a collapsed group. // The inner
      carries the group label so list semantics are preserved // for screen readers even when the visual header is hidden. - function pushCollapsedGroup(children: ReadonlyArray, label: string) { + function pushCollapsedGroup( + children: ReadonlyArray, + label: string, + { leadingDivider = false }: { leadingDivider?: boolean } = {} + ) { const iconChildren = children.filter(child => (child as SideNavigationProps.Link).icon); if (iconChildren.length === 0) { return; } + // A section's title is hidden when collapsed; render a divider in its place + if (leadingDivider) { + pushDivider(); + } const groupElements = iconChildren.map((child, childIndex) => { const childPosition = `${position ? `${position},` : ''}${itemid},${childIndex + 1}`; return ( @@ -166,7 +191,7 @@ export function NavigationItemsList({ key={`group-${itemid}`} className={clsx( styles['list-item--group'], - prevItem?.type === 'divider' && styles['list-item--group-no-padding-start'], + (leadingDivider || prevItem?.type === 'divider') && styles['list-item--group-no-padding-start'], nextItem?.type === 'divider' && styles['list-item--group-no-padding-end'] )} > @@ -196,7 +221,7 @@ export function NavigationItemsList({ : (item as SideNavigationProps.SectionGroup).items.flatMap(child => child.type === 'section' ? (child as SideNavigationProps.Section).items : [child] ); - pushCollapsedGroup(childItems, sectionLabel); + pushCollapsedGroup(childItems, sectionLabel, { leadingDivider: true }); return; } if (collapsed && item.type !== 'divider' && !(item as SideNavigationProps.Link).icon) { @@ -204,20 +229,7 @@ export function NavigationItemsList({ } switch (item.type) { case 'divider': { - const dividerIndex = lists.length; - lists[dividerIndex] = { - listVariant: variant, - element: ( -
      - -
      - ), - }; - currentListIndex = lists.length; - lists[currentListIndex] = { - listVariant: variant, - items: [], - }; + pushDivider(); return; } case 'link': { diff --git a/src/side-navigation/styles.scss b/src/side-navigation/styles.scss index 721e49ed75..e391eaeda1 100644 --- a/src/side-navigation/styles.scss +++ b/src/side-navigation/styles.scss @@ -154,9 +154,6 @@ $expandable-icon-negative-margin: awsui.$space-l; padding-block: 0; padding-inline: $item-padding-inline; list-style: none; - @include styles.with-motion { - transition: margin awsui.$motion-duration-expressive awsui.$motion-easing-responsive; - } // Remove margin from first item in side nav, outer block margins are covered by list-container .list-variant-root--first > &:first-child { margin-block-start: 0px; @@ -168,9 +165,6 @@ $expandable-icon-negative-margin: awsui.$space-l; justify-content: center; padding-block: 0; padding-inline: 0; - @include styles.with-motion { - transition: margin awsui.$motion-duration-expressive awsui.$motion-easing-responsive; - } } &--group { @@ -284,8 +278,8 @@ $expandable-icon-negative-margin: awsui.$space-l; // ========================================================================== .link { @include styles.font-body-m; - color: awsui.$color-text-body-secondary; - display: inline-flex; + color: awsui.$color-text-side-navigation-item-default; + display: flex; padding-block: $item-padding-block; min-inline-size: awsui.$size-side-navigation-item-height; padding-inline: $item-padding-inline; @@ -295,17 +289,20 @@ $expandable-icon-negative-margin: awsui.$space-l; font-weight: styles.$font-weight-normal; -webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto; - @include styles.with-motion { - transition: - background-color awsui.$motion-duration-expressive awsui.$motion-easing-responsive, - color awsui.$motion-duration-expressive awsui.$motion-easing-expressive; - } &-active { font-weight: awsui.$font-wayfinding-link-active-weight; @include styles.font-smoothing; color: awsui.$color-text-side-navigation-item-active; background-color: awsui.$color-background-side-navigation-item-active; + + &.link--collapsed { + color: awsui.$color-text-side-navigation-item-active-collapsed; + background-color: awsui.$color-background-side-navigation-item-active-collapsed; + &:hover { + color: awsui.$color-text-side-navigation-item-active-collapsed; + } + } } &--collapsed { diff --git a/style-dictionary/one-theme/colors.ts b/style-dictionary/one-theme/colors.ts index e95130f580..2f3ea53d39 100644 --- a/style-dictionary/one-theme/colors.ts +++ b/style-dictionary/one-theme/colors.ts @@ -164,7 +164,10 @@ const tokens: StyleDictionary.ColorsDictionary = { // ── Side Nav ───────────────────────────────────────────────────────────────── colorTextSideNavigationItemActive: { light: '{colorPrimary600}', dark: '{colorPrimary300}' }, - colorBackgroundSideNavigationItemActive: { light: '{colorPrimary50}', dark: 'rgba(0, 153, 255, 0.13)' }, + colorTextSideNavigationItemActiveCollapsed: { light: '{colorNeutral100}', dark: '{colorNeutral1000}' }, + colorTextSideNavigationItemDefault: { light: '{colorTextBodyDefault}', dark: '{colorTextBodyDefault}' }, + colorBackgroundSideNavigationItemActive: { light: 'transparent', dark: 'transparent' }, + colorBackgroundSideNavigationItemActiveCollapsed: { light: '{colorPrimary500}', dark: '{colorPrimary500}' }, // ── Dropzone ────────────────────────────────────────────────────────────── colorDropzoneBackgroundDefault: { light: '{colorWhite}', dark: '{colorNeutral850}' }, diff --git a/style-dictionary/one-theme/sizes.ts b/style-dictionary/one-theme/sizes.ts index feb9d6ed01..134a391460 100644 --- a/style-dictionary/one-theme/sizes.ts +++ b/style-dictionary/one-theme/sizes.ts @@ -8,6 +8,7 @@ import { tokens as parentTokens } from '../visual-refresh/sizes.js'; const tokens: StyleDictionary.SizesDictionary = { sizeVerticalInput: { comfortable: '30px', compact: '26px' }, + sizeSideNavigationItemHeight: { comfortable: '30px', compact: '26px' }, }; const expandedTokens: StyleDictionary.ExpandedDensityScopeDictionary = merge( diff --git a/style-dictionary/one-theme/spacing.ts b/style-dictionary/one-theme/spacing.ts index 1dcdfcc0cf..a6a3ddb679 100644 --- a/style-dictionary/one-theme/spacing.ts +++ b/style-dictionary/one-theme/spacing.ts @@ -14,6 +14,8 @@ const tokens: StyleDictionary.SpacingDictionary = { spaceTokenVertical: '1px', spaceFieldVertical: { comfortable: '4px', compact: '2px' }, spaceStatusIndicatorPaddingHorizontal: '2px', + spaceSideNavigationItemGap: '5px', + spaceSideNavigationItemCollapsedGap: '5px', }; const expandedTokens: StyleDictionary.ExpandedDensityScopeDictionary = merge( diff --git a/style-dictionary/utils/token-names.ts b/style-dictionary/utils/token-names.ts index a7e65153ab..57f06daeb1 100644 --- a/style-dictionary/utils/token-names.ts +++ b/style-dictionary/utils/token-names.ts @@ -580,6 +580,7 @@ export type ColorsTokenName = | 'colorBackgroundInputDisabled' | 'colorBackgroundItemSelected' | 'colorBackgroundSideNavigationItemActive' + | 'colorBackgroundSideNavigationItemActiveCollapsed' | 'colorBackgroundLayoutMain' | 'colorBackgroundDrawer' | 'colorBackgroundDrawerBackdrop' @@ -852,6 +853,8 @@ export type ColorsTokenName = | 'colorGapGlobalDrawer' | 'colorItemSelected' | 'colorTextSideNavigationItemActive' + | 'colorTextSideNavigationItemActiveCollapsed' + | 'colorTextSideNavigationItemDefault' | 'colorBackgroundActionCardDefault' | 'colorBackgroundActionCardHover' | 'colorBackgroundActionCardActive' diff --git a/style-dictionary/visual-refresh/colors.ts b/style-dictionary/visual-refresh/colors.ts index f8758c8bb5..5d3ea7eaaa 100644 --- a/style-dictionary/visual-refresh/colors.ts +++ b/style-dictionary/visual-refresh/colors.ts @@ -62,6 +62,7 @@ const tokens: StyleDictionary.ColorsDictionary = { colorBackgroundInputDisabled: { light: '{colorNeutral250}', dark: '{colorNeutral800}' }, colorBackgroundItemSelected: { light: '{colorPrimary50}', dark: '{colorPrimary1000}' }, colorBackgroundSideNavigationItemActive: 'transparent', + colorBackgroundSideNavigationItemActiveCollapsed: '{colorBackgroundSideNavigationItemActive}', colorBackgroundLayoutMain: { light: '{colorWhite}', dark: '{colorNeutral850}' }, colorBackgroundDrawer: '{colorBackgroundLayoutPanelContent}', colorBackgroundBackdrop: '{colorGreyOpaque70}', @@ -149,6 +150,8 @@ const tokens: StyleDictionary.ColorsDictionary = { colorTextButtonPrimaryDisabled: { light: '{colorNeutral500}', dark: '{colorNeutral500}' }, colorItemSelected: { light: '{colorPrimary600}', dark: '{colorPrimary400}' }, colorTextSideNavigationItemActive: '{colorTextAccent}', + colorTextSideNavigationItemActiveCollapsed: '{colorTextSideNavigationItemActive}', + colorTextSideNavigationItemDefault: '{colorTextBodySecondary}', colorBorderCalendarGrid: 'transparent', colorBorderCalendarGridSelectedFocusRing: { light: '{colorNeutral100}', dark: '{colorNeutral850}' }, colorBorderCellShaded: { light: '{colorNeutral300}', dark: '{colorNeutral700}' }, diff --git a/style-dictionary/visual-refresh/metadata/colors.ts b/style-dictionary/visual-refresh/metadata/colors.ts index c52b6282a1..ae94283197 100644 --- a/style-dictionary/visual-refresh/metadata/colors.ts +++ b/style-dictionary/visual-refresh/metadata/colors.ts @@ -175,6 +175,11 @@ const metadata: StyleDictionary.MetadataIndex = { public: true, themeable: true, }, + colorBackgroundSideNavigationItemActiveCollapsed: { + description: 'The background color of an active side navigation item while the navigation is collapsed.', + public: false, + themeable: true, + }, colorBackgroundLayoutMain: { description: 'The background color of the main content area on a page. For example: content area in app layout.', public: true, @@ -488,6 +493,16 @@ const metadata: StyleDictionary.MetadataIndex = { public: true, themeable: true, }, + colorTextSideNavigationItemActiveCollapsed: { + description: 'The text (and icon) color of an active side navigation item while the navigation is collapsed.', + public: false, + themeable: true, + }, + colorTextSideNavigationItemDefault: { + description: 'The text (and icon) color of a non-active side navigation item.', + public: false, + themeable: true, + }, colorBorderCard: { description: 'The border color of a card.', public: true,