diff --git a/packages/web-react/src/components/Navigation/Navigation.tsx b/packages/web-react/src/components/Navigation/Navigation.tsx new file mode 100644 index 0000000000..b27049daec --- /dev/null +++ b/packages/web-react/src/components/Navigation/Navigation.tsx @@ -0,0 +1,22 @@ +'use client'; + +import classNames from 'classnames'; +import React from 'react'; +import { useStyleProps } from '../../hooks'; +import { SpiritNavigationProps } from '../../types'; +import { useNavigationStyleProps } from './useNavigationStyleProps'; + +const Navigation = (props: SpiritNavigationProps): JSX.Element => { + const { children, ...restProps } = props; + + const { classProps } = useNavigationStyleProps(); + const { styleProps, props: otherProps } = useStyleProps(restProps); + + return ( + + ); +}; + +export default Navigation; diff --git a/packages/web-react/src/components/Navigation/NavigationItem.tsx b/packages/web-react/src/components/Navigation/NavigationItem.tsx new file mode 100644 index 0000000000..bb32fff3ca --- /dev/null +++ b/packages/web-react/src/components/Navigation/NavigationItem.tsx @@ -0,0 +1,19 @@ +'use client'; + +import React from 'react'; +import { useStyleProps } from '../../hooks'; +import { SpiritNavigationItemProps } from '../../types'; + +const NavigationItem = (props: SpiritNavigationItemProps): JSX.Element => { + const { children, ...restProps } = props; + + const { styleProps, props: otherProps } = useStyleProps(restProps); + + return ( +
  • + {children} +
  • + ); +}; + +export default NavigationItem; diff --git a/packages/web-react/src/components/Navigation/NavigationLink.tsx b/packages/web-react/src/components/Navigation/NavigationLink.tsx new file mode 100644 index 0000000000..c47c532ac5 --- /dev/null +++ b/packages/web-react/src/components/Navigation/NavigationLink.tsx @@ -0,0 +1,43 @@ +'use client'; + +import classNames from 'classnames'; +import React, { ElementType, forwardRef } from 'react'; +import { useStyleProps } from '../../hooks'; +import { PolymorphicRef, SpiritNavigationLinkProps } from '../../types'; +import { useNavigationLinkProps } from './useNavigationLinkProps'; +import { useNavigationLinkStyleProps } from './useNavigationLinkStyleProps'; + +const defaultProps: Partial = { + elementType: 'a', +}; + +/* We need an exception for components exported with forwardRef */ +/* eslint no-underscore-dangle: ['error', { allow: ['_NavigationLink'] }] */ +const _NavigationLink = ( + props: SpiritNavigationLinkProps, + ref: PolymorphicRef, +): JSX.Element => { + const propsWithDefaults = { ...defaultProps, ...props }; + const { elementType = defaultProps.elementType as ElementType, children, ...restProps } = propsWithDefaults; + const ElementTag = propsWithDefaults.isDisabled ? 'span' : elementType; + + const { navigationLinkProps } = useNavigationLinkProps(propsWithDefaults); + const { classProps, props: modifiedProps } = useNavigationLinkStyleProps(restProps); + const { styleProps, props: otherProps } = useStyleProps(modifiedProps); + + return ( + + {children} + + ); +}; + +const NavigationLink = forwardRef>(_NavigationLink); + +export default NavigationLink; diff --git a/packages/web-react/src/components/Navigation/README.md b/packages/web-react/src/components/Navigation/README.md new file mode 100644 index 0000000000..4dd8ce4653 --- /dev/null +++ b/packages/web-react/src/components/Navigation/README.md @@ -0,0 +1,134 @@ +# Navigation + +The `Navigation` component is a container for the navigation links of the application. + +It consists of a these parts: + +- [Navigation](#navigation) +- [NavigationItem](#navigation-item) +- [NavigationLink](#navigation-link) + +## Navigation + +The `Navigation` is a `nav` wrapper for navigation items. + +```jsx +import { Navigation } from '@lmc-eu/spirit-web-react'; + +{/* Navigation items go here */}; +``` + +It centres its children vertically, and if the children do not include `NavigationLink` components, +it will apply a gap between them. + +ℹ️ Don't forget to add the `aria-label` attribute to the `Navigation` component for correct accessible state. + +### API + +| Name | Type | Default | Required | Description | +| ---------- | --------------------------------------------------------------------------------------- | ------- | -------- | ------------------------- | +| `children` | `ReactElement` \| `ReactElement` \| Array of these types | `null` | ✓ | Content of the Navigation | + +The components accept [additional attributes][readme-additional-attributes]. +If you need more control over the styling of a component, you can use [style props][readme-style-props] +and [escape hatches][readme-escape-hatches]. + +## Navigation Item + +The `NavigationItem` is a container for navigation links. + +```jsx +import { NavigationItem } from '@lmc-eu/spirit-web-react'; + +{/* Navigation links go here */}; +``` + +### API + +| Name | Type | Default | Required | Description | +| ---------- | ----------------------- | ------- | -------- | ----------------------------- | +| `children` | `string` \| `ReactNode` | `null` | ✓ | Content of the NavigationItem | + +The components accept [additional attributes][readme-additional-attributes]. +If you need more control over the styling of a component, you can use [style props][readme-style-props] +and [escape hatches][readme-escape-hatches]. + +## Navigation Link + +The `NavigationLink` is component that is styled to be used as a navigation link. + +```jsx +import { NavigationLink } from '@lmc-eu/spirit-web-react'; + +Link; +``` + +It can obtain `isSelected` or `isDisabled` states by adding the respective props. + +```jsx +Selected Link +Disabled Link +``` + +ℹ️ Don't forget to add the `aria-current="page"` attribute for correct accessible state if selected. + +ℹ️ Please note that in the `isDisabled` state the `NavigationLink` will be an `span` tag. + +If the `NavigationLink` is inside a [`UNSTABLE_Header`][web-react-unstable-header] component, it will +inherit the height of the `Header`. + +### API + +| Name | Type | Default | Required | Description | +| ------------- | --------------------------------- | ------- | -------- | ----------------------------- | +| `children` | `string` \| `ReactNode` | `null` | ✓ | Content of the NavigationLink | +| `elementType` | `ElementType` | `a` | ✕ | Type of element used as | +| `href` | `string` | - | ✕ | URL of the link | +| `isDisabled` | `boolean` | `false` | ✕ | Whether the link is disabled | +| `isSelected` | `boolean` | `false` | ✕ | Whether the link is selected | +| `ref` | `ForwardedRef` | — | ✕ | Anchor element reference | +| `target` | `string` | `null` | ✕ | Link target | + +The components accept [additional attributes][readme-additional-attributes]. +If you need more control over the styling of a component, you can use [style props][readme-style-props] +and [escape hatches][readme-escape-hatches]. + +### Full Example + +With NavigationLink components: + +```jsx + + + + Selected Link + + + + + Disabled Link + + + + Link + + +``` + +With Buttons: + +```jsx + + + Button + + + Button + + +``` + +[readme-additional-attributes]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#additional-attributes +[readme-escape-hatches]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#escape-hatches +[readme-style-props]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#style-props +[web-react-unstable-header]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/UNSTABLE_Header/README.md diff --git a/packages/web-react/src/components/Navigation/__tests__/Navigation.test.tsx b/packages/web-react/src/components/Navigation/__tests__/Navigation.test.tsx new file mode 100644 index 0000000000..621ac52c91 --- /dev/null +++ b/packages/web-react/src/components/Navigation/__tests__/Navigation.test.tsx @@ -0,0 +1,35 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import Navigation from '../Navigation'; + +describe('Navigation', () => { + classNamePrefixProviderTest(Navigation, 'Navigation'); + + stylePropsTest(Navigation); + + restPropsTest(Navigation, 'nav'); + + beforeEach(() => { + render( + +
  • Content
  • +
    , + ); + }); + + it('should have default classname', () => { + expect(screen.getByRole('navigation')).toHaveClass('Navigation'); + }); + + it('should render list and children', () => { + expect(screen.getByRole('list')).toBeInTheDocument(); + }); + + it('should render children', () => { + expect(screen.getByText('Content')).toBeInTheDocument(); + }); +}); diff --git a/packages/web-react/src/components/Navigation/__tests__/NavigationItem.test.tsx b/packages/web-react/src/components/Navigation/__tests__/NavigationItem.test.tsx new file mode 100644 index 0000000000..2037bdb321 --- /dev/null +++ b/packages/web-react/src/components/Navigation/__tests__/NavigationItem.test.tsx @@ -0,0 +1,24 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import NavigationItem from '../NavigationItem'; + +describe('NavigationItem', () => { + stylePropsTest(NavigationItem); + + restPropsTest(NavigationItem, 'li'); + + it('should have correct role', () => { + render(Content); + + expect(screen.getByRole('listitem')).toBeInTheDocument(); + }); + + it('should render children', () => { + render(Content); + + expect(screen.getByRole('listitem')).toHaveTextContent('Content'); + }); +}); diff --git a/packages/web-react/src/components/Navigation/__tests__/NavigationLink.test.tsx b/packages/web-react/src/components/Navigation/__tests__/NavigationLink.test.tsx new file mode 100644 index 0000000000..6e9a2d5b1f --- /dev/null +++ b/packages/web-react/src/components/Navigation/__tests__/NavigationLink.test.tsx @@ -0,0 +1,49 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import NavigationLink from '../NavigationLink'; + +describe('NavigationLink', () => { + classNamePrefixProviderTest(NavigationLink, 'NavigationLink'); + + stylePropsTest(NavigationLink); + + restPropsTest(NavigationLink, 'a'); + + it('should have default classname', () => { + render(Content); + + expect(screen.getByRole('link')).toHaveClass('NavigationLink'); + }); + + it('should have selected classname', () => { + render( + + Content + , + ); + + expect(screen.getByRole('link')).toHaveClass('NavigationLink NavigationLink--selected'); + }); + + it('should have disabled classname and correct elementType', () => { + render( + + Content + , + ); + + expect(screen.getByText('Content')).toHaveClass('NavigationLink NavigationLink--disabled'); + expect(screen.getByText('Content')).toContainHTML('span'); + expect(screen.queryByRole('link')).not.toBeInTheDocument(); + }); + + it('should render children', () => { + render(Content); + + expect(screen.getByText('Content')).toBeInTheDocument(); + }); +}); diff --git a/packages/web-react/src/components/Navigation/__tests__/useNavigationLinkProps.test.ts b/packages/web-react/src/components/Navigation/__tests__/useNavigationLinkProps.test.ts new file mode 100644 index 0000000000..2a56d6456c --- /dev/null +++ b/packages/web-react/src/components/Navigation/__tests__/useNavigationLinkProps.test.ts @@ -0,0 +1,97 @@ +import { renderHook } from '@testing-library/react'; +import { SpiritNavigationLinkProps } from '../../../types'; +import { useNavigationLinkProps } from '../useNavigationLinkProps'; + +describe('useNavigationLinkProps', () => { + it('should return defaults props', () => { + const props: SpiritNavigationLinkProps = { + href: '/', + target: '_blank', + isSelected: false, + isDisabled: false, + }; + const { result } = renderHook(() => useNavigationLinkProps(props)); + + expect(result.current).toStrictEqual({ + navigationLinkProps: { + href: '/', + rel: undefined, + target: '_blank', + }, + }); + }); + + it('should return defaults if element is different from anchor', () => { + const props: SpiritNavigationLinkProps<'span'> = { + elementType: 'span', + href: '/', + target: '_blank', + isSelected: false, + isDisabled: false, + }; + const { result } = renderHook(() => useNavigationLinkProps(props as SpiritNavigationLinkProps)); + + expect(result.current).toStrictEqual({ + navigationLinkProps: { + href: undefined, + rel: undefined, + target: undefined, + }, + }); + }); + + it('should return for isDisabled', () => { + const props: SpiritNavigationLinkProps = { + href: '/', + target: '_blank', + isSelected: false, + isDisabled: true, + }; + const { result } = renderHook(() => useNavigationLinkProps(props)); + + expect(result.current).toStrictEqual({ + navigationLinkProps: { + href: undefined, + rel: undefined, + target: undefined, + }, + }); + }); + + it('should return for isSelected', () => { + const props: SpiritNavigationLinkProps = { + href: '/', + target: '_blank', + isSelected: true, + isDisabled: false, + }; + const { result } = renderHook(() => useNavigationLinkProps(props)); + + expect(result.current).toStrictEqual({ + navigationLinkProps: { + href: '/', + rel: undefined, + target: '_blank', + }, + }); + }); + + it('should return for elementType', () => { + const props: SpiritNavigationLinkProps<'div'> = { + elementType: 'div', + href: '/', + target: '_blank', + isSelected: false, + isDisabled: false, + }; + const { result } = renderHook(() => useNavigationLinkProps(props as SpiritNavigationLinkProps)); + + expect(result.current).toStrictEqual({ + navigationLinkProps: { + href: undefined, + rel: undefined, + target: undefined, + }, + }); + }); +}); diff --git a/packages/web-react/src/components/Navigation/__tests__/useNavigationLinkStyleProps.test.ts b/packages/web-react/src/components/Navigation/__tests__/useNavigationLinkStyleProps.test.ts new file mode 100644 index 0000000000..d7681c4400 --- /dev/null +++ b/packages/web-react/src/components/Navigation/__tests__/useNavigationLinkStyleProps.test.ts @@ -0,0 +1,26 @@ +import { renderHook } from '@testing-library/react'; +import { SpiritNavigationLinkProps } from '../../../types'; +import { useNavigationLinkStyleProps } from '../useNavigationLinkStyleProps'; + +describe('useNavigationLinkStyleProps', () => { + it('should return defaults', () => { + const props = {}; + const { result } = renderHook(() => useNavigationLinkStyleProps(props)); + + expect(result.current.classProps).toBe('NavigationLink'); + }); + + it('should return disabled class', () => { + const props: SpiritNavigationLinkProps = { isDisabled: true }; + const { result } = renderHook(() => useNavigationLinkStyleProps(props)); + + expect(result.current.classProps).toBe('NavigationLink NavigationLink--disabled'); + }); + + it('should return selected class', () => { + const props = { isSelected: true }; + const { result } = renderHook(() => useNavigationLinkStyleProps(props)); + + expect(result.current.classProps).toBe('NavigationLink NavigationLink--selected'); + }); +}); diff --git a/packages/web-react/src/components/Navigation/__tests__/useNavigationStyleProps.test.ts b/packages/web-react/src/components/Navigation/__tests__/useNavigationStyleProps.test.ts new file mode 100644 index 0000000000..a1efc2675e --- /dev/null +++ b/packages/web-react/src/components/Navigation/__tests__/useNavigationStyleProps.test.ts @@ -0,0 +1,10 @@ +import { renderHook } from '@testing-library/react'; +import { useNavigationStyleProps } from '../useNavigationStyleProps'; + +describe('useNavigationStyleProps', () => { + it('should return defaults', () => { + const { result } = renderHook(() => useNavigationStyleProps()); + + expect(result.current.classProps).toBe('Navigation'); + }); +}); diff --git a/packages/web-react/src/components/Navigation/demo/NavigationButtons.tsx b/packages/web-react/src/components/Navigation/demo/NavigationButtons.tsx new file mode 100644 index 0000000000..cb0cb2f749 --- /dev/null +++ b/packages/web-react/src/components/Navigation/demo/NavigationButtons.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { ButtonLink } from '../../ButtonLink'; +import Navigation from '../Navigation'; +import NavigationItem from '../NavigationItem'; + +const NavigationDefault = () => { + return ( + + + Button + + + + Button + + + + ); +}; +export default NavigationDefault; diff --git a/packages/web-react/src/components/Navigation/demo/NavigationDefault.tsx b/packages/web-react/src/components/Navigation/demo/NavigationDefault.tsx new file mode 100644 index 0000000000..9792166066 --- /dev/null +++ b/packages/web-react/src/components/Navigation/demo/NavigationDefault.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import Navigation from '../Navigation'; +import NavigationItem from '../NavigationItem'; + +const NavigationDefault = () => { + return ( + + Item + + ); +}; +export default NavigationDefault; diff --git a/packages/web-react/src/components/Navigation/demo/NavigationLink.tsx b/packages/web-react/src/components/Navigation/demo/NavigationLink.tsx new file mode 100644 index 0000000000..7ecc70c589 --- /dev/null +++ b/packages/web-react/src/components/Navigation/demo/NavigationLink.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import Navigation from '../Navigation'; +import NavigationItem from '../NavigationItem'; +import NavigationLink from '../NavigationLink'; + +const NavigationDefault = () => { + return ( + + + Link + + + + Selected + + + + + Disabled + + + + ); +}; +export default NavigationDefault; diff --git a/packages/web-react/src/components/Navigation/demo/index.tsx b/packages/web-react/src/components/Navigation/demo/index.tsx new file mode 100644 index 0000000000..01462fe662 --- /dev/null +++ b/packages/web-react/src/components/Navigation/demo/index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import DocsSection from '../../../../docs/DocsSections'; +import NavigationButtons from './NavigationButtons'; +import NavigationDefault from './NavigationDefault'; +import NavigationLink from './NavigationLink'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + + + + + , +); diff --git a/packages/web-react/src/components/Navigation/index.html b/packages/web-react/src/components/Navigation/index.html new file mode 100644 index 0000000000..59b5d4e1c8 --- /dev/null +++ b/packages/web-react/src/components/Navigation/index.html @@ -0,0 +1 @@ +{{> web-react/demo title="Navigation" parentPageName="Components" }} diff --git a/packages/web-react/src/components/Navigation/index.ts b/packages/web-react/src/components/Navigation/index.ts new file mode 100644 index 0000000000..8cf2651bce --- /dev/null +++ b/packages/web-react/src/components/Navigation/index.ts @@ -0,0 +1,8 @@ +'use client'; + +export { default as Navigation } from './Navigation'; +export { default as NavigationItem } from './NavigationItem'; +export { default as NavigationLink } from './NavigationLink'; +export * from './useNavigationStyleProps'; +export * from './useNavigationLinkProps'; +export * from './useNavigationLinkStyleProps'; diff --git a/packages/web-react/src/components/Navigation/stories/Navigation.stories.tsx b/packages/web-react/src/components/Navigation/stories/Navigation.stories.tsx new file mode 100644 index 0000000000..123f16c34a --- /dev/null +++ b/packages/web-react/src/components/Navigation/stories/Navigation.stories.tsx @@ -0,0 +1,43 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import Navigation from '../Navigation'; +import NavigationItem from '../NavigationItem'; +import NavigationLink from '../NavigationLink'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Navigation', + component: Navigation, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + children: { + control: 'object', + }, + }, + args: { + children: ( + <> + + + Home + + + + Not Home + + + ), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + name: 'Navigation', +}; diff --git a/packages/web-react/src/components/Navigation/stories/NavigationItem.stories.tsx b/packages/web-react/src/components/Navigation/stories/NavigationItem.stories.tsx new file mode 100644 index 0000000000..60ec676e0a --- /dev/null +++ b/packages/web-react/src/components/Navigation/stories/NavigationItem.stories.tsx @@ -0,0 +1,31 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import NavigationItem from '../NavigationItem'; +import NavigationLink from '../NavigationLink'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Navigation', + component: NavigationItem, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + args: { + children: Link, + }, +}; + +export default meta; +type Story = StoryObj; + +export const NavigationItemPlayground: Story = { + name: 'NavigationItem', + render: (args) => ( +
      + +
    + ), +}; diff --git a/packages/web-react/src/components/Navigation/stories/NavigationLink.stories.tsx b/packages/web-react/src/components/Navigation/stories/NavigationLink.stories.tsx new file mode 100644 index 0000000000..5c531ee0da --- /dev/null +++ b/packages/web-react/src/components/Navigation/stories/NavigationLink.stories.tsx @@ -0,0 +1,41 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import NavigationLink from '../NavigationLink'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Navigation', + component: NavigationLink, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + isDisabled: { + control: 'boolean', + table: { + defaultValue: { summary: 'false' }, + }, + }, + isSelected: { + control: 'boolean', + table: { + defaultValue: { summary: 'false' }, + }, + }, + }, + args: { + children: 'Link', + isDisabled: false, + isSelected: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const NavigationLinkPlayground: Story = { + name: 'NavigationLink', +}; diff --git a/packages/web-react/src/components/Navigation/useNavigationLinkProps.ts b/packages/web-react/src/components/Navigation/useNavigationLinkProps.ts new file mode 100644 index 0000000000..30a8ea89dd --- /dev/null +++ b/packages/web-react/src/components/Navigation/useNavigationLinkProps.ts @@ -0,0 +1,21 @@ +import { SpiritNavigationLinkProps } from '../../types'; + +export type UseNavigationLinkProps = Partial; + +export type UseNavigationLinkReturn = { + navigationLinkProps: UseNavigationLinkProps; +}; + +export const useNavigationLinkProps = (props: UseNavigationLinkProps): UseNavigationLinkReturn => { + const { elementType = 'a', isDisabled, href, target, rel } = props; + + const navigationLinkProps: Partial = { + href: elementType === 'a' && !isDisabled ? href : undefined, + target: elementType === 'a' && !isDisabled ? target : undefined, + rel: elementType === 'a' ? rel : undefined, + }; + + return { + navigationLinkProps, + }; +}; diff --git a/packages/web-react/src/components/Navigation/useNavigationLinkStyleProps.ts b/packages/web-react/src/components/Navigation/useNavigationLinkStyleProps.ts new file mode 100644 index 0000000000..bdd2cca267 --- /dev/null +++ b/packages/web-react/src/components/Navigation/useNavigationLinkStyleProps.ts @@ -0,0 +1,31 @@ +import classNames from 'classnames'; +import { ElementType } from 'react'; +import { useClassNamePrefix } from '../../hooks'; +import { LinkProps, SpiritNavigationLinkProps } from '../../types'; + +export interface NavigationLinkStyles { + /** className props */ + classProps: string | null; + /** props to be passed to the input element */ + props: Partial>; +} + +export function useNavigationLinkStyleProps( + props: SpiritNavigationLinkProps, +): NavigationLinkStyles { + const { isDisabled, isSelected, ...restProps } = props; + + const navigationLinkClass = useClassNamePrefix('NavigationLink'); + const navigationLinkDisabledClass = `${navigationLinkClass}--disabled`; + const navigationLinkSelectedClass = `${navigationLinkClass}--selected`; + + const className = classNames(navigationLinkClass, { + [navigationLinkDisabledClass]: isDisabled, + [navigationLinkSelectedClass]: isSelected, + }); + + return { + classProps: className, + props: restProps, + }; +} diff --git a/packages/web-react/src/components/Navigation/useNavigationStyleProps.ts b/packages/web-react/src/components/Navigation/useNavigationStyleProps.ts new file mode 100644 index 0000000000..608f5492f7 --- /dev/null +++ b/packages/web-react/src/components/Navigation/useNavigationStyleProps.ts @@ -0,0 +1,13 @@ +import { useClassNamePrefix } from '../../hooks'; + +export interface NavigationStyles { + classProps: string; +} + +export const useNavigationStyleProps = () => { + const navigationClass = useClassNamePrefix('Navigation'); + + return { + classProps: navigationClass, + }; +}; diff --git a/packages/web-react/src/components/UNSTABLE_Header/README.md b/packages/web-react/src/components/UNSTABLE_Header/README.md index 2a37500ab9..b4dc55c6e4 100644 --- a/packages/web-react/src/components/UNSTABLE_Header/README.md +++ b/packages/web-react/src/components/UNSTABLE_Header/README.md @@ -20,7 +20,7 @@ import { UNSTABLE_Header } from '@lmc-eu/spirit-web-react'; {/* Content go here */}; ``` -It also sets CSS variable for the Header height which can be used in other nested components. +It also sets CSS variable for the Header height which can be used by nested components. ### API @@ -36,9 +36,6 @@ and [escape hatches][readme-escape-hatches]. The `UNSTABLE_HeaderLogo` component is a container for the logo. -Without any modifier, Header is ready to contain necessary blocks in a classic -left-to-right layout (in LTR documents). - ```jsx import { UNSTABLE_HeaderLogo } from '@lmc-eu/spirit-web-react'; @@ -80,7 +77,7 @@ Use [`Container`][web-react-container] and [`Flex`][web-react-flex] components t ``` -This way you can modify the layout of the Header content easily and modify it how you need. +This way you can easily modify the layout of the Header content. For example you can make the content centered by setting the `Flex` alignment properties to center. @@ -94,7 +91,7 @@ For example you can make the content centered by setting the `Flex` alignment pr ``` -Or you can make modify gaps between the content by setting the `Flex` spacing property. +Or you can modify the gaps between the content by setting the `Flex` `spacing` property. ```jsx diff --git a/packages/web-react/src/components/UNSTABLE_Header/useUnstableHeaderStyleProps.ts b/packages/web-react/src/components/UNSTABLE_Header/useUnstableHeaderStyleProps.ts index 247d7b4608..ec38f218e7 100644 --- a/packages/web-react/src/components/UNSTABLE_Header/useUnstableHeaderStyleProps.ts +++ b/packages/web-react/src/components/UNSTABLE_Header/useUnstableHeaderStyleProps.ts @@ -1,5 +1,5 @@ import { useClassNamePrefix } from '../../hooks'; -import { SpiritHeaderProps } from './UNSTABLE_Header'; +import { SpiritHeaderProps } from '../../types'; export interface HeaderStyles { classProps: { diff --git a/packages/web-react/src/components/index.ts b/packages/web-react/src/components/index.ts index 857be0d09b..f9b932fa78 100644 --- a/packages/web-react/src/components/index.ts +++ b/packages/web-react/src/components/index.ts @@ -22,6 +22,7 @@ export * from './Icon'; export * from './Item'; export * from './Link'; export * from './Modal'; +export * from './Navigation'; export * from './NoSsr'; export * from './Pagination'; export * from './PartnerLogo'; @@ -42,6 +43,7 @@ export * from './Toast'; export * from './Tooltip'; export * from './UNSTABLE_ActionLayout'; export * from './UNSTABLE_Avatar'; +export * from './UNSTABLE_Header'; export * from './UNSTABLE_EmptyState'; export * from './UNSTABLE_Slider'; export * from './UNSTABLE_Toggle'; diff --git a/packages/web-react/src/types/index.ts b/packages/web-react/src/types/index.ts index 84dcbe757b..d99aaf4fb1 100644 --- a/packages/web-react/src/types/index.ts +++ b/packages/web-react/src/types/index.ts @@ -20,6 +20,7 @@ export * from './item'; export * from './label'; export * from './link'; export * from './modal'; +export * from './navigation'; export * from './pagination'; export * from './pill'; export * from './radio'; diff --git a/packages/web-react/src/types/navigation.ts b/packages/web-react/src/types/navigation.ts new file mode 100644 index 0000000000..2c3f9c490e --- /dev/null +++ b/packages/web-react/src/types/navigation.ts @@ -0,0 +1,42 @@ +import { ElementType, ReactElement } from 'react'; +import { NavigationItem } from '../components'; +import { LinkTarget } from './link'; +import { + AriaLabelingProps, + ChildrenProps, + SpiritPolymorphicElementPropsWithRef, + StyleProps, + TransferProps, +} from './shared'; + +export interface NavigationLinkBaseProps extends ChildrenProps, StyleProps, AriaLabelingProps, TransferProps { + /** NavigationLink's href attribute */ + href?: string; + /** Whether is the NavigationLink disabled */ + isDisabled?: boolean; + /** Whether is the NavigationLink selected */ + isSelected?: boolean; + /** NavigationLink's target attribute */ + target?: LinkTarget; +} + +export type NavigationLinkProps = { + /** + * The HTML element or React element used to render the button, e.g. 'div', 'a', or `RouterLink`. + * + * @default 'a' + */ + elementType?: E; +} & NavigationLinkBaseProps; + +export interface SpiritNavigationItemProps extends ChildrenProps, StyleProps {} + +export type SpiritNavigationLinkProps = NavigationLinkProps & + SpiritPolymorphicElementPropsWithRef>; + +export interface SpiritNavigationProps extends StyleProps, AriaLabelingProps { + children: + | ReactElement + | ReactElement + | Array | ReactElement>; +} diff --git a/packages/web-react/src/types/unstableHeader.ts b/packages/web-react/src/types/unstableHeader.ts index bf3186a998..f970e0592f 100644 --- a/packages/web-react/src/types/unstableHeader.ts +++ b/packages/web-react/src/types/unstableHeader.ts @@ -1,6 +1,6 @@ -import { ChildrenProps, SpiritPolymorphicElementPropsWithRef, StyleProps, TransferProps } from './shared'; -import { LinkTarget } from './link'; import { ElementType } from 'react'; +import { LinkTarget } from './link'; +import { ChildrenProps, SpiritPolymorphicElementPropsWithRef, StyleProps, TransferProps } from './shared'; export interface HeaderLogoBaseProps extends ChildrenProps, StyleProps, TransferProps { /** Header's href attribute */ diff --git a/tests/e2e/demo-homepages.spec.ts-snapshots/web-react-chromium-linux.png b/tests/e2e/demo-homepages.spec.ts-snapshots/web-react-chromium-linux.png index b30023dc88..07611c423a 100644 Binary files a/tests/e2e/demo-homepages.spec.ts-snapshots/web-react-chromium-linux.png and b/tests/e2e/demo-homepages.spec.ts-snapshots/web-react-chromium-linux.png differ