Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{
"mcp": {
"servers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
"[javascript]": {
Expand All @@ -17,10 +25,7 @@
"source.fixAll.stylelint": "explicit"
}
},
"stylelint.validate": [
"css",
"postcss"
],
"stylelint.validate": ["css", "postcss"],
"json.schemas": [
{
"fileMatch": ["*.docs.json"],
Expand Down
2 changes: 1 addition & 1 deletion examples/nextjs/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import './.next/types/routes.d.ts'
import './.next/dev/types/routes.d.ts'

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
42 changes: 42 additions & 0 deletions examples/nextjs/src/app/underlinenav/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client'

import React from 'react'
import {UnderlineNav} from '@primer/react'

export default function UnderlineNavPage() {
const [selectedIndex, setSelectedIndex] = React.useState<number | null>(1)

const items: {navigation: string; counter?: number | string; href?: string}[] = [
{navigation: 'Code', href: '#code'},
{navigation: 'Issues', counter: '12K', href: '#issues'},
{navigation: 'Pull Requests', counter: 13, href: '#pull-requests'},
{navigation: 'Discussions', counter: 5, href: '#discussions'},
{navigation: 'Actions', counter: 4, href: '#actions'},
{navigation: 'Projects', counter: 9, href: '#projects'},
{navigation: 'Insights', counter: '0', href: '#insights'},
{navigation: 'Settings', counter: 10, href: '#settings'},
{navigation: 'Security', href: '#security'},
]

return (
<div style={{margin: '0 auto', padding: '16px'}}>
<h1 style={{marginBottom: '16px'}}>UnderlineNav - Overflow on Narrow Screen</h1>
<UnderlineNav aria-label="Repository">
{items.map((item, index) => (
<UnderlineNav.Item
key={item.navigation}
aria-current={index === selectedIndex ? 'page' : undefined}
onSelect={event => {
event.preventDefault()
setSelectedIndex(index)
}}
counter={item.counter}
href={item.href}
>
{item.navigation}
</UnderlineNav.Item>
))}
</UnderlineNav>
</div>
)
}
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
],
"scripts": {
"build": "./script/build",
"build:components": "npx rollup -c",
"clean": "rimraf dist generated",
"start": "concurrently npm:start:*",
"start:storybook": "STORYBOOK=true storybook dev -p 6006",
Expand Down
123 changes: 123 additions & 0 deletions packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,126 @@ export const VariantFlush = () => {
</UnderlineNav>
)
}

/**
* - At extra-narrow viewport (< 544px): Shows first 2 items inline; rest in menu
* - At narrow viewport (544px - 768px): Shows first 3 items inline; rest in menu
* - At regular viewport (768px - 1024px): Shows first 5 items inline; rest in menu
* - At medium viewport (1024px - 1280px): Shows first 6 items inline; rest in menu
* - At large viewport (1280px - 1400px): Shows first 7 items inline; rest in menu
* - At wide viewport (> 1400px): Shows all items inline; menu is hidden
*/
export const ResponsiveOverflow = () => {
const [selectedIndex, setSelectedIndex] = React.useState<number | null>(0)

return (
<UnderlineNav
aria-label="Repository"
responsiveOverflow={{
xnarrow: [0, 1], // Show first 2 items at extra small (< 544px)
narrow: [0, 1, 2], // Show first 3 items at narrow (544px - 768px)
regular: [0, 1, 2, 3, 4], // Show first 5 items at regular (768px - 1024px)
medium: [0, 1, 2, 3, 4, 5], // Show first 6 items at medium (1024px - 1280px)
large: [0, 1, 2, 3, 4, 5, 6], // Show first 7 items at large (1280px - 1400px)
wide: 'all', // Show all items at wide (> 1400px, hide menu)
}}
>
{items.map((item, index) => (
<UnderlineNav.Item
key={item.navigation}
leadingVisual={item.icon}
aria-current={index === selectedIndex ? 'page' : undefined}
target="_self"
onSelect={event => {
event.preventDefault()
setSelectedIndex(index)
}}
counter={item.counter}
href={item.href}
>
{item.navigation}
</UnderlineNav.Item>
))}
</UnderlineNav>
)
}

export const ResponsiveOverflowXNarrow = () => {
return <ResponsiveOverflow />
}

ResponsiveOverflowXNarrow.parameters = {
viewport: {
viewports: {
...INITIAL_VIEWPORTS,
xnarrowScreen: {
name: 'Extra Narrow Screen',
styles: {
width: '400px',
height: '100%',
},
},
},
defaultViewport: 'xnarrowScreen',
},
}

export const ResponsiveOverflowNarrow = () => {
return <ResponsiveOverflow />
}

ResponsiveOverflowNarrow.parameters = {
viewport: {
viewports: {
...INITIAL_VIEWPORTS,
narrowScreen: {
name: 'Narrow Screen',
styles: {
width: '600px',
height: '100%',
},
},
},
defaultViewport: 'narrowScreen',
},
}

export const ResponsiveOverflowRegular = () => {
return <ResponsiveOverflow />
}

ResponsiveOverflowRegular.parameters = {
viewport: {
viewports: {
...INITIAL_VIEWPORTS,
regularScreen: {
name: 'Regular Screen',
styles: {
width: '900px',
height: '100%',
},
},
},
defaultViewport: 'regularScreen',
},
}

export const ResponsiveOverflowWide = () => {
return <ResponsiveOverflow />
}

ResponsiveOverflowWide.parameters = {
viewport: {
viewports: {
...INITIAL_VIEWPORTS,
wideScreen: {
name: 'Wide Screen',
styles: {
width: '1500px',
height: '100%',
},
},
},
defaultViewport: 'wideScreen',
},
}
85 changes: 85 additions & 0 deletions packages/react/src/UnderlineNav/UnderlineNav.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,88 @@
margin-left: 0;
}
}

/* Container for overflow menu when using responsive mode */
.ResponsiveOverflowContainer {
display: flex;
align-items: center;
}

/* Responsive menu visibility - extra-narrow viewport (max-width: 544px) */
@media (max-width: 544px) {
.ResponsiveOverflowContainer[data-hide-menu-xnarrow='true'] {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}

/* Use :has() to target menu item li elements, excluding the container */
li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-xnarrow='true']) {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}
}

/* Responsive menu visibility - narrow viewport (544px - 768px) */
@media (min-width: 544px) and (max-width: 768px) {
.ResponsiveOverflowContainer[data-hide-menu-narrow='true'] {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}

li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-narrow='true']) {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}
}

/* Responsive menu visibility - regular viewport (768px - 1024px) */
@media (min-width: 768px) and (max-width: 1024px) {
.ResponsiveOverflowContainer[data-hide-menu-regular='true'] {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}

li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-regular='true']) {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}
}

/* Responsive menu visibility - medium viewport (1024px - 1280px) */
@media (min-width: 1024px) and (max-width: 1280px) {
.ResponsiveOverflowContainer[data-hide-menu-medium='true'] {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}

li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-medium='true']) {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}
}

/* Responsive menu visibility - large viewport (1280px - 1400px) */
@media (min-width: 1280px) and (max-width: 1400px) {
.ResponsiveOverflowContainer[data-hide-menu-large='true'] {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}

li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-large='true']) {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}
}

/* Responsive menu visibility - wide viewport (min-width: 1400px) */
@media (min-width: 1400px) {
.ResponsiveOverflowContainer[data-hide-menu-wide='true'] {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}

li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-wide='true']) {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}
}
Loading
Loading