diff --git a/.changeset/forty-ants-tell.md b/.changeset/forty-ants-tell.md new file mode 100644 index 00000000000..40b88457099 --- /dev/null +++ b/.changeset/forty-ants-tell.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Remove experimental TabPanels component in preference of UnderlinePanels diff --git a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-colorblind-linux.png b/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-colorblind-linux.png deleted file mode 100644 index 4da8225feea..00000000000 Binary files a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-dimmed-linux.png b/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-dimmed-linux.png deleted file mode 100644 index 19a719c94be..00000000000 Binary files a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-dimmed-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-high-contrast-linux.png b/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-high-contrast-linux.png deleted file mode 100644 index 03eb5821d1e..00000000000 Binary files a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-linux.png b/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-linux.png deleted file mode 100644 index 1c31459e7dd..00000000000 Binary files a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-tritanopia-linux.png b/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-tritanopia-linux.png deleted file mode 100644 index 4da8225feea..00000000000 Binary files a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-dark-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-colorblind-linux.png b/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-colorblind-linux.png deleted file mode 100644 index 1287b052243..00000000000 Binary files a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-high-contrast-linux.png b/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-high-contrast-linux.png deleted file mode 100644 index cf950f760e2..00000000000 Binary files a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-linux.png b/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-linux.png deleted file mode 100644 index 1d5cba68e7a..00000000000 Binary files a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-tritanopia-linux.png b/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-tritanopia-linux.png deleted file mode 100644 index 1287b052243..00000000000 Binary files a/.playwright/snapshots/components/TabPanels.test.ts-snapshots/TabPanels-Default-light-tritanopia-linux.png and /dev/null differ diff --git a/docs/content/drafts/TabPanels.mdx b/docs/content/drafts/TabPanels.mdx deleted file mode 100644 index 0d8755f5edc..00000000000 --- a/docs/content/drafts/TabPanels.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -componentId: tab_panels -title: TabPanels -status: Draft -source: https://github.com/primer/react/blob/main/src/TabPanels ---- - -import data from '../../../packages/react/src/drafts/TabPanels/TabPanels.docs.json' - -```js -import {TabPanels} from '@primer/react/drafts' -``` - -**Attention:** Make sure to properly label your `TabPanels` with an `aria-label` to provide context about the subject of your `TabPanels`. - -`TabPanels.Tab` elements are wired up to their `TabPanels.Panel` elements based on the index order that they exist in the document. - -## Examples - -```jsx live drafts - - Tab 1 - Tab 2 - Tab 3 - Panel 1 - Panel 2 - Panel 3 - -``` - -## Props - - diff --git a/e2e/components/TabPanels.test.ts b/e2e/components/TabPanels.test.ts deleted file mode 100644 index 3bc1b18772a..00000000000 --- a/e2e/components/TabPanels.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {test, expect} from '@playwright/test' -import {visit} from '../test-helpers/storybook' -import {themes} from '../test-helpers/themes' - -test.describe('TabPanels', () => { - test.describe('Default', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'drafts-components-tabpanels--default', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`TabPanels.Default.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'drafts-components-tabpanels--default', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) -}) diff --git a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap index 518f01381c6..748404c99b9 100644 --- a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap @@ -344,10 +344,6 @@ exports[`@primer/react/drafts should not update exports without a semver change "type TableRowProps", "type TableSubtitleProps", "type TableTitleProps", - "TabPanels", - "type TabPanelsPanelProps", - "type TabPanelsProps", - "type TabPanelsTabProps", "type TitleProps", "Tooltip", "TooltipContext", @@ -460,10 +456,6 @@ exports[`@primer/react/experimental should not update exports without a semver c "type TableRowProps", "type TableSubtitleProps", "type TableTitleProps", - "TabPanels", - "type TabPanelsPanelProps", - "type TabPanelsProps", - "type TabPanelsTabProps", "type TitleProps", "Tooltip", "TooltipContext", diff --git a/packages/react/src/drafts/TabPanels/TabPanels.docs.json b/packages/react/src/drafts/TabPanels/TabPanels.docs.json deleted file mode 100644 index ab7e522421b..00000000000 --- a/packages/react/src/drafts/TabPanels/TabPanels.docs.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "id": "tab_panels", - "name": "TabPanels", - "status": "draft", - "a11yReviewed": false, - "stories": [], - "importPath": "@primer/react/experimental", - "props": [ - { - "name": "aria-label", - "type": "string", - "description": "Used to set the `aria-label` on the `role=\"tablist\"` element. Either aria-label or aria-labelledby must be provided." - }, - { - "name": "aria-labelledby", - "type": "string", - "description": "Used to set the `aria-labelledby` on the `role=\"tablist\"` element. Either aria-label or aria-labelledby must be provided." - }, - { - "name": "children", - "type": "React.ReactNode", - "description": "The content of the component, can contain Tabs, Panels but also content before and after Tabs and after the Panels." - }, - { - "name": "id", - "type": "string", - "description": "The id of the tab container, used to generate child ids." - }, - { - "name": "defaultTabIndex", - "type": "number", - "description": "The 0-based index of the tab that is selected by default when the component is loaded." - }, - { - "name": "selectedTabIndex", - "type": "number", - "description": "The 0-based index of the tab that is selected." - }, - { - "name": "onChange", - "type": "function", - "description": "Callback fired when the tab container changes (bubbles, cancelable): fired on `` before a new tab is selected and visibility is updated. `event.tab` is the tab that will be focused and `tab.panel` is the panel that will be shown if the event isn't cancelled." - }, - { - "name": "onChanged", - "type": "function", - "description": "Callback fired when the tab container changes (bubbles): fired on `` after a new tab is selected and visibility is updated. `event.tab` is the tab that is now active (and will be focused right after this event) and `event.panel` is the newly visible tab panel." - }, - { - "name": "sx", - "type": "SystemStyleObject" - } - ], - "subcomponents": [ - { - "name": "TabPanels.Tab", - "props": [ - { - "name": "sx", - "type": "SystemStyleObject" - } - ] - }, - { - "name": "TabPanels.Panel", - "props": [ - { - "name": "children", - "type": "React.ReactNode", - "description": "The content of the panel." - }, - { - "name": "sx", - "type": "SystemStyleObject" - } - ] - } - ] -} diff --git a/packages/react/src/drafts/TabPanels/TabPanels.features.stories.tsx b/packages/react/src/drafts/TabPanels/TabPanels.features.stories.tsx deleted file mode 100644 index c6689d4dae8..00000000000 --- a/packages/react/src/drafts/TabPanels/TabPanels.features.stories.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react' -import type {Meta} from '@storybook/react' -import TabPanels from './TabPanels' -import type {ComponentProps} from '../../utils/types' -import {Button} from '../../Button' - -export default { - title: 'Drafts/Components/TabPanels/Features', - component: TabPanels, -} as Meta> - -export const DefaultTab = () => ( - - One - Two - Three - One - Two - Three - -) - -export const SelectedTab = () => ( - - One - Two - Three - One - Two - Three - -) - -export const LabelledBy = () => ( - <> -

TabPanels example

- - One - Two - Three - One - Two - Three - - -) - -export const AdditionalContent = () => ( - - - One - Two - Three - - One - Two - Three -
Additional content after the panels
-
-) - -export const ManyTabs = () => ( - - One - Two - Three - Four - Five - Six - Seven - Eight - Nine - Ten - Eleven - One - Two - Three - Four - Five - Six - Seven - Eight - Nine - Ten - Eleven - -) - -export const AlternativeStructure = () => ( - - One - One - Two - Two - Three - Three - -) diff --git a/packages/react/src/drafts/TabPanels/TabPanels.stories.tsx b/packages/react/src/drafts/TabPanels/TabPanels.stories.tsx deleted file mode 100644 index e690f86d16b..00000000000 --- a/packages/react/src/drafts/TabPanels/TabPanels.stories.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import type {Meta, StoryFn} from '@storybook/react' -import TabPanels from './TabPanels' -import type {ComponentProps} from '../../utils/types' - -export default { - title: 'Drafts/Components/TabPanels', - component: TabPanels, -} as Meta> - -export const Playground: StoryFn> = args => ( - - Tab 1 - Tab 2 - Tab 3 - Panel 1 - Panel 2 - Panel 3 - -) - -Playground.args = { - 'aria-label': 'Select a tab', - defaultTabIndex: 0, - id: 'tab-panels', -} -Playground.argTypes = { - 'aria-label': { - type: 'string', - }, - selectedTabIndex: { - type: 'number', - }, - defaultTabIndex: { - type: 'number', - }, - id: { - type: 'string', - }, -} - -export const Default = () => ( - - Tab 1 - Tab 2 - Tab 3 - Panel 1 - Panel 2 - Panel 3 - -) diff --git a/packages/react/src/drafts/TabPanels/TabPanels.tsx b/packages/react/src/drafts/TabPanels/TabPanels.tsx deleted file mode 100644 index ba73e72783b..00000000000 --- a/packages/react/src/drafts/TabPanels/TabPanels.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../../utils/polymorphic' -import clsx from 'clsx' -import React from 'react' -import styled from 'styled-components' -import {get} from '../../constants' -import {TabContainerElement} from '@github/tab-container-element' -import {createComponent} from '../../utils/create-component' -import sx, {type SxProp} from '../../sx' -import type {ComponentProps} from '../../utils/types' -import getGlobalFocusStyles from '../../internal/utils/getGlobalFocusStyles' - -const TAB_CLASS = 'TabPanel-tab' - -const tabContainerComponent = createComponent(TabContainerElement, 'tab-container') -const TabContainer = styled(tabContainerComponent)` - & > :not([role='tabpanel']) { - display: inline-block; - } - - &::part(tablist-wrapper) { - margin-top: 0; - margin-bottom: 16px; - border-bottom: 1px solid ${get('colors.border.default')}; - } - - &:not(:defined) [role='tabpanel'] { - margin-top: 17px; - display: none; - } - - &:not(:defined) [role='tab']:nth-of-type(1)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(1), - &:not(:defined) [role='tab']:nth-of-type(2)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(2), - &:not(:defined) [role='tab']:nth-of-type(3)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(3), - &:not(:defined) [role='tab']:nth-of-type(4)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(4), - &:not(:defined) [role='tab']:nth-of-type(5)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(5), - &:not(:defined) [role='tab']:nth-of-type(6)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(6), - &:not(:defined) [role='tab']:nth-of-type(7)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(7), - &:not(:defined) [role='tab']:nth-of-type(8)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(8), - &:not(:defined) [role='tab']:nth-of-type(9)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(9), - &:not(:defined) [role='tab']:nth-of-type(10)[aria-selected='true'] ~ [role='tabpanel']:nth-of-type(10) { - display: block; - } - - &:not(:defined):not(:has([aria-selected='true'])) [role='tabpanel']:first-of-type { - display: block; - } - - &:not(:has([aria-selected='true'])) [role='tab'] ~ [role='tab'] { - color: ${get('colors.fg.muted')}; - background-color: transparent; - border: 1px solid transparent; - } - - &:not(:has([aria-selected='true'])) [role='tab'], - & [role='tab'][aria-selected='true'] { - color: ${get('colors.fg.default')}; - border-color: ${get('colors.border.default')}; - border-top-right-radius: ${get('radii.2')}; - border-top-left-radius: ${get('radii.2')}; - background-color: ${get('colors.canvas.default')}; - } - - &:not(:defined):not(:has([aria-selected='true'])) [role='tab'] ~ [role='tab'], - &:not(:defined):has([aria-selected='true']) [role='tab']:not([aria-selected='true']) { - padding: 8px 16px; - border-bottom: 1px solid ${get('colors.border.default')}; - } - - &:not(:defined) :not([role='tabpanel']) { - vertical-align: top; - } - - ${sx}; -` - -type Label = { - 'aria-label': string - 'aria-labelledby'?: never -} - -type Labelledby = { - 'aria-label'?: never - 'aria-labelledby': string -} - -type Labelled = Label | Labelledby - -export type TabPanelsProps = ComponentProps & { - id?: string -} & Labelled - -function TabPanels({children, defaultTabIndex, ...props}: TabPanelsProps) { - // We need to always call React.useId() because - // React Hooks must be called in the exact same order in every component render - const defaultId = React.useId() - const parentId = props.id ?? defaultId - - if (defaultTabIndex !== undefined) { - // Add 'dafault-tab' to props - props['default-tab'] = defaultTabIndex - } - - // Loop through the chidren, if it's a tab, then add id="{id}-tab-{index}" - // If it's a panel, then add aria-labelledby="{id}-tab-{index}" - let tabIndex = 0 - let panelIndex = 0 - - const childrenWithProps = React.Children.map(children, child => { - if (React.isValidElement(child) && child.type === Tab) { - if (props.selectedTabIndex === tabIndex) { - return React.cloneElement(child, {id: `${parentId}-tab-${tabIndex++}`, selected: true}) - } - - return React.cloneElement(child, {id: `${parentId}-tab-${tabIndex++}`}) - } - if (React.isValidElement(child) && child.type === Panel) { - return React.cloneElement(child, {'aria-labelledby': `${parentId}-tab-${panelIndex++}`}) - } - return child - }) - - return ( - - {childrenWithProps} - - ) -} - -export type TabPanelsTabProps = React.DetailedHTMLProps, HTMLButtonElement> & { - selected?: boolean -} & SxProp - -const Tab = styled.button.attrs(props => ({ - className: clsx(TAB_CLASS, props.className), - role: 'tab', - 'aria-selected': !!props.selected, - suppressHydrationWarning: true, -}))` - padding: 8px 16px 9px 16px; - font-size: ${get('fontSizes.1')}; - line-height: 23px; - color: ${get('colors.fg.muted')}; - text-decoration: none; - background-color: transparent; - border: 1px solid transparent; - border-bottom: 0; - margin-bottom: -1px; - cursor: pointer; - - ${getGlobalFocusStyles('-6px')}; - - &:hover, - &:focus { - color: ${get('colors.fg.default')}; - text-decoration: none; - } - - &:hover { - transition-duration: 0.1s; - transition-property: color; - } - - ${sx}; -` as PolymorphicForwardRefComponent<'button', TabPanelsTabProps> - -Tab.displayName = 'TabPanels.Tab' - -export type TabPanelsPanelProps = React.HTMLAttributes & { - children: React.ReactNode -} & SxProp - -const Panel = styled.div.attrs(() => ({ - role: 'tabpanel', - suppressHydrationWarning: true, -}))` - ${sx}; -` - -Panel.displayName = 'TabPanels.Panel' - -export default Object.assign(TabPanels, {Panel, Tab}) diff --git a/packages/react/src/drafts/TabPanels/__tests__/TabPanels.test.tsx b/packages/react/src/drafts/TabPanels/__tests__/TabPanels.test.tsx deleted file mode 100644 index 4ca42156275..00000000000 --- a/packages/react/src/drafts/TabPanels/__tests__/TabPanels.test.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react' -import {render, screen} from '@testing-library/react' -import TabPanels from '../TabPanels' -import TabContainerElement from '@github/tab-container-element' - -// Mock the selectTab method, jsdom doesn't like it -// But that doesn't matter because we're not testing the web component here -// Just the connection to the web component -TabContainerElement.prototype.selectTab = jest.fn() - -describe('TabPanels', () => { - //Reset mocks after each test - afterEach(() => { - jest.restoreAllMocks() - }) - - it('renders children correctly', () => { - render( - - Tab 1 - Tab 2 - Panel 1 - Panel 2 - , - ) - - expect(screen.getByText('Tab 1')).toBeInTheDocument() - expect(screen.getByText('Tab 2')).toBeInTheDocument() - expect(screen.getByText('Panel 1')).toBeInTheDocument() - expect(screen.getByText('Panel 2')).toBeInTheDocument() - }) - - it('auto generates parent id correctly', () => { - render( - - Tab 1 - Tab 2 - Panel 1 - Panel 2 - , - ) - - const incorrectId = 'undefined-tab-0' - - expect(screen.getByText('Tab 1').id).not.toBe(incorrectId) - }) - - it('applies aria-selected to first tab when selected', () => { - render( - - Tab 1 - Tab 2 - Panel 1 - Panel 2 - , - ) - - expect(screen.getByText('Tab 1')).toHaveAttribute('aria-selected', 'true') - expect(screen.getByText('Tab 2')).toHaveAttribute('aria-selected', 'false') - }) - - it('applies aria-selected to second tab when selected', () => { - render( - - Tab 1 - Tab 2 - Panel 1 - Panel 2 - , - ) - - expect(screen.getByText('Tab 1')).toHaveAttribute('aria-selected', 'false') - expect(screen.getByText('Tab 2')).toHaveAttribute('aria-selected', 'true') - }) -}) diff --git a/packages/react/src/drafts/TabPanels/index.ts b/packages/react/src/drafts/TabPanels/index.ts deleted file mode 100644 index 483de0e2b8f..00000000000 --- a/packages/react/src/drafts/TabPanels/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {default} from './TabPanels' -export type {TabPanelsProps, TabPanelsTabProps, TabPanelsPanelProps} from './TabPanels' diff --git a/packages/react/src/drafts/index.ts b/packages/react/src/drafts/index.ts index 16a78befb4a..a39980acf1a 100644 --- a/packages/react/src/drafts/index.ts +++ b/packages/react/src/drafts/index.ts @@ -67,8 +67,6 @@ export type { NavListDividerProps, } from '../NavList' export * from './SelectPanel2' -export {default as TabPanels} from './TabPanels' -export type {TabPanelsProps, TabPanelsTabProps, TabPanelsPanelProps} from './TabPanels' export * from '../TooltipV2' export * from '../ActionBar'