Skip to content
Open
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 packages/__docs__/versioned-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import * as V11_6 from '@instructure/ui/v11_6'
import * as V11_7 from '@instructure/ui/v11_7'
import * as V11_8 from '@instructure/ui/v11_8'
// eslint-disable-next-line no-restricted-imports
import { Guidelines } from './src/Guidelines'
// eslint-disable-next-line no-restricted-imports
Expand All @@ -33,20 +34,24 @@ import { ToggleBlockquote } from './src/ToggleBlockquote'
// eslint-disable-next-line no-restricted-imports
import { V12ChangelogTable } from './src/V12ChangelogTable'

const docsComponents = { Guidelines, Figure, ToggleBlockquote, V12ChangelogTable }
const docsComponents = {
Guidelines,
Figure,
ToggleBlockquote,
V12ChangelogTable
}

const versions: Record<string, Record<string, any>> = {
v11_6: { ...V11_6, ...docsComponents } as any,
v11_7: { ...V11_7, ...docsComponents } as any,
v11_8: { ...V11_8, ...docsComponents } as any
}

/**
* Returns the full component map for a given library version.
* Version exports are merged with docs-specific components (Guidelines,
* Figure, ToggleBlockquote, V12ChangelogTable). Defaults to v11_6.
*/
export function getComponentsForVersion(
version?: string
): Record<string, any> {
export function getComponentsForVersion(version?: string): Record<string, any> {
return versions[version ?? 'v11_6'] ?? versions['v11_6']
}
17 changes: 12 additions & 5 deletions packages/ui-tag/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,19 @@
"require": "./lib/exports/b.js",
"default": "./es/exports/b.js"
},
"./v11_8": {
"src": "./src/exports/c.ts",
"types": "./types/exports/c.d.ts",
"import": "./es/exports/c.js",
"require": "./lib/exports/c.js",
"default": "./es/exports/c.js"
},
"./latest": {
"src": "./src/exports/b.ts",
"types": "./types/exports/b.d.ts",
"import": "./es/exports/b.js",
"require": "./lib/exports/b.js",
"default": "./es/exports/b.js"
"src": "./src/exports/c.ts",
"types": "./types/exports/c.d.ts",
"import": "./es/exports/c.js",
"require": "./lib/exports/c.js",
"default": "./es/exports/c.js"
}
}
}
151 changes: 132 additions & 19 deletions packages/ui-tag/src/Tag/__tests__/Tag.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,51 +22,164 @@
* SOFTWARE.
*/

import { render } from 'vitest-browser-react'
import { page, userEvent } from 'vitest/browser'
import { describe, it, expect, vi } from 'vitest'
import { ComponentType } from 'react'
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import '@testing-library/jest-dom'
import { vi } from 'vitest'

import { runAxeCheck } from '@instructure/ui-axe-check'
import type { ViewProps } from '@instructure/ui-view/latest'
import { Tag } from '@instructure/ui-tag/latest'
import { View } from '@instructure/ui-view/latest'

const originalOmitViewProps = View.omitViewProps

describe('<Tag />', async () => {
beforeAll(() => {
// View component read Component.name instead of Component.displayName
// causing [undefined] in error messages
type TagComponentType = ComponentType & {
name: 'Tag'
}

View.omitViewProps = (props, Component) => {
const ModifiedComponent = {
...Component,
name: 'Tag'
} as TagComponentType
return originalOmitViewProps(props, ModifiedComponent)
}
})
afterAll(() => {
View.omitViewProps = originalOmitViewProps
})

it('should display text', async () => {
await render(<Tag text="Summer" />)
const tag = page.getByText('Summer').element()
render(<Tag text="Summer" />)
const tag = screen.getByText('Summer')

expect(tag).toBeInTheDocument()
})

it('should render as a button and respond to onClick event', async () => {
it('should render the body as a button and respond to onClick event', async () => {
const onClick = vi.fn()
await render(
<Tag data-testid="summer-button" text="Summer" onClick={onClick} />
)
render(<Tag text="Summer" onClick={onClick} />)

const button = page.getByTestId('summer-button').element()
const button = screen.getByRole('button')

await userEvent.click(button)

await vi.waitFor(() => {
await waitFor(() => {
expect(onClick).toHaveBeenCalledTimes(1)
expect(button.tagName).toBe('BUTTON')
})
})

it('should render a close icon when it is dismissible and clickable', async () => {
const onClick = vi.fn()
const { container } = await render(
<Tag text="Summer" onClick={onClick} dismissible={true} />
it('should render the body as a link when an href is provided', async () => {
render(<Tag text="Summer" href="/summer" />)

const link = screen.getByRole('link')

expect(link.tagName).toBe('A')
expect(link).toHaveAttribute('href', '/summer')
})

it('should render a link and still fire onClick when both href and onClick are provided', async () => {
const onClick = vi.fn((e) => e.preventDefault())
render(<Tag text="Summer" href="/summer" onClick={onClick} />)

const link = screen.getByRole('link')
await userEvent.click(link)

await waitFor(() => {
expect(link.tagName).toBe('A')
expect(onClick).toHaveBeenCalledTimes(1)
})
})

it('should render a leading icon before the text', async () => {
const { container } = render(
<Tag text="Summer" renderIcon={<svg name="calendar" />} />
)
const icon = container.querySelector('svg')
const icon = container.querySelector('svg[name="calendar"]')

expect(icon).toBeInTheDocument()
})

it('should render a close button and fire onDismiss when it is dismissible', async () => {
const onDismiss = vi.fn()
const { container } = render(
<Tag text="Summer" dismissible onDismiss={onDismiss} />
)
const icon = container.querySelector('svg')
expect(icon).toHaveAttribute('name', 'X')

const closeButton = screen.getByRole('button')
await userEvent.click(closeButton)

await waitFor(() => {
expect(onDismiss).toHaveBeenCalledTimes(1)
})
})

it('should only dismiss when the close button is clicked, not the body', async () => {
const onClick = vi.fn()
const onDismiss = vi.fn()
render(
<Tag text="Summer" onClick={onClick} dismissible onDismiss={onDismiss} />
)

// body button (contains the text) and close button (contains the X icon)
const body = screen.getByText('Summer').closest('button')!
await userEvent.click(body)

await waitFor(() => {
expect(onClick).toHaveBeenCalledTimes(1)
expect(onDismiss).not.toHaveBeenCalled()
})
})

it('should not fire onClick or onDismiss when disabled', async () => {
const onClick = vi.fn()
const onDismiss = vi.fn()
const { container } = render(
<Tag
text="Summer"
onClick={onClick}
dismissible
onDismiss={onDismiss}
disabled
/>
)

const buttons = container.querySelectorAll('button')
buttons.forEach((button) => {
fireEvent.click(button, { button: 0, detail: 1 })
})

await waitFor(() => {
expect(onClick).not.toHaveBeenCalled()
expect(onDismiss).not.toHaveBeenCalled()
})
})

it('should move focus to the link body then the close button via Tab', async () => {
render(
<Tag text="Summer" href="/summer" dismissible onDismiss={() => {}} />
)
const link = screen.getByRole('link')
const closeButton = screen.getByRole('button')

await userEvent.tab()
expect(link).toHaveFocus()

await userEvent.tab()
expect(closeButton).toHaveFocus()
})

it('should meet a11y standards', async () => {
const { container } = await render(<Tag text="Summer" />)
const { container } = render(<Tag text="Summer" />)
const axeCheck = await runAxeCheck(container)

expect(axeCheck).toBe(true)
Expand All @@ -89,7 +202,7 @@ describe('<Tag />', async () => {
.spyOn(console, 'error')
.mockImplementation(() => {})

await render(<Tag text="Summer" {...props} />)
render(<Tag text="Summer" {...props} />)
const warning = `Warning: [Tag] prop '${prop}' is not allowed.`

expect(consoleError.mock.calls[0][0]).toBe(warning)
Expand All @@ -100,7 +213,7 @@ describe('<Tag />', async () => {
const props = { [prop]: allowedProps[prop] }
const consoleError = vi.spyOn(console, 'error')

await render(<Tag text="Summer" {...props} />)
render(<Tag text="Summer" {...props} />)

expect(consoleError).not.toHaveBeenCalled()
consoleError.mockRestore()
Expand Down
4 changes: 2 additions & 2 deletions packages/ui-tag/src/Tag/v2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
import { Component } from 'react'

import { XInstUIIcon } from '@instructure/ui-icons'
import { View } from '@instructure/ui-view/latest'
import type { ViewProps } from '@instructure/ui-view/latest'
import { View } from '@instructure/ui-view/v11_7'
import type { ViewProps } from '@instructure/ui-view/v11_7'
import { omitProps } from '@instructure/ui-react-utils'
import { isActiveElement } from '@instructure/ui-dom-utils'
import { withStyleNew } from '@instructure/emotion'
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-tag/src/Tag/v2/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/

import React from 'react'
import type { ViewProps } from '@instructure/ui-view/latest'
import type { ViewProps } from '@instructure/ui-view/v11_7'
import type {
Spacing,
WithStyleProps,
Expand Down
Loading
Loading