diff --git a/.changeset/shiny-taxis-cover.md b/.changeset/shiny-taxis-cover.md new file mode 100644 index 000000000..af3ed87ff --- /dev/null +++ b/.changeset/shiny-taxis-cover.md @@ -0,0 +1,32 @@ +--- +'@primer/react-brand': minor +--- + +Updates to `SubNav` component + +- New anchor-based navigation pattern available: + + Use `variant="anchor"` on `SubNav.SubMenu` to apply anchor navigation as an alternative to the default dropdown-based submenu. + + ```jsx + + Heading + + Link with anchor navigation + + Anchor link one + Anchor link two + Anchor link three + Anchor link four + + + Link + Link + + ``` + +- Overlay now closes when user clicks outside of it + +- Dropdown submenus now appear with white and black background and foreground colors respectively, irrespective of color mode. + +- Various other visual updates to improve brand-alignment. These include adjustments to text size, weight, color and iconography. diff --git a/apps/docs/content/components/SubNav.mdx b/apps/docs/content/components/SubNav.mdx index 6e4626398..766162b0f 100644 --- a/apps/docs/content/components/SubNav.mdx +++ b/apps/docs/content/components/SubNav.mdx @@ -7,6 +7,7 @@ description: A sub nav is a secondary navigation element, typically positioned b import {Label} from '@primer/react' import {PropTableValues} from '../../src/components' +import {SubNavSubMenuVariants} from '@primer/react-brand' ```js import {SubNav} from '@primer/react-brand' @@ -63,13 +64,15 @@ import {SubNav} from '@primer/react-brand' ``` -### Optional sub menu +### Optional submenu + +Submenus appear as a dropdown by default. ```jsx live Features - + Actions Actions feature one @@ -79,9 +82,29 @@ import {SubNav} from '@primer/react-brand' Packages + Copilot + Code review + + +``` + +### Optional anchor-based submenu + +```jsx live + + + Features - Copilot + Actions + + Actions feature one + Actions feature two + Actions feature three + Actions feature four + + Packages + Copilot Code review @@ -119,6 +142,7 @@ import {SubNav} from '@primer/react-brand' ### SubNav.SubMenu -| name | type | default | required | description | -| ---------- | ------------- | ------- | -------- | ---------------------------- | -| `children` | `SubNav.Link` | | `false` | Container for sub menu links | +| name | type | default | required | description | +| ---------- | --------------------------------------------------------------------- | ----------- | -------- | ------------------------------------- | +| `children` | `SubNav.Link` | | `false` | Container for submenu links | +| `variant` | | `'default'` | `false` | Alternative presentation for submenus | diff --git a/package-lock.json b/package-lock.json index 30713c696..0df4059c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ }, "apps/storybook": { "name": "@primer/brand-storybook", - "version": "0.42.1", + "version": "0.43.0", "license": "MIT", "devDependencies": { "@babel/preset-env": "^7.22.0", @@ -28590,7 +28590,7 @@ }, "packages/design-tokens": { "name": "@primer/brand-primitives", - "version": "0.42.1", + "version": "0.43.0", "license": "MIT", "devDependencies": { "@primer/primitives": "9.1.1", @@ -28603,7 +28603,7 @@ }, "packages/e2e": { "name": "@primer/brand-e2e", - "version": "0.42.1", + "version": "0.43.0", "license": "MIT", "devDependencies": { "@github/axe-github": "^0.5.0", @@ -28617,7 +28617,7 @@ }, "packages/fonts": { "name": "@primer/brand-fonts", - "version": "0.42.1", + "version": "0.43.0", "license": "MIT", "engines": { "node": ">=16.0.0", @@ -28626,7 +28626,7 @@ }, "packages/react": { "name": "@primer/react-brand", - "version": "0.42.1", + "version": "0.43.0", "license": "MIT", "dependencies": { "@oddbird/popover-polyfill": "0.4.3", @@ -29862,7 +29862,7 @@ }, "packages/repo-configs": { "name": "@primer/brand-config", - "version": "0.42.1", + "version": "0.43.0", "license": "MIT" } }, diff --git a/packages/design-tokens/src/tokens/functional/components/sub-nav/colors.js b/packages/design-tokens/src/tokens/functional/components/sub-nav/colors.js index 8564ca0e7..704114d6f 100644 --- a/packages/design-tokens/src/tokens/functional/components/sub-nav/colors.js +++ b/packages/design-tokens/src/tokens/functional/components/sub-nav/colors.js @@ -7,14 +7,14 @@ module.exports = { dark: 'var(--brand-color-text-default)', }, active: { - value: 'var(--base-color-scale-blue-5)', - dark: 'var(--base-color-scale-blue-3)', + value: 'var(--brand-color-text-default)', + dark: 'var(--brand-color-text-default)', }, }, subMenu: { bgColor: { value: 'var(--base-color-scale-white-0)', - dark: 'var(--base-color-scale-black-0)', + dark: 'var(--base-color-scale-white-0)', }, }, }, diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index b55ae745c..49166d1f1 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -592,7 +592,7 @@ ### Patch Changes -- [#568](https://github.com/primer/brand/pull/568) [`40a129d`](https://github.com/primer/brand/commit/40a129d78024612b625238d8a826fc06aa933465) Thanks [@rezrah](https://github.com/rezrah)! - Added support for optional `Button` and sub menu's in `SubNav` component. +- [#568](https://github.com/primer/brand/pull/568) [`40a129d`](https://github.com/primer/brand/commit/40a129d78024612b625238d8a826fc06aa933465) Thanks [@rezrah](https://github.com/rezrah)! - Added support for optional `Button` and submenu's in `SubNav` component. Also added `fullWidth` prop to optionally remove the default component padding. diff --git a/packages/react/src/SubNav/SubNav.features.stories.tsx b/packages/react/src/SubNav/SubNav.features.stories.tsx index b8f0159e6..4c6f224b3 100644 --- a/packages/react/src/SubNav/SubNav.features.stories.tsx +++ b/packages/react/src/SubNav/SubNav.features.stories.tsx @@ -1,13 +1,18 @@ import React from 'react' import {Meta} from '@storybook/react' import {INITIAL_VIEWPORTS} from '@storybook/addon-viewport' +import {linkTo} from '@storybook/addon-links' import {SubNav} from './SubNav' -import bgPath from '../fixtures/images/background-stars.png' -import {ThemeProvider} from '../ThemeProvider' import {Box} from '../Box' import {Hero} from '../Hero' import {Grid} from '../Grid' +import {Heading} from '../Heading' +import {Text} from '../Text' +import {RedlineBackground} from '../component-helpers' +import {Stack} from '../Stack' +import {expect, userEvent, within} from '@storybook/test' +import {Button} from '../Button' export default { title: 'Components/SubNav/Features', @@ -20,7 +25,7 @@ export default { }, } as Meta -export const ExampleUsage = ({hasShadow, ...args}) => ( +export const DropdownVariant = ({hasShadow, ...args}) => ( @@ -54,31 +59,35 @@ export const ExampleUsage = ({hasShadow, ...args}) => ( ) -ExampleUsage.parameters = { +DropdownVariant.parameters = { layout: 'fullscreen', } -ExampleUsage.decorators = [ - Story => ( - - - - - - ), -] - -export const NarrowExampleUsage = args => -NarrowExampleUsage.parameters = { +export const NarrowDropdownVariant = args => +NarrowDropdownVariant.parameters = { layout: 'fullscreen', viewport: { defaultViewport: 'iphonex', }, } -NarrowExampleUsage.decorators = [Story => ] +export const NarrowDropdownVariantMenuOpen = args => + +NarrowDropdownVariantMenuOpen.parameters = { + layout: 'fullscreen', + viewport: { + defaultViewport: 'iphonex', + }, +} +NarrowDropdownVariantMenuOpen.play = async ({canvasElement}) => { + const canvas = within(canvasElement) + await userEvent.click(canvas.getByTestId('SubNav-root-button')) + const overlayMenu = canvas.getByTestId('SubNav-root-overlay') + const firstLink = within(overlayMenu).getAllByRole('link')[0] + expect(firstLink).toHaveFocus() +} -export const WithShadow = args => +export const WithShadow = args => WithShadow.parameters = { layout: 'fullscreen', } @@ -137,6 +146,92 @@ export const LongerHeading = args => ( Call to action ) -FullWidth.parameters = { +LongerHeading.parameters = { layout: 'fullscreen', } + +const AnchorNavVariantData = { + ['Scale']: 'scale', + ['AI']: 'ai', + ['Security']: 'security', + ['Reliability']: 'reliability', +} + +export const AnchorNavVariant = args => ( + + + + { + e.preventDefault() + linkTo('Components/SubNav/Features', 'AnchorNavVariant') + }} + > + Enterprise + + + Overview + + {Object.entries(AnchorNavVariantData).map(([label, href]) => ( + + {label} + + ))} + + + Advanced Security + Copilot Enterprise + Premium Support + + + {Object.entries(AnchorNavVariantData).map(([key, value]) => ( + + + {key} + SubNav is a component that allows users to navigate to different sections of a page. + Learn more + + + ))} + + +) +AnchorNavVariant.parameters = { + layout: 'fullscreen', +} + +const customViewports = { + Narrow: { + name: 'Narrow', + styles: { + width: '280px', + height: '600px', + }, + }, +} + +export const NarrowAnchorNavVariant = args => +NarrowAnchorNavVariant.parameters = { + layout: 'fullscreen', + viewport: { + viewports: customViewports, + defaultViewport: 'Narrow', + }, +} + +export const NarrowAnchorNavVariantMenuOpen = args => +NarrowAnchorNavVariantMenuOpen.parameters = { + layout: 'fullscreen', + viewport: { + viewports: customViewports, + defaultViewport: 'Narrow', + }, +} +NarrowAnchorNavVariantMenuOpen.play = async ({canvasElement}) => { + const canvas = within(canvasElement) + await userEvent.click(canvas.getByTestId('SubNav-root-button')) + const overlayMenu = canvas.getByTestId('SubNav-root-overlay') + const firstLink = within(overlayMenu).getAllByRole('link')[0] + expect(firstLink).toHaveFocus() +} diff --git a/packages/react/src/SubNav/SubNav.module.css b/packages/react/src/SubNav/SubNav.module.css index 01769b14a..4e7e42d46 100644 --- a/packages/react/src/SubNav/SubNav.module.css +++ b/packages/react/src/SubNav/SubNav.module.css @@ -1,13 +1,22 @@ -.SubNav { +.SubNav__container { position: absolute; width: 100%; +} + +.SubNav__container--with-anchor-nav { + display: unset; + position: relative; +} + +.SubNav { + width: 100%; display: flex; padding: var(--base-size-16); z-index: 1; } .SubNav__heading { - font-weight: var(--base-text-weight-bold); + font-weight: var(--base-text-weight-semibold); color: var(--brand-color-text-default); font-family: var(--brand-heading-fontFamily); text-decoration: none; @@ -18,6 +27,10 @@ text-decoration: none !important; /* dotcom override */ } +.SubNav--header-container-outer { + width: 100%; +} + .SubNav__heading-container { position: relative; z-index: 9998; @@ -52,15 +65,122 @@ padding: 0; } +.SubNav__links-overlay > span { + display: none; +} + .SubNav__sub-menu-toggle { display: none; } +.SubNav__heading-separator { + position: relative; + top: var(--base-size-2); + color: var(--brand-color-text-muted); +} + +/* + * Anchor Nav Submenu + */ + +.SubNav__anchor-menu-outer-container { + position: sticky; + top: -1px; + z-index: 91; /* must be higher than subdomain nav bar */ +} + +.SubNav__sub-menu--anchor { + display: flex; + padding-block-start: var(--base-size-12); + padding-block-end: var(--base-size-20); +} + +.SubNav__anchor-menu-outer-container--stuck { + background-color: var(--brand-color-canvas-default); +} + +.SubNav__sub-menu--anchor .SubNav__sub-menu-list { + display: inline-flex; + list-style-type: none; + margin: 0; + padding: 0; + gap: var(--base-size-20); +} + +.SubNav__sub-menu--anchor .SubNav__link--is-in-view .SubNav__link-label { + color: var(--brand-color-text-default); +} + +.SubNav__sub-menu--anchor .SubNav__link--is-in-view .SubNav__link-label::after, +.SubNav__link:hover .SubNav__link-label::after, +.SubNav__link:focus-visible .SubNav__link-label::after, +.SubNav__link[aria-current]:not([aria-current='false']) .SubNav__link-label::after { + opacity: 1; + transform: translate3d(0, 0.2em, 0); + transform: scale(0.8, 1); +} + +.SubNav__sub-menu--anchor .SubNav__link--is-in-view .SubNav__link-label::after, +.SubNav__link:active .SubNav__link-label::after { + border-color: var(--brand-color-text-default); + transform: scale(0.9, 1); +} + +.SubNav__link[data-active='true'] .SubNav__link-label::after, +.SubNav__sub-menu--anchor .SubNav__link--is-in-view .SubNav__link-label::after { + border-color: var(--brand-color-text-default); + opacity: 1; +} + +.SubNav__link:focus-visible .SubNav__link-label::after, +.SubNav__sub-menu--anchor .SubNav__link--is-in-view:focus-visible .SubNav__link-label::after { + opacity: 0; +} + +.SubNav__link:hover .SubNav__link-label::after, +.SubNav__link[aria-current]:not([aria-current='false']) * { + color: var(--brand-SubNav-color-link-active); + text-decoration: none !important; /* dotcom override */ +} + +.SubNav__link-label::after { + content: ''; + position: absolute; + bottom: calc(var(--base-size-2) * -1); + left: 0; + width: 100%; + height: 2px; + border-width: 2px; + border-bottom: var(--base-size-2) solid var(--brand-color-text-muted); + transition: opacity var(--brand-animation-duration-fast), transform var(--brand-animation-duration-fast), + border-color var(--brand-animation-duration-fast); + opacity: 1; + transform: scale(0); + transform-origin: center; +} + +.SubNav__link[aria-current]:not([aria-current='false']) .SubNav__link-label::after { + border-color: var(--brand-color-text-default); +} + +.SubNav__link:hover .SubNav__link-label { + transition: color var(--brand-animation-duration-fast) var(--brand-animation-easing-default); + color: var(--brand-SubNav-color-link-active); +} + +.SubNav__link:hover .SubNav__link-label::after { + border-color: var(--brand-color-text-default); +} + /* * Narrow breakpoint */ @media screen and (max-width: 63.24rem) { + .SubNav { + position: relative; + } + .SubNav::before { content: ''; position: absolute; @@ -72,34 +192,57 @@ z-index: 9997; } - .SubNav--open::after { + .SubNav::after { content: ''; - background-color: var(--base-color-scale-black-0); - opacity: 0.3; + z-index: -1; position: fixed; + background-color: var(--base-color-scale-black-0); + opacity: 0; + visibility: hidden; top: 0; right: 0; left: 0; bottom: 0; width: 100%; height: 100%; - z-index: -1; + } + + .SubNav--open::after { + opacity: 0.3; + visibility: visible; + transition: visibility var(--brand-animation-duration-default) var(--brand-animation-easing-default), + opacity var(--brand-animation-duration-default) var(--brand-animation-easing-default); } .SubNav__heading { - font-size: var(--base-size-20); - line-height: var(--base-size-20); + font-size: var(--base-size-16); + line-height: var(--base-size-24); } .SubNav--open { display: block; } + .SubNav--open + .SubNav__anchor-menu-outer-container { + z-index: -1; + } + .SubNav--open::before { background-color: var(--brand-color-canvas-default); animation: fade-in 0.3s var(--brand-animation-easing-glide) forwards; } + .SubNav__header-container { + display: flex; + width: 100%; + white-space: pre; + } + + .SubNav__heading-separator { + margin-inline-end: var(--base-size-16); + z-index: 9998; + } + .SubNav__links-overlay { position: relative; display: flex; @@ -107,6 +250,7 @@ flex-direction: column; justify-content: center; z-index: 9998; + display: none; } .SubNav__links-overlay--open { @@ -117,19 +261,33 @@ padding-block-end: var(--base-size-16); } + .SubNav__links-overlay--open .SubNav__link:hover .SubNav__link-label { + color: var(--brand-color-text-default); + } + + .SubNav__links-overlay--open .SubNav__link--has-sub-menu:hover .SubNav__link-label { + color: var(--brand-color-text-muted); + } + .SubNav__overlay-toggle { background-color: transparent; border: none; cursor: pointer; display: flex; - position: absolute; - top: var(--base-size-16); - right: var(--base-size-16); + position: relative; + width: 100%; + display: flex; + order: 1; z-index: 9999; padding-inline: 0; } + .SubNav__overlay-toggle-content { + justify-content: space-between; + width: 100%; + } + .SubNav--full-width .SubNav__overlay-toggle { right: 0; } @@ -150,6 +308,11 @@ padding: var(--base-size-8) 0; animation: fade-in 0.3s var(--brand-animation-easing-glide) forwards; } + + .SubNav__links-overlay--open .SubNav__link--has-sub-menu { + padding-block: 0; + } + .SubNav__links-overlay--open .SubNav__action-container { width: 100%; } @@ -164,6 +327,68 @@ margin: 0; padding-inline-start: var(--base-size-16); } + + .SubNav__sub-menu--dropdown .SubNav__link:hover .SubNav__link-label { + color: var(--brand-color-text-default) !important; + } + + .SubNav__anchor-menu-container { + z-index: 99; + overflow-x: auto; + overflow-y: hidden; + width: 100%; + } + + .SubNav__anchor-menu-outer-container { + overflow: hidden; + position: sticky; + top: -1px; + } + + .SubNav__anchor-menu-outer-container::after { + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: var(--base-size-32); + background: linear-gradient(to left, var(--brand-color-canvas-default), transparent); + pointer-events: none; + z-index: 100; + } + + .SubNav__link-label { + font-size: var(--brand-text-size-200); + font-weight: var(--base-text-weight-semibold); + } + + .SubNav__sub-menu--anchor { + padding-inline: var(--base-size-16); + padding-block-end: var(--base-size-16); + padding-block-start: var(--base-size-16); + } + + .SubNav__sub-menu--anchor .SubNav__link-label { + font-size: var(--brand-text-size-100); + line-height: var(--brand-text-lineHeight-100); + letter-spacing: var(--brand-text-letterSpacing-100); + } + + .SubNav__sub-menu--anchor .SubNav__sub-menu-list { + padding-inline-end: var(--base-size-32); + } + + .SubNav__sub-menu--anchor .SubNav__link { + display: block; + white-space: pre; + position: relative; + } + + .SubNav .SubNav__link:hover .SubNav__link-label::after, + .SubNav .SubNav__link:focus-visible .SubNav__link-label::after, + .SubNav .SubNav__link[aria-current]:not([aria-current='false']) .SubNav__link-label::after { + display: none; + } } .SubNav__overlay-toggle-icon { @@ -188,6 +413,14 @@ .SubNav--full-width { padding-inline: 0; } + + .SubNav__sub-menu--anchor { + padding-inline: var(--base-size-24); + } +} + +.SubNav__heading-label { + white-space: pre; } /* @@ -198,6 +431,12 @@ .SubNav { padding: var(--base-size-16) var(--base-size-32); align-items: center; + display: flex; + z-index: 92; + } + + .SubNav:has(+ .SubNav__anchor-menu-container) { + padding-block-end: 0; } .SubNav--full-width { @@ -209,23 +448,28 @@ } .SubNav__heading { - font-size: var(--base-size-20); - line-height: var(--base-size-20); + font-size: calc(var(--base-size-20) - 2px); + line-height: var(--base-size-24); } - .SubNav { + .SubNav__heading-separator { + margin-inline-end: var(--base-size-20); + } + + .SubNav--header-container-outer { display: flex; + align-items: center; } .SubNav__heading-container { - margin-inline-end: var(--base-size-24); + margin-inline-end: var(--base-size-20); } .SubNav__links-overlay { align-items: center; display: flex; - gap: var(--base-size-24); - z-index: 1; + gap: var(--base-size-20); + z-index: 92; flex-grow: 1; } @@ -236,80 +480,54 @@ padding: 4px 0; } - .SubNav__link:hover *, - .SubNav__link[aria-current]:not([aria-current='false']) * { - color: var(--brand-SubNav-color-link-active); - text-decoration: none !important; /* dotcom override */ + .SubNav__link-label { + font-size: var(--brand-text-size-100); + line-height: var(--brand-text-lineHeight-100); } - .SubNav__link-label::after { + /* To fix hover distance between link and dropdown */ + .SubNav__link.SubNav__link--has-sub-menu::after { content: ''; position: absolute; - bottom: calc(var(--base-size-8) * -1); + bottom: calc(var(--base-size-12) * -1); left: 0; width: 100%; - height: 2px; - background-color: var(--brand-SubNav-color-link-active); - transition: opacity var(--brand-animation-duration-fast), transform var(--brand-animation-duration-fast), - background-color var(--brand-animation-duration-fast); - opacity: 1; - transform: scale(0); - transform-origin: center; - } - - .SubNav__link:hover .SubNav__link-label::after, - .SubNav__link:focus .SubNav__link-label::after, - .SubNav__link[aria-current]:not([aria-current='false']) .SubNav__link-label::after { - opacity: 1; - transform: translate3d(0, 0.2em, 0); - transform: scale(0.8, 1); + height: var(--base-size-12); + background: transparent; } - .SubNav__link:active .SubNav__link-label::after { - background-color: var(--brand-color-text-default); - transform: scale(0.9, 1); - } - - .SubNav__link[data-active='true'] .SubNav__link-label::after { - background-color: var(--brand-color-text-default); - opacity: 1; - } - - .SubNav__link:focus-visible .SubNav__link-label::after { - opacity: 0; - } - - .SubNav__sub-menu { + .SubNav__sub-menu.SubNav__sub-menu--dropdown { background-clip: padding-box; background-color: var(--brand-SubNav-color-subMenu-bgColor); border: var(--borderWidth-thin, max(1px, 0.0625rem)) solid var(--borderColor-default, var(--color-border-default)); - border-radius: var(--borderRadius-medium, 0.375rem); + border-radius: var(--brand-borderRadius-xlarge, 0.375rem); box-shadow: var(--shadow-floating-legacy, var(--color-shadow-large)); left: 0; list-style: none; - margin-top: calc(var(--base-size-4) / 4 * -4); - padding: var(--base-size-16) var(--base-size-24); + margin-block-start: var(--base-size-8); + padding: var(--base-size-24); + padding-inline-end: var(--base-size-24); position: absolute; top: 100%; - z-index: 100; + z-index: 9998; transition-timing-function: var(--brand-animation-easing-glide); transition-duration: var(--brand-animation-duration-fast); transition-property: opacity, transform; left: calc(var(--base-size-4) / 4 * -16); visibility: hidden; opacity: 0; - transform: scale(0.99) translateY(-0.7em); + transform: scale(0.99) translateY(-0.7em) translateX(-8px); transform-origin: top; display: flex; flex-direction: column; - gap: var(--base-size-16); + gap: var(--base-size-8); width: var(--brand-SubNav-width-subMenu); } - .SubNav__link--expanded .SubNav__sub-menu { + .SubNav__link--expanded .SubNav__sub-menu.SubNav__sub-menu--dropdown { visibility: visible; opacity: 1; - transform: scale(1) translateY(0); + transform: scale(1) translateY(0) translateX(-8px); box-shadow: var(--brand-SubNav-shadow); } @@ -328,20 +546,11 @@ display: block; } - .SubNav__sub-menu .SubNav__link-label { - color: var(--brand-color-text-default); - font-weight: var(--brand-text-weight-100); - font-size: var(--brand-text-size-100); - line-height: var(--brand-text-lineHeight-100); - letter-spacing: var(--brand-text-letterSpacing-100); + .SubNav__sub-menu--dropdown .SubNav__link:hover .SubNav__link-label { + color: var(--brand-color-text-default) !important; } - .SubNav__sub-menu .SubNav__link:hover .SubNav__link-label { - color: var(--brand-SubNav-color-link-active); - } - - .SubNav__sub-menu .SubNav__link-label::after, - .SubNav__link--has-sub-menu .SubNav__link-label::after { + .SubNav__sub-menu--dropdown .SubNav__link-label::after { display: none; } @@ -361,6 +570,10 @@ .SubNav__link--has-sub-menu:hover .SubNav__sub-menu-icon { transform: translateY(2px); } + + .SubNav__sub-menu--anchor { + padding-inline: var(--base-size-32); + } } @keyframes fade-in { diff --git a/packages/react/src/SubNav/SubNav.module.css.d.ts b/packages/react/src/SubNav/SubNav.module.css.d.ts index 7d8e95c10..6ff7a5f6a 100644 --- a/packages/react/src/SubNav/SubNav.module.css.d.ts +++ b/packages/react/src/SubNav/SubNav.module.css.d.ts @@ -1,6 +1,9 @@ declare const styles: { + readonly "SubNav__container": string; + readonly "SubNav__container--with-anchor-nav": string; readonly "SubNav": string; readonly "SubNav__heading": string; + readonly "SubNav--header-container-outer": string; readonly "SubNav__heading-container": string; readonly "SubNav--has-shadow": string; readonly "SubNav--full-width": string; @@ -9,18 +12,28 @@ declare const styles: { readonly "SubNav__sub-menu-children": string; readonly "SubNav__links-overlay": string; readonly "SubNav__sub-menu-toggle": string; + readonly "SubNav__heading-separator": string; + readonly "SubNav__anchor-menu-outer-container": string; + readonly "SubNav__sub-menu--anchor": string; + readonly "SubNav__anchor-menu-outer-container--stuck": string; + readonly "SubNav__sub-menu-list": string; + readonly "SubNav__link--is-in-view": string; + readonly "SubNav__link-label": string; + readonly "SubNav__link": string; readonly "SubNav--open": string; readonly "fade-in": string; + readonly "SubNav__header-container": string; readonly "SubNav__links-overlay--open": string; + readonly "SubNav__link--has-sub-menu": string; readonly "SubNav__overlay-toggle": string; - readonly "SubNav__link": string; + readonly "SubNav__overlay-toggle-content": string; readonly "SubNav__action": string; readonly "SubNav__sub-menu": string; + readonly "SubNav__sub-menu--dropdown": string; + readonly "SubNav__anchor-menu-container": string; readonly "SubNav__overlay-toggle-icon": string; - readonly "SubNav__overlay-toggle-content": string; - readonly "SubNav__link-label": string; + readonly "SubNav__heading-label": string; readonly "SubNav__link--expanded": string; - readonly "SubNav__link--has-sub-menu": string; readonly "fade-in-down": string; }; export = styles; diff --git a/packages/react/src/SubNav/SubNav.stories.tsx b/packages/react/src/SubNav/SubNav.stories.tsx index 4cdd51a94..869711681 100644 --- a/packages/react/src/SubNav/SubNav.stories.tsx +++ b/packages/react/src/SubNav/SubNav.stories.tsx @@ -14,20 +14,12 @@ const Template: StoryFn = args => ( Heading - - Link - - Link feature one - Link feature two - Link feature three - Link feature four - - - Link + Link Link Link + Link Link diff --git a/packages/react/src/SubNav/SubNav.test.tsx b/packages/react/src/SubNav/SubNav.test.tsx index 05392611c..5026d9cba 100644 --- a/packages/react/src/SubNav/SubNav.test.tsx +++ b/packages/react/src/SubNav/SubNav.test.tsx @@ -6,6 +6,11 @@ import {axe, toHaveNoViolations} from 'jest-axe' import {SubNav} from './SubNav' import '../test-utils/mocks/match-media-mock' import userEvent from '@testing-library/user-event' +import {useWindowSize} from '../hooks/useWindowSize' + +jest.mock('../hooks/useWindowSize') +const mockUseWindowSize = useWindowSize as jest.Mock +mockUseWindowSize.mockImplementation(() => ({isLarge: false})) expect.extend(toHaveNoViolations) @@ -38,6 +43,17 @@ const MockSubNavFixture = ({data = mockLinkData, ...rest}) => { } describe('SubNav', () => { + beforeEach(() => { + // IntersectionObserver isn't available in test environment + const mockIntersectionObserver = jest.fn() + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }) + window.IntersectionObserver = mockIntersectionObserver + }) + afterEach(cleanup) it('renders the root element correctly into the document', () => { @@ -59,10 +75,10 @@ describe('SubNav', () => { expect(getByTestId(SubNav.testIds.overlay).querySelectorAll('a').length).toBe(mockLinkData.length) }) - it('has a button that opens the menu when clicked', () => { + it('has a button that opens the menu when clicked', async () => { const {getByTestId} = render() - const buttonEl = getByTestId(SubNav.testIds.button) + const buttonEl = getByTestId('SubNav-root-button') const overlayEl = getByTestId(SubNav.testIds.overlay) expect(buttonEl).toBeInTheDocument() @@ -70,9 +86,9 @@ describe('SubNav', () => { // check aria roles are correct by default expect(buttonEl).toHaveAttribute('aria-expanded', 'false') - fireEvent.click(buttonEl) - expect(overlayEl).toHaveClass('SubNav__links-overlay--open') + userEvent.click(buttonEl) + expect(overlayEl).toHaveClass('SubNav__links-overlay--open') // check aria roles have updated expect(buttonEl).toHaveAttribute('aria-expanded', 'true') }) @@ -108,7 +124,9 @@ describe('SubNav', () => { expect(results).toHaveNoViolations() }) - it('shows subitems when the submenu toggle is activated', async () => { + it('shows subitems when the submenu toggle is activated at large viewports', async () => { + mockUseWindowSize.mockImplementation(() => ({isLarge: true})) + const {getByRole, getAllByTestId} = render( @@ -126,6 +144,7 @@ describe('SubNav', () => { ) userEvent.tab() + expect(getByRole('link', {name: 'Copilot'})).toHaveFocus() const toggleSubmenuButton = getByRole('button', {name: 'Open submenu'}) @@ -138,7 +157,7 @@ describe('SubNav', () => { expect(toggleSubmenuButton).toHaveFocus() expect(toggleSubmenuButton).toHaveAttribute('aria-expanded', 'true') - const expanded = getAllByTestId('SubNav-root-link')[0] + const expanded = getAllByTestId(SubNav.testIds.subMenu)[0] userEvent.tab() expect(within(expanded).getByRole('link', {name: 'Copilot feature page one'})).toHaveFocus() diff --git a/packages/react/src/SubNav/SubNav.tsx b/packages/react/src/SubNav/SubNav.tsx index 772f92fb8..c4b6e75f3 100644 --- a/packages/react/src/SubNav/SubNav.tsx +++ b/packages/react/src/SubNav/SubNav.tsx @@ -1,19 +1,24 @@ import React, { Children, + createContext, forwardRef, isValidElement, memo, useCallback, + useContext, + useEffect, + useMemo, + useRef, useState, type PropsWithChildren, type ReactElement, type ReactNode, type RefObject, } from 'react' -import {Button, ButtonSizes, ButtonVariants, Text} from '..' +import {Button, ButtonSizes, ButtonVariants, Text, ThemeProvider, useWindowSize} from '..' import {default as clsx} from 'clsx' -import {ChevronDownIcon, XIcon} from '@primer/octicons-react' +import {ChevronDownIcon, ChevronUpIcon} from '@primer/octicons-react' import {useId} from '@reach/auto-id' import {useKeyboardEscape} from '../hooks/useKeyboardEscape' import {useFocusTrap} from '../hooks/useFocusTrap' @@ -31,6 +36,7 @@ import '@primer/brand-primitives/lib/design-tokens/css/tokens/functional/compone /** * Main Stylesheet (as a CSS Module) */ import styles from './SubNav.module.css' +import {createPortal} from 'react-dom' const testIds = { root: 'SubNav-root', @@ -49,6 +55,68 @@ const testIds = { get action() { return `${this.root}-action` }, + get subMenu() { + return `${this.root}-sub-menu` + }, +} + +export const SubNavSubMenuVariants = ['dropdown', 'anchor'] as const +type SubMenuVariants = (typeof SubNavSubMenuVariants)[number] + +type SubNavContextType = { + portalRef: RefObject +} + +const SubNavContext = createContext(undefined) + +export const useSubNavContext = () => { + const context = useContext(SubNavContext) + if (!context) { + throw new Error('useSubNavContext must be used within a SubNavProvider') + } + return context +} + +function SubNavProvider({children}: {children: React.ReactNode}) { + const anchoredNavPortalRef = React.useRef(null) + + const value = useMemo( + () => ({ + portalRef: anchoredNavPortalRef, + }), + [], + ) + + useEffect(() => { + const menuContainer = anchoredNavPortalRef.current + + const observer = new IntersectionObserver( + ([entry]) => { + entry.target.classList.toggle(styles['SubNav__anchor-menu-outer-container--stuck'], entry.intersectionRatio < 1) + }, + {threshold: [1]}, + ) + + if (menuContainer) { + observer.observe(menuContainer) + } + + return () => { + if (menuContainer) { + observer.unobserve(menuContainer) + } + } + }, []) + + return ( + + {children} + + + + + + ) } export type SubNavProps = { @@ -62,44 +130,84 @@ export type SubNavProps = { } & PropsWithChildren> const _SubNavRoot = memo(({id, children, className, 'data-testid': testId, fullWidth, hasShadow}: SubNavProps) => { + const rootRef = React.useRef(null) const navRef = React.useRef(null) const overlayRef = React.useRef(null) const [isOpenAtNarrow, setIsOpenAtNarrow] = useState(false) const idForLinkContainer = useId() + const [hasAnchoredNav, setHasAnchoredNav] = useState(false) + + const {isLarge} = useWindowSize() + + const childrenArr = Children.toArray(children) const closeMenuCallback = useCallback(() => { + if (isLarge) return setIsOpenAtNarrow(false) - }, []) + }, [isLarge]) const handleMenuToggle = useCallback(() => { - setIsOpenAtNarrow(!isOpenAtNarrow) - }, [isOpenAtNarrow]) + if (isLarge) return + setIsOpenAtNarrow(prev => !prev) + }, [isLarge]) - useOnClickOutside(navRef, closeMenuCallback) + useOnClickOutside(rootRef, closeMenuCallback) useKeyboardEscape(closeMenuCallback) useFocusTrap({containerRef: overlayRef, restoreFocusOnCleanUp: true, disabled: !isOpenAtNarrow}) - const activeLink = Children.toArray(children).find(child => { + useEffect(() => { + if (isOpenAtNarrow && !isLarge) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = 'auto' + } + }, [isOpenAtNarrow, isLarge]) + + const activeLink = childrenArr.find(child => { if (isValidElement(child)) { return child.props['aria-current'] } }) as React.ReactElement | undefined + useEffect(() => { + // check if there is an anchored nav in the SubNav.SubMenu child + const hasAnchorVariant = childrenArr.some(child => { + if (isValidElement(child) && child.type === SubNavLink) { + const [, subMenu] = child.props.children + if (subMenu?.props?.variant === 'anchor') { + return true + } + } + }) + setHasAnchoredNav(hasAnchorVariant) + }, [childrenArr]) + const { heading: HeadingChild, links: LinkChildren, action: ActionChild, - } = Children.toArray(children).reduce( + } = childrenArr.reduce( (acc: {heading?: ReactNode; links: ReactElement[]; action?: ReactNode}, child) => { if (isValidElement(child)) { if (child.type === SubNavHeading) { acc.heading = child } else if (child.type === SubNavLink) { - acc.links.push( - React.cloneElement(child as ReactElement, { - onClick: child.props['aria-current'] ? handleMenuToggle : child.props.onClick, - }), - ) + const [link, subMenu] = child.props.children + + if (subMenu?.props?.variant === 'anchor') { + acc.links.push( + React.cloneElement(child as ReactElement, { + children: [link], + onClick: child.props['aria-current'] ? closeMenuCallback : child.props.onClick, + }), + ) + } else { + acc.links.push( + React.cloneElement(child as ReactElement, { + onClick: child.props['aria-current'] ? closeMenuCallback : child.props.onClick, + }), + ) + } } else if (child.type === _SubNavAction) { acc.action = child } @@ -109,49 +217,93 @@ const _SubNavRoot = memo(({id, children, className, 'data-testid': testId, fullW {heading: undefined, links: [], action: undefined}, ) + // The values are different types depending on whether a submenu is present + const activeLinklabel = + typeof activeLink?.props.children === 'string' ? activeLink.props.children : activeLink?.props.children[0] + // needed to prevent rendering of anchor subnav inside the narrow element + const MaybeSubNav = activeLink?.props.children?.[1]?.props?.variant === 'anchor' && activeLink.props.children?.[1] + return ( - - {HeadingChild && {HeadingChild}} - {LinkChildren.length && ( - + + - {LinkChildren} - {ActionChild && {ActionChild}} - - )} - - {isOpenAtNarrow ? ( - - ) : ( - - {activeLink && activeLink.props.children} - + + + {HeadingChild && {HeadingChild}} + + + + + + + + + + + + + {activeLink && activeLinklabel && !isLarge && ( + + + + {activeLinklabel} + + {isOpenAtNarrow ? ( + + ) : ( + + )} + + + )} + {MaybeSubNav && MaybeSubNav} + + {LinkChildren.length && ( + + {LinkChildren} + {ActionChild && {ActionChild}} + + )} - )} - - + + + ) }) @@ -177,12 +329,17 @@ const SubNavHeading = ({href, children, className, 'data-testid': testID, ...pro type SubNavLinkProps = { href: string 'data-testid'?: string + _variant?: SubMenuVariants } & PropsWithChildren> & BaseProps const SubNavLinkWithSubmenu = forwardRef( - ({children, href, 'aria-current': ariaCurrent, 'data-testid': testId, className, ...props}, forwardedRef) => { + ( + {children, href, 'aria-current': ariaCurrent, 'data-testid': testId, className, _variant, ...props}, + forwardedRef, + ) => { const submenuId = useId() + const {isLarge} = useWindowSize() const [isExpanded, setIsExpanded] = useState(false) const ref = useProvidedRefOrCreate(forwardedRef as RefObject) @@ -195,10 +352,8 @@ const SubNavLinkWithSubmenu = forwardRef( const [label, SubMenuChildren] = children as ReactNode[] - const onKeyDown = useCallback((e: React.KeyboardEvent) => { - if (['Enter', ' '].includes(e.key)) { - setIsExpanded(prev => !prev) - } + const handleOnClick = useCallback(() => { + setIsExpanded(prev => !prev) }, []) return ( @@ -208,7 +363,7 @@ const SubNavLinkWithSubmenu = forwardRef( styles['SubNav__link--has-sub-menu'], isExpanded && styles['SubNav__link--expanded'], )} - data-testid={testId || testIds.link} + data-testid={testId || testIds.subMenu} ref={ref} onMouseOver={() => setIsExpanded(true)} onMouseOut={() => setIsExpanded(false)} @@ -225,19 +380,28 @@ const SubNavLinkWithSubmenu = forwardRef( aria-current={ariaCurrent} {...props} > - + {label} - - - + {isLarge && ( + + + + )} + {SubMenuChildren} @@ -247,16 +411,47 @@ const SubNavLinkWithSubmenu = forwardRef( ) const SubNavLink = forwardRef((props, ref) => { - const hasSubMenu = Children.toArray(props.children).some(child => { + const [isInView, setIsInView] = useState(false) + const childrenArr = Children.toArray(props.children) + + const hasSubMenu = childrenArr.some(child => { if (isValidElement(child)) { return child.type === _SubMenu } }) + useEffect(() => { + if (hasSubMenu) return + const targetId = props.href.replace('#', '') + const target = document.getElementById(targetId) + if (!target) return + + const topOfWindow = '0px 0px -100%' + const observerParams = {threshold: 0, root: null, rootMargin: topOfWindow} + + const handleIntersectionUpdate: IntersectionObserverCallback = ([entry]) => { + setIsInView(entry.isIntersecting) + } + + const observer = new IntersectionObserver(handleIntersectionUpdate, observerParams) + observer.observe(target) + return () => observer.disconnect() + }, [hasSubMenu, props.href]) + if (hasSubMenu) { + const isAnchorVariantSubMenu = childrenArr.some(child => { + if (isValidElement(child)) { + return child.type === _SubMenu && child.props.variant === 'anchor' + } + }) + return ( - } /> + } + _variant={isAnchorVariantSubMenu ? 'anchor' : undefined} + /> ) } @@ -267,13 +462,24 @@ const SubNavLink = forwardRef } {...rest} > - + {children} @@ -281,12 +487,76 @@ const SubNavLink = forwardRef>) { - return ( - - {children} - - ) +type SubMenuProps = { + variant?: SubMenuVariants +} & React.HTMLAttributes & + BaseProps + +function _SubMenu({children, className, variant = 'dropdown', ...props}: SubMenuProps) { + const context = React.useContext(SubNavContext) + const navRef = useRef(null) + + const {isLarge} = useWindowSize() + + /** + * Effect is needed to prevent the bubbling of onClick events to the overlay trigger. + * Removing this effect will cause clicks on the anchor nav element to toggle the overlay. + */ + useEffect(() => { + const handleClick = (e: MouseEvent) => { + if (navRef.current && !navRef.current.contains(e.target as Node)) { + return + } + + if (!(e.target instanceof HTMLAnchorElement)) { + e.stopPropagation() + } + } + + if (variant === 'anchor') { + document.addEventListener('click', handleClick, true) // Capture phase + } + + return () => { + document.removeEventListener('click', handleClick, true) + } + }, [variant]) + + if (variant === 'anchor' && context?.portalRef.current) { + return createPortal( + + + {React.Children.map(children, child => { + if (isValidElement(child)) { + return React.cloneElement(child as React.ReactElement, { + onClick: e => { + if (child.props.onClick) { + child.props.onClick(e) + } + }, + }) + } + })} + + , + context.portalRef.current, + ) + } else { + const Tag = isLarge ? ThemeProvider : React.Fragment + + return ( + + + {children} + + + ) + } } type SubNavActionProps = { diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts b/packages/react/src/SubNav/SubNav.visual.spec.ts index d332d9bf6..4a18a02a1 100644 --- a/packages/react/src/SubNav/SubNav.visual.spec.ts +++ b/packages/react/src/SubNav/SubNav.visual.spec.ts @@ -14,9 +14,9 @@ test.describe('Visual Comparison: SubNav', () => { expect(await page.screenshot({fullPage: true})).toMatchSnapshot() }) - test('SubNav / Example Usage', async ({page}) => { + test('SubNav / Dropdown Variant', async ({page}) => { await page.goto( - 'http://localhost:6006/iframe.html?args=&id=components-subnav-features--example-usage&viewMode=story', + 'http://localhost:6006/iframe.html?args=&id=components-subnav-features--dropdown-variant&viewMode=story', ) await page.waitForTimeout(500) @@ -24,11 +24,24 @@ test.describe('Visual Comparison: SubNav', () => { }) // eslint-disable-next-line i18n-text/no-en - test.describe('Mobile viewport test for Narrow Example Usage', () => { + test.describe('Mobile viewport test for Narrow Dropdown Variant', () => { test.use({viewport: {width: 360, height: 800}}) - test('SubNav / Narrow Example Usage', async ({page}) => { + test('SubNav / Narrow Dropdown Variant', async ({page}) => { await page.goto( - 'http://localhost:6006/iframe.html?args=&id=components-subnav-features--narrow-example-usage&viewMode=story', + 'http://localhost:6006/iframe.html?args=&id=components-subnav-features--narrow-dropdown-variant&viewMode=story', + ) + + await page.waitForTimeout(500) + expect(await page.screenshot({fullPage: true})).toMatchSnapshot() + }) + }) + + // eslint-disable-next-line i18n-text/no-en + test.describe('Mobile viewport test for Narrow Dropdown Variant Menu Open', () => { + test.use({viewport: {width: 360, height: 800}}) + test('SubNav / Narrow Dropdown Variant Menu Open', async ({page}) => { + await page.goto( + 'http://localhost:6006/iframe.html?args=&id=components-subnav-features--narrow-dropdown-variant-menu-open&viewMode=story', ) await page.waitForTimeout(500) @@ -69,4 +82,39 @@ test.describe('Visual Comparison: SubNav', () => { await page.waitForTimeout(500) expect(await page.screenshot({fullPage: true})).toMatchSnapshot() }) + + test('SubNav / Anchor Nav Variant', async ({page}) => { + await page.goto( + 'http://localhost:6006/iframe.html?args=&id=components-subnav-features--anchor-nav-variant&viewMode=story', + ) + + await page.waitForTimeout(500) + expect(await page.screenshot({fullPage: true})).toMatchSnapshot() + }) + + // eslint-disable-next-line i18n-text/no-en + test.describe('Mobile viewport test for Narrow Anchor Nav Variant', () => { + test.use({viewport: {width: 360, height: 800}}) + test('SubNav / Narrow Anchor Nav Variant', async ({page}) => { + await page.goto( + 'http://localhost:6006/iframe.html?args=&id=components-subnav-features--narrow-anchor-nav-variant&viewMode=story', + ) + + await page.waitForTimeout(500) + expect(await page.screenshot({fullPage: true})).toMatchSnapshot() + }) + }) + + // eslint-disable-next-line i18n-text/no-en + test.describe('Mobile viewport test for Narrow Anchor Nav Variant Menu Open', () => { + test.use({viewport: {width: 360, height: 800}}) + test('SubNav / Narrow Anchor Nav Variant Menu Open', async ({page}) => { + await page.goto( + 'http://localhost:6006/iframe.html?args=&id=components-subnav-features--narrow-anchor-nav-variant-menu-open&viewMode=story', + ) + + await page.waitForTimeout(500) + expect(await page.screenshot({fullPage: true})).toMatchSnapshot() + }) + }) }) diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-6184a--SubNav-Narrow-Anchor-Nav-Variant-Menu-Open-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-6184a--SubNav-Narrow-Anchor-Nav-Variant-Menu-Open-1-linux.png new file mode 100644 index 000000000..54a97b5db Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-6184a--SubNav-Narrow-Anchor-Nav-Variant-Menu-Open-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-d89c8-av-Variant-SubNav-Narrow-Anchor-Nav-Variant-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-d89c8-av-Variant-SubNav-Narrow-Anchor-Nav-Variant-1-linux.png new file mode 100644 index 000000000..fe7834587 Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-d89c8-av-Variant-SubNav-Narrow-Anchor-Nav-Variant-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-e0f45-en-SubNav-Narrow-Dropdown-Variant-Menu-Open-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-e0f45-en-SubNav-Narrow-Dropdown-Variant-Menu-Open-1-linux.png new file mode 100644 index 000000000..248d817bb Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-e0f45-en-SubNav-Narrow-Dropdown-Variant-Menu-Open-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-e12be-w-Example-Usage-SubNav-Narrow-Example-Usage-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-e12be-w-Example-Usage-SubNav-Narrow-Example-Usage-1-linux.png deleted file mode 100644 index 4c0fccb7f..000000000 Binary files a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-e12be-w-Example-Usage-SubNav-Narrow-Example-Usage-1-linux.png and /dev/null differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-ec549-down-Variant-SubNav-Narrow-Dropdown-Variant-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-ec549-down-Variant-SubNav-Narrow-Dropdown-Variant-1-linux.png new file mode 100644 index 000000000..c56730c21 Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-ec549-down-Variant-SubNav-Narrow-Dropdown-Variant-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-for-Full-Width-Narrow-SubNav-Full-Width-Narrow-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-for-Full-Width-Narrow-SubNav-Full-Width-Narrow-1-linux.png index fcf8fbe74..f5ea85a6f 100644 Binary files a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-for-Full-Width-Narrow-SubNav-Full-Width-Narrow-1-linux.png and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-for-Full-Width-Narrow-SubNav-Full-Width-Narrow-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Anchor-Nav-Variant-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Anchor-Nav-Variant-1-linux.png new file mode 100644 index 000000000..f4ee5a3e1 Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Anchor-Nav-Variant-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Default-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Default-1-linux.png index 77062dd72..81616708e 100644 Binary files a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Default-1-linux.png and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Default-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Dropdown-Variant-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Dropdown-Variant-1-linux.png new file mode 100644 index 000000000..f5d5dc16a Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Dropdown-Variant-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Example-Usage-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Example-Usage-1-linux.png deleted file mode 100644 index a4f88590c..000000000 Binary files a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Example-Usage-1-linux.png and /dev/null differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Full-Width-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Full-Width-1-linux.png index a4f926519..ec970f064 100644 Binary files a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Full-Width-1-linux.png and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Full-Width-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Longer-Heading-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Longer-Heading-1-linux.png index 6fb709c8d..018b1d25d 100644 Binary files a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Longer-Heading-1-linux.png and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Longer-Heading-1-linux.png differ diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-With-Shadow-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-With-Shadow-1-linux.png index cc6d90cf2..039146419 100644 Binary files a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-With-Shadow-1-linux.png and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-With-Shadow-1-linux.png differ diff --git a/packages/react/src/component-helpers.tsx b/packages/react/src/component-helpers.tsx index e7126f85d..9b8a4f55e 100644 --- a/packages/react/src/component-helpers.tsx +++ b/packages/react/src/component-helpers.tsx @@ -1,4 +1,4 @@ -import React, {Ref, PropsWithChildren} from 'react' +import React, {Ref, PropsWithChildren, CSSProperties} from 'react' import {AnimateProps} from './animation/AnimationProvider' /** @@ -16,9 +16,15 @@ type RedlineBackgroundProps = { height?: number hasBorder?: boolean className?: string + style?: CSSProperties } -export function RedlineBackground({height, hasBorder = true, ...rest}: PropsWithChildren) { +export function RedlineBackground({ + height, + hasBorder = true, + style, + ...rest +}: PropsWithChildren) { return ( diff --git a/packages/react/src/recipes/FeaturePreviewLPs/FeaturePreviewLevelTwo/FeaturePreviewLevelTwo.visual.spec.ts-snapshots/Visual-Comparison-FeaturePreviewLevelTwo-FeaturePreviewLevelTwo-2-4-variant-1-linux.png b/packages/react/src/recipes/FeaturePreviewLPs/FeaturePreviewLevelTwo/FeaturePreviewLevelTwo.visual.spec.ts-snapshots/Visual-Comparison-FeaturePreviewLevelTwo-FeaturePreviewLevelTwo-2-4-variant-1-linux.png index 02fb97162..71176c18c 100644 Binary files a/packages/react/src/recipes/FeaturePreviewLPs/FeaturePreviewLevelTwo/FeaturePreviewLevelTwo.visual.spec.ts-snapshots/Visual-Comparison-FeaturePreviewLevelTwo-FeaturePreviewLevelTwo-2-4-variant-1-linux.png and b/packages/react/src/recipes/FeaturePreviewLPs/FeaturePreviewLevelTwo/FeaturePreviewLevelTwo.visual.spec.ts-snapshots/Visual-Comparison-FeaturePreviewLevelTwo-FeaturePreviewLevelTwo-2-4-variant-1-linux.png differ