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
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.
69 changes: 69 additions & 0 deletions e2e/components/TopicTag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {test, expect} from '@playwright/test'
import {visit} from '../test-helpers/storybook'
import {themes} from '../test-helpers/themes'
import {viewports} from '../test-helpers/viewports'

const stories = [
{
title: 'Default',
id: 'experimental-components-topictag--default',
},
] as const

test.describe('TopicTag', () => {
for (const story of stories) {
test.describe(story.title, () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: story.id,
globals: {
colorScheme: theme,
},
})
await page.setViewportSize({width: 400, height: 200})

// Default state
await expect(page).toHaveScreenshot(`TopicTag.${story.title}.${theme}.png`)

// Hover state
await page.getByText('React').hover()
await expect(page).toHaveScreenshot(`TopicTag.${story.title}.${theme}.hover.png`)

// Focus state
// eslint-disable-next-line github/no-blur
await page.getByText('React').blur()
await page.getByText('React').focus()
await expect(page).toHaveScreenshot(`TopicTag.${story.title}.${theme}.focus.png`)
})
})
}
})
}

test.describe('As Group', () => {
const story = {
title: 'As Group',
id: 'experimental-components-topictag-features--as-group',
}

test('default @vrt', async ({page}) => {
await visit(page, {
id: story.id,
})

// Viewport: xs
await page.setViewportSize({width: viewports['primer.breakpoint.xs'], height: 500})
await expect(page).toHaveScreenshot(`TopicTag.${story.title}.xs.png`)

// Viewport: sm
await page.setViewportSize({width: viewports['primer.breakpoint.sm'], height: 500})
await expect(page).toHaveScreenshot(`TopicTag.${story.title}.sm.png`)

// Viewport: md
await page.setViewportSize({width: viewports['primer.breakpoint.md'], height: 500})
await expect(page).toHaveScreenshot(`TopicTag.${story.title}.md.png`)
})
})
})
21 changes: 21 additions & 0 deletions packages/react/src/TopicTag/TopicTag.docs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": "topic_tag",
"name": "TopicTag",
"status": "alpha",
"a11yReviewed": false,
"stories": [],
"importPath": "@primer/react/experimental",
"props": [
{
"name": "as",
"type": "React.ElementType",
"description": "The HTML element or React component to render as the root element"
},
{
"name": "className",
"type": "string",
"description": "Class name for custom styling"
}
],
"subcomponents": []
}
39 changes: 39 additions & 0 deletions packages/react/src/TopicTag/TopicTag.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {Meta} from '@storybook/react-vite'
import {TopicTag} from './TopicTag'
import {TopicTagGroup} from './TopicTagGroup'

export default {
title: 'Experimental/Components/TopicTag/Features',
component: TopicTag,
} satisfies Meta<typeof TopicTag>

export const AsLink = () => (
<TopicTag as="a" href="/topics/react">
React
</TopicTag>
)

export const AsGroup = () => {
const tags = [
'react',
'nodejs',
'javascript',
'd3',
'teachers',
'community',
'education',
'programming',
'curriculum',
'math',
]

return (
<TopicTagGroup>
{tags.map(tag => (
<TopicTag as="a" href={`/topics/${tag.toLowerCase()}`} key={tag}>
{tag}
</TopicTag>
))}
</TopicTagGroup>
)
}
25 changes: 25 additions & 0 deletions packages/react/src/TopicTag/TopicTag.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.TopicTag {
background-color: var(--bgColor-accent-muted);
color: var(--fgColor-accent);
font-size: var(--text-body-size-small);
font-weight: var(--base-text-weight-semibold);
/* --text-body-lineHeight-small */
/* stylelint-disable-next-line primer/typography */
line-height: 1.66667;
border-radius: var(--borderRadius-full);
padding: var(--base-size-2) var(--base-size-12);
border: var(--borderWidth-thin) solid var(--topicTag-borderColor, transparent);
display: inline-flex;
white-space: nowrap;

&:hover {
background-color: var(--bgColor-accent-emphasis);
color: var(--fgColor-onEmphasis);
}
}

/* Add a reset to when TopicTag is an <a> element since our link styles apply an underline text-decoration by default */
/* stylelint-disable-next-line selector-no-qualifying-type */
:where(a.TopicTag) {
text-decoration: none;
}
13 changes: 13 additions & 0 deletions packages/react/src/TopicTag/TopicTag.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type {Meta, StoryObj} from '@storybook/react-vite'
import {TopicTag} from './TopicTag'

export default {
title: 'Experimental/Components/TopicTag',
component: TopicTag,
} satisfies Meta<typeof TopicTag>

export const Default = () => <TopicTag>React</TopicTag>

export const Playground: StoryObj<typeof TopicTag> = {
render: args => <TopicTag {...args}>React</TopicTag>,
}
41 changes: 41 additions & 0 deletions packages/react/src/TopicTag/TopicTag.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {render, screen} from '@testing-library/react'
import {describe, test, expect, vi} from 'vitest'
import {userEvent} from 'vitest/browser'
import {TopicTag} from '../TopicTag'

describe('TopicTag', () => {
test('defaults to <button> semantics', async () => {
const onClick = vi.fn()
render(<TopicTag onClick={onClick}>test</TopicTag>)

await userEvent.click(screen.getByRole('button', {name: 'test'}))
expect(onClick).toHaveBeenCalled()
})

test('support <a> semantics through `href` prop', async () => {
const onClick = vi.fn()
render(
<TopicTag as="a" href="#test" onClick={onClick}>
test
</TopicTag>,
)

await userEvent.click(screen.getByRole('link', {name: 'test'}))
expect(onClick).toHaveBeenCalled()
})

test('supports `className` merging', () => {
const {container} = render(<TopicTag className="custom-class">test</TopicTag>)
expect(container.firstChild).toHaveClass('custom-class')
})

test('additional props are supplied to outermost element', () => {
const {container} = render(
<TopicTag data-testid="test" id="test-id">
test
</TopicTag>,
)
expect(container.firstChild).toHaveAttribute('data-testid', 'test')
expect(container.firstChild).toHaveAttribute('id', 'test-id')
})
})
26 changes: 26 additions & 0 deletions packages/react/src/TopicTag/TopicTag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {clsx} from 'clsx'
import type {ElementType} from 'react'
import buttonResetClasses from '../internal/components/ButtonReset.module.css'
import classes from './TopicTag.module.css'

type TopicTagProps<As extends ElementType> = {
as?: As
className?: string
} & Omit<React.ComponentPropsWithoutRef<As>, 'as' | 'className'>

function TopicTag<As extends ElementType = 'button'>({as, children, className, ...rest}: TopicTagProps<As>) {
const BaseComponent = as ?? 'button'
return (
<BaseComponent
{...rest}
className={clsx(className, classes.TopicTag, {
[buttonResetClasses.ButtonReset]: BaseComponent === 'button',
})}
>
{children}
</BaseComponent>
)
}

export {TopicTag}
export type {TopicTagProps}
6 changes: 6 additions & 0 deletions packages/react/src/TopicTag/TopicTagGroup.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.TopicTagGroup {
display: flex;
flex-wrap: wrap;
column-gap: var(--base-size-2);
row-gap: var(--base-size-8);
}
16 changes: 16 additions & 0 deletions packages/react/src/TopicTag/TopicTagGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {clsx} from 'clsx'
import type React from 'react'
import classes from './TopicTagGroup.module.css'

type TopicTagGroupProps = React.HTMLAttributes<HTMLElement>

function TopicTagGroup({children, ...rest}: TopicTagGroupProps) {
return (
<div {...rest} className={clsx(classes.TopicTagGroup)}>
{children}
</div>
)
}

export {TopicTagGroup}
export type {TopicTagGroupProps}
2 changes: 2 additions & 0 deletions packages/react/src/TopicTag/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {TopicTag} from './TopicTag'
export type {TopicTagProps} from './TopicTag'
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ exports[`@primer/react/experimental > should not update exports without a semver
"type TitleProps",
"Tooltip",
"type TooltipProps",
"TopicTag",
"type TopicTagProps",
"UnderlinePanels",
"type UnderlinePanelsPanelProps",
"type UnderlinePanelsProps",
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/experimental/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,6 @@ export type {IssueLabelProps} from './IssueLabel'

export * from '../KeybindingHint'
export * from './Tabs'

export {TopicTag} from '../TopicTag'
export type {TopicTagProps} from '../TopicTag'
Loading