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 ( - - - + + + + + {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. -->
Text elementIcon with backgroundIcon without background
Text elementIcon with backgroundIcon without background