diff --git a/packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.module.scss b/packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.module.scss new file mode 100644 index 000000000..e17377d31 --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.module.scss @@ -0,0 +1,114 @@ +@use '../../../../style-dictionary/dist/scss/fixed-variables.scss' as fixed; +@use '../../text-styles.scss'; + +.link-card-medium { + display: flex; + padding: fixed.$spacing-4 fixed.$spacing-5; + align-items: flex-start; + gap: 1rem; + + border-radius: var(--px-border-radius-large); + border: 1px solid var(--px-color-border-subtle); + background: var(--px-color-surface-default); + position: relative; + &:hover { + outline: 1px solid var(--px-color-border-subtle); + h1, + h2, + h3, + h4, + h5, + h6, + span { + @extend .heading-underline; + } + cursor: pointer; + } + &:hover .arrow-wrapper { + padding-inline-start: 8px; + transition: 100ms; + } + &:focus-visible { + outline: 3px solid var(--px-color-border-focus-outline); + outline-offset: 6px; + box-shadow: 0 0 0 3px var(--px-color-border-focus-boxshadow); + } +} + +.link-card-small { + display: flex; + padding: fixed.$spacing-3 fixed.$spacing-4; + align-items: flex-start; + gap: 0.75rem; + + border-radius: var(--px-border-radius-medium); + border: 1px solid var(--px-color-border-subtle); + background: var(--px-color-surface-default); + position: relative; + &:hover { + outline: 1px solid var(--px-color-border-subtle); + h1, + h2, + h3, + h4, + h5, + h6, + span { + @extend .heading-underline; + } + cursor: pointer; + } + &:hover .arrow-wrapper { + padding-inline-start: 8px; + transition: 100ms; + } + &:focus-visible { + outline: 3px solid var(--px-color-border-focus-outline); + outline-offset: 6px; + box-shadow: 0 0 0 3px var(--px-color-border-focus-boxshadow); + } +} +.content-wrapper { + display: inline-flex; + flex-direction: column; + row-gap: 0.25rem; + align-self: center; +} + +.icon-wrapper { + display: flex; + width: 44px; + height: 44px; + padding: 0.625rem; + justify-content: center; + align-items: center; + aspect-ratio: 1/1; + background: var(--px-color-surface-moderate); + + border-radius: var(--px-border-radius-medium); +} + +.heading-wrapper { + align-self: stretch; +} + +.child-wrapper { + display: flex; + align-items: flex-start; + justify-content: flex-start; +} +.arrow-wrapper { + display: flex; + align-items: center; + justify-content: center; + align-self: center; + width: 32px; + height: 32px; + border: none; + background-color: transparent; + margin-left: auto; +} +a { + text-decoration: none; + color: inherit; +} diff --git a/packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.stories.tsx b/packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.stories.tsx new file mode 100644 index 000000000..4d6ad0503 --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.stories.tsx @@ -0,0 +1,101 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { LinkCard, LinkCardProps } from './LinkCard'; + +const meta: Meta = { + component: LinkCard, + title: 'Components/LinkCard', +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + headingText: 'Heading Link card with header', + description: 'This is a medium link card with heading and description.', + href: 'https://www.ssb.no ', + icon: 'Book', + }, + render: (args: LinkCardProps) => , +}; + +export const WithoutHeading: Story = { + args: { + icon: 'Book', + headingText: 'Heading Link card with header', + description: + 'This is a medium link card without heading, but with description.', + href: 'https://www.ssb.no ', + }, + render: (args) => , +}; + +export const WithHeadingMedium: Story = { + args: { + headingText: 'Heading Link card with header level 2', + icon: 'Book', + description: 'This is a medium link card with heading and description.', + headingType: 'h2', + href: 'https://www.ssb.no ', + size: 'medium', + }, + render: (args: LinkCardProps) => , +}; + +export const WithHeadingSmall: Story = { + args: { + headingText: 'Heading Link card with header level 2', + icon: 'Book', + description: 'This is a small link card with heading and description.', + headingType: 'h2', + size: 'small', + href: 'https://www.ssb.no ', + newTab: true, + }, + render: (args: LinkCardProps) => , +}; + +export const WithoutDescriptionSmall: Story = { + args: { + headingText: 'Heading Link card with header level 2', + icon: 'Book', + headingType: 'h2', + size: 'small', + href: 'https://www.ssb.no ', + newTab: true, + }, + render: (args: LinkCardProps) => , +}; + +export const WithSpanHeadingMedium: Story = { + args: { + headingText: 'Heading Link card with header span', + icon: 'Book', + description: 'This is a medium link card with heading and description.', + headingType: 'span', + href: 'https://www.ssb.no ', + size: 'medium', + }, + render: (args: LinkCardProps) => , +}; +export const WithSpanHeadingSmall: Story = { + args: { + headingText: 'Heading Link card with header span', + icon: 'Book', + description: 'This is a small link card with heading and description.', + href: 'https://www.ssb.no ', + headingType: 'span', + size: 'small', + }, + render: (args: LinkCardProps) => , +}; +export const WithoutIcon: Story = { + args: { + headingText: 'Heading Link card without icon', + description: 'This is a small link card with heading and description.', + href: 'https://www.ssb.no ', + headingType: 'span', + size: 'small', + }, + render: (args: LinkCardProps) => , +}; diff --git a/packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.tsx b/packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.tsx new file mode 100644 index 000000000..4e7adb342 --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.tsx @@ -0,0 +1,126 @@ +import cl from 'clsx'; +import React from 'react'; +import styles from './LinkCard.module.scss'; +import BodyLong from '../Typography/BodyLong/BodyLong'; +import BodyShort from '../Typography/BodyShort/BodyShort'; +import { getIconDirection } from '../../util/util'; + +import { Heading, Icon, IconProps } from '@pxweb2/pxweb2-ui'; + +export interface LinkCardProps { + icon?: IconProps['iconName']; + headingText: string; + description?: string; + href: string; + newTab?: boolean; + headingType?: 'span' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; + size?: 'small' | 'medium'; + readonly languageDirection?: 'ltr' | 'rtl'; +} + +export function LinkCard({ + icon, + headingText, + description, + href, + newTab = true, + headingType = 'span', + size = 'medium', + languageDirection = 'ltr', +}: LinkCardProps) { + const iconArrow = getIconDirection( + languageDirection, + 'ArrowRight', + 'ArrowLeft', + ); + + let headingLevel: '2' | '3' | '4' | '5' | '6' | undefined; + switch (headingType) { + case 'h2': + headingLevel = '2'; + break; + case 'h3': + headingLevel = '3'; + break; + case 'h4': + headingLevel = '4'; + break; + case 'h5': + headingLevel = '5'; + break; + case 'h6': + headingLevel = '6'; + break; + default: + headingLevel = undefined; + } + + const headingSize = size === 'small' ? 'xsmall' : 'small'; + + const handleClick = () => { + const link = document.createElement('a'); + link.href = href; + link.rel = 'noopener noreferrer'; + if (newTab) { + link.target = '_blank'; + } + link.click(); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleClick(); + } + }; + + return ( +
+ {icon && ( +
+ +
+ )} +
+ {headingText && + (headingType === 'span' ? ( + + {headingText} + + ) : ( + + {headingText} + + ))} + {description && ( +
+ {size === 'medium' && ( + {description} + )} + {size === 'small' && ( + {description} + )} +
+ )} +
+
+ +
+
+ ); +} diff --git a/packages/pxweb2-ui/src/lib/components/SideSheet/SideSheet.tsx b/packages/pxweb2-ui/src/lib/components/SideSheet/SideSheet.tsx index f3c7a3c0d..f8697f086 100644 --- a/packages/pxweb2-ui/src/lib/components/SideSheet/SideSheet.tsx +++ b/packages/pxweb2-ui/src/lib/components/SideSheet/SideSheet.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react'; import classes from './SideSheet.module.scss'; import Heading from '../Typography/Heading/Heading'; import Button from '../Button/Button'; +import { LinkCard } from '../LinkCard/LinkCard'; export interface SideSheetProps { readonly heading: string; @@ -11,7 +12,7 @@ export interface SideSheetProps { readonly isOpen: boolean; readonly onClose?: () => void; readonly className?: string; - readonly children: React.ReactNode; + // readonly children: React.ReactNode; } export function SideSheet({ @@ -20,7 +21,7 @@ export function SideSheet({ isOpen, onClose, className = '', - children, + // children, }: SideSheetProps) { const cssClasses = className.length > 0 ? ' ' + className : ''; const [isSideSheetOpen, setIsSideSheetOpen] = useState(isOpen); @@ -87,7 +88,15 @@ export function SideSheet({ > -
{children}
+
+ {' '} + +
); diff --git a/packages/pxweb2/src/app/components/TableInformation/TableInformation.spec.tsx b/packages/pxweb2/src/app/components/TableInformation/TableInformation.spec.tsx deleted file mode 100644 index e522a836d..000000000 --- a/packages/pxweb2/src/app/components/TableInformation/TableInformation.spec.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom/vitest'; - -import TableInformation from './TableInformation'; -import classes from './TableInformation.module.scss'; -import { mockHTMLDialogElement } from '@pxweb2/pxweb2-ui/src/lib/util/test-utils'; -import { renderWithProviders } from '../../util/testing-utils'; -import { AppContext } from '../../context/AppProvider'; - -describe('TableInformation', () => { - beforeEach(() => { - mockHTMLDialogElement(); - }); - - it('should render successfully', () => { - const { baseElement } = renderWithProviders( - { - return; - }} - />, - ); - expect(baseElement).toBeTruthy(); - }); - - it('should set the active tab when selectedTab is provided', () => { - renderWithProviders( - { - return; - }} - />, - ); - - const activeTab = screen.getByRole('tab', { selected: true }); - expect(activeTab).toHaveAttribute('id', 'tab-details'); - }); - - it('should render the correct tab panel content', () => { - renderWithProviders( - { - return; - }} - />, - ); - - const tabPanel = screen.getByText( - 'presentation_page.main_content.about_table.definitions.title', - ); - expect(tabPanel).toBeInTheDocument(); - }); - - it('should reset scroll position when activeTab changes', () => { - const { container } = renderWithProviders( - { - return; - }} - />, - ); - - const tabsContent = container.querySelector( - `.${classes.tabsContent}`, - ) as HTMLElement; - expect(tabsContent.scrollTop).toBe(0); - tabsContent.scrollTop = 100; - expect(tabsContent.scrollTop).toBe(100); - - const contactTab = screen.getByRole('tab', { - name: 'presentation_page.main_content.about_table.contact.title', - }); - fireEvent.click(contactTab); - - expect(tabsContent.scrollTop).toBe(0); - }); - - it('renders the BottomSheet when isMobile is true', () => { - // Mock the AppContext to simulate isMobile = true - const mockAppContextValue = { - isInitialized: true, - isTablet: false, - isMobile: true, - skipToMainFocused: false, - setSkipToMainFocused: vi.fn(), - }; - - const { container } = renderWithProviders( - - { - return; - }} - selectedTab="tab-footnotes" - /> - , - ); - - // Ensure BottomSheet is rendered - const tabsContent = container.querySelector( - `.${classes.tabsContent}`, - ) as HTMLElement; - expect(tabsContent).toHaveClass(classes.mobileView); - }); - - it('renders the SideSheet when isMobile is false', () => { - // Mock the AppContext to simulate isMobile = false - const mockAppContextValue = { - isInitialized: true, - isTablet: false, - isMobile: false, - skipToMainFocused: false, - setSkipToMainFocused: vi.fn(), - }; - - const { container } = renderWithProviders( - - { - return; - }} - selectedTab="tab-footnotes" - /> - , - ); - - // Ensure SideSheet is rendered - const tabsContent = container.querySelector( - `.${classes.tabsContent}`, - ) as HTMLElement; - expect(tabsContent).not.toHaveClass(classes.mobileView); - }); -});