; counter?: number}[] = [
- {navigation: 'Code', icon: CodeIcon},
- {navigation: 'Issues', icon: IssueOpenedIcon, counter: 120},
- {navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: 13},
- {navigation: 'Discussions', icon: CommentDiscussionIcon, counter: 5},
- {navigation: 'Actions', counter: 4},
- {navigation: 'Projects', icon: ProjectIcon, counter: 9},
- {navigation: 'Insights', icon: GraphIcon},
- {navigation: 'Settings', counter: 10},
- {navigation: 'Security', icon: ShieldLockIcon},
- ]
-
- return (
-
-
- {items.map(item => (
-
- {item.navigation}
-
- ))}
-
- {displayExtraEl && }
-
- )
-}
-
-describe('UnderlineNav', () => {
- behavesAsComponent({
- Component: UnderlineNav,
- options: {skipAs: true, skipSx: true},
- toRender: () => ,
- })
-
- checkExports('UnderlineNav2', {
- default: undefined,
- UnderlineNav,
- })
- it('renders aria-current attribute to be pages when an item is selected', () => {
- const {getByRole} = render()
- const selectedNavLink = getByRole('link', {name: 'Code'})
- expect(selectedNavLink.getAttribute('aria-current')).toBe('page')
- })
- it('renders aria-label attribute correctly', () => {
- const {container, getByRole} = render()
- expect(container.getElementsByTagName('nav').length).toEqual(1)
- const nav = getByRole('navigation')
- expect(nav.getAttribute('aria-label')).toBe('Repository')
- })
- it('renders icons correctly', () => {
- const {getByRole} = render()
- const nav = getByRole('navigation')
- expect(nav.getElementsByTagName('svg').length).toEqual(7)
- })
- it('fires onSelect on click', async () => {
- const onSelect = jest.fn()
- const {getByRole} = render(
-
- Item 1
- Item 2
- Item 3
- ,
- )
- const item = getByRole('link', {name: 'Item 1'})
- const user = userEvent.setup()
- await user.click(item)
- expect(onSelect).toHaveBeenCalledTimes(1)
- })
- it('fires onSelect on keypress', async () => {
- const onSelect = jest.fn()
- const {getByRole} = render(
-
- Item 1
- Item 2
-
- Item 3
-
- ,
- )
- const item = getByRole('link', {name: 'Item 1'})
- const user = userEvent.setup()
- await user.tab() // tab into the story, this should focus on the first link
- expect(item).toEqual(document.activeElement)
- await user.keyboard('{Enter}')
- // Enter keypress fires both click and keypress events
- expect(onSelect).toHaveBeenCalledTimes(2)
- await user.keyboard(' ') // space
- expect(onSelect).toHaveBeenCalledTimes(3)
- })
- it('respects counter prop', () => {
- const {getByRole} = render()
- const item = getByRole('link', {name: 'Issues (120)'})
- const counter = item.getElementsByTagName('span')[3]
- expect(counter.textContent).toBe('120')
- expect(counter).toHaveAttribute('aria-hidden', 'true')
- })
- it('renders the content of visually hidden span properly for screen readers', () => {
- const {getByRole} = render()
- const item = getByRole('link', {name: 'Issues (120)'})
- const counter = item.getElementsByTagName('span')[4]
- // non breaking space unified code
- expect(counter.textContent).toBe('\u00A0(120)')
- })
- it('respects loadingCounters prop', () => {
- const {getByRole} = render()
- const item = getByRole('link', {name: 'Actions'})
- const loadingCounter = item.getElementsByTagName('span')[2]
- expect(loadingCounter.className).toContain('LoadingCounter')
- expect(loadingCounter.textContent).toBe('')
- })
- it('renders a visually hidden h2 heading for screen readers when aria-label is present', () => {
- const {getByRole} = render()
- const heading = getByRole('heading', {name: 'Repository navigation'})
- // check if heading is h2 tag
- expect(heading.tagName).toBe('H2')
- expect(heading.className).toContain('VisuallyHidden')
- expect(heading.textContent).toBe('Repository navigation')
- })
- it('throws an error when there are multiple items that have aria-current', () => {
- const spy = jest.spyOn(console, 'error').mockImplementation()
- expect(() => {
- render(
-
- Item 1
- Item 2
- ,
- )
- }).toThrow('Only one current element is allowed')
- expect(spy).toHaveBeenCalled()
- spy.mockRestore()
- })
-})
-
-describe('Keyboard Navigation', () => {
- it('should move focus to the next/previous item on the list with the tab key', async () => {
- const {getByRole} = render()
- const item = getByRole('link', {name: 'Code'})
- const nextItem = getByRole('link', {name: 'Issues (120)'})
- const user = userEvent.setup()
- await user.tab() // tab into the story, this should focus on the first link
- expect(item).toEqual(document.activeElement) // check if the first item is focused
- await user.tab()
- // focus should be on the next item
- expect(nextItem).toHaveFocus()
- })
-})
-
-checkStoriesForAxeViolations('UnderlineNav2.examples', '../UnderlineNav2/')
diff --git a/src/UnderlineNav2/UnderlineNav.tsx b/src/UnderlineNav2/UnderlineNav.tsx
deleted file mode 100644
index 38aad66a99f..00000000000
--- a/src/UnderlineNav2/UnderlineNav.tsx
+++ /dev/null
@@ -1,407 +0,0 @@
-import React, {useRef, forwardRef, useCallback, useState, MutableRefObject, RefObject, useEffect} from 'react'
-import Box from '../Box'
-import sx, {merge, BetterSystemStyleObject, SxProp} from '../sx'
-import {UnderlineNavContext} from './UnderlineNavContext'
-import {useResizeObserver, ResizeObserverEntry} from '../hooks/useResizeObserver'
-import {useTheme} from '../ThemeProvider'
-import {ChildWidthArray, ResponsiveProps, ChildSize} from './types'
-import VisuallyHidden from '../_VisuallyHidden'
-import {moreBtnStyles, getDividerStyle, getNavStyles, ulStyles, menuStyles, menuItemStyles, GAP} from './styles'
-import styled from 'styled-components'
-import {Button} from '../Button'
-import {TriangleDownIcon} from '@primer/octicons-react'
-import {useOnEscapePress} from '../hooks/useOnEscapePress'
-import {useOnOutsideClick} from '../hooks/useOnOutsideClick'
-import {useId} from '../hooks/useId'
-import {ActionList} from '../ActionList'
-import {defaultSxProp} from '../utils/defaultSxProp'
-import CounterLabel from '../CounterLabel'
-import {LoadingCounter} from './LoadingCounter'
-import {invariant} from '../utils/invariant'
-
-export type UnderlineNavProps = {
- children: React.ReactNode
- 'aria-label'?: React.AriaAttributes['aria-label']
- as?: React.ElementType
- sx?: SxProp['sx']
- /**
- * loading state for all counters. It displays loading animation for individual counters (UnderlineNav.Item) until all are resolved. It is needed to prevent multiple layout shift.
- */
- loadingCounters?: boolean
-}
-// When page is loaded, we don't have ref for the more button as it is not on the DOM yet.
-// However, we need to calculate number of possible items when the more button present as well. So using the width of the more button as a constant.
-const MORE_BTN_WIDTH = 86
-// The height is needed to make sure we don't have a layout shift when the more button is the only item in the nav.
-const MORE_BTN_HEIGHT = 45
-
-// Needed this because passing a ref using HTMLULListElement to `Box` causes a type error
-const NavigationList = styled.ul`
- ${sx};
-`
-
-const MoreMenuListItem = styled.li`
- display: flex;
- align-items: center;
- height: ${MORE_BTN_HEIGHT}px;
-`
-
-const overflowEffect = (
- navWidth: number,
- moreMenuWidth: number,
- childArray: Array,
- childWidthArray: ChildWidthArray,
- noIconChildWidthArray: ChildWidthArray,
- updateListAndMenu: (props: ResponsiveProps, iconsVisible: boolean) => void,
-) => {
- let iconsVisible = true
- if (childWidthArray.length === 0) {
- updateListAndMenu({items: childArray, menuItems: []}, iconsVisible)
- }
- const numberOfItemsPossible = calculatePossibleItems(childWidthArray, navWidth)
- const numberOfItemsWithoutIconPossible = calculatePossibleItems(noIconChildWidthArray, navWidth)
- // We need to take more menu width into account when calculating the number of items possible
- const numberOfItemsPossibleWithMoreMenu = calculatePossibleItems(
- noIconChildWidthArray,
- navWidth,
- moreMenuWidth || MORE_BTN_WIDTH,
- )
- const items: Array = []
- const menuItems: Array = []
-
- // First, we check if we can fit all the items with their icons
- if (childArray.length <= numberOfItemsPossible) {
- items.push(...childArray)
- } else if (childArray.length <= numberOfItemsWithoutIconPossible) {
- // if we can't fit all the items with their icons, we check if we can fit all the items without their icons
- iconsVisible = false
- items.push(...childArray)
- } else {
- // if we can't fit all the items without their icons, we keep the icons hidden and show the ones that doesn't fit into the list in the overflow menu
- iconsVisible = false
-
- /* Below is an accessibiility requirement. Never show only one item in the overflow menu.
- * If there is only one item left to display in the overflow menu according to the calculation,
- * we need to pull another item from the list into the overflow menu.
- */
- const numberOfItemsInMenu = childArray.length - numberOfItemsPossibleWithMoreMenu
- const numberOfListItems =
- numberOfItemsInMenu === 1 ? numberOfItemsPossibleWithMoreMenu - 1 : numberOfItemsPossibleWithMoreMenu
- for (const [index, child] of childArray.entries()) {
- if (index < numberOfListItems) {
- items.push(child)
- } else {
- const ariaCurrent = child.props['aria-current']
- const isCurrent = Boolean(ariaCurrent) && ariaCurrent !== 'false'
- // We need to make sure to keep the selected item always visible.
- // To do that, we swap the selected item with the last item in the list to make it visible. (When there is at least 1 item in the list to swap.)
- if (isCurrent && numberOfListItems > 0) {
- // If selected item couldn't make in to the list, we swap it with the last item in the list.
- const indexToReplaceAt = numberOfListItems - 1 // because we are replacing the last item in the list
- // splice method modifies the array by removing 1 item here at the given index and replace it with the "child" element then returns the removed item.
- const propsectiveAction = items.splice(indexToReplaceAt, 1, child)[0]
- menuItems.push(propsectiveAction)
- } else {
- menuItems.push(child)
- }
- }
- }
- }
- updateListAndMenu({items, menuItems}, iconsVisible)
-}
-
-const getValidChildren = (children: React.ReactNode) => {
- return React.Children.toArray(children).filter(child => React.isValidElement(child)) as React.ReactElement[]
-}
-
-const calculatePossibleItems = (childWidthArray: ChildWidthArray, navWidth: number, moreMenuWidth = 0) => {
- const widthToFit = navWidth - moreMenuWidth
- let breakpoint = childWidthArray.length // assume all items will fit
- let sumsOfChildWidth = 0
- for (const [index, childWidth] of childWidthArray.entries()) {
- sumsOfChildWidth = sumsOfChildWidth + childWidth.width + GAP
- if (sumsOfChildWidth > widthToFit) {
- breakpoint = index
- break
- } else {
- continue
- }
- }
- return breakpoint
-}
-
-export const UnderlineNav = forwardRef(
- (
- {
- as = 'nav',
- 'aria-label': ariaLabel,
- sx: sxProp = defaultSxProp,
- loadingCounters = false,
- children,
- }: UnderlineNavProps,
- forwardedRef,
- ) => {
- const backupRef = useRef(null)
- const navRef = (forwardedRef ?? backupRef) as MutableRefObject
- const listRef = useRef(null)
- const moreMenuRef = useRef(null)
- const moreMenuBtnRef = useRef(null)
- const containerRef = React.useRef(null)
- const disclosureWidgetId = useId()
-
- const {theme} = useTheme()
- const [isWidgetOpen, setIsWidgetOpen] = useState(false)
- const [iconsVisible, setIconsVisible] = useState(true)
- const [childWidthArray, setChildWidthArray] = useState([])
- const [noIconChildWidthArray, setNoIconChildWidthArray] = useState([])
-
- const validChildren = getValidChildren(children)
-
- // Responsive props object manages which items are in the list and which items are in the menu.
- const [responsiveProps, setResponsiveProps] = useState({
- items: validChildren,
- menuItems: [],
- })
-
- // Make sure to have the fresh props data for list items when children are changed (keeping aria-current up-to-date)
- const listItems = responsiveProps.items.map(item => {
- return validChildren.find(child => child.key === item.key) ?? item
- })
-
- // Make sure to have the fresh props data for menu items when children are changed (keeping aria-current up-to-date)
- const menuItems = responsiveProps.menuItems.map(menuItem => {
- return validChildren.find(child => child.key === menuItem.key) ?? menuItem
- })
- // This is the case where the viewport is too narrow to show any list item with the more menu. In this case, we only show the dropdown
- const onlyMenuVisible = responsiveProps.items.length === 0
-
- if (__DEV__) {
- // Practically, this is not a conditional hook, it is just making sure this hook runs only on DEV not PROD.
- // eslint-disable-next-line react-hooks/rules-of-hooks
- useEffect(() => {
- // Address illegal state where there are multiple items that have `aria-current='page'` attribute
- const activeElements = validChildren.filter(child => {
- return child.props['aria-current'] !== undefined
- })
- invariant(activeElements.length <= 1, 'Only one current element is allowed')
- invariant(ariaLabel, 'Use the `aria-label` prop to provide an accessible label for assistive technology')
- })
- }
-
- function getItemsWidth(itemText: string): number {
- return noIconChildWidthArray.find(item => item.text === itemText)?.width ?? 0
- }
-
- const swapMenuItemWithListItem = (
- prospectiveListItem: React.ReactElement,
- indexOfProspectiveListItem: number,
- event: React.MouseEvent | React.KeyboardEvent,
- callback: (props: ResponsiveProps, displayIcons: boolean) => void,
- ) => {
- // get the selected menu item's width
- const widthToFitIntoList = getItemsWidth(prospectiveListItem.props.children)
- // Check if there is any empty space on the right side of the list
- const availableSpace =
- navRef.current.getBoundingClientRect().width - (listRef.current?.getBoundingClientRect().width ?? 0)
-
- // Calculate how many items need to be pulled in to the menu to make room for the selected menu item
- // I.e. if we need to pull 2 items in (index 0 and index 1), breakpoint (index) will return 1.
- const index = getBreakpointForItemSwapping(widthToFitIntoList, availableSpace)
- const indexToSliceAt = responsiveProps.items.length - 1 - index
- // Form the new list of items
- const itemsLeftInList = [...responsiveProps.items].slice(0, indexToSliceAt)
- const updatedItemList = [...itemsLeftInList, prospectiveListItem]
- // Form the new menu items
- const itemsToAddToMenu = [...responsiveProps.items].slice(indexToSliceAt)
- const updatedMenuItems = [...menuItems]
- // Add itemsToAddToMenu array's items to the menu at the index of the prospectiveListItem and remove 1 count of items (prospectiveListItem)
- updatedMenuItems.splice(indexOfProspectiveListItem, 1, ...itemsToAddToMenu)
- callback({items: updatedItemList, menuItems: updatedMenuItems}, false)
- }
- // How many items do we need to pull in to the menu to make room for the selected menu item.
- function getBreakpointForItemSwapping(widthToFitIntoList: number, availableSpace: number) {
- let widthToSwap = 0
- let breakpoint = 0
- for (const [index, item] of [...responsiveProps.items].reverse().entries()) {
- widthToSwap += getItemsWidth(item.props.children)
- if (widthToFitIntoList < widthToSwap + availableSpace) {
- breakpoint = index
- break
- }
- }
- return breakpoint
- }
-
- const updateListAndMenu = useCallback((props: ResponsiveProps, displayIcons: boolean) => {
- setResponsiveProps(props)
- setIconsVisible(displayIcons)
- }, [])
- const setChildrenWidth = useCallback((size: ChildSize) => {
- setChildWidthArray(arr => {
- const newArr = [...arr, size]
- return newArr
- })
- }, [])
-
- const setNoIconChildrenWidth = useCallback((size: ChildSize) => {
- setNoIconChildWidthArray(arr => {
- const newArr = [...arr, size]
- return newArr
- })
- }, [])
-
- const closeOverlay = React.useCallback(() => {
- setIsWidgetOpen(false)
- }, [setIsWidgetOpen])
-
- const focusOnMoreMenuBtn = React.useCallback(() => {
- moreMenuBtnRef.current?.focus()
- }, [])
-
- const onAnchorClick = useCallback((event: React.MouseEvent) => {
- if (event.defaultPrevented || event.button !== 0) {
- return
- }
- setIsWidgetOpen(isWidgetOpen => !isWidgetOpen)
- }, [])
-
- useOnEscapePress(
- (event: KeyboardEvent) => {
- if (isWidgetOpen) {
- event.preventDefault()
- closeOverlay()
- focusOnMoreMenuBtn()
- }
- },
- [isWidgetOpen],
- )
-
- useOnOutsideClick({onClickOutside: closeOverlay, containerRef, ignoreClickRefs: [moreMenuBtnRef]})
-
- useResizeObserver((resizeObserverEntries: ResizeObserverEntry[]) => {
- const navWidth = resizeObserverEntries[0].contentRect.width
- const moreMenuWidth = moreMenuRef.current?.getBoundingClientRect().width ?? 0
- navWidth !== 0 &&
- overflowEffect(
- navWidth,
- moreMenuWidth,
- validChildren,
- childWidthArray,
- noIconChildWidthArray,
- updateListAndMenu,
- )
- }, navRef as RefObject)
-
- return (
-
- {ariaLabel && {`${ariaLabel} navigation`}}
- (getNavStyles(theme), sxProp)}
- aria-label={ariaLabel}
- ref={navRef}
- >
-
- {listItems}
- {menuItems.length > 0 && (
-
- {!onlyMenuVisible && }
-
-
- {menuItems.map((menuItem, index) => {
- const {
- children: menuItemChildren,
- counter,
- 'aria-current': ariaCurrent,
- onSelect,
- ...menuItemProps
- } = menuItem.props
-
- // This logic is used to pop the selected item out of the menu and into the list when the navigation is control externally
- if (Boolean(ariaCurrent) && ariaCurrent !== 'false') {
- const event = new MouseEvent('click')
- !onlyMenuVisible &&
- swapMenuItemWithListItem(
- menuItem,
- index,
- // @ts-ignore - not a big deal because it is internally creating an event but ask help
- event as React.MouseEvent,
- updateListAndMenu,
- )
- }
-
- return (
- | React.KeyboardEvent,
- ) => {
- // When there are no items in the list, do not run the swap function as we want to keep everything in the menu.
- !onlyMenuVisible && swapMenuItemWithListItem(menuItem, index, event, updateListAndMenu)
- closeOverlay()
- focusOnMoreMenuBtn()
- // fire onSelect event that comes from the UnderlineNav.Item (if it is defined)
- typeof onSelect === 'function' && onSelect(event)
- }}
- {...menuItemProps}
- >
-
- {menuItemChildren}
- {loadingCounters ? (
-
- ) : (
- counter !== undefined && (
-
- {counter}
-
- )
- )}
-
-
- )
- })}
-
-
- )}
-
-
-
- )
- },
-)
-
-UnderlineNav.displayName = 'UnderlineNav'
diff --git a/src/UnderlineNav2/UnderlineNav2.docs.json b/src/UnderlineNav2/UnderlineNav2.docs.json
deleted file mode 100644
index 33f307eb029..00000000000
--- a/src/UnderlineNav2/UnderlineNav2.docs.json
+++ /dev/null
@@ -1,88 +0,0 @@
-{
- "id": "drafts_underline_nav2",
- "name": "UnderlineNav",
- "status": "draft",
- "a11yReviewed": true,
- "stories": [],
- "props": [
- {
- "name": "afterSelect",
- "type": "(event) => void",
- "defaultValue": "",
- "description": "The handler that gets called when a nav link child is selected"
- },
- {
- "name": "aria-label",
- "type": "string",
- "defaultValue": "",
- "description": "A unique name for the rendered 'nav' landmark. It will also be used to label the arrow\nbuttons that control the scroll behaviour on coarse pointer devices. (I.e.\n'Scroll ${aria-label} left/right')\n"
- },
- {
- "name": "children",
- "type": "UnderlineNav.Item[]",
- "defaultValue": "",
- "required": true,
- "description": ""
- },
- {
- "name": "loadingCounters",
- "type": "boolean",
- "defaultValue": "false",
- "description": "Whether the navigation items are in loading state. Component waits for all the counters to finish loading to prevent multiple layout shifts."
- },
- {
- "name": "sx",
- "type": "SystemStyleObject"
- }
- ],
- "subcomponents": [
- {
- "name": "UnderlineNav.Item",
- "props": [
- {
- "name": "aria-current",
- "type": "| 'page' | 'step' | 'location' | 'date' | 'time' | true | false",
- "defaultValue": "false",
- "description": "Set `aria-current` to `\"page\"` to indicate that the item represents the current page. Set `aria-current` to `\"location\"` to indicate that the item represents the current location on a page. For more information about `aria-current`, see [MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current)."
- },
- {
- "name": "counter",
- "type": "number",
- "defaultValue": "",
- "description": "The number to display in the counter label."
- },
- {
- "name": "href",
- "type": "string",
- "defaultValue": "",
- "description": "The URL that the item navigates to. 'href' is passed to the underlying '' element. If 'as' is specified, the component may need different props and 'href' is ignored. (Required prop for the react router is 'to' for example)"
- },
- {
- "name": "icon",
- "type": "Component",
- "defaultValue": "",
- "description": "The leading icon comes before item label"
- },
- {
- "name": "onSelect",
- "type": "(event) => void",
- "defaultValue": "",
- "description": "The handler that gets called when a nav link is selected. For example, a manual route binding can be done here(I.e. 'navigate(href)' for the react router.)"
- },
- {
- "name": "as",
- "type": "React.ElementType",
- "defaultValue": "\"a\""
- },
- {
- "name": "sx",
- "type": "SystemStyleObject"
- }
- ],
- "passthrough": {
- "element": "a",
- "url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#Attributes"
- }
- }
- ]
-}
diff --git a/src/UnderlineNav2/UnderlineNav2.features.stories.tsx b/src/UnderlineNav2/UnderlineNav2.features.stories.tsx
deleted file mode 100644
index 28dac55595a..00000000000
--- a/src/UnderlineNav2/UnderlineNav2.features.stories.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import React from 'react'
-import {
- IconProps,
- EyeIcon,
- CodeIcon,
- IssueOpenedIcon,
- GitPullRequestIcon,
- CommentDiscussionIcon,
- PlayIcon,
- ProjectIcon,
- GraphIcon,
- ShieldLockIcon,
- GearIcon,
-} from '@primer/octicons-react'
-import {Meta} from '@storybook/react'
-import {UnderlineNav} from './index'
-import {INITIAL_VIEWPORTS} from '@storybook/addon-viewport'
-
-export default {
- title: 'Drafts/Components/UnderlineNav/Features',
-} as Meta
-
-export const Default = () => {
- return (
-
- Code
- Issues
- Pull Requests
-
- )
-}
-export const WithIcons = () => {
- return (
-
- Code
-
- Issues
-
-
- Pull Requests
-
-
- Discussions
-
- Projects
-
- )
-}
-
-export const WithCounterLabels = () => {
- return (
-
-
- Code
-
-
- Issues
-
-
- )
-}
-
-const items: {navigation: string; icon: React.FC; counter?: number | string; href?: string}[] = [
- {navigation: 'Code', icon: CodeIcon, href: '#code'},
- {navigation: 'Issues', icon: IssueOpenedIcon, counter: '12K', href: '#issues'},
- {navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: 13, href: '#pull-requests'},
- {navigation: 'Discussions', icon: CommentDiscussionIcon, counter: 5, href: '#discussions'},
- {navigation: 'Actions', icon: PlayIcon, counter: 4, href: '#actions'},
- {navigation: 'Projects', icon: ProjectIcon, counter: 9, href: '#projects'},
- {navigation: 'Insights', icon: GraphIcon, counter: '0', href: '#insights'},
- {navigation: 'Settings', icon: GearIcon, counter: 10, href: '#settings'},
- {navigation: 'Security', icon: ShieldLockIcon, href: '#security'},
-]
-
-export const OverflowTemplate = ({initialSelectedIndex = 1}: {initialSelectedIndex?: number}) => {
- const [selectedIndex, setSelectedIndex] = React.useState(initialSelectedIndex)
- return (
-
- {items.map((item, index) => (
- {
- event.preventDefault()
- setSelectedIndex(index)
- }}
- counter={item.counter}
- href={item.href}
- >
- {item.navigation}
-
- ))}
-
- )
-}
-
-export const OverflowOnNarrowScreen = () => {
- return
-}
-
-OverflowOnNarrowScreen.parameters = {
- viewport: {
- viewports: {
- ...INITIAL_VIEWPORTS,
- narrowScreen: {
- name: 'Narrow Screen',
- styles: {
- width: '800px',
- height: '100%',
- },
- },
- },
- defaultViewport: 'narrowScreen',
- },
-}
-
-export const CountersLoadingState = () => {
- const [selectedIndex, setSelectedIndex] = React.useState(1)
-
- return (
-
- {items.map((item, index) => (
- setSelectedIndex(index)}
- counter={item.counter}
- >
- {item.navigation}
-
- ))}
-
- )
-}
diff --git a/src/UnderlineNav2/UnderlineNav2.stories.tsx b/src/UnderlineNav2/UnderlineNav2.stories.tsx
deleted file mode 100644
index b8d4c6d48a0..00000000000
--- a/src/UnderlineNav2/UnderlineNav2.stories.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react'
-import {ComponentMeta, ComponentStory} from '@storybook/react'
-import {UnderlineNav} from './index'
-import {UnderlineNavItem} from './UnderlineNavItem'
-
-const excludedControlKeys = ['sx', 'as', 'variant', 'align', 'afterSelect']
-
-export default {
- title: 'Drafts/Components/UnderlineNav',
- component: UnderlineNav,
- subcomponents: {UnderlineNavItem},
- parameters: {
- controls: {
- expanded: true,
- // variant and size are developed in the first design iteration but then they are abondened.
- // Still keeping them on the source code for future reference but they are not exposed as props.
- exclude: excludedControlKeys,
- },
- },
- argTypes: {
- 'aria-label': {
- type: {
- name: 'string',
- },
- },
- loadingCounters: {
- control: {
- type: 'boolean',
- },
- },
- },
- args: {
- 'aria-label': 'Repository',
- loadingCounters: false,
- },
-} as ComponentMeta
-
-export const Default: ComponentStory = () => {
- const children = ['Code', 'Pull requests', 'Actions', 'Projects', 'Wiki']
- return (
-
- {children.map((child: string, index: number) => (
-
- {child}
-
- ))}
-
- )
-}
-
-export const Playground: ComponentStory = args => {
- const children = ['Code', 'Pull requests', 'Actions', 'Projects', 'Wiki']
- return (
-
- {children.map((child: string, index: number) => (
-
- {child}
-
- ))}
-
- )
-}
diff --git a/src/UnderlineNav2/__snapshots__/UnderlineNav.test.tsx.snap b/src/UnderlineNav2/__snapshots__/UnderlineNav.test.tsx.snap
deleted file mode 100644
index 7087305ccb7..00000000000
--- a/src/UnderlineNav2/__snapshots__/UnderlineNav.test.tsx.snap
+++ /dev/null
@@ -1,804 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`UnderlineNav renders consistently 1`] = `
-.c1 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- padding-left: 16px;
- padding-right: 16px;
- -webkit-box-pack: start;
- -webkit-justify-content: flex-start;
- -ms-flex-pack: start;
- justify-content: flex-start;
- border-bottom: 1px solid;
- border-bottom-color: hsla(210,18%,87%,1);
- align: row;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- min-height: 48px;
-}
-
-.c3 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-flex-direction: column;
- -ms-flex-direction: column;
- flex-direction: column;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
-}
-
-.c5 {
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- display: -webkit-inline-box;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
- margin-right: 8px;
-}
-
-.c6 {
- font-weight: 600;
-}
-
-.c8 {
- margin-left: 8px;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
-}
-
-.c9 {
- display: inline-block;
- padding: 2px 5px;
- font-size: 12px;
- font-weight: 600;
- line-height: 1;
- border-radius: 20px;
- background-color: rgba(175,184,193,0.2);
- color: #1F2328;
-}
-
-.c9:empty {
- display: none;
-}
-
-.c0 {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- -webkit-clip: rect(0,0,0,0);
- clip: rect(0,0,0,0);
- white-space: nowrap;
- border-width: 0;
-}
-
-.c4 {
- color: #0969da;
- -webkit-text-decoration: none;
- text-decoration: none;
- position: relative;
- display: -webkit-inline-box;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
- color: #1F2328;
- text-align: center;
- -webkit-text-decoration: none;
- text-decoration: none;
- line-height: calc(20/14);
- border-radius: 6px;
- font-size: 14px;
- padding-left: 8px;
- padding-right: 8px;
- padding-top: calc((2rem - 1.25rem) / 2);
- padding-bottom: calc((2rem - 1.25rem) / 2);
-}
-
-.c4:hover {
- -webkit-text-decoration: underline;
- text-decoration: underline;
-}
-
-.c4:is(button) {
- display: inline-block;
- padding: 0;
- font-size: inherit;
- white-space: nowrap;
- cursor: pointer;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- background-color: transparent;
- border: 0;
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
-}
-
-.c4 span[data-component="icon"] {
- color: #656d76;
-}
-
-.c4:focus {
- outline: 2px solid transparent;
-}
-
-.c4:focus {
- box-shadow: inset 0 0 0 2px #0969da;
-}
-
-.c4:focus:not(:focus-visible) {
- box-shadow: none;
-}
-
-.c4:focus-visible {
- outline: 2px solid transparent;
- box-shadow: inset 0 0 0 2px #0969da;
-}
-
-.c4 span[data-content]::before {
- content: attr(data-content);
- display: block;
- height: 0;
- font-weight: 600;
- visibility: hidden;
- white-space: nowrap;
-}
-
-.c4::after {
- position: absolute;
- right: 50%;
- bottom: calc(50% - 25px);
- width: 100%;
- height: 2px;
- content: "";
- background-color: #fd8c73;
- border-radius: 0;
- -webkit-transform: translate(50%,-50%);
- -ms-transform: translate(50%,-50%);
- transform: translate(50%,-50%);
-}
-
-.c7 {
- color: #0969da;
- -webkit-text-decoration: none;
- text-decoration: none;
- position: relative;
- display: -webkit-inline-box;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
- color: #1F2328;
- text-align: center;
- -webkit-text-decoration: none;
- text-decoration: none;
- line-height: calc(20/14);
- border-radius: 6px;
- font-size: 14px;
- padding-left: 8px;
- padding-right: 8px;
- padding-top: calc((2rem - 1.25rem) / 2);
- padding-bottom: calc((2rem - 1.25rem) / 2);
-}
-
-.c7:hover {
- -webkit-text-decoration: underline;
- text-decoration: underline;
-}
-
-.c7:is(button) {
- display: inline-block;
- padding: 0;
- font-size: inherit;
- white-space: nowrap;
- cursor: pointer;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- background-color: transparent;
- border: 0;
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
-}
-
-.c7 span[data-component="icon"] {
- color: #656d76;
-}
-
-.c7:focus {
- outline: 2px solid transparent;
-}
-
-.c7:focus {
- box-shadow: inset 0 0 0 2px #0969da;
-}
-
-.c7:focus:not(:focus-visible) {
- box-shadow: none;
-}
-
-.c7:focus-visible {
- outline: 2px solid transparent;
- box-shadow: inset 0 0 0 2px #0969da;
-}
-
-.c7 span[data-content]::before {
- content: attr(data-content);
- display: block;
- height: 0;
- font-weight: 600;
- visibility: hidden;
- white-space: nowrap;
-}
-
-.c7::after {
- position: absolute;
- right: 50%;
- bottom: calc(50% - 25px);
- width: 100%;
- height: 2px;
- content: "";
- background-color: transparent;
- border-radius: 0;
- -webkit-transform: translate(50%,-50%);
- -ms-transform: translate(50%,-50%);
- transform: translate(50%,-50%);
-}
-
-.c2 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- list-style: none;
- white-space: nowrap;
- padding-top: 0;
- padding-bottom: 0;
- padding-left: 0;
- padding-right: 0;
- margin: 0;
- margin-bottom: -1px;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- gap: 8px;
- position: relative;
-}
-
-@media (hover:hover) {
- .c4:hover {
- background-color: rgba(175,184,193,0.2);
- -webkit-transition: background .12s ease-out;
- transition: background .12s ease-out;
- -webkit-text-decoration: none;
- text-decoration: none;
- }
-}
-
-@media (forced-colors:active) {
- .c4::after {
- background-color: LinkText;
- }
-}
-
-@media (hover:hover) {
- .c7:hover {
- background-color: rgba(175,184,193,0.2);
- -webkit-transition: background .12s ease-out;
- transition: background .12s ease-out;
- -webkit-text-decoration: none;
- text-decoration: none;
- }
-}
-
-@media (forced-colors:active) {
- .c7::after {
- background-color: transparent;
- }
-}
-
-
-
- Repository navigation
-
-
-
-`;
diff --git a/src/UnderlineNav2/index.ts b/src/UnderlineNav2/index.ts
deleted file mode 100644
index f49517d61f5..00000000000
--- a/src/UnderlineNav2/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import {UnderlineNav as Nav, UnderlineNavProps} from './UnderlineNav'
-import {UnderlineNavItem, UnderlineNavItemProps} from './UnderlineNavItem'
-
-const UnderlineNav = Object.assign(Nav, {
- Item: UnderlineNavItem,
-})
-
-export {UnderlineNav}
-
-export type {UnderlineNavProps, UnderlineNavItemProps}
diff --git a/src/__tests__/__snapshots__/UnderlineNavLink.test.tsx.snap b/src/__tests__/__snapshots__/UnderlineNavLink.test.tsx.snap
deleted file mode 100644
index aaed08f5179..00000000000
--- a/src/__tests__/__snapshots__/UnderlineNavLink.test.tsx.snap
+++ /dev/null
@@ -1,118 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`UnderlineNav.Link renders consistently 1`] = `
-.c0 {
- padding: 16px 8px;
- margin-right: 16px;
- font-size: 14px;
- line-height: 1.5;
- color: #1F2328;
- text-align: center;
- border-bottom: 2px solid transparent;
- -webkit-text-decoration: none;
- text-decoration: none;
-}
-
-.c0:hover,
-.c0:focus {
- color: #1F2328;
- -webkit-text-decoration: none;
- text-decoration: none;
- border-bottom-color: rgba(175,184,193,0.2);
- -webkit-transition: border-bottom-color 0.2s ease;
- transition: border-bottom-color 0.2s ease;
-}
-
-.c0:hover .PRC-UnderlineNav-octicon,
-.c0:focus .PRC-UnderlineNav-octicon {
- color: #656d76;
-}
-
-.c0.PRC-selected {
- color: #1F2328;
- border-bottom-color: #fd8c73;
-}
-
-.c0.PRC-selected .PRC-UnderlineNav-octicon {
- color: #1F2328;
-}
-
-.c0:focus:not(:disabled) {
- box-shadow: none;
- outline: 2px solid #0969da;
- outline-offset: -8px;
-}
-
-.c0:focus:not(:disabled):not(:focus-visible) {
- outline: solid 1px transparent;
-}
-
-.c0:focus-visible:not(:disabled) {
- box-shadow: none;
- outline: 2px solid #0969da;
- outline-offset: -8px;
-}
-
-
-`;
-
-exports[`UnderlineNav.Link respects the "selected" prop 1`] = `
-.c0 {
- padding: 16px 8px;
- margin-right: 16px;
- font-size: 14px;
- line-height: 1.5;
- color: #1F2328;
- text-align: center;
- border-bottom: 2px solid transparent;
- -webkit-text-decoration: none;
- text-decoration: none;
-}
-
-.c0:hover,
-.c0:focus {
- color: #1F2328;
- -webkit-text-decoration: none;
- text-decoration: none;
- border-bottom-color: rgba(175,184,193,0.2);
- -webkit-transition: border-bottom-color 0.2s ease;
- transition: border-bottom-color 0.2s ease;
-}
-
-.c0:hover .PRC-UnderlineNav-octicon,
-.c0:focus .PRC-UnderlineNav-octicon {
- color: #656d76;
-}
-
-.c0.PRC-selected {
- color: #1F2328;
- border-bottom-color: #fd8c73;
-}
-
-.c0.PRC-selected .PRC-UnderlineNav-octicon {
- color: #1F2328;
-}
-
-.c0:focus:not(:disabled) {
- box-shadow: none;
- outline: 2px solid #0969da;
- outline-offset: -8px;
-}
-
-.c0:focus:not(:disabled):not(:focus-visible) {
- outline: solid 1px transparent;
-}
-
-.c0:focus-visible:not(:disabled) {
- box-shadow: none;
- outline: 2px solid #0969da;
- outline-offset: -8px;
-}
-
-
-`;
diff --git a/src/__tests__/__snapshots__/exports.test.ts.snap b/src/__tests__/__snapshots__/exports.test.ts.snap
index 90e28194736..2e0cfbd48ec 100644
--- a/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -72,7 +72,6 @@ exports[`@primer/react should not update exports without a semver change 1`] = `
"TreeView",
"Truncate",
"UnderlineNav",
- "UnderlineNav2",
"merge",
"registerPortalRoot",
"sx",
@@ -109,30 +108,22 @@ exports[`@primer/react/decprecated should not update exports without a semver ch
"ButtonTableList",
"FilterList",
"FilteredSearch",
+ "UnderlineNav",
]
`;
exports[`@primer/react/drafts should not update exports without a semver change 1`] = `
[
"Blankslate",
- "Content",
"DataTable",
"Dialog",
- "Footer",
- "Header",
"Hidden",
"InlineAutocomplete",
"MarkdownEditor",
"MarkdownViewer",
"NavList",
"PageHeader",
- "Pane",
- "Root",
- "SegmentedControl",
- "SplitPageLayout",
"Table",
- "TreeView",
- "UnderlineNav",
"callbackCancelledResult",
"useCombobox",
"useDynamicTextareaHeight",
diff --git a/src/deprecated/UnderlineNav/UnderlineNav.docs.json b/src/deprecated/UnderlineNav/UnderlineNav.docs.json
new file mode 100644
index 00000000000..349c1d6b8a0
--- /dev/null
+++ b/src/deprecated/UnderlineNav/UnderlineNav.docs.json
@@ -0,0 +1,57 @@
+{
+ "id": "underline_nav",
+ "name": "UnderlineNav",
+ "status": "alpha",
+ "a11yReviewed": false,
+ "stories": [],
+ "props": [
+ {
+ "name": "actions",
+ "type": "React.ReactNode",
+ "description": "Place another element, such as a button, to the opposite side of the navigation items."
+ },
+ {
+ "name": "align",
+ "type": "'right'",
+ "description": "Use `right` to have navigation items aligned right."
+ },
+ {
+ "name": "full",
+ "type": "boolean",
+ "description": "Used to make navigation fill the width of the container."
+ },
+ {
+ "name": "aria-label",
+ "type": "string",
+ "description": "Used to set the `aria-label` on the top level `