From 95aac777601c402ef85f10ea5be36cae8e33d40c Mon Sep 17 00:00:00 2001
From: Richard Dinh <1038306+flacoman91@users.noreply.github.com>
Date: Thu, 14 May 2026 07:41:33 -0700
Subject: [PATCH 01/10] Update Icon stories (#580)
Fixes a silly linting warning. th / table headers need to live inside a table row
---
src/components/Icon/icon.stories.tsx | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/components/Icon/icon.stories.tsx b/src/components/Icon/icon.stories.tsx
index fab54da915..bf13d4829d 100644
--- a/src/components/Icon/icon.stories.tsx
+++ b/src/components/Icon/icon.stories.tsx
@@ -102,8 +102,9 @@ export const DocumentIcons = (): ReactElement => (
{makeRows(documentIcons)}
);
-export const FinancialProductsServicesAndConceptIcons =
- (): ReactElement => {makeRows(financialIcons)};
+export const FinancialProductsServicesAndConceptIcons = (): ReactElement => (
+ {makeRows(financialIcons)}
+);
export const ExpenseIcons = (): ReactElement => (
{makeRows(expenseIcons)}
@@ -131,9 +132,11 @@ export const IconWithText: Story = {
return (
- | Text element |
- Icon with background |
- Icon without background |
+
+ | Text element |
+ Icon with background |
+ Icon without background |
+
{acceptableLevels.map(({ type, text }) => (
From e29af0a21362e1b51c930488ab167dfe399a3615 Mon Sep 17 00:00:00 2001
From: Richard Dinh <1038306+flacoman91@users.noreply.github.com>
Date: Thu, 14 May 2026 08:32:17 -0700
Subject: [PATCH 02/10] Update SecondaryNav component, styles, tests, and
stories.
---
.../SecondaryNav/secondary-nav.scss | 297 +++++++++++++++---
.../SecondaryNav/secondary-nav.stories.tsx | 112 ++++---
.../SecondaryNav/secondary-nav.test.tsx | 66 ++--
src/components/SecondaryNav/secondary-nav.tsx | 205 ++++++++----
4 files changed, 495 insertions(+), 185 deletions(-)
diff --git a/src/components/SecondaryNav/secondary-nav.scss b/src/components/SecondaryNav/secondary-nav.scss
index b641817c24..260a63f68a 100644
--- a/src/components/SecondaryNav/secondary-nav.scss
+++ b/src/components/SecondaryNav/secondary-nav.scss
@@ -1,72 +1,283 @@
-// Secondary navigation (left panel / "Navigate this section" pattern)
-// Matches consumerfinance.gov compliance section sidebar
-// Active = black 5px left border; hover = green 5px left border
-// @see https://www.consumerfinance.gov/compliance/supervisory-highlights/
-// @see https://www.consumerfinance.gov/static/css/main.a624b7218b13.css
+// 1:1 with consumerfinance.gov cfgov/unprocessed/css/organisms/secondary-nav.scss
+// Units: px values in cfgov source are converted via math.div(..., $base-font-size-px)
+// to em (padding, parent link size) or rem (header label). Compiled cf.gov CSS shows e.g.
+// .875rem crumbs elsewhere; secondary-nav uses 1rem label, 1.125em parent links, 0.625em/0.9375em header padding.
+// @see https://github.com/cfpb/consumerfinance.gov/blob/main/cfgov/unprocessed/css/organisms/secondary-nav.scss
+@use 'sass:math';
+@use '@cfpb/cfpb-design-system/src/elements/abstracts' as *;
+@use '@cfpb/cfpb-design-system/src/utilities' as *;
.o-secondary-nav {
+ //
+ // Header
+ //
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ border: 0;
+ cursor: pointer;
+ padding: (math.div(10px, $base-font-size-px) + em)
+ (math.div(15px, $base-font-size-px) + em);
+
+ &:focus {
+ outline: 1px dotted var(--black);
+ outline-offset: 1px;
+ }
+
+ .o-secondary-nav__cue-close,
+ .o-secondary-nav__cue-open {
+ display: none;
+ }
+
+ &[aria-expanded='false'] .o-secondary-nav__cue-open {
+ display: block;
+ }
+
+ &[aria-expanded='true'] .o-secondary-nav__cue-close {
+ display: block;
+ }
+ }
+
+ // Using the button element with .o-secondary-nav__header requires setting
+ // an explicit width.
+ button.o-secondary-nav__header {
+ background-color: transparent;
+ width: 100%;
+ text-align: left;
+ }
+
+ &__cues {
+ min-width: 60px;
+ text-align: right;
+ color: var(--pacific);
+ font-size: math.div($btn-font-size, $base-font-size-px) + em;
+ line-height: math.div($base-line-height-px, $btn-font-size);
+ }
+
+ &__label {
+ // Grow to available width.
+ flex-grow: 1;
+
+ font-size: math.div(16px, $base-font-size-px) + rem;
+ font-weight: 600;
+ letter-spacing: 1px;
+ color: var(--pacific);
+
+ line-height: math.div(22px, $size-v);
+ margin-bottom: 0;
+ }
+
+ &__content {
+ padding: math.div(15px, $base-font-size-px) + em;
+ padding-top: 0;
+
+ // The divider between __header and __content.
+ &::before {
+ content: '';
+ display: block;
+ border-top: 1px solid var(--gray-40);
+ padding-top: math.div(15px, $base-font-size-px) + em;
+ }
+
+ &::after {
+ padding-bottom: math.div(15px, $base-font-size-px) + em;
+ width: 100%;
+ }
+ }
+
&__list {
+ padding-left: 0;
list-style: none;
- margin: 0;
- padding: 0;
- &--children {
- padding-left: 0.9375rem;
+ > li {
+ margin-left: 0;
}
}
- &__item {
- margin: 0;
- padding: 0;
+ &__list--children {
+ margin-left: math.div(math.div($grid-gutter-width, 2), $base-font-size-px) +
+ em;
+
+ // Desktop and above.
+ @include respond-to-min($bp-med-min) {
+ // Add 5px for the border to half the gutter
+ margin-left: math.div(
+ math.div($grid-gutter-width, 2) + 5px,
+ $base-font-size-px
+ ) +
+ em;
+ }
}
&__link {
- display: block;
- padding: 0.5rem 0 0.5rem 0.9375rem;
- color: var(--pacific);
- text-decoration: none;
- border: solid transparent;
- border-width: 0 0 0 5px;
+ display: inline-block;
+
+ // Break the menu word when it is too wide to fit in the sidebar area.
+ // These two values usurp the deprecated `word-break: break-word;`.
overflow-wrap: anywhere;
word-break: normal;
- &:hover,
- &:focus {
- border-left-color: var(--green);
- color: var(--black);
- text-decoration-color: var(--green);
+ border-style: solid;
+ border-left-width: 5px;
+ border-top-width: 0;
+ border-bottom-width: 0;
+ border-right-width: 0;
+ border-color: transparent;
+
+ &:hover {
+ border-color: var(--green);
}
&:focus {
- outline: 1px dotted var(--pacific);
+ display: block;
outline-offset: -1px;
}
- &:visited {
- color: var(--pacific);
- text-decoration-color: transparent;
+ @include u-link-colors(
+ var(--pacific),
+ var(--pacific),
+ var(--black),
+ var(--black),
+ var(--black),
+ transparent,
+ transparent,
+ var(--green),
+ var(--green),
+ var(--green)
+ );
+
+ // Tablet and below.
+ @include respond-to-max($bp-sm-max) {
+ display: block;
+
+ padding: math.div(math.div($grid-gutter-width, 2), $base-font-size-px) +
+ em;
+ }
+
+ // Desktop and above.
+ @include respond-to-min($bp-med-min) {
+ padding-top: math.div(10px, $base-font-size-px) + em;
+ padding-bottom: math.div(10px, $base-font-size-px) + em;
+ padding-left: math.div(
+ math.div($grid-gutter-width, 2),
+ $base-font-size-px
+ ) +
+ em;
}
&--current {
- border-left-color: var(--black);
- color: var(--black);
- cursor: text;
- text-decoration: none;
- text-decoration-color: var(--black);
-
- &:hover,
- &:focus,
- &:visited {
- border-left-color: var(--black);
- color: var(--black);
- text-decoration: none;
- text-decoration-color: var(--black);
- }
+ border-color: var(--black);
+
+ @include u-link-colors(
+ var(--black),
+ var(--black),
+ var(--black),
+ var(--black),
+ var(--black),
+ var(--black),
+ var(--black),
+ var(--black),
+ var(--black),
+ var(--black)
+ );
}
&--parent {
- font-size: 18px;
- font-weight: 500;
+ margin-bottom: inherit;
+
+ @include heading-4($has-margin-bottom: false, $is-responsive: false);
+ }
+ }
+
+ // Tablet and below.
+ @include respond-to-max($bp-sm-max) {
+ background: var(--gray-5);
+ border-bottom: 1px solid var(--gray-40);
+ margin-left: -0.9375rem;
+ margin-right: -0.9375rem;
+
+ // Add drop-shadow.
+ box-shadow: 0 5px 5px rgb(0, 0, 0, 20%);
+
+
+ // cfgov initializes FlyoutMenu + MaxHeightTransition in SecondaryNav.js.
+ // Collapse content from header state when that JS is not running.
+ &__header[aria-expanded='false'] ~ &__content {
+ display: none;
+ }
+ }
+
+ @include respond-to-range($bp-sm-min, $bp-sm-max) {
+ margin-left: -1.875rem;
+ margin-right: -1.875rem;
+ }
+
+ // Desktop and above.
+ @include respond-to-min($bp-med-min) {
+ .o-secondary-nav {
+ background: none;
+
+ &__header {
+ display: none;
+ }
+
+ &__content {
+ // These two !important values override basic expandable styling,
+ // because these do not function like expandables on med+ screens.
+ display: block !important;
+ max-height: 100% !important;
+ padding: 0;
+
+ &::before {
+ display: none;
+ }
+ }
+ }
+ }
+
+ // Don't print the secondary navigation.
+ @media print {
+ display: none;
+ }
+}
+
+// Right-to-left (RTL) layout.
+html[lang='ar'] {
+ .o-secondary-nav {
+ button.o-secondary-nav__header {
+ text-align: right;
+ }
+
+ &__cues {
+ text-align: left;
+ }
+
+ &__list--parents {
+ padding-right: 0;
+ }
+
+ &__link {
+ border-left-width: 0;
+ border-right-width: 5px;
+ }
+
+ // Desktop and above.
+ @include respond-to-min($bp-med-min) {
+ &__link {
+ padding-right: math.div(
+ math.div($grid-gutter-width, 2),
+ $base-font-size-px
+ ) +
+ em;
+ }
+
+ &__list--parents {
+ padding-right: math.div(
+ math.div($grid-gutter-width, 2),
+ $base-font-size-px
+ ) +
+ em;
+ }
}
}
}
diff --git a/src/components/SecondaryNav/secondary-nav.stories.tsx b/src/components/SecondaryNav/secondary-nav.stories.tsx
index 26f12f56ec..c962e1a2b7 100644
--- a/src/components/SecondaryNav/secondary-nav.stories.tsx
+++ b/src/components/SecondaryNav/secondary-nav.stories.tsx
@@ -11,19 +11,19 @@ const meta: Meta = {
description: {
component: `
Secondary navigation for in-page or section navigation, typically shown in a left sidebar.
-Matches the "Navigate this section" pattern used on [consumerfinance.gov](https://www.consumerfinance.gov/compliance/supervisory-highlights/).
### Usage
-- Pass \`items\` with \`href\`, \`label\`, and optional \`isActive\` for the current page.
-- Items can have optional \`children\` for sub-menu items. Parent items with children can omit \`href\` when active (section header).
-- Use \`ariaLabel\` to describe the nav for screen readers.
+- **Flat list (no \`children\`):** use \`isActive\` on the current top-level item.
+- **With \`children\`:** only children should use \`isActive\` for a subpage, unless the “current” page is the parent index—in that case set \`isActive\` on the parent and leave children inactive.
+- Default \`ariaLabel\` is \`Section\`. \`mobileToggleLabel\` defaults to **Navigate this section**.
`,
},
},
},
argTypes: {
ariaLabel: { control: 'text' },
+ mobileToggleLabel: { control: 'text' },
},
};
@@ -31,74 +31,94 @@ export default meta;
type Story = StoryObj;
-const defaultItems: SecondaryNavItem[] = [
- { href: '#section-1', label: 'Section 1' },
- { href: '#section-2', label: 'Section 2', isActive: true },
+/** 1. Flat links only; none marked current. */
+const basicNoChildren: SecondaryNavItem[] = [
+ { href: '#topic-a', label: 'Section A' },
+ { href: '#topic-b', label: 'Section B' },
+ { href: '#topic-c', label: 'Section C' },
+];
+
+/** 2. Flat list; one top-level item is the current page. */
+const basicNoChildrenWithCurrent: SecondaryNavItem[] = [
+ { href: '#topic-a', label: 'Section A' },
+ { href: '#topic-b', label: 'Section B', isActive: true },
+ { href: '#topic-c', label: 'Section C' },
+];
+
+/** 3. Nested items; no \`isActive\` on parents or children. */
+const withChildrenNoActive: SecondaryNavItem[] = [
+ {
+ label: 'Section 1',
+ href: '#section-1',
+ children: [
+ { href: '#section-1-a', label: 'Item A' },
+ { href: '#section-1-b', label: 'Item B' },
+ ],
+ },
+ { href: '#section-2', label: 'Section 2' },
{ href: '#section-3', label: 'Section 3' },
- { href: '#section-4', label: 'Section 4' },
- { href: '#section-5', label: 'Section 5' },
- { href: '#section-6', label: 'Section 6' },
- { href: '#section-7', label: 'Section 7' },
];
-const itemsWithSubMenu: SecondaryNavItem[] = [
+/** 4. Current page is the parent “index”; children are links but none are active. */
+const withChildrenActiveParent: SecondaryNavItem[] = [
{
label: 'Section 1',
- href: '/section-1',
+ href: '#section-1',
+ isActive: true,
children: [
- { href: '/section-1/item-a', label: 'Item A', isActive: true },
- { href: '/section-1/item-b', label: 'Item B' },
- { href: '/section-1/item-c', label: 'Item C' },
+ { href: '#section-1-a', label: 'Item A' },
+ { href: '#section-1-b', label: 'Item B' },
],
},
- { href: '/section-2', label: 'Section 2' },
- { href: '/section-3', label: 'Section 3' },
- { href: '/section-4', label: 'Section 4' },
- { href: '/section-5', label: 'Section 5' },
- { href: '/section-6', label: 'Section 6' },
- { href: '/section-7', label: 'Section 7' },
+ { href: '#section-2', label: 'Section 2' },
];
-export const Default: Story = {
+/** 5. Typical subpage: one child is the current page. */
+const withChildrenActiveChild: SecondaryNavItem[] = [
+ {
+ label: 'Section 1',
+ href: '#section-1',
+ children: [
+ { href: '#section-1-a', label: 'Item A', isActive: true },
+ { href: '#section-1-b', label: 'Item B' },
+ { href: '#section-1-c', label: 'Item C' },
+ ],
+ },
+ { href: '#section-2', label: 'Section 2' },
+ { href: '#section-3', label: 'Section 3' },
+];
+
+export const BasicMenuNoChildren: Story = {
+ name: 'Basic',
args: {
- items: defaultItems,
- ariaLabel: 'Page navigation',
+ items: basicNoChildren,
},
- render: (args) => ,
};
-export const WithShortList: Story = {
+export const BasicMenuNoChildrenOneActive: Story = {
+ name: 'One active item',
args: {
- items: [
- { href: '#overview', label: 'Overview' },
- { href: '#rules', label: 'Rules', isActive: true },
- { href: '#resources', label: 'Resources' },
- ],
- ariaLabel: 'On this page',
+ items: basicNoChildrenWithCurrent,
},
- render: (args) => ,
};
-export const WithSubMenu: Story = {
+export const MenuWithChildrenNoActive: Story = {
+ name: 'With children',
args: {
- items: itemsWithSubMenu,
- ariaLabel: 'Section',
+ items: withChildrenNoActive,
},
- render: (args) => ,
};
-export const NoActiveItem: Story = {
+export const MenuWithChildrenActiveParent: Story = {
+ name: 'With children, active parent',
args: {
- items: defaultItems.map(({ isActive: _isActive, ...item }) => item),
- ariaLabel: 'Page navigation',
+ items: withChildrenActiveParent,
},
- render: (args) => ,
};
-export const EmptyList: Story = {
+export const MenuWithChildrenActiveChild: Story = {
+ name: 'With children, active child',
args: {
- items: [],
- ariaLabel: 'Page navigation',
+ items: withChildrenActiveChild,
},
- render: (args) => ,
};
diff --git a/src/components/SecondaryNav/secondary-nav.test.tsx b/src/components/SecondaryNav/secondary-nav.test.tsx
index 3d3b1b5704..62c71c5906 100644
--- a/src/components/SecondaryNav/secondary-nav.test.tsx
+++ b/src/components/SecondaryNav/secondary-nav.test.tsx
@@ -1,7 +1,7 @@
import '@testing-library/jest-dom';
-import { render, screen } from '@testing-library/react';
-import { SecondaryNav } from './secondary-nav';
+import { fireEvent, render, screen } from '@testing-library/react';
import type { SecondaryNavItem } from './secondary-nav';
+import { SecondaryNav } from './secondary-nav';
describe('', () => {
const defaultItems: SecondaryNavItem[] = [
@@ -12,7 +12,7 @@ describe('', () => {
it('renders a nav with the default aria-label', () => {
render();
- const nav = screen.getByRole('navigation', { name: 'Page navigation' });
+ const nav = screen.getByRole('navigation', { name: 'Section' });
expect(nav).toBeInTheDocument();
expect(nav).toHaveClass('o-secondary-nav');
});
@@ -24,38 +24,43 @@ describe('', () => {
).toBeInTheDocument();
});
- it('renders all items as links; active link has aria-current', () => {
+ it('renders a mobile toggle button with aria-expanded', () => {
render();
- const linkA = screen.getByRole('link', { name: 'Link A' });
- const linkB = screen.getByRole('link', { name: 'Link B' });
- const linkC = screen.getByRole('link', { name: 'Link C' });
- expect(linkA).toHaveAttribute('href', '/a');
- expect(linkB).toHaveAttribute('href', '/b');
- expect(linkB).toHaveAttribute('aria-current', 'page');
- expect(linkC).toHaveAttribute('href', '/c');
+ const toggleButton = screen.getByTestId('secondary-nav-toggle');
+ expect(toggleButton).toHaveClass('o-secondary-nav__header');
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
});
- it('sets data-nav-is-active on the li for the active item', () => {
+ it('toggles aria-expanded when the button is clicked', () => {
render();
- const listItems = screen.getAllByRole('listitem');
- expect(listItems).toHaveLength(3);
- expect(listItems[0]).not.toHaveAttribute('data-nav-is-active');
- expect(listItems[1]).toHaveAttribute('data-nav-is-active', 'true');
- expect(listItems[2]).not.toHaveAttribute('data-nav-is-active');
+ const toggleButton = screen.getByTestId('secondary-nav-toggle');
+ fireEvent.click(toggleButton);
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'true');
+ fireEvent.click(toggleButton);
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
});
- it('renders no list when items is empty', () => {
- render();
- expect(screen.queryByRole('list')).toBeNull();
+ it('renders anchors; active item has no href and aria-current', () => {
+ render();
+ const linkA = screen.getByRole('link', { name: 'Link A' });
+ const linkC = screen.getByRole('link', { name: 'Link C' });
+ expect(linkA).toHaveAttribute('href', '/a');
+ expect(linkC).toHaveAttribute('href', '/c');
+
+ const current = screen.getByText('Link B');
+ expect(current.tagName).toBe('A');
+ expect(current).not.toHaveAttribute('href');
+ expect(current).toHaveAttribute('aria-current', 'page');
});
it('applies custom className', () => {
render();
- const nav = screen.getByRole('navigation', { name: 'Page navigation' });
+ const nav = screen.getByRole('navigation', { name: 'Section' });
expect(nav).toHaveClass('o-secondary-nav');
expect(nav).toHaveClass('custom-nav');
});
+
it('renders child items when parent has children', () => {
const itemsWithChildren: SecondaryNavItem[] = [
{
@@ -69,17 +74,12 @@ describe('', () => {
];
render();
expect(screen.getByText('Parent')).toBeInTheDocument();
- expect(screen.getByRole('link', { name: 'Child A' })).toHaveAttribute(
- 'href',
- '/child-a',
- );
- expect(screen.getByRole('link', { name: 'Child B' })).toHaveAttribute(
- 'href',
- '/child-b',
- );
- expect(screen.getByRole('link', { name: 'Child A' })).toHaveAttribute(
- 'aria-current',
- 'page',
- );
+ const childB = screen.getByRole('link', { name: 'Child B' });
+ expect(childB).toHaveAttribute('href', '/child-b');
+
+ const childA = screen.getByText('Child A');
+ expect(childA.tagName).toBe('A');
+ expect(childA).not.toHaveAttribute('href');
+ expect(childA).toHaveAttribute('aria-current', 'page');
});
});
diff --git a/src/components/SecondaryNav/secondary-nav.tsx b/src/components/SecondaryNav/secondary-nav.tsx
index ba9e1903f2..b0ccb2bf51 100644
--- a/src/components/SecondaryNav/secondary-nav.tsx
+++ b/src/components/SecondaryNav/secondary-nav.tsx
@@ -1,7 +1,7 @@
import classnames from 'classnames';
import type { HTMLAttributes } from 'react';
-import { JSX } from 'react';
-import Link from '../Link/link';
+import { JSX, useEffect, useState } from 'react';
+import { Icon } from '../Icon/icon';
import './secondary-nav.scss';
export interface SecondaryNavChildItem {
@@ -27,86 +27,165 @@ export interface SecondaryNavProperties extends HTMLAttributes {
*/
items: SecondaryNavItem[];
/**
- * Accessible label for the nav landmark. Defaults to "Page navigation".
+ * Accessible label for the nav landmark. Matches cfgov gettext('Section').
*/
ariaLabel?: string;
+ /**
+ * Label for the mobile header. Matches cfgov _('Navigate this section').
+ */
+ mobileToggleLabel?: string;
}
/**
- * Secondary navigation (e.g. left panel "Navigate this section") for in-page or section navigation.
- * Matches the pattern used on consumerfinance.gov compliance and other CFPB pages.
+ * Markup and classes match cfgov `secondary-nav.html` / `SecondaryNav.js` on
+ * consumerfinance.gov (FlyoutMenu + MaxHeightTransition are not initialized here;
+ * mobile expand/collapse follows `aria-expanded` on `.o-secondary-nav__header`).
*
- * @see https://www.consumerfinance.gov/compliance/supervisory-highlights/
+ * Typography and spacing live in `secondary-nav.scss` (cfgov organism): DS math from
+ * `$base-font-size-px` produces **em** (e.g. header padding, 1.125em parent links) and
+ * **rem** (header label), not px in this file.
+ *
+ * @see https://github.com/cfpb/consumerfinance.gov/blob/main/cfgov/v1/jinja2/v1/includes/organisms/secondary-nav.html
+ * @see https://github.com/cfpb/consumerfinance.gov/blob/main/cfgov/unprocessed/css/organisms/secondary-nav.scss
*/
export const SecondaryNav = ({
items,
- ariaLabel = 'Page navigation',
+ ariaLabel = 'Section',
+ mobileToggleLabel = 'Navigate this section',
className,
...properties
}: SecondaryNavProperties): JSX.Element => {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ // Align with cfgov small-screen layout: when the viewport crosses into the
+ // mobile breakpoint, hide the flyout so the collapsed header + chevron show.
+ // (matches max-width in secondary-nav.scss / $bp-sm-max → 56.25em.)
+ useEffect(() => {
+ if (!globalThis.window?.matchMedia) {
+ return;
+ }
+
+ const mediaQuery = globalThis.window.matchMedia('(max-width: 56.25em)');
+
+ const collapseForMobileLayout = (): void => {
+ if (mediaQuery.matches) {
+ setIsExpanded(false);
+ }
+ };
+
+ collapseForMobileLayout();
+ mediaQuery.addEventListener('change', collapseForMobileLayout);
+
+ return () => {
+ mediaQuery.removeEventListener('change', collapseForMobileLayout);
+ };
+ }, []);
+
+ const onToggle = (): void => {
+ setIsExpanded((isOpen) => !isOpen);
+ };
+
+ const onLinkClick = (): void => {
+ setIsExpanded(false);
+ };
+
return (
);
From aeb5affba210836c1e5c4e4b2c12e268cca4d36f Mon Sep 17 00:00:00 2001
From: Richard Dinh <1038306+flacoman91@users.noreply.github.com>
Date: Thu, 14 May 2026 08:32:17 -0700
Subject: [PATCH 03/10] fix bleed so tablet acts like phone
---
src/components/SecondaryNav/secondary-nav.scss | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/components/SecondaryNav/secondary-nav.scss b/src/components/SecondaryNav/secondary-nav.scss
index 260a63f68a..03feedd4b8 100644
--- a/src/components/SecondaryNav/secondary-nav.scss
+++ b/src/components/SecondaryNav/secondary-nav.scss
@@ -207,11 +207,6 @@
}
}
- @include respond-to-range($bp-sm-min, $bp-sm-max) {
- margin-left: -1.875rem;
- margin-right: -1.875rem;
- }
-
// Desktop and above.
@include respond-to-min($bp-med-min) {
.o-secondary-nav {
From bf8d3e36b29fb581227df8967eadbc70055b79d8 Mon Sep 17 00:00:00 2001
From: Richard Dinh <1038306+flacoman91@users.noreply.github.com>
Date: Thu, 14 May 2026 14:30:18 -0700
Subject: [PATCH 04/10] fixing focus on secondary nav. fixing negative margins
---
.storybook/preview-head.html | 37 ++++++++++++++++++++++++++++++------
1 file changed, 31 insertions(+), 6 deletions(-)
diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
index e1ae5119b7..cb6f59345b 100644
--- a/.storybook/preview-head.html
+++ b/.storybook/preview-head.html
@@ -7,21 +7,46 @@
- Nested "All viewports" (`?responsivePreview=off`): body padding 0; `#storybook-root` uses
vertical inset for focus plus small horizontal inset so outlines are not clipped at edges.
Full-bleed stories pass `sbNestedCanvasPadding=flush` (via `parameters.sbNestedCanvasPadding`
- in preview.js) to use zero inset instead. -->
+ in preview.js) to use zero inset instead.
+ SecondaryNav: cfgov negative h-margins are overridden in nested mode. Default root uses
+ horizontal padding so :focus-visible rings are not clipped; the nav bleeds by the same
+ amount so the bar still spans the full iframe. The mobile header uses a negative
+ outline-offset so the dotted focus ring sits slightly inside the tap target. -->