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
5 changes: 5 additions & 0 deletions .changeset/major-adults-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Add UnderlinePanels2 Component
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions e2e/components/UnderlinePanels2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {test, expect} from '@playwright/test'
import {visit} from '../test-helpers/storybook'
import {themes} from '../test-helpers/themes'

const stories: Array<{title: string; id: string}> = [
{
title: 'Default',
id: 'experimental-components-underlinepanels2--default',
},
{
title: 'Dev Default',
id: 'experimental-components-underlinepanels2-dev--default',
},
{
title: 'Labelled By External Element',
id: 'experimental-components-underlinepanels2-features--labelled-by-external-element',
},
{
title: 'Selected Tab',
id: 'experimental-components-underlinepanels2-features--selected-tab',
},
{
title: 'With Counters',
id: 'experimental-components-underlinepanels2-features--with-counters',
},
{
title: 'With Counters In Loading State',
id: 'experimental-components-underlinepanels2-features--with-counters-in-loading-state',
},
{
title: 'With Icons',
id: 'experimental-components-underlinepanels2-features--with-icons',
},
{
title: 'With Icons Hidden On Narrow Screen',
id: 'experimental-components-underlinepanels2-features--with-icons-hidden-on-narrow-screen',
},
]

test.describe('UnderlinePanels2', () => {
for (const story of stories) {
test.describe(story.title, () => {
for (const theme of themes) {
test.describe(theme, () => {
test('@vrt', async ({page}) => {
await visit(page, {
id: story.id,
globals: {
colorScheme: theme,
},
})

await expect(page).toHaveScreenshot(`UnderlinePanels2.${story.title}.${theme}.png`)
})
})
}
})
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ exports[`@primer/react/experimental > should not update exports without a semver
"Tooltip",
"type TooltipProps",
"UnderlinePanels",
"UnderlinePanels2",
"type UnderlinePanels2PanelProps",
"type UnderlinePanels2Props",
"type UnderlinePanels2TabProps",
"type UnderlinePanelsPanelProps",
"type UnderlinePanelsProps",
"type UnderlinePanelsTabProps",
Expand Down
19 changes: 12 additions & 7 deletions packages/react/src/experimental/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, {
createContext,
useContext,
useId,
useMemo,
type AriaAttributes,
type ElementRef,
type PropsWithChildren,
} from 'react'
import useIsomorphicLayoutEffect from '../../utils/useIsomorphicLayoutEffect'
import {useControllableState} from '../../hooks/useControllableState'
import {useProvidedRefOrCreate} from '../../hooks'
import {useId, useProvidedRefOrCreate} from '../../hooks'

/**
* Props to be used when the Tabs component's state is controlled by the parent
Expand Down Expand Up @@ -53,7 +52,10 @@ type UncontrolledTabsProps = {
onValueChange?: ({value}: {value: string}) => void
}

type TabsProps = PropsWithChildren<ControlledTabsProps | UncontrolledTabsProps>
type TabsProps = {
/** Base id for generating unique ids for tabs and panels */
id?: string
} & PropsWithChildren<ControlledTabsProps | UncontrolledTabsProps>

/**
* The Tabs component provides the base structure for a tabbed interface, without providing any formal requirement on DOM structure or styling.
Expand All @@ -62,9 +64,9 @@ type TabsProps = PropsWithChildren<ControlledTabsProps | UncontrolledTabsProps>
* This is intended to be used in conjunction with the `useTab`, `useTabList`, and `useTabPanel` hooks to build a fully accessible tabs component.
* The `Tab`, `TabList`, and `TabPanel` components are provided for convenience to showcase the API & implementation.
*/
function Tabs(props: TabsProps) {
function Tabs({id, ...props}: TabsProps) {
const {children, onValueChange} = props
const groupId = useId()
const groupId = useId(id)

const [selectedValue, setSelectedValue] = useControllableState<string>({
name: 'tab-selection',
Expand Down Expand Up @@ -92,15 +94,18 @@ function Tabs(props: TabsProps) {
}

type Label = {
/** The accessible label for the tab list */
'aria-label': string
}

type LabelledBy = {
/** The id of an external element that labels the tab list */
'aria-labelledby': string
}

type Labelled = Label | LabelledBy
type TabListProps = Labelled & React.HTMLAttributes<HTMLElement>
type TabListProps = Labelled &
Pick<React.HTMLAttributes<HTMLElement>, 'aria-orientation' | 'aria-label' | 'aria-labelledby'>

function useTabList<T extends HTMLElement>(
props: TabListProps & {
Expand Down Expand Up @@ -182,7 +187,7 @@ function useTabList<T extends HTMLElement>(
}
}

function TabList({children, ...rest}: TabListProps) {
function TabList({children, ...rest}: React.PropsWithChildren<TabListProps & React.HTMLAttributes<HTMLDivElement>>) {
const {tabListProps} = useTabList<HTMLDivElement>(rest)

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type {ComponentProps} from '../../utils/types'
import type {Meta, StoryFn} from '@storybook/react-vite'
import {UnderlinePanels} from './UnderlinePanels2'

export default {
title: 'Experimental/Components/UnderlinePanels2/Dev',
component: UnderlinePanels,
subcomponents: {Tab: UnderlinePanels.Tab, Panel: UnderlinePanels.Panel, TabList: UnderlinePanels.TabList},
} as Meta<ComponentProps<typeof UnderlinePanels>>

export const Default = () => (
<UnderlinePanels defaultValue="tab-1">
<UnderlinePanels.TabList aria-label="Select a tab">
<UnderlinePanels.Tab value="tab-1">Tab 1</UnderlinePanels.Tab>
<UnderlinePanels.Tab value="tab-2">Tab 2</UnderlinePanels.Tab>
<UnderlinePanels.Tab value="tab-3">Tab 3</UnderlinePanels.Tab>
</UnderlinePanels.TabList>
<UnderlinePanels.Panel value="tab-1">Panel 1</UnderlinePanels.Panel>
<UnderlinePanels.Panel value="tab-2">Panel 2</UnderlinePanels.Panel>
<UnderlinePanels.Panel value="tab-3">Panel 3</UnderlinePanels.Panel>
</UnderlinePanels>
)

export const SingleTabPlayground: StoryFn<ComponentProps<typeof UnderlinePanels.Tab>> = args => {
return (
<UnderlinePanels aria-label="Select a tab" defaultValue="users">
<UnderlinePanels.TabList aria-label="Select a tab">
<UnderlinePanels.Tab {...args} value="users">
Users
</UnderlinePanels.Tab>
</UnderlinePanels.TabList>
<UnderlinePanels.Panel value="users">Users Panel</UnderlinePanels.Panel>
</UnderlinePanels>
)
}

SingleTabPlayground.args = {
counter: '14K',
disabled: false,
}

SingleTabPlayground.argTypes = {
disabled: {
control: {
type: 'boolean',
},
},
counter: {
control: {
type: 'text',
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
{
"id": "underline_panels2",
"name": "UnderlinePanels2",
"status": "draft",
"a11yReviewed": false,
"stories": [
{
"id": "experimental-components-underlinepanels2--default"
},
{
"id": "experimental-components-underlinepanels2-features--labelled-by-external-element"
},
{
"id": "experimental-components-underlinepanels2-features--selected-tab"
},
{
"id": "experimental-components-underlinepanels2-features--with-counters"
},
{
"id": "experimental-components-underlinepanels2-features--with-counters-in-loading-state"
},
{
"id": "experimental-components-underlinepanels2-features--with-icons"
},
{
"id": "experimental-components-underlinepanels2-features--with-icons-hidden-on-narrow-screen"
}
],
"importPath": "@primer/react/experimental",
"props": [
{
"name": "id",
"type": "string",
"derive": true
},
{
"name": "value",
"type": "string",
"derive": true
},
{
"name": "defaultValue",
"type": "string",
"derive": true
},
{
"name": "onValueChange",
"derive": true
},
{
"name": "children",
"type": "React.ReactNode",
"derive": true
},
{
"name": "loadingCounters",
"type": "boolean",
"derive": true
}
],
"subcomponents": [
{
"name": "UnderlinePanels2.TabList",
"props": [
{
"name": "aria-label",
"type": "string",
"derive": true
},
{
"name": "aria-labelledby",
"type": "string",
"derive": true
},
{
"name": "children",
"type": "React.ReactNode",
"derive": true
},
{
"name": "ref",
"type": "React.Ref<HTMLUListElement>",
"derive": true
}
],
"passthrough": {
"element": "ul",
"url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul#Attributes"
}
},
{
"name": "UnderlinePanels2.Tab",
"props": [
{
"name": "value",
"type": "string",
"derive": true
},
{
"name": "counter",
"type": "number | string",
"derive": true
},
{
"name": "disabled",
"type": "boolean",
"derive": true
},
{
"name": "icon",
"type": "Icon",
"derive": true
},
{
"name": "children",
"type": "React.ReactNode",
"derive": true
}
]
},
{
"name": "UnderlinePanels2.Panel",
"props": [
{
"name": "value",
"type": "string",
"derive": true
},
{
"name": "children",
"type": "React.ReactNode",
"derive": true
}
],
"passthrough": {
"element": "div",
"url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div#Attributes"
}
}
]
}
Loading
Loading