Compare commits
2 Commits
master
...
fix/3727-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
089bbe7c4f | ||
|
|
c62999879f |
@@ -1,13 +1,13 @@
|
|||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
|
import { LinkChips } from "@scandic-hotels/design-system/LinkChips"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||||
|
|
||||||
import { getCampaignOverviewPage } from "@/lib/trpc/memoizedRequests"
|
import { getCampaignOverviewPage } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import { TopCampaign } from "@/components/ContentType/CampaignOverviewPage/TopCampaign"
|
import { TopCampaign } from "@/components/ContentType/CampaignOverviewPage/TopCampaign"
|
||||||
import LinkChips from "@/components/TempDesignSystem/LinkChips"
|
|
||||||
|
|
||||||
import Blocks from "./Blocks"
|
import Blocks from "./Blocks"
|
||||||
import CampaignOverviewPageSkeleton from "./CampaignOverviewPageSkeleton"
|
import CampaignOverviewPageSkeleton from "./CampaignOverviewPageSkeleton"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "@scandic-hotels/design-system/Breadcrumbs"
|
} from "@scandic-hotels/design-system/Breadcrumbs"
|
||||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||||
import Image from "@scandic-hotels/design-system/Image"
|
import Image from "@scandic-hotels/design-system/Image"
|
||||||
|
import { LinkChips } from "@scandic-hotels/design-system/LinkChips"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ import { Breadcrumbs } from "@/components/Breadcrumbs"
|
|||||||
import HeaderDynamicContent from "@/components/Headers/DynamicContent"
|
import HeaderDynamicContent from "@/components/Headers/DynamicContent"
|
||||||
import { HeroVideo } from "@/components/HeroVideo"
|
import { HeroVideo } from "@/components/HeroVideo"
|
||||||
import MeetingPackageWidget from "@/components/MeetingPackageWidget"
|
import MeetingPackageWidget from "@/components/MeetingPackageWidget"
|
||||||
import LinkChips from "@/components/TempDesignSystem/LinkChips"
|
|
||||||
|
|
||||||
import styles from "./collectionPage.module.css"
|
import styles from "./collectionPage.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Suspense } from "react"
|
|||||||
|
|
||||||
import { BreadcrumbsSkeleton } from "@scandic-hotels/design-system/Breadcrumbs"
|
import { BreadcrumbsSkeleton } from "@scandic-hotels/design-system/Breadcrumbs"
|
||||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||||
|
import { LinkChips } from "@scandic-hotels/design-system/LinkChips"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||||
|
|
||||||
@@ -16,7 +17,6 @@ import { HeroVideo } from "@/components/HeroVideo"
|
|||||||
import Sidebar from "@/components/Sidebar"
|
import Sidebar from "@/components/Sidebar"
|
||||||
import SidebarSkeleton from "@/components/Sidebar/SidebarSkeleton"
|
import SidebarSkeleton from "@/components/Sidebar/SidebarSkeleton"
|
||||||
import StickyMeetingPackageWidget from "@/components/StickyMeetingPackageWidget"
|
import StickyMeetingPackageWidget from "@/components/StickyMeetingPackageWidget"
|
||||||
import LinkChips from "@/components/TempDesignSystem/LinkChips"
|
|
||||||
|
|
||||||
import styles from "./contentPage.module.css"
|
import styles from "./contentPage.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { ChipLink } from "@scandic-hotels/design-system/ChipLink"
|
|
||||||
import { Chips } from "@scandic-hotels/design-system/Chips"
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
||||||
|
|
||||||
interface LinkChipsProps {
|
|
||||||
chips: {
|
|
||||||
url: string
|
|
||||||
title: string
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LinkChips({ chips }: LinkChipsProps) {
|
|
||||||
if (!chips.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chips>
|
|
||||||
{chips.map(({ title, url }) => (
|
|
||||||
<ChipLink key={`${title}-${url}`} href={url}>
|
|
||||||
{title}
|
|
||||||
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
|
|
||||||
</ChipLink>
|
|
||||||
))}
|
|
||||||
</Chips>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||||
|
import { describe, expect, it, vi, afterEach } from "vitest"
|
||||||
|
import { render, screen, cleanup } from "@testing-library/react"
|
||||||
|
import userEvent from "@testing-library/user-event"
|
||||||
|
|
||||||
|
import { ChipButton } from "./ChipButton"
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ChipButton accessibility", () => {
|
||||||
|
describe("semantic HTML", () => {
|
||||||
|
it("uses proper button element", () => {
|
||||||
|
render(<ChipButton>Button</ChipButton>)
|
||||||
|
const button = screen.getByRole("button")
|
||||||
|
expect(button.tagName).toBe("BUTTON")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("has accessible button text", () => {
|
||||||
|
render(<ChipButton>Filter by price</ChipButton>)
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Filter by price" })
|
||||||
|
).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("button type defaults to button (not submit)", () => {
|
||||||
|
render(<ChipButton>Not Submit</ChipButton>)
|
||||||
|
const button = screen.getByRole("button")
|
||||||
|
// React Aria Components Button defaults to type="button"
|
||||||
|
expect(button.getAttribute("type")).toBe("button")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("disabled state", () => {
|
||||||
|
it("disabled button has disabled attribute", () => {
|
||||||
|
render(<ChipButton isDisabled>Disabled</ChipButton>)
|
||||||
|
const button = screen.getByRole("button", { name: "Disabled" })
|
||||||
|
expect(button).toHaveProperty("disabled", true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("disabled button is not focusable", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(
|
||||||
|
<div>
|
||||||
|
<ChipButton isDisabled>Disabled</ChipButton>
|
||||||
|
<ChipButton>Enabled</ChipButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
// Focus should skip disabled button and go to enabled one
|
||||||
|
expect(document.activeElement).toBe(
|
||||||
|
screen.getByRole("button", { name: "Enabled" })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("keyboard navigation", () => {
|
||||||
|
it("button is keyboard accessible", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ChipButton>Accessible Button</ChipButton>)
|
||||||
|
|
||||||
|
const button = screen.getByRole("button")
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(button)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("multiple buttons maintain logical focus order", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(
|
||||||
|
<div>
|
||||||
|
<ChipButton>First</ChipButton>
|
||||||
|
<ChipButton>Second</ChipButton>
|
||||||
|
<ChipButton>Third</ChipButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const firstButton = screen.getByRole("button", { name: "First" })
|
||||||
|
const secondButton = screen.getByRole("button", { name: "Second" })
|
||||||
|
const thirdButton = screen.getByRole("button", { name: "Third" })
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(firstButton)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(secondButton)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(thirdButton)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Enter key activates button", async () => {
|
||||||
|
const onPress = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ChipButton onPress={onPress}>Activate</ChipButton>)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
await user.keyboard("{Enter}")
|
||||||
|
expect(onPress).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Space key activates button", async () => {
|
||||||
|
const onPress = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ChipButton onPress={onPress}>Activate</ChipButton>)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
await user.keyboard(" ")
|
||||||
|
expect(onPress).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("screen reader support", () => {
|
||||||
|
it("button has accessible name from content", () => {
|
||||||
|
render(<ChipButton>Descriptive Button Text</ChipButton>)
|
||||||
|
const button = screen.getByRole("button")
|
||||||
|
expect(button.textContent?.trim().length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("button with icon and text has accessible name", () => {
|
||||||
|
render(
|
||||||
|
<ChipButton>
|
||||||
|
<span aria-hidden="true">✓</span>
|
||||||
|
Selected Filter
|
||||||
|
</ChipButton>
|
||||||
|
)
|
||||||
|
const button = screen.getByRole("button")
|
||||||
|
expect(button.textContent).toContain("Selected Filter")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("selected state accessibility", () => {
|
||||||
|
it("selected state is indicated via aria-pressed", () => {
|
||||||
|
render(
|
||||||
|
<ChipButton selected aria-pressed={true}>
|
||||||
|
Selected
|
||||||
|
</ChipButton>
|
||||||
|
)
|
||||||
|
const button = screen.getByRole("button", { name: "Selected" })
|
||||||
|
expect(button.getAttribute("aria-pressed")).toBe("true")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||||
|
import { describe, expect, it, vi, afterEach } from "vitest"
|
||||||
|
import { render, screen, cleanup, fireEvent } from "@testing-library/react"
|
||||||
|
import userEvent from "@testing-library/user-event"
|
||||||
|
|
||||||
|
import { ChipButton } from "./ChipButton"
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ChipButton", () => {
|
||||||
|
describe("rendering", () => {
|
||||||
|
it("renders as a button element", () => {
|
||||||
|
render(<ChipButton>Click me</ChipButton>)
|
||||||
|
const button = screen.getByRole("button", { name: "Click me" })
|
||||||
|
expect(button).toBeTruthy()
|
||||||
|
expect(button.tagName).toBe("BUTTON")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders children content", () => {
|
||||||
|
render(<ChipButton>Button Content</ChipButton>)
|
||||||
|
expect(screen.getByText("Button Content")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders with multiple children", () => {
|
||||||
|
render(
|
||||||
|
<ChipButton>
|
||||||
|
<span>Icon</span>
|
||||||
|
Label
|
||||||
|
</ChipButton>
|
||||||
|
)
|
||||||
|
expect(screen.getByText("Icon")).toBeTruthy()
|
||||||
|
expect(screen.getByText("Label")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("variants", () => {
|
||||||
|
it("renders Default variant", () => {
|
||||||
|
render(<ChipButton variant="Default">Default</ChipButton>)
|
||||||
|
expect(screen.getByRole("button", { name: "Default" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders Outlined variant", () => {
|
||||||
|
render(<ChipButton variant="Outlined">Outlined</ChipButton>)
|
||||||
|
expect(screen.getByRole("button", { name: "Outlined" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders FilterRounded variant", () => {
|
||||||
|
render(<ChipButton variant="FilterRounded">Filter</ChipButton>)
|
||||||
|
expect(screen.getByRole("button", { name: "Filter" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("selected state", () => {
|
||||||
|
it("renders with selected=false by default", () => {
|
||||||
|
render(<ChipButton>Not Selected</ChipButton>)
|
||||||
|
expect(screen.getByRole("button", { name: "Not Selected" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders with selected=true", () => {
|
||||||
|
render(<ChipButton selected>Selected</ChipButton>)
|
||||||
|
expect(screen.getByRole("button", { name: "Selected" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("sizes", () => {
|
||||||
|
it("renders Medium size", () => {
|
||||||
|
render(<ChipButton size="Medium">Medium</ChipButton>)
|
||||||
|
expect(screen.getByRole("button", { name: "Medium" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders Large size (default)", () => {
|
||||||
|
render(<ChipButton size="Large">Large</ChipButton>)
|
||||||
|
expect(screen.getByRole("button", { name: "Large" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("props", () => {
|
||||||
|
it("applies custom className", () => {
|
||||||
|
render(<ChipButton className="custom-class">Button</ChipButton>)
|
||||||
|
const button = screen.getByRole("button", { name: "Button" })
|
||||||
|
expect(button.className).toContain("custom-class")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can be disabled", () => {
|
||||||
|
render(<ChipButton isDisabled>Disabled</ChipButton>)
|
||||||
|
const button = screen.getByRole("button", { name: "Disabled" })
|
||||||
|
expect(button).toHaveProperty("disabled", true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("interactions", () => {
|
||||||
|
it("calls onPress when clicked", () => {
|
||||||
|
const onPress = vi.fn()
|
||||||
|
render(<ChipButton onPress={onPress}>Click me</ChipButton>)
|
||||||
|
|
||||||
|
// React Aria Components Button uses onPress which listens to click events
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Click me" }))
|
||||||
|
expect(onPress).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not call onPress when disabled", () => {
|
||||||
|
const onPress = vi.fn()
|
||||||
|
render(
|
||||||
|
<ChipButton isDisabled onPress={onPress}>
|
||||||
|
Disabled
|
||||||
|
</ChipButton>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Disabled" }))
|
||||||
|
expect(onPress).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("keyboard navigation", () => {
|
||||||
|
it("is focusable via keyboard", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ChipButton>Focusable</ChipButton>)
|
||||||
|
|
||||||
|
const button = screen.getByRole("button", { name: "Focusable" })
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(button)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can be activated with Enter key", async () => {
|
||||||
|
const onPress = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ChipButton onPress={onPress}>Press Enter</ChipButton>)
|
||||||
|
|
||||||
|
screen.getByRole("button", { name: "Press Enter" })
|
||||||
|
await user.tab()
|
||||||
|
await user.keyboard("{Enter}")
|
||||||
|
expect(onPress).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can be activated with Space key", async () => {
|
||||||
|
const onPress = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ChipButton onPress={onPress}>Press Space</ChipButton>)
|
||||||
|
|
||||||
|
screen.getByRole("button", { name: "Press Space" })
|
||||||
|
await user.tab()
|
||||||
|
await user.keyboard(" ")
|
||||||
|
expect(onPress).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||||
|
import { describe, expect, it, afterEach } from "vitest"
|
||||||
|
import { render, screen, cleanup } from "@testing-library/react"
|
||||||
|
import userEvent from "@testing-library/user-event"
|
||||||
|
|
||||||
|
import { ChipLink } from "./ChipLink"
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ChipLink accessibility", () => {
|
||||||
|
describe("semantic HTML", () => {
|
||||||
|
it("uses proper link element for navigation", () => {
|
||||||
|
render(<ChipLink href="/test">Test Link</ChipLink>)
|
||||||
|
const link = screen.getByRole("link")
|
||||||
|
expect(link.tagName).toBe("A")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("has accessible link text", () => {
|
||||||
|
render(<ChipLink href="/hotels">View Hotels</ChipLink>)
|
||||||
|
expect(screen.getByRole("link", { name: "View Hotels" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("link has descriptive href attribute", () => {
|
||||||
|
render(<ChipLink href="/hotels/stockholm">Stockholm Hotels</ChipLink>)
|
||||||
|
const link = screen.getByRole("link", { name: "Stockholm Hotels" })
|
||||||
|
expect(link.getAttribute("href")).toBe("/hotels/stockholm")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("keyboard navigation", () => {
|
||||||
|
it("link is keyboard accessible", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ChipLink href="/test">Accessible Link</ChipLink>)
|
||||||
|
|
||||||
|
const link = screen.getByRole("link")
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(link)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("multiple links maintain logical focus order", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(
|
||||||
|
<div>
|
||||||
|
<ChipLink href="/first">First Link</ChipLink>
|
||||||
|
<ChipLink href="/second">Second Link</ChipLink>
|
||||||
|
<ChipLink href="/third">Third Link</ChipLink>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const firstLink = screen.getByRole("link", { name: "First Link" })
|
||||||
|
const secondLink = screen.getByRole("link", { name: "Second Link" })
|
||||||
|
const thirdLink = screen.getByRole("link", { name: "Third Link" })
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(firstLink)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(secondLink)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(thirdLink)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("screen reader support", () => {
|
||||||
|
it("link has accessible name from content", () => {
|
||||||
|
render(<ChipLink href="/test">Descriptive Link Text</ChipLink>)
|
||||||
|
const link = screen.getByRole("link")
|
||||||
|
expect(link.textContent?.trim().length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("link with icon and text has accessible name", () => {
|
||||||
|
render(
|
||||||
|
<ChipLink href="/test">
|
||||||
|
<span aria-hidden="true">→</span>
|
||||||
|
Next Page
|
||||||
|
</ChipLink>
|
||||||
|
)
|
||||||
|
const link = screen.getByRole("link")
|
||||||
|
expect(link.textContent).toContain("Next Page")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||||
|
import { describe, expect, it, afterEach } from "vitest"
|
||||||
|
import { render, screen, cleanup } from "@testing-library/react"
|
||||||
|
import userEvent from "@testing-library/user-event"
|
||||||
|
|
||||||
|
import { ChipLink } from "./ChipLink"
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ChipLink", () => {
|
||||||
|
describe("rendering", () => {
|
||||||
|
it("renders as a link element", () => {
|
||||||
|
render(<ChipLink href="/test">Test Link</ChipLink>)
|
||||||
|
const link = screen.getByRole("link", { name: "Test Link" })
|
||||||
|
expect(link).toBeTruthy()
|
||||||
|
expect(link.tagName).toBe("A")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders children content", () => {
|
||||||
|
render(<ChipLink href="/test">Link Content</ChipLink>)
|
||||||
|
expect(screen.getByText("Link Content")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("applies correct href attribute", () => {
|
||||||
|
render(<ChipLink href="/destination">Go somewhere</ChipLink>)
|
||||||
|
const link = screen.getByRole("link", { name: "Go somewhere" })
|
||||||
|
expect(link.getAttribute("href")).toBe("/destination")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders with multiple children", () => {
|
||||||
|
render(
|
||||||
|
<ChipLink href="/test">
|
||||||
|
<span>Icon</span>
|
||||||
|
Text
|
||||||
|
</ChipLink>
|
||||||
|
)
|
||||||
|
expect(screen.getByText("Icon")).toBeTruthy()
|
||||||
|
expect(screen.getByText("Text")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("props", () => {
|
||||||
|
it("applies custom className", () => {
|
||||||
|
render(
|
||||||
|
<ChipLink href="/test" className="custom-class">
|
||||||
|
Link
|
||||||
|
</ChipLink>
|
||||||
|
)
|
||||||
|
const link = screen.getByRole("link", { name: "Link" })
|
||||||
|
expect(link.className).toContain("custom-class")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("supports target attribute", () => {
|
||||||
|
render(
|
||||||
|
<ChipLink href="/external" target="_blank">
|
||||||
|
External Link
|
||||||
|
</ChipLink>
|
||||||
|
)
|
||||||
|
const link = screen.getByRole("link", { name: "External Link" })
|
||||||
|
expect(link.getAttribute("target")).toBe("_blank")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("supports rel attribute", () => {
|
||||||
|
render(
|
||||||
|
<ChipLink href="/external" rel="noopener noreferrer">
|
||||||
|
External Link
|
||||||
|
</ChipLink>
|
||||||
|
)
|
||||||
|
const link = screen.getByRole("link", { name: "External Link" })
|
||||||
|
expect(link.getAttribute("rel")).toBe("noopener noreferrer")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("keyboard navigation", () => {
|
||||||
|
it("is focusable via keyboard", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ChipLink href="/test">Focusable Link</ChipLink>)
|
||||||
|
|
||||||
|
const link = screen.getByRole("link", { name: "Focusable Link" })
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(link)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||||
|
import { describe, expect, it, afterEach } from "vitest"
|
||||||
|
import { render, screen, cleanup } from "@testing-library/react"
|
||||||
|
import userEvent from "@testing-library/user-event"
|
||||||
|
|
||||||
|
import { ChipStatic } from "./ChipStatic"
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ChipStatic accessibility", () => {
|
||||||
|
describe("semantic HTML", () => {
|
||||||
|
it("uses span element (non-interactive)", () => {
|
||||||
|
render(<ChipStatic>Static Label</ChipStatic>)
|
||||||
|
const chip = screen.getByText("Static Label")
|
||||||
|
expect(chip.tagName).toBe("SPAN")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("is not a button or link", () => {
|
||||||
|
render(<ChipStatic>Not Interactive</ChipStatic>)
|
||||||
|
expect(screen.queryByRole("button")).toBeNull()
|
||||||
|
expect(screen.queryByRole("link")).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("content is visible and readable", () => {
|
||||||
|
render(<ChipStatic>Readable Content</ChipStatic>)
|
||||||
|
expect(screen.getByText("Readable Content")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("non-interactive behavior", () => {
|
||||||
|
it("is not focusable by default", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(
|
||||||
|
<div>
|
||||||
|
<ChipStatic>Static Chip</ChipStatic>
|
||||||
|
<button>Focusable Button</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
// Focus should skip the static chip and go directly to the button
|
||||||
|
expect(document.activeElement).toBe(
|
||||||
|
screen.getByRole("button", { name: "Focusable Button" })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not receive focus when tabbing through page", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(
|
||||||
|
<div>
|
||||||
|
<button>First</button>
|
||||||
|
<ChipStatic>Static</ChipStatic>
|
||||||
|
<button>Second</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const firstButton = screen.getByRole("button", { name: "First" })
|
||||||
|
const secondButton = screen.getByRole("button", { name: "Second" })
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(firstButton)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(secondButton)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("screen reader support", () => {
|
||||||
|
it("has visible text content", () => {
|
||||||
|
render(<ChipStatic>Screen Reader Text</ChipStatic>)
|
||||||
|
const chip = screen.getByText("Screen Reader Text")
|
||||||
|
expect(chip.textContent?.trim().length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("text content is accessible in the DOM", () => {
|
||||||
|
render(<ChipStatic>Status: Active</ChipStatic>)
|
||||||
|
expect(screen.getByText("Status: Active")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("color contrast considerations", () => {
|
||||||
|
it("Neutral color variant renders", () => {
|
||||||
|
render(<ChipStatic color="Neutral">Neutral</ChipStatic>)
|
||||||
|
expect(screen.getByText("Neutral")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Subtle color variant renders", () => {
|
||||||
|
render(<ChipStatic color="Subtle">Subtle</ChipStatic>)
|
||||||
|
expect(screen.getByText("Subtle")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Disabled color variant renders", () => {
|
||||||
|
render(<ChipStatic color="Disabled">Disabled</ChipStatic>)
|
||||||
|
expect(screen.getByText("Disabled")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("text sizing", () => {
|
||||||
|
it("xs size renders readable text", () => {
|
||||||
|
render(<ChipStatic size="xs">Extra Small</ChipStatic>)
|
||||||
|
expect(screen.getByText("Extra Small")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sm size renders readable text", () => {
|
||||||
|
render(<ChipStatic size="sm">Small</ChipStatic>)
|
||||||
|
expect(screen.getByText("Small")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("lg size renders readable text", () => {
|
||||||
|
render(<ChipStatic size="lg">Large</ChipStatic>)
|
||||||
|
expect(screen.getByText("Large")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||||
|
import { describe, expect, it, afterEach } from "vitest"
|
||||||
|
import { render, screen, cleanup } from "@testing-library/react"
|
||||||
|
|
||||||
|
import { ChipStatic } from "./ChipStatic"
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ChipStatic", () => {
|
||||||
|
describe("rendering", () => {
|
||||||
|
it("renders as a span element", () => {
|
||||||
|
render(<ChipStatic>Static Chip</ChipStatic>)
|
||||||
|
const chip = screen.getByText("Static Chip")
|
||||||
|
expect(chip).toBeTruthy()
|
||||||
|
expect(chip.tagName).toBe("SPAN")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders children content", () => {
|
||||||
|
render(<ChipStatic>Chip Content</ChipStatic>)
|
||||||
|
expect(screen.getByText("Chip Content")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders with multiple children", () => {
|
||||||
|
render(
|
||||||
|
<ChipStatic>
|
||||||
|
<span>Icon</span>
|
||||||
|
Label
|
||||||
|
</ChipStatic>
|
||||||
|
)
|
||||||
|
expect(screen.getByText("Icon")).toBeTruthy()
|
||||||
|
expect(screen.getByText("Label")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("color variants", () => {
|
||||||
|
it("renders Neutral color (default)", () => {
|
||||||
|
render(<ChipStatic color="Neutral">Neutral</ChipStatic>)
|
||||||
|
expect(screen.getByText("Neutral")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders Subtle color", () => {
|
||||||
|
render(<ChipStatic color="Subtle">Subtle</ChipStatic>)
|
||||||
|
expect(screen.getByText("Subtle")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders Disabled color", () => {
|
||||||
|
render(<ChipStatic color="Disabled">Disabled</ChipStatic>)
|
||||||
|
expect(screen.getByText("Disabled")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("sizes", () => {
|
||||||
|
it("renders xs size", () => {
|
||||||
|
render(<ChipStatic size="xs">Extra Small</ChipStatic>)
|
||||||
|
expect(screen.getByText("Extra Small")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders sm size (default)", () => {
|
||||||
|
render(<ChipStatic size="sm">Small</ChipStatic>)
|
||||||
|
expect(screen.getByText("Small")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders lg size", () => {
|
||||||
|
render(<ChipStatic size="lg">Large</ChipStatic>)
|
||||||
|
expect(screen.getByText("Large")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("typography", () => {
|
||||||
|
it("uses uppercase typography by default", () => {
|
||||||
|
render(<ChipStatic>Default Case</ChipStatic>)
|
||||||
|
expect(screen.getByText("Default Case")).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uses lowercase typography when lowerCase is true", () => {
|
||||||
|
render(<ChipStatic lowerCase>Lower Case</ChipStatic>)
|
||||||
|
expect(screen.getByText("Lower Case")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("props", () => {
|
||||||
|
it("applies custom className", () => {
|
||||||
|
render(<ChipStatic className="custom-class">Styled</ChipStatic>)
|
||||||
|
const chip = screen.getByText("Styled")
|
||||||
|
expect(chip.className).toContain("custom-class")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("edge cases", () => {
|
||||||
|
it("handles empty string children", () => {
|
||||||
|
const emptyString = ""
|
||||||
|
const { container } = render(<ChipStatic>{emptyString}</ChipStatic>)
|
||||||
|
const span = container.querySelector("span")
|
||||||
|
expect(span).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles numeric children", () => {
|
||||||
|
render(<ChipStatic>{42}</ChipStatic>)
|
||||||
|
expect(screen.getByText("42")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { describe, expect, it, afterEach } from "vitest"
|
||||||
|
import { render, screen, cleanup } from "@testing-library/react"
|
||||||
|
import userEvent from "@testing-library/user-event"
|
||||||
|
|
||||||
|
import { LinkChips } from "./LinkChips"
|
||||||
|
import type { LinkChipsProps } from "./types"
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultChips: LinkChipsProps["chips"] = [
|
||||||
|
{ title: "Hotels in Stockholm", url: "/hotels/stockholm" },
|
||||||
|
{ title: "Hotels in Copenhagen", url: "/hotels/copenhagen" },
|
||||||
|
{ title: "Hotels in Oslo", url: "/hotels/oslo" },
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("LinkChips accessibility", () => {
|
||||||
|
describe("semantic HTML", () => {
|
||||||
|
it("uses proper link elements for navigation", () => {
|
||||||
|
render(<LinkChips chips={defaultChips} />)
|
||||||
|
const links = screen.getAllByRole("link")
|
||||||
|
expect(links.length).toBe(3)
|
||||||
|
links.forEach((link) => {
|
||||||
|
expect(link.tagName).toBe("A")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("has accessible link text", () => {
|
||||||
|
render(<LinkChips chips={defaultChips} />)
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "Hotels in Stockholm" })
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "Hotels in Copenhagen" })
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(screen.getByRole("link", { name: "Hotels in Oslo" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("links have descriptive href attributes", () => {
|
||||||
|
render(<LinkChips chips={defaultChips} />)
|
||||||
|
const stockholmLink = screen.getByRole("link", {
|
||||||
|
name: "Hotels in Stockholm",
|
||||||
|
})
|
||||||
|
expect(stockholmLink.getAttribute("href")).toBe("/hotels/stockholm")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("keyboard navigation", () => {
|
||||||
|
it("all links are keyboard accessible", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<LinkChips chips={defaultChips} />)
|
||||||
|
|
||||||
|
const links = screen.getAllByRole("link")
|
||||||
|
expect(links.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
// Tab through all links
|
||||||
|
for (const link of links) {
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(link)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("maintains logical focus order", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<LinkChips chips={defaultChips} />)
|
||||||
|
|
||||||
|
const firstLink = screen.getByRole("link", {
|
||||||
|
name: "Hotels in Stockholm",
|
||||||
|
})
|
||||||
|
const secondLink = screen.getByRole("link", {
|
||||||
|
name: "Hotels in Copenhagen",
|
||||||
|
})
|
||||||
|
const thirdLink = screen.getByRole("link", { name: "Hotels in Oslo" })
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(firstLink)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(secondLink)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(thirdLink)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("screen reader support", () => {
|
||||||
|
it("links have accessible names", () => {
|
||||||
|
render(<LinkChips chips={defaultChips} />)
|
||||||
|
const links = screen.getAllByRole("link")
|
||||||
|
links.forEach((link) => {
|
||||||
|
// Check that link has text content (accessible name)
|
||||||
|
expect(link.textContent?.trim().length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("empty state", () => {
|
||||||
|
it("does not render anything when chips array is empty", () => {
|
||||||
|
const { container } = render(<LinkChips chips={[]} />)
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
expect(screen.queryAllByRole("link")).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||||
|
|
||||||
|
import { LinkChips } from "./LinkChips"
|
||||||
|
|
||||||
|
const meta: Meta<typeof LinkChips> = {
|
||||||
|
title: "Product Components/LinkChips",
|
||||||
|
component: LinkChips,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof LinkChips>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
chips: [
|
||||||
|
{ title: "Hotels in Stockholm", url: "/hotels/stockholm" },
|
||||||
|
{ title: "Hotels in Copenhagen", url: "/hotels/copenhagen" },
|
||||||
|
{ title: "Hotels in Oslo", url: "/hotels/oslo" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SingleChip: Story = {
|
||||||
|
args: {
|
||||||
|
chips: [{ title: "View all hotels", url: "/hotels" }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ManyChips: Story = {
|
||||||
|
args: {
|
||||||
|
chips: [
|
||||||
|
{ title: "Stockholm", url: "/hotels/stockholm" },
|
||||||
|
{ title: "Copenhagen", url: "/hotels/copenhagen" },
|
||||||
|
{ title: "Oslo", url: "/hotels/oslo" },
|
||||||
|
{ title: "Helsinki", url: "/hotels/helsinki" },
|
||||||
|
{ title: "Gothenburg", url: "/hotels/gothenburg" },
|
||||||
|
{ title: "Malmö", url: "/hotels/malmo" },
|
||||||
|
{ title: "Bergen", url: "/hotels/bergen" },
|
||||||
|
{ title: "Tampere", url: "/hotels/tampere" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { describe, expect, it, afterEach } from "vitest"
|
||||||
|
import { render, screen, cleanup } from "@testing-library/react"
|
||||||
|
import userEvent from "@testing-library/user-event"
|
||||||
|
|
||||||
|
import { LinkChips } from "./LinkChips"
|
||||||
|
import type { LinkChipsProps } from "./types"
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultChips: LinkChipsProps["chips"] = [
|
||||||
|
{ title: "Hotels in Stockholm", url: "/hotels/stockholm" },
|
||||||
|
{ title: "Hotels in Copenhagen", url: "/hotels/copenhagen" },
|
||||||
|
{ title: "Hotels in Oslo", url: "/hotels/oslo" },
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("LinkChips", () => {
|
||||||
|
describe("rendering", () => {
|
||||||
|
it("renders all chip links", () => {
|
||||||
|
render(<LinkChips chips={defaultChips} />)
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "Hotels in Stockholm" })
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "Hotels in Copenhagen" })
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(screen.getByRole("link", { name: "Hotels in Oslo" })).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders chip links with correct href attributes", () => {
|
||||||
|
render(<LinkChips chips={defaultChips} />)
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "Hotels in Stockholm" }).getAttribute("href")
|
||||||
|
).toBe("/hotels/stockholm")
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "Hotels in Copenhagen" }).getAttribute("href")
|
||||||
|
).toBe("/hotels/copenhagen")
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "Hotels in Oslo" }).getAttribute("href")
|
||||||
|
).toBe("/hotels/oslo")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns null when chips array is empty", () => {
|
||||||
|
const { container } = render(<LinkChips chips={[]} />)
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles single chip", () => {
|
||||||
|
const singleChip = [{ title: "View all hotels", url: "/hotels" }]
|
||||||
|
render(<LinkChips chips={singleChip} />)
|
||||||
|
expect(screen.getByRole("link", { name: "View all hotels" })).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "View all hotels" }).getAttribute("href")
|
||||||
|
).toBe("/hotels")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("keyboard navigation", () => {
|
||||||
|
it("allows keyboard navigation between links", async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<LinkChips chips={defaultChips} />)
|
||||||
|
|
||||||
|
const firstLink = screen.getByRole("link", {
|
||||||
|
name: "Hotels in Stockholm",
|
||||||
|
})
|
||||||
|
const secondLink = screen.getByRole("link", {
|
||||||
|
name: "Hotels in Copenhagen",
|
||||||
|
})
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(firstLink)
|
||||||
|
|
||||||
|
await user.tab()
|
||||||
|
expect(document.activeElement).toBe(secondLink)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("edge cases", () => {
|
||||||
|
it("handles chips with duplicate titles but different URLs", () => {
|
||||||
|
const duplicateTitles: LinkChipsProps["chips"] = [
|
||||||
|
{ title: "Hotels", url: "/hotels/stockholm" },
|
||||||
|
{ title: "Hotels", url: "/hotels/copenhagen" },
|
||||||
|
]
|
||||||
|
render(<LinkChips chips={duplicateTitles} />)
|
||||||
|
const links = screen.getAllByRole("link", { name: "Hotels" })
|
||||||
|
expect(links).toHaveLength(2)
|
||||||
|
expect(links[0].getAttribute("href")).toBe("/hotels/stockholm")
|
||||||
|
expect(links[1].getAttribute("href")).toBe("/hotels/copenhagen")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { ChipLink } from "../ChipLink"
|
||||||
|
import { Chips } from "../Chips"
|
||||||
|
import { MaterialIcon } from "../Icons/MaterialIcon"
|
||||||
|
import type { LinkChipsProps } from "./types"
|
||||||
|
|
||||||
|
export function LinkChips({ chips }: LinkChipsProps) {
|
||||||
|
if (!chips.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chips>
|
||||||
|
{chips.map(({ title, url }) => (
|
||||||
|
<ChipLink key={`${title}-${url}`} href={url}>
|
||||||
|
{title}
|
||||||
|
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
|
||||||
|
</ChipLink>
|
||||||
|
))}
|
||||||
|
</Chips>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { LinkChips } from "./LinkChips"
|
||||||
|
export type { LinkChipsProps } from "./types"
|
||||||
6
packages/design-system/lib/components/LinkChips/types.ts
Normal file
6
packages/design-system/lib/components/LinkChips/types.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface LinkChipsProps {
|
||||||
|
chips: {
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"./ChipLink": "./lib/components/ChipLink/index.tsx",
|
"./ChipLink": "./lib/components/ChipLink/index.tsx",
|
||||||
"./Chips": "./lib/components/Chips/index.tsx",
|
"./Chips": "./lib/components/Chips/index.tsx",
|
||||||
"./ChipStatic": "./lib/components/ChipStatic/index.tsx",
|
"./ChipStatic": "./lib/components/ChipStatic/index.tsx",
|
||||||
|
"./LinkChips": "./lib/components/LinkChips/index.tsx",
|
||||||
"./CodeRateCard": "./lib/components/RateCard/Code/index.tsx",
|
"./CodeRateCard": "./lib/components/RateCard/Code/index.tsx",
|
||||||
"./ContentCard": "./lib/components/ContentCard/index.tsx",
|
"./ContentCard": "./lib/components/ContentCard/index.tsx",
|
||||||
"./DeprecatedSelect": "./lib/components/DeprecatedSelect/index.tsx",
|
"./DeprecatedSelect": "./lib/components/DeprecatedSelect/index.tsx",
|
||||||
|
|||||||
Reference in New Issue
Block a user